More expansions in template repositories (#10021)
* Super expansion * Explain which features are in 1.11 vs 1.12 * Move imports Signed-off-by: jolheiser <john.olheiser@gmail.com>
This commit is contained in:
		
							parent
							
								
									206a031b38
								
							
						
					
					
						commit
						28216bde46
					
				|  | @ -1,23 +1,20 @@ | |||
| --- | ||||
| date: "2019-11-28:00:00+02:00" | ||||
| title: "The .gitea Directory" | ||||
| slug: "gitea-directory" | ||||
| weight: 40 | ||||
| title: "Template Repositories" | ||||
| slug: "template-repositories" | ||||
| weight: 14 | ||||
| toc: true | ||||
| draft: false | ||||
| menu: | ||||
|   sidebar: | ||||
|     parent: "features" | ||||
|     name: "The .gitea Directory" | ||||
|     weight: 50 | ||||
|     identifier: "gitea-directory" | ||||
|     parent: "usage" | ||||
|     name: "Template Repositories" | ||||
|     weight: 14 | ||||
|     identifier: "template-repositories" | ||||
| --- | ||||
| 
 | ||||
| # The .gitea directory | ||||
| Gitea repositories can include a `.gitea` directory at their base which will store settings/configurations for certain features. | ||||
| 
 | ||||
| ## Templates | ||||
| Gitea includes template repositories, and one feature implemented with them is auto-expansion of specific variables within your template files.   | ||||
| ## Template Repositories | ||||
| Gitea `1.11.0` and above includes template repositories, and one feature implemented with them is auto-expansion of specific variables within your template files.   | ||||
| To tell Gitea which files to expand, you must include a `template` file inside the `.gitea` directory of the template repository.   | ||||
| Gitea uses [gobwas/glob](https://github.com/gobwas/glob) for its glob syntax. It closely resembles a traditional `.gitignore`, however there may be slight differences. | ||||
| 
 | ||||
|  | @ -42,15 +39,34 @@ a/b/c/d.json | |||
| In any file matched by the above globs, certain variables will be expanded.   | ||||
| All variables must be of the form `$VAR` or `${VAR}`. To escape an expansion, use a double `$$`, such as `$$VAR` or `$${VAR}` | ||||
| 
 | ||||
| | Variable             | Expands To                                          | | ||||
| |----------------------|-----------------------------------------------------| | ||||
| | REPO_NAME            | The name of the generated repository                | | ||||
| | TEMPLATE_NAME        | The name of the template repository                 | | ||||
| | REPO_DESCRIPTION     | The description of the generated repository         | | ||||
| | TEMPLATE_DESCRIPTION | The description of the template repository          | | ||||
| | REPO_LINK            | The URL to the generated repository                 | | ||||
| | TEMPLATE_LINK        | The URL to the template repository                  | | ||||
| | REPO_HTTPS_URL       | The HTTP(S) clone link for the generated repository | | ||||
| | TEMPLATE_HTTPS_URL   | The HTTP(S) clone link for the template repository  | | ||||
| | REPO_SSH_URL         | The SSH clone link for the generated repository     | | ||||
| | TEMPLATE_SSH_URL     | The SSH clone link for the template repository      | | ||||
| | Variable             | Expands To                                          | Transformable | | ||||
| |----------------------|-----------------------------------------------------|---------------| | ||||
| | REPO_NAME            | The name of the generated repository                | ✓             | | ||||
| | TEMPLATE_NAME        | The name of the template repository                 | ✓             | | ||||
| | REPO_DESCRIPTION     | The description of the generated repository         | ✘             | | ||||
| | TEMPLATE_DESCRIPTION | The description of the template repository          | ✘             | | ||||
| | REPO_OWNER           | The owner of the generated repository               | ✓             | | ||||
| | TEMPLATE_OWNER       | The owner of the template repository                | ✓             | | ||||
| | REPO_LINK            | The URL to the generated repository                 | ✘             | | ||||
| | TEMPLATE_LINK        | The URL to the template repository                  | ✘             | | ||||
| | REPO_HTTPS_URL       | The HTTP(S) clone link for the generated repository | ✘             | | ||||
| | TEMPLATE_HTTPS_URL   | The HTTP(S) clone link for the template repository  | ✘             | | ||||
| | REPO_SSH_URL         | The SSH clone link for the generated repository     | ✘             | | ||||
| | TEMPLATE_SSH_URL     | The SSH clone link for the template repository      | ✘             | | ||||
| 
 | ||||
| ### Transformers :robot: | ||||
| Gitea `1.12.0` adds a few transformers to some of the applicable variables above.   | ||||
| For example, to get `REPO_NAME` in `PASCAL`-case, your template would use `${REPO_NAME_PASCAL}` | ||||
| 
 | ||||
| Feeding `go-sdk` to the available transformers yields... | ||||
| 
 | ||||
| | Transformer | Effect     | | ||||
| |-------------|------------| | ||||
| | SNAKE       | go_sdk     | | ||||
| | KEBAB       | go-sdk     | | ||||
| | CAMEL       | goSdk      | | ||||
| | PASCAL      | GoSdk      | | ||||
| | LOWER       | go-sdk     | | ||||
| | UPPER       | GO-SDK     | | ||||
| | TITLE       | Go-Sdk     | | ||||
| 
 | ||||
							
								
								
									
										1
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										1
									
								
								go.mod
								
								
								
								
							|  | @ -50,6 +50,7 @@ require ( | |||
| 	github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 | ||||
| 	github.com/google/go-github/v24 v24.0.1 | ||||
| 	github.com/gorilla/context v1.1.1 | ||||
| 	github.com/huandu/xstrings v1.3.0 | ||||
| 	github.com/issue9/assert v1.3.2 // indirect | ||||
| 	github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c | ||||
| 	github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | ||||
|  |  | |||
							
								
								
									
										2
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										2
									
								
								go.sum
								
								
								
								
							|  | @ -306,6 +306,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | |||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||
| github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo= | ||||
| github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= | ||||
| github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= | ||||
| github.com/issue9/assert v1.3.2 h1:IaTa37u4m1fUuTH9K9ldO5IONKVDXjLiUO1T9vj0OF0= | ||||
| github.com/issue9/assert v1.3.2/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio= | ||||
|  |  | |||
|  | @ -16,38 +16,62 @@ import ( | |||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 
 | ||||
| 	"github.com/huandu/xstrings" | ||||
| ) | ||||
| 
 | ||||
| type transformer struct { | ||||
| 	Name      string | ||||
| 	Transform func(string) string | ||||
| } | ||||
| 
 | ||||
| type expansion struct { | ||||
| 	Name         string | ||||
| 	Value        string | ||||
| 	Transformers []transformer | ||||
| } | ||||
| 
 | ||||
| var defaultTransformers = []transformer{ | ||||
| 	{Name: "SNAKE", Transform: xstrings.ToSnakeCase}, | ||||
| 	{Name: "KEBAB", Transform: xstrings.ToKebabCase}, | ||||
| 	{Name: "CAMEL", Transform: func(str string) string { | ||||
| 		return xstrings.FirstRuneToLower(xstrings.ToCamelCase(str)) | ||||
| 	}}, | ||||
| 	{Name: "PASCAL", Transform: xstrings.ToCamelCase}, | ||||
| 	{Name: "LOWER", Transform: strings.ToLower}, | ||||
| 	{Name: "UPPER", Transform: strings.ToUpper}, | ||||
| 	{Name: "TITLE", Transform: strings.Title}, | ||||
| } | ||||
| 
 | ||||
| func generateExpansion(src string, templateRepo, generateRepo *models.Repository) string { | ||||
| 	return os.Expand(src, func(key string) string { | ||||
| 		switch key { | ||||
| 		case "REPO_NAME": | ||||
| 			return generateRepo.Name | ||||
| 		case "TEMPLATE_NAME": | ||||
| 			return templateRepo.Name | ||||
| 		case "REPO_DESCRIPTION": | ||||
| 			return generateRepo.Description | ||||
| 		case "TEMPLATE_DESCRIPTION": | ||||
| 			return templateRepo.Description | ||||
| 		case "REPO_OWNER": | ||||
| 			return generateRepo.OwnerName | ||||
| 		case "TEMPLATE_OWNER": | ||||
| 			return templateRepo.OwnerName | ||||
| 		case "REPO_LINK": | ||||
| 			return generateRepo.Link() | ||||
| 		case "TEMPLATE_LINK": | ||||
| 			return templateRepo.Link() | ||||
| 		case "REPO_HTTPS_URL": | ||||
| 			return generateRepo.CloneLink().HTTPS | ||||
| 		case "TEMPLATE_HTTPS_URL": | ||||
| 			return templateRepo.CloneLink().HTTPS | ||||
| 		case "REPO_SSH_URL": | ||||
| 			return generateRepo.CloneLink().SSH | ||||
| 		case "TEMPLATE_SSH_URL": | ||||
| 			return templateRepo.CloneLink().SSH | ||||
| 		default: | ||||
| 			return key | ||||
| 	expansions := []expansion{ | ||||
| 		{Name: "REPO_NAME", Value: generateRepo.Name, Transformers: defaultTransformers}, | ||||
| 		{Name: "TEMPLATE_NAME", Value: templateRepo.Name, Transformers: defaultTransformers}, | ||||
| 		{Name: "REPO_DESCRIPTION", Value: generateRepo.Description, Transformers: nil}, | ||||
| 		{Name: "TEMPLATE_DESCRIPTION", Value: templateRepo.Description, Transformers: nil}, | ||||
| 		{Name: "REPO_OWNER", Value: generateRepo.OwnerName, Transformers: defaultTransformers}, | ||||
| 		{Name: "TEMPLATE_OWNER", Value: templateRepo.OwnerName, Transformers: defaultTransformers}, | ||||
| 		{Name: "REPO_LINK", Value: generateRepo.Link(), Transformers: nil}, | ||||
| 		{Name: "TEMPLATE_LINK", Value: templateRepo.Link(), Transformers: nil}, | ||||
| 		{Name: "REPO_HTTPS_URL", Value: generateRepo.CloneLink().HTTPS, Transformers: nil}, | ||||
| 		{Name: "TEMPLATE_HTTPS_URL", Value: templateRepo.CloneLink().HTTPS, Transformers: nil}, | ||||
| 		{Name: "REPO_SSH_URL", Value: generateRepo.CloneLink().SSH, Transformers: nil}, | ||||
| 		{Name: "TEMPLATE_SSH_URL", Value: templateRepo.CloneLink().SSH, Transformers: nil}, | ||||
| 	} | ||||
| 
 | ||||
| 	var expansionMap = make(map[string]string) | ||||
| 	for _, e := range expansions { | ||||
| 		expansionMap[e.Name] = e.Value | ||||
| 		for _, tr := range e.Transformers { | ||||
| 			expansionMap[fmt.Sprintf("%s_%s", e.Name, tr.Name)] = tr.Transform(e.Value) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return os.Expand(src, func(key string) string { | ||||
| 		if expansion, ok := expansionMap[key]; ok { | ||||
| 			return expansion | ||||
| 		} | ||||
| 		return key | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
|  | @ -104,41 +128,43 @@ func generateRepoCommit(repo, templateRepo, generateRepo *models.Repository, tmp | |||
| 		return fmt.Errorf("checkGiteaTemplate: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := os.Remove(gt.Path); err != nil { | ||||
| 		return fmt.Errorf("remove .giteatemplate: %v", err) | ||||
| 	} | ||||
| 	if gt != nil { | ||||
| 		if err := os.Remove(gt.Path); err != nil { | ||||
| 			return fmt.Errorf("remove .giteatemplate: %v", err) | ||||
| 		} | ||||
| 
 | ||||
| 	// Avoid walking tree if there are no globs
 | ||||
| 	if len(gt.Globs()) > 0 { | ||||
| 		tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" | ||||
| 		if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error { | ||||
| 			if walkErr != nil { | ||||
| 				return walkErr | ||||
| 			} | ||||
| 
 | ||||
| 			if info.IsDir() { | ||||
| 				return nil | ||||
| 			} | ||||
| 
 | ||||
| 			base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) | ||||
| 			for _, g := range gt.Globs() { | ||||
| 				if g.Match(base) { | ||||
| 					content, err := ioutil.ReadFile(path) | ||||
| 					if err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 
 | ||||
| 					if err := ioutil.WriteFile(path, | ||||
| 						[]byte(generateExpansion(string(content), templateRepo, generateRepo)), | ||||
| 						0644); err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 					break | ||||
| 		// Avoid walking tree if there are no globs
 | ||||
| 		if len(gt.Globs()) > 0 { | ||||
| 			tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/" | ||||
| 			if err := filepath.Walk(tmpDirSlash, func(path string, info os.FileInfo, walkErr error) error { | ||||
| 				if walkErr != nil { | ||||
| 					return walkErr | ||||
| 				} | ||||
| 
 | ||||
| 				if info.IsDir() { | ||||
| 					return nil | ||||
| 				} | ||||
| 
 | ||||
| 				base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash) | ||||
| 				for _, g := range gt.Globs() { | ||||
| 					if g.Match(base) { | ||||
| 						content, err := ioutil.ReadFile(path) | ||||
| 						if err != nil { | ||||
| 							return err | ||||
| 						} | ||||
| 
 | ||||
| 						if err := ioutil.WriteFile(path, | ||||
| 							[]byte(generateExpansion(string(content), templateRepo, generateRepo)), | ||||
| 							0644); err != nil { | ||||
| 							return err | ||||
| 						} | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 				return nil | ||||
| 			}); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			return nil | ||||
| 		}); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,24 @@ | |||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||
| *.o | ||||
| *.a | ||||
| *.so | ||||
| 
 | ||||
| # Folders | ||||
| _obj | ||||
| _test | ||||
| 
 | ||||
| # Architecture specific extensions/prefixes | ||||
| *.[568vq] | ||||
| [568vq].out | ||||
| 
 | ||||
| *.cgo1.go | ||||
| *.cgo2.c | ||||
| _cgo_defun.c | ||||
| _cgo_gotypes.go | ||||
| _cgo_export.* | ||||
| 
 | ||||
| _testmain.go | ||||
| 
 | ||||
| *.exe | ||||
| *.test | ||||
| *.prof | ||||
|  | @ -0,0 +1,7 @@ | |||
| language: go | ||||
| install: | ||||
|   - go get golang.org/x/tools/cmd/cover | ||||
|   - go get github.com/mattn/goveralls | ||||
| script: | ||||
|   - go test -v -covermode=count -coverprofile=coverage.out | ||||
|   - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ] && [ ! -z "$COVERALLS_TOKEN" ]; then $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN; fi' | ||||
|  | @ -0,0 +1,23 @@ | |||
| # Contributing # | ||||
| 
 | ||||
| Thanks for your contribution in advance. No matter what you will contribute to this project, pull request or bug report or feature discussion, it's always highly appreciated. | ||||
| 
 | ||||
| ## New API or feature ## | ||||
| 
 | ||||
| I want to speak more about how to add new functions to this package. | ||||
| 
 | ||||
| Package `xstring` is a collection of useful string functions which should be implemented in Go. It's a bit subject to say which function should be included and which should not. I set up following rules in order to make it clear and as objective as possible. | ||||
| 
 | ||||
| * Rule 1: Only string algorithm, which takes string as input, can be included. | ||||
| * Rule 2: If a function has been implemented in package `string`, it must not be included. | ||||
| * Rule 3: If a function is not language neutral, it must not be included. | ||||
| * Rule 4: If a function is a part of standard library in other languages, it can be included. | ||||
| * Rule 5: If a function is quite useful in some famous framework or library, it can be included. | ||||
| 
 | ||||
| New function must be discussed in project issues before submitting any code. If a pull request with new functions is sent without any ref issue, it will be rejected. | ||||
| 
 | ||||
| ## Pull request ## | ||||
| 
 | ||||
| Pull request is always welcome. Just make sure you have run `go fmt` and all test cases passed before submit. | ||||
| 
 | ||||
| If the pull request is to add a new API or feature, don't forget to update README.md and add new API in function list. | ||||
|  | @ -0,0 +1,22 @@ | |||
| The MIT License (MIT) | ||||
| 
 | ||||
| Copyright (c) 2015 Huan Du | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| 
 | ||||
|  | @ -0,0 +1,117 @@ | |||
| # xstrings # | ||||
| 
 | ||||
| [](https://travis-ci.org/huandu/xstrings) | ||||
| [](https://godoc.org/github.com/huandu/xstrings) | ||||
| [](https://goreportcard.com/report/github.com/huandu/xstrings) | ||||
| [](https://coveralls.io/github/huandu/xstrings?branch=master) | ||||
| 
 | ||||
| Go package [xstrings](https://godoc.org/github.com/huandu/xstrings) is a collection of string functions, which are widely used in other languages but absent in Go package [strings](http://golang.org/pkg/strings). | ||||
| 
 | ||||
| All functions are well tested and carefully tuned for performance. | ||||
| 
 | ||||
| ## Propose a new function ## | ||||
| 
 | ||||
| Please review [contributing guideline](CONTRIBUTING.md) and [create new issue](https://github.com/huandu/xstrings/issues) to state why it should be included. | ||||
| 
 | ||||
| ## Install ## | ||||
| 
 | ||||
| Use `go get` to install this library. | ||||
| 
 | ||||
|     go get github.com/huandu/xstrings | ||||
| 
 | ||||
| ## API document ## | ||||
| 
 | ||||
| See [GoDoc](https://godoc.org/github.com/huandu/xstrings) for full document. | ||||
| 
 | ||||
| ## Function list ## | ||||
| 
 | ||||
| Go functions have a unique naming style. One, who has experience in other language but new in Go, may have difficulties to find out right string function to use. | ||||
| 
 | ||||
| Here is a list of functions in [strings](http://golang.org/pkg/strings) and [xstrings](https://godoc.org/github.com/huandu/xstrings) with enough extra information about how to map these functions to their friends in other languages. Hope this list could be helpful for fresh gophers. | ||||
| 
 | ||||
| ### Package `xstrings` functions ### | ||||
| 
 | ||||
| *Keep this table sorted by Function in ascending order.* | ||||
| 
 | ||||
| | Function | Friends | # | | ||||
| | -------- | ------- | --- | | ||||
| | [Center](https://godoc.org/github.com/huandu/xstrings#Center) | `str.center` in Python; `String#center` in Ruby | [#30](https://github.com/huandu/xstrings/issues/30) | | ||||
| | [Count](https://godoc.org/github.com/huandu/xstrings#Count) | `String#count` in Ruby | [#16](https://github.com/huandu/xstrings/issues/16) | | ||||
| | [Delete](https://godoc.org/github.com/huandu/xstrings#Delete) | `String#delete` in Ruby | [#17](https://github.com/huandu/xstrings/issues/17) | | ||||
| | [ExpandTabs](https://godoc.org/github.com/huandu/xstrings#ExpandTabs) | `str.expandtabs` in Python | [#27](https://github.com/huandu/xstrings/issues/27) | | ||||
| | [FirstRuneToLower](https://godoc.org/github.com/huandu/xstrings#FirstRuneToLower) | `lcfirst` in PHP or Perl | [#15](https://github.com/huandu/xstrings/issues/15) | | ||||
| | [FirstRuneToUpper](https://godoc.org/github.com/huandu/xstrings#FirstRuneToUpper) | `String#capitalize` in Ruby; `ucfirst` in PHP or Perl | [#15](https://github.com/huandu/xstrings/issues/15) | | ||||
| | [Insert](https://godoc.org/github.com/huandu/xstrings#Insert) | `String#insert` in Ruby | [#18](https://github.com/huandu/xstrings/issues/18) | | ||||
| | [LastPartition](https://godoc.org/github.com/huandu/xstrings#LastPartition) | `str.rpartition` in Python; `String#rpartition` in Ruby | [#19](https://github.com/huandu/xstrings/issues/19) | | ||||
| | [LeftJustify](https://godoc.org/github.com/huandu/xstrings#LeftJustify) | `str.ljust` in Python; `String#ljust` in Ruby | [#28](https://github.com/huandu/xstrings/issues/28) | | ||||
| | [Len](https://godoc.org/github.com/huandu/xstrings#Len) | `mb_strlen` in PHP | [#23](https://github.com/huandu/xstrings/issues/23) | | ||||
| | [Partition](https://godoc.org/github.com/huandu/xstrings#Partition) | `str.partition` in Python; `String#partition` in Ruby | [#10](https://github.com/huandu/xstrings/issues/10) | | ||||
| | [Reverse](https://godoc.org/github.com/huandu/xstrings#Reverse) | `String#reverse` in Ruby; `strrev` in PHP; `reverse` in Perl | [#7](https://github.com/huandu/xstrings/issues/7) | | ||||
| | [RightJustify](https://godoc.org/github.com/huandu/xstrings#RightJustify) | `str.rjust` in Python; `String#rjust` in Ruby | [#29](https://github.com/huandu/xstrings/issues/29) | | ||||
| | [RuneWidth](https://godoc.org/github.com/huandu/xstrings#RuneWidth) | - | [#27](https://github.com/huandu/xstrings/issues/27) | | ||||
| | [Scrub](https://godoc.org/github.com/huandu/xstrings#Scrub) | `String#scrub` in Ruby | [#20](https://github.com/huandu/xstrings/issues/20) | | ||||
| | [Shuffle](https://godoc.org/github.com/huandu/xstrings#Shuffle) | `str_shuffle` in PHP | [#13](https://github.com/huandu/xstrings/issues/13) | | ||||
| | [ShuffleSource](https://godoc.org/github.com/huandu/xstrings#ShuffleSource) | `str_shuffle` in PHP | [#13](https://github.com/huandu/xstrings/issues/13) | | ||||
| | [Slice](https://godoc.org/github.com/huandu/xstrings#Slice) | `mb_substr` in PHP | [#9](https://github.com/huandu/xstrings/issues/9) | | ||||
| | [Squeeze](https://godoc.org/github.com/huandu/xstrings#Squeeze) | `String#squeeze` in Ruby | [#11](https://github.com/huandu/xstrings/issues/11) | | ||||
| | [Successor](https://godoc.org/github.com/huandu/xstrings#Successor) | `String#succ` or `String#next` in Ruby | [#22](https://github.com/huandu/xstrings/issues/22) | | ||||
| | [SwapCase](https://godoc.org/github.com/huandu/xstrings#SwapCase) | `str.swapcase` in Python; `String#swapcase` in Ruby | [#12](https://github.com/huandu/xstrings/issues/12) | | ||||
| | [ToCamelCase](https://godoc.org/github.com/huandu/xstrings#ToCamelCase) | `String#camelize` in RoR | [#1](https://github.com/huandu/xstrings/issues/1) | | ||||
| | [ToKebab](https://godoc.org/github.com/huandu/xstrings#ToKebabCase) | - | [#41](https://github.com/huandu/xstrings/issues/41) | | ||||
| | [ToSnakeCase](https://godoc.org/github.com/huandu/xstrings#ToSnakeCase) | `String#underscore` in RoR | [#1](https://github.com/huandu/xstrings/issues/1) | | ||||
| | [Translate](https://godoc.org/github.com/huandu/xstrings#Translate) | `str.translate` in Python; `String#tr` in Ruby; `strtr` in PHP; `tr///` in Perl | [#21](https://github.com/huandu/xstrings/issues/21) | | ||||
| | [Width](https://godoc.org/github.com/huandu/xstrings#Width) | `mb_strwidth` in PHP | [#26](https://github.com/huandu/xstrings/issues/26) | | ||||
| | [WordCount](https://godoc.org/github.com/huandu/xstrings#WordCount) | `str_word_count` in PHP | [#14](https://github.com/huandu/xstrings/issues/14) | | ||||
| | [WordSplit](https://godoc.org/github.com/huandu/xstrings#WordSplit) | - | [#14](https://github.com/huandu/xstrings/issues/14) | | ||||
| 
 | ||||
| ### Package `strings` functions ### | ||||
| 
 | ||||
| *Keep this table sorted by Function in ascending order.* | ||||
| 
 | ||||
| | Function | Friends | | ||||
| | -------- | ------- | | ||||
| | [Contains](http://golang.org/pkg/strings/#Contains) | `String#include?` in Ruby | | ||||
| | [ContainsAny](http://golang.org/pkg/strings/#ContainsAny) | - | | ||||
| | [ContainsRune](http://golang.org/pkg/strings/#ContainsRune) | - | | ||||
| | [Count](http://golang.org/pkg/strings/#Count) | `str.count` in Python; `substr_count` in PHP | | ||||
| | [EqualFold](http://golang.org/pkg/strings/#EqualFold) | `stricmp` in PHP; `String#casecmp` in Ruby | | ||||
| | [Fields](http://golang.org/pkg/strings/#Fields) | `str.split` in Python; `split` in Perl; `String#split` in Ruby | | ||||
| | [FieldsFunc](http://golang.org/pkg/strings/#FieldsFunc) | - | | ||||
| | [HasPrefix](http://golang.org/pkg/strings/#HasPrefix) | `str.startswith` in Python; `String#start_with?` in Ruby | | ||||
| | [HasSuffix](http://golang.org/pkg/strings/#HasSuffix) | `str.endswith` in Python; `String#end_with?` in Ruby | | ||||
| | [Index](http://golang.org/pkg/strings/#Index) | `str.index` in Python; `String#index` in Ruby; `strpos` in PHP; `index` in Perl | | ||||
| | [IndexAny](http://golang.org/pkg/strings/#IndexAny) | - | | ||||
| | [IndexByte](http://golang.org/pkg/strings/#IndexByte) | - | | ||||
| | [IndexFunc](http://golang.org/pkg/strings/#IndexFunc) | - | | ||||
| | [IndexRune](http://golang.org/pkg/strings/#IndexRune) | - | | ||||
| | [Join](http://golang.org/pkg/strings/#Join) | `str.join` in Python; `Array#join` in Ruby; `implode` in PHP; `join` in Perl | | ||||
| | [LastIndex](http://golang.org/pkg/strings/#LastIndex) | `str.rindex` in Python; `String#rindex`; `strrpos` in PHP; `rindex` in Perl | | ||||
| | [LastIndexAny](http://golang.org/pkg/strings/#LastIndexAny) | - | | ||||
| | [LastIndexFunc](http://golang.org/pkg/strings/#LastIndexFunc) | - | | ||||
| | [Map](http://golang.org/pkg/strings/#Map) | `String#each_codepoint` in Ruby | | ||||
| | [Repeat](http://golang.org/pkg/strings/#Repeat) | operator `*` in Python and Ruby; `str_repeat` in PHP | | ||||
| | [Replace](http://golang.org/pkg/strings/#Replace) | `str.replace` in Python; `String#sub` in Ruby; `str_replace` in PHP | | ||||
| | [Split](http://golang.org/pkg/strings/#Split) | `str.split` in Python; `String#split` in Ruby; `explode` in PHP; `split` in Perl | | ||||
| | [SplitAfter](http://golang.org/pkg/strings/#SplitAfter) | - | | ||||
| | [SplitAfterN](http://golang.org/pkg/strings/#SplitAfterN) | - | | ||||
| | [SplitN](http://golang.org/pkg/strings/#SplitN) | `str.split` in Python; `String#split` in Ruby; `explode` in PHP; `split` in Perl | | ||||
| | [Title](http://golang.org/pkg/strings/#Title) | `str.title` in Python | | ||||
| | [ToLower](http://golang.org/pkg/strings/#ToLower) | `str.lower` in Python; `String#downcase` in Ruby; `strtolower` in PHP; `lc` in Perl | | ||||
| | [ToLowerSpecial](http://golang.org/pkg/strings/#ToLowerSpecial) | - | | ||||
| | [ToTitle](http://golang.org/pkg/strings/#ToTitle) | - | | ||||
| | [ToTitleSpecial](http://golang.org/pkg/strings/#ToTitleSpecial) | - | | ||||
| | [ToUpper](http://golang.org/pkg/strings/#ToUpper) | `str.upper` in Python; `String#upcase` in Ruby; `strtoupper` in PHP; `uc` in Perl | | ||||
| | [ToUpperSpecial](http://golang.org/pkg/strings/#ToUpperSpecial) | - | | ||||
| | [Trim](http://golang.org/pkg/strings/#Trim) | `str.strip` in Python; `String#strip` in Ruby; `trim` in PHP | | ||||
| | [TrimFunc](http://golang.org/pkg/strings/#TrimFunc) | - | | ||||
| | [TrimLeft](http://golang.org/pkg/strings/#TrimLeft) | `str.lstrip` in Python; `String#lstrip` in Ruby; `ltrim` in PHP | | ||||
| | [TrimLeftFunc](http://golang.org/pkg/strings/#TrimLeftFunc) | - | | ||||
| | [TrimPrefix](http://golang.org/pkg/strings/#TrimPrefix) | - | | ||||
| | [TrimRight](http://golang.org/pkg/strings/#TrimRight) | `str.rstrip` in Python; `String#rstrip` in Ruby; `rtrim` in PHP | | ||||
| | [TrimRightFunc](http://golang.org/pkg/strings/#TrimRightFunc) | - | | ||||
| | [TrimSpace](http://golang.org/pkg/strings/#TrimSpace) | `str.strip` in Python; `String#strip` in Ruby; `trim` in PHP | | ||||
| | [TrimSuffix](http://golang.org/pkg/strings/#TrimSuffix) | `String#chomp` in Ruby; `chomp` in Perl | | ||||
| 
 | ||||
| ## License ## | ||||
| 
 | ||||
| This library is licensed under MIT license. See LICENSE for details. | ||||
|  | @ -0,0 +1,25 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| ) | ||||
| 
 | ||||
| const bufferMaxInitGrowSize = 2048 | ||||
| 
 | ||||
| // Lazy initialize a buffer.
 | ||||
| func allocBuffer(orig, cur string) *bytes.Buffer { | ||||
| 	output := &bytes.Buffer{} | ||||
| 	maxSize := len(orig) * 4 | ||||
| 
 | ||||
| 	// Avoid to reserve too much memory at once.
 | ||||
| 	if maxSize > bufferMaxInitGrowSize { | ||||
| 		maxSize = bufferMaxInitGrowSize | ||||
| 	} | ||||
| 
 | ||||
| 	output.Grow(maxSize) | ||||
| 	output.WriteString(orig[:len(orig)-len(cur)]) | ||||
| 	return output | ||||
| } | ||||
|  | @ -0,0 +1,404 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"math/rand" | ||||
| 	"unicode" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
| 
 | ||||
| // ToCamelCase is to convert words separated by space, underscore and hyphen to camel case.
 | ||||
| //
 | ||||
| // Some samples.
 | ||||
| //     "some_words"      => "SomeWords"
 | ||||
| //     "http_server"     => "HttpServer"
 | ||||
| //     "no_https"        => "NoHttps"
 | ||||
| //     "_complex__case_" => "_Complex_Case_"
 | ||||
| //     "some words"      => "SomeWords"
 | ||||
| func ToCamelCase(str string) string { | ||||
| 	if len(str) == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
| 
 | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	var r0, r1 rune | ||||
| 	var size int | ||||
| 
 | ||||
| 	// leading connector will appear in output.
 | ||||
| 	for len(str) > 0 { | ||||
| 		r0, size = utf8.DecodeRuneInString(str) | ||||
| 		str = str[size:] | ||||
| 
 | ||||
| 		if !isConnector(r0) { | ||||
| 			r0 = unicode.ToUpper(r0) | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		buf.WriteRune(r0) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(str) == 0 { | ||||
| 		// A special case for a string contains only 1 rune.
 | ||||
| 		if size != 0 { | ||||
| 			buf.WriteRune(r0) | ||||
| 		} | ||||
| 
 | ||||
| 		return buf.String() | ||||
| 	} | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r1 = r0 | ||||
| 		r0, size = utf8.DecodeRuneInString(str) | ||||
| 		str = str[size:] | ||||
| 
 | ||||
| 		if isConnector(r0) && isConnector(r1) { | ||||
| 			buf.WriteRune(r1) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if isConnector(r1) { | ||||
| 			r0 = unicode.ToUpper(r0) | ||||
| 		} else { | ||||
| 			r0 = unicode.ToLower(r0) | ||||
| 			buf.WriteRune(r1) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	buf.WriteRune(r0) | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| // ToSnakeCase can convert all upper case characters in a string to
 | ||||
| // snake case format.
 | ||||
| //
 | ||||
| // Some samples.
 | ||||
| //     "FirstName"  => "first_name"
 | ||||
| //     "HTTPServer" => "http_server"
 | ||||
| //     "NoHTTPS"    => "no_https"
 | ||||
| //     "GO_PATH"    => "go_path"
 | ||||
| //     "GO PATH"    => "go_path"      // space is converted to underscore.
 | ||||
| //     "GO-PATH"    => "go_path"      // hyphen is converted to underscore.
 | ||||
| //     "HTTP2XX"    => "http_2xx"     // insert an underscore before a number and after an alphabet.
 | ||||
| //     "http2xx"    => "http_2xx"
 | ||||
| //     "HTTP20xOK"  => "http_20x_ok"
 | ||||
| func ToSnakeCase(str string) string { | ||||
| 	return camelCaseToLowerCase(str, '_') | ||||
| } | ||||
| 
 | ||||
| // ToKebabCase can convert all upper case characters in a string to
 | ||||
| // kebab case format.
 | ||||
| //
 | ||||
| // Some samples.
 | ||||
| //     "FirstName"  => "first-name"
 | ||||
| //     "HTTPServer" => "http-server"
 | ||||
| //     "NoHTTPS"    => "no-https"
 | ||||
| //     "GO_PATH"    => "go-path"
 | ||||
| //     "GO PATH"    => "go-path"      // space is converted to '-'.
 | ||||
| //     "GO-PATH"    => "go-path"      // hyphen is converted to '-'.
 | ||||
| //     "HTTP2XX"    => "http-2xx"     // insert a '-' before a number and after an alphabet.
 | ||||
| //     "http2xx"    => "http-2xx"
 | ||||
| //     "HTTP20xOK"  => "http-20x-ok"
 | ||||
| func ToKebabCase(str string) string { | ||||
| 	return camelCaseToLowerCase(str, '-') | ||||
| } | ||||
| 
 | ||||
| func camelCaseToLowerCase(str string, connector rune) string { | ||||
| 	if len(str) == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
| 
 | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	var prev, r0, r1 rune | ||||
| 	var size int | ||||
| 
 | ||||
| 	r0 = connector | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		prev = r0 | ||||
| 		r0, size = utf8.DecodeRuneInString(str) | ||||
| 		str = str[size:] | ||||
| 
 | ||||
| 		switch { | ||||
| 		case r0 == utf8.RuneError: | ||||
| 			buf.WriteRune(r0) | ||||
| 
 | ||||
| 		case unicode.IsUpper(r0): | ||||
| 			if prev != connector && !unicode.IsNumber(prev) { | ||||
| 				buf.WriteRune(connector) | ||||
| 			} | ||||
| 
 | ||||
| 			buf.WriteRune(unicode.ToLower(r0)) | ||||
| 
 | ||||
| 			if len(str) == 0 { | ||||
| 				break | ||||
| 			} | ||||
| 
 | ||||
| 			r0, size = utf8.DecodeRuneInString(str) | ||||
| 			str = str[size:] | ||||
| 
 | ||||
| 			if !unicode.IsUpper(r0) { | ||||
| 				buf.WriteRune(r0) | ||||
| 				break | ||||
| 			} | ||||
| 
 | ||||
| 			// find next non-upper-case character and insert connector properly.
 | ||||
| 			// it's designed to convert `HTTPServer` to `http_server`.
 | ||||
| 			// if there are more than 2 adjacent upper case characters in a word,
 | ||||
| 			// treat them as an abbreviation plus a normal word.
 | ||||
| 			for len(str) > 0 { | ||||
| 				r1 = r0 | ||||
| 				r0, size = utf8.DecodeRuneInString(str) | ||||
| 				str = str[size:] | ||||
| 
 | ||||
| 				if r0 == utf8.RuneError { | ||||
| 					buf.WriteRune(unicode.ToLower(r1)) | ||||
| 					buf.WriteRune(r0) | ||||
| 					break | ||||
| 				} | ||||
| 
 | ||||
| 				if !unicode.IsUpper(r0) { | ||||
| 					if isConnector(r0) { | ||||
| 						r0 = connector | ||||
| 
 | ||||
| 						buf.WriteRune(unicode.ToLower(r1)) | ||||
| 					} else if unicode.IsNumber(r0) { | ||||
| 						// treat a number as an upper case rune
 | ||||
| 						// so that both `http2xx` and `HTTP2XX` can be converted to `http_2xx`.
 | ||||
| 						buf.WriteRune(unicode.ToLower(r1)) | ||||
| 						buf.WriteRune(connector) | ||||
| 						buf.WriteRune(r0) | ||||
| 					} else { | ||||
| 						buf.WriteRune(connector) | ||||
| 						buf.WriteRune(unicode.ToLower(r1)) | ||||
| 						buf.WriteRune(r0) | ||||
| 					} | ||||
| 
 | ||||
| 					break | ||||
| 				} | ||||
| 
 | ||||
| 				buf.WriteRune(unicode.ToLower(r1)) | ||||
| 			} | ||||
| 
 | ||||
| 			if len(str) == 0 || r0 == connector { | ||||
| 				buf.WriteRune(unicode.ToLower(r0)) | ||||
| 			} | ||||
| 
 | ||||
| 		case unicode.IsNumber(r0): | ||||
| 			if prev != connector && !unicode.IsNumber(prev) { | ||||
| 				buf.WriteRune(connector) | ||||
| 			} | ||||
| 
 | ||||
| 			buf.WriteRune(r0) | ||||
| 
 | ||||
| 		default: | ||||
| 			if isConnector(r0) { | ||||
| 				r0 = connector | ||||
| 			} | ||||
| 
 | ||||
| 			buf.WriteRune(r0) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| func isConnector(r rune) bool { | ||||
| 	return r == '-' || r == '_' || unicode.IsSpace(r) | ||||
| } | ||||
| 
 | ||||
| // SwapCase will swap characters case from upper to lower or lower to upper.
 | ||||
| func SwapCase(str string) string { | ||||
| 	var r rune | ||||
| 	var size int | ||||
| 
 | ||||
| 	buf := &bytes.Buffer{} | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		switch { | ||||
| 		case unicode.IsUpper(r): | ||||
| 			buf.WriteRune(unicode.ToLower(r)) | ||||
| 
 | ||||
| 		case unicode.IsLower(r): | ||||
| 			buf.WriteRune(unicode.ToUpper(r)) | ||||
| 
 | ||||
| 		default: | ||||
| 			buf.WriteRune(r) | ||||
| 		} | ||||
| 
 | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| // FirstRuneToUpper converts first rune to upper case if necessary.
 | ||||
| func FirstRuneToUpper(str string) string { | ||||
| 	if str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	r, size := utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 	if !unicode.IsLower(r) { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	buf.WriteRune(unicode.ToUpper(r)) | ||||
| 	buf.WriteString(str[size:]) | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| // FirstRuneToLower converts first rune to lower case if necessary.
 | ||||
| func FirstRuneToLower(str string) string { | ||||
| 	if str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	r, size := utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 	if !unicode.IsUpper(r) { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	buf := &bytes.Buffer{} | ||||
| 	buf.WriteRune(unicode.ToLower(r)) | ||||
| 	buf.WriteString(str[size:]) | ||||
| 	return buf.String() | ||||
| } | ||||
| 
 | ||||
| // Shuffle randomizes runes in a string and returns the result.
 | ||||
| // It uses default random source in `math/rand`.
 | ||||
| func Shuffle(str string) string { | ||||
| 	if str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	runes := []rune(str) | ||||
| 	index := 0 | ||||
| 
 | ||||
| 	for i := len(runes) - 1; i > 0; i-- { | ||||
| 		index = rand.Intn(i + 1) | ||||
| 
 | ||||
| 		if i != index { | ||||
| 			runes[i], runes[index] = runes[index], runes[i] | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return string(runes) | ||||
| } | ||||
| 
 | ||||
| // ShuffleSource randomizes runes in a string with given random source.
 | ||||
| func ShuffleSource(str string, src rand.Source) string { | ||||
| 	if str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	runes := []rune(str) | ||||
| 	index := 0 | ||||
| 	r := rand.New(src) | ||||
| 
 | ||||
| 	for i := len(runes) - 1; i > 0; i-- { | ||||
| 		index = r.Intn(i + 1) | ||||
| 
 | ||||
| 		if i != index { | ||||
| 			runes[i], runes[index] = runes[index], runes[i] | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return string(runes) | ||||
| } | ||||
| 
 | ||||
| // Successor returns the successor to string.
 | ||||
| //
 | ||||
| // If there is one alphanumeric rune is found in string, increase the rune by 1.
 | ||||
| // If increment generates a "carry", the rune to the left of it is incremented.
 | ||||
| // This process repeats until there is no carry, adding an additional rune if necessary.
 | ||||
| //
 | ||||
| // If there is no alphanumeric rune, the rightmost rune will be increased by 1
 | ||||
| // regardless whether the result is a valid rune or not.
 | ||||
| //
 | ||||
| // Only following characters are alphanumeric.
 | ||||
| //     * a - z
 | ||||
| //     * A - Z
 | ||||
| //     * 0 - 9
 | ||||
| //
 | ||||
| // Samples (borrowed from ruby's String#succ document):
 | ||||
| //     "abcd"      => "abce"
 | ||||
| //     "THX1138"   => "THX1139"
 | ||||
| //     "<<koala>>" => "<<koalb>>"
 | ||||
| //     "1999zzz"   => "2000aaa"
 | ||||
| //     "ZZZ9999"   => "AAAA0000"
 | ||||
| //     "***"       => "**+"
 | ||||
| func Successor(str string) string { | ||||
| 	if str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	var r rune | ||||
| 	var i int | ||||
| 	carry := ' ' | ||||
| 	runes := []rune(str) | ||||
| 	l := len(runes) | ||||
| 	lastAlphanumeric := l | ||||
| 
 | ||||
| 	for i = l - 1; i >= 0; i-- { | ||||
| 		r = runes[i] | ||||
| 
 | ||||
| 		if ('a' <= r && r <= 'y') || | ||||
| 			('A' <= r && r <= 'Y') || | ||||
| 			('0' <= r && r <= '8') { | ||||
| 			runes[i]++ | ||||
| 			carry = ' ' | ||||
| 			lastAlphanumeric = i | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		switch r { | ||||
| 		case 'z': | ||||
| 			runes[i] = 'a' | ||||
| 			carry = 'a' | ||||
| 			lastAlphanumeric = i | ||||
| 
 | ||||
| 		case 'Z': | ||||
| 			runes[i] = 'A' | ||||
| 			carry = 'A' | ||||
| 			lastAlphanumeric = i | ||||
| 
 | ||||
| 		case '9': | ||||
| 			runes[i] = '0' | ||||
| 			carry = '0' | ||||
| 			lastAlphanumeric = i | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Needs to add one character for carry.
 | ||||
| 	if i < 0 && carry != ' ' { | ||||
| 		buf := &bytes.Buffer{} | ||||
| 		buf.Grow(l + 4) // Reserve enough space for write.
 | ||||
| 
 | ||||
| 		if lastAlphanumeric != 0 { | ||||
| 			buf.WriteString(str[:lastAlphanumeric]) | ||||
| 		} | ||||
| 
 | ||||
| 		buf.WriteRune(carry) | ||||
| 
 | ||||
| 		for _, r = range runes[lastAlphanumeric:] { | ||||
| 			buf.WriteRune(r) | ||||
| 		} | ||||
| 
 | ||||
| 		return buf.String() | ||||
| 	} | ||||
| 
 | ||||
| 	// No alphanumeric character. Simply increase last rune's value.
 | ||||
| 	if lastAlphanumeric == l { | ||||
| 		runes[l-1]++ | ||||
| 	} | ||||
| 
 | ||||
| 	return string(runes) | ||||
| } | ||||
|  | @ -0,0 +1,120 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"unicode" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
| 
 | ||||
| // Len returns str's utf8 rune length.
 | ||||
| func Len(str string) int { | ||||
| 	return utf8.RuneCountInString(str) | ||||
| } | ||||
| 
 | ||||
| // WordCount returns number of words in a string.
 | ||||
| //
 | ||||
| // Word is defined as a locale dependent string containing alphabetic characters,
 | ||||
| // which may also contain but not start with `'` and `-` characters.
 | ||||
| func WordCount(str string) int { | ||||
| 	var r rune | ||||
| 	var size, n int | ||||
| 
 | ||||
| 	inWord := false | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		switch { | ||||
| 		case isAlphabet(r): | ||||
| 			if !inWord { | ||||
| 				inWord = true | ||||
| 				n++ | ||||
| 			} | ||||
| 
 | ||||
| 		case inWord && (r == '\'' || r == '-'): | ||||
| 			// Still in word.
 | ||||
| 
 | ||||
| 		default: | ||||
| 			inWord = false | ||||
| 		} | ||||
| 
 | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	return n | ||||
| } | ||||
| 
 | ||||
| const minCJKCharacter = '\u3400' | ||||
| 
 | ||||
| // Checks r is a letter but not CJK character.
 | ||||
| func isAlphabet(r rune) bool { | ||||
| 	if !unicode.IsLetter(r) { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	switch { | ||||
| 	// Quick check for non-CJK character.
 | ||||
| 	case r < minCJKCharacter: | ||||
| 		return true | ||||
| 
 | ||||
| 	// Common CJK characters.
 | ||||
| 	case r >= '\u4E00' && r <= '\u9FCC': | ||||
| 		return false | ||||
| 
 | ||||
| 	// Rare CJK characters.
 | ||||
| 	case r >= '\u3400' && r <= '\u4D85': | ||||
| 		return false | ||||
| 
 | ||||
| 	// Rare and historic CJK characters.
 | ||||
| 	case r >= '\U00020000' && r <= '\U0002B81D': | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // Width returns string width in monotype font.
 | ||||
| // Multi-byte characters are usually twice the width of single byte characters.
 | ||||
| //
 | ||||
| // Algorithm comes from `mb_strwidth` in PHP.
 | ||||
| // http://php.net/manual/en/function.mb-strwidth.php
 | ||||
| func Width(str string) int { | ||||
| 	var r rune | ||||
| 	var size, n int | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 		n += RuneWidth(r) | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	return n | ||||
| } | ||||
| 
 | ||||
| // RuneWidth returns character width in monotype font.
 | ||||
| // Multi-byte characters are usually twice the width of single byte characters.
 | ||||
| //
 | ||||
| // Algorithm comes from `mb_strwidth` in PHP.
 | ||||
| // http://php.net/manual/en/function.mb-strwidth.php
 | ||||
| func RuneWidth(r rune) int { | ||||
| 	switch { | ||||
| 	case r == utf8.RuneError || r < '\x20': | ||||
| 		return 0 | ||||
| 
 | ||||
| 	case '\x20' <= r && r < '\u2000': | ||||
| 		return 1 | ||||
| 
 | ||||
| 	case '\u2000' <= r && r < '\uFF61': | ||||
| 		return 2 | ||||
| 
 | ||||
| 	case '\uFF61' <= r && r < '\uFFA0': | ||||
| 		return 1 | ||||
| 
 | ||||
| 	case '\uFFA0' <= r: | ||||
| 		return 2 | ||||
| 	} | ||||
| 
 | ||||
| 	return 0 | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| // Package xstrings is to provide string algorithms which are useful but not included in `strings` package.
 | ||||
| // See project home page for details. https://github.com/huandu/xstrings
 | ||||
| //
 | ||||
| // Package xstrings assumes all strings are encoded in utf8.
 | ||||
| package xstrings | ||||
|  | @ -0,0 +1,170 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
| 
 | ||||
| // ExpandTabs can expand tabs ('\t') rune in str to one or more spaces dpending on
 | ||||
| // current column and tabSize.
 | ||||
| // The column number is reset to zero after each newline ('\n') occurring in the str.
 | ||||
| //
 | ||||
| // ExpandTabs uses RuneWidth to decide rune's width.
 | ||||
| // For example, CJK characters will be treated as two characters.
 | ||||
| //
 | ||||
| // If tabSize <= 0, ExpandTabs panics with error.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     ExpandTabs("a\tbc\tdef\tghij\tk", 4) => "a   bc  def ghij    k"
 | ||||
| //     ExpandTabs("abcdefg\thij\nk\tl", 4)  => "abcdefg hij\nk   l"
 | ||||
| //     ExpandTabs("z中\t文\tw", 4)           => "z中 文  w"
 | ||||
| func ExpandTabs(str string, tabSize int) string { | ||||
| 	if tabSize <= 0 { | ||||
| 		panic("tab size must be positive") | ||||
| 	} | ||||
| 
 | ||||
| 	var r rune | ||||
| 	var i, size, column, expand int | ||||
| 	var output *bytes.Buffer | ||||
| 
 | ||||
| 	orig := str | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		if r == '\t' { | ||||
| 			expand = tabSize - column%tabSize | ||||
| 
 | ||||
| 			if output == nil { | ||||
| 				output = allocBuffer(orig, str) | ||||
| 			} | ||||
| 
 | ||||
| 			for i = 0; i < expand; i++ { | ||||
| 				output.WriteByte(byte(' ')) | ||||
| 			} | ||||
| 
 | ||||
| 			column += expand | ||||
| 		} else { | ||||
| 			if r == '\n' { | ||||
| 				column = 0 | ||||
| 			} else { | ||||
| 				column += RuneWidth(r) | ||||
| 			} | ||||
| 
 | ||||
| 			if output != nil { | ||||
| 				output.WriteRune(r) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if output == nil { | ||||
| 		return orig | ||||
| 	} | ||||
| 
 | ||||
| 	return output.String() | ||||
| } | ||||
| 
 | ||||
| // LeftJustify returns a string with pad string at right side if str's rune length is smaller than length.
 | ||||
| // If str's rune length is larger than length, str itself will be returned.
 | ||||
| //
 | ||||
| // If pad is an empty string, str will be returned.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     LeftJustify("hello", 4, " ")    => "hello"
 | ||||
| //     LeftJustify("hello", 10, " ")   => "hello     "
 | ||||
| //     LeftJustify("hello", 10, "123") => "hello12312"
 | ||||
| func LeftJustify(str string, length int, pad string) string { | ||||
| 	l := Len(str) | ||||
| 
 | ||||
| 	if l >= length || pad == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	remains := length - l | ||||
| 	padLen := Len(pad) | ||||
| 
 | ||||
| 	output := &bytes.Buffer{} | ||||
| 	output.Grow(len(str) + (remains/padLen+1)*len(pad)) | ||||
| 	output.WriteString(str) | ||||
| 	writePadString(output, pad, padLen, remains) | ||||
| 	return output.String() | ||||
| } | ||||
| 
 | ||||
| // RightJustify returns a string with pad string at left side if str's rune length is smaller than length.
 | ||||
| // If str's rune length is larger than length, str itself will be returned.
 | ||||
| //
 | ||||
| // If pad is an empty string, str will be returned.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     RightJustify("hello", 4, " ")    => "hello"
 | ||||
| //     RightJustify("hello", 10, " ")   => "     hello"
 | ||||
| //     RightJustify("hello", 10, "123") => "12312hello"
 | ||||
| func RightJustify(str string, length int, pad string) string { | ||||
| 	l := Len(str) | ||||
| 
 | ||||
| 	if l >= length || pad == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	remains := length - l | ||||
| 	padLen := Len(pad) | ||||
| 
 | ||||
| 	output := &bytes.Buffer{} | ||||
| 	output.Grow(len(str) + (remains/padLen+1)*len(pad)) | ||||
| 	writePadString(output, pad, padLen, remains) | ||||
| 	output.WriteString(str) | ||||
| 	return output.String() | ||||
| } | ||||
| 
 | ||||
| // Center returns a string with pad string at both side if str's rune length is smaller than length.
 | ||||
| // If str's rune length is larger than length, str itself will be returned.
 | ||||
| //
 | ||||
| // If pad is an empty string, str will be returned.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     Center("hello", 4, " ")    => "hello"
 | ||||
| //     Center("hello", 10, " ")   => "  hello   "
 | ||||
| //     Center("hello", 10, "123") => "12hello123"
 | ||||
| func Center(str string, length int, pad string) string { | ||||
| 	l := Len(str) | ||||
| 
 | ||||
| 	if l >= length || pad == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	remains := length - l | ||||
| 	padLen := Len(pad) | ||||
| 
 | ||||
| 	output := &bytes.Buffer{} | ||||
| 	output.Grow(len(str) + (remains/padLen+1)*len(pad)) | ||||
| 	writePadString(output, pad, padLen, remains/2) | ||||
| 	output.WriteString(str) | ||||
| 	writePadString(output, pad, padLen, (remains+1)/2) | ||||
| 	return output.String() | ||||
| } | ||||
| 
 | ||||
| func writePadString(output *bytes.Buffer, pad string, padLen, remains int) { | ||||
| 	var r rune | ||||
| 	var size int | ||||
| 
 | ||||
| 	repeats := remains / padLen | ||||
| 
 | ||||
| 	for i := 0; i < repeats; i++ { | ||||
| 		output.WriteString(pad) | ||||
| 	} | ||||
| 
 | ||||
| 	remains = remains % padLen | ||||
| 
 | ||||
| 	if remains != 0 { | ||||
| 		for i := 0; i < remains; i++ { | ||||
| 			r, size = utf8.DecodeRuneInString(pad) | ||||
| 			output.WriteRune(r) | ||||
| 			pad = pad[size:] | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| module github.com/huandu/xstrings | ||||
| 
 | ||||
| go 1.12 | ||||
|  | @ -0,0 +1,217 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"strings" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
| 
 | ||||
| // Reverse a utf8 encoded string.
 | ||||
| func Reverse(str string) string { | ||||
| 	var size int | ||||
| 
 | ||||
| 	tail := len(str) | ||||
| 	buf := make([]byte, tail) | ||||
| 	s := buf | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		_, size = utf8.DecodeRuneInString(str) | ||||
| 		tail -= size | ||||
| 		s = append(s[:tail], []byte(str[:size])...) | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	return string(buf) | ||||
| } | ||||
| 
 | ||||
| // Slice a string by rune.
 | ||||
| //
 | ||||
| // Start must satisfy 0 <= start <= rune length.
 | ||||
| //
 | ||||
| // End can be positive, zero or negative.
 | ||||
| // If end >= 0, start and end must satisfy start <= end <= rune length.
 | ||||
| // If end < 0, it means slice to the end of string.
 | ||||
| //
 | ||||
| // Otherwise, Slice will panic as out of range.
 | ||||
| func Slice(str string, start, end int) string { | ||||
| 	var size, startPos, endPos int | ||||
| 
 | ||||
| 	origin := str | ||||
| 
 | ||||
| 	if start < 0 || end > len(str) || (end >= 0 && start > end) { | ||||
| 		panic("out of range") | ||||
| 	} | ||||
| 
 | ||||
| 	if end >= 0 { | ||||
| 		end -= start | ||||
| 	} | ||||
| 
 | ||||
| 	for start > 0 && len(str) > 0 { | ||||
| 		_, size = utf8.DecodeRuneInString(str) | ||||
| 		start-- | ||||
| 		startPos += size | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if end < 0 { | ||||
| 		return origin[startPos:] | ||||
| 	} | ||||
| 
 | ||||
| 	endPos = startPos | ||||
| 
 | ||||
| 	for end > 0 && len(str) > 0 { | ||||
| 		_, size = utf8.DecodeRuneInString(str) | ||||
| 		end-- | ||||
| 		endPos += size | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if len(str) == 0 && (start > 0 || end > 0) { | ||||
| 		panic("out of range") | ||||
| 	} | ||||
| 
 | ||||
| 	return origin[startPos:endPos] | ||||
| } | ||||
| 
 | ||||
| // Partition splits a string by sep into three parts.
 | ||||
| // The return value is a slice of strings with head, match and tail.
 | ||||
| //
 | ||||
| // If str contains sep, for example "hello" and "l", Partition returns
 | ||||
| //     "he", "l", "lo"
 | ||||
| //
 | ||||
| // If str doesn't contain sep, for example "hello" and "x", Partition returns
 | ||||
| //     "hello", "", ""
 | ||||
| func Partition(str, sep string) (head, match, tail string) { | ||||
| 	index := strings.Index(str, sep) | ||||
| 
 | ||||
| 	if index == -1 { | ||||
| 		head = str | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	head = str[:index] | ||||
| 	match = str[index : index+len(sep)] | ||||
| 	tail = str[index+len(sep):] | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // LastPartition splits a string by last instance of sep into three parts.
 | ||||
| // The return value is a slice of strings with head, match and tail.
 | ||||
| //
 | ||||
| // If str contains sep, for example "hello" and "l", LastPartition returns
 | ||||
| //     "hel", "l", "o"
 | ||||
| //
 | ||||
| // If str doesn't contain sep, for example "hello" and "x", LastPartition returns
 | ||||
| //     "", "", "hello"
 | ||||
| func LastPartition(str, sep string) (head, match, tail string) { | ||||
| 	index := strings.LastIndex(str, sep) | ||||
| 
 | ||||
| 	if index == -1 { | ||||
| 		tail = str | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	head = str[:index] | ||||
| 	match = str[index : index+len(sep)] | ||||
| 	tail = str[index+len(sep):] | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Insert src into dst at given rune index.
 | ||||
| // Index is counted by runes instead of bytes.
 | ||||
| //
 | ||||
| // If index is out of range of dst, panic with out of range.
 | ||||
| func Insert(dst, src string, index int) string { | ||||
| 	return Slice(dst, 0, index) + src + Slice(dst, index, -1) | ||||
| } | ||||
| 
 | ||||
| // Scrub scrubs invalid utf8 bytes with repl string.
 | ||||
| // Adjacent invalid bytes are replaced only once.
 | ||||
| func Scrub(str, repl string) string { | ||||
| 	var buf *bytes.Buffer | ||||
| 	var r rune | ||||
| 	var size, pos int | ||||
| 	var hasError bool | ||||
| 
 | ||||
| 	origin := str | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		if r == utf8.RuneError { | ||||
| 			if !hasError { | ||||
| 				if buf == nil { | ||||
| 					buf = &bytes.Buffer{} | ||||
| 				} | ||||
| 
 | ||||
| 				buf.WriteString(origin[:pos]) | ||||
| 				hasError = true | ||||
| 			} | ||||
| 		} else if hasError { | ||||
| 			hasError = false | ||||
| 			buf.WriteString(repl) | ||||
| 
 | ||||
| 			origin = origin[pos:] | ||||
| 			pos = 0 | ||||
| 		} | ||||
| 
 | ||||
| 		pos += size | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if buf != nil { | ||||
| 		buf.WriteString(origin) | ||||
| 		return buf.String() | ||||
| 	} | ||||
| 
 | ||||
| 	// No invalid byte.
 | ||||
| 	return origin | ||||
| } | ||||
| 
 | ||||
| // WordSplit splits a string into words. Returns a slice of words.
 | ||||
| // If there is no word in a string, return nil.
 | ||||
| //
 | ||||
| // Word is defined as a locale dependent string containing alphabetic characters,
 | ||||
| // which may also contain but not start with `'` and `-` characters.
 | ||||
| func WordSplit(str string) []string { | ||||
| 	var word string | ||||
| 	var words []string | ||||
| 	var r rune | ||||
| 	var size, pos int | ||||
| 
 | ||||
| 	inWord := false | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		switch { | ||||
| 		case isAlphabet(r): | ||||
| 			if !inWord { | ||||
| 				inWord = true | ||||
| 				word = str | ||||
| 				pos = 0 | ||||
| 			} | ||||
| 
 | ||||
| 		case inWord && (r == '\'' || r == '-'): | ||||
| 			// Still in word.
 | ||||
| 
 | ||||
| 		default: | ||||
| 			if inWord { | ||||
| 				inWord = false | ||||
| 				words = append(words, word[:pos]) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		pos += size | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if inWord { | ||||
| 		words = append(words, word[:pos]) | ||||
| 	} | ||||
| 
 | ||||
| 	return words | ||||
| } | ||||
|  | @ -0,0 +1,547 @@ | |||
| // Copyright 2015 Huan Du. All rights reserved.
 | ||||
| // Licensed under the MIT license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package xstrings | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"unicode" | ||||
| 	"unicode/utf8" | ||||
| ) | ||||
| 
 | ||||
| type runeRangeMap struct { | ||||
| 	FromLo rune // Lower bound of range map.
 | ||||
| 	FromHi rune // An inclusive higher bound of range map.
 | ||||
| 	ToLo   rune | ||||
| 	ToHi   rune | ||||
| } | ||||
| 
 | ||||
| type runeDict struct { | ||||
| 	Dict [unicode.MaxASCII + 1]rune | ||||
| } | ||||
| 
 | ||||
| type runeMap map[rune]rune | ||||
| 
 | ||||
| // Translator can translate string with pre-compiled from and to patterns.
 | ||||
| // If a from/to pattern pair needs to be used more than once, it's recommended
 | ||||
| // to create a Translator and reuse it.
 | ||||
| type Translator struct { | ||||
| 	quickDict  *runeDict       // A quick dictionary to look up rune by index. Only available for latin runes.
 | ||||
| 	runeMap    runeMap         // Rune map for translation.
 | ||||
| 	ranges     []*runeRangeMap // Ranges of runes.
 | ||||
| 	mappedRune rune            // If mappedRune >= 0, all matched runes are translated to the mappedRune.
 | ||||
| 	reverted   bool            // If to pattern is empty, all matched characters will be deleted.
 | ||||
| 	hasPattern bool | ||||
| } | ||||
| 
 | ||||
| // NewTranslator creates new Translator through a from/to pattern pair.
 | ||||
| func NewTranslator(from, to string) *Translator { | ||||
| 	tr := &Translator{} | ||||
| 
 | ||||
| 	if from == "" { | ||||
| 		return tr | ||||
| 	} | ||||
| 
 | ||||
| 	reverted := from[0] == '^' | ||||
| 	deletion := len(to) == 0 | ||||
| 
 | ||||
| 	if reverted { | ||||
| 		from = from[1:] | ||||
| 	} | ||||
| 
 | ||||
| 	var fromStart, fromEnd, fromRangeStep rune | ||||
| 	var toStart, toEnd, toRangeStep rune | ||||
| 	var fromRangeSize, toRangeSize rune | ||||
| 	var singleRunes []rune | ||||
| 
 | ||||
| 	// Update the to rune range.
 | ||||
| 	updateRange := func() { | ||||
| 		// No more rune to read in the to rune pattern.
 | ||||
| 		if toEnd == utf8.RuneError { | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		if toRangeStep == 0 { | ||||
| 			to, toStart, toEnd, toRangeStep = nextRuneRange(to, toEnd) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// Current range is not empty. Consume 1 rune from start.
 | ||||
| 		if toStart != toEnd { | ||||
| 			toStart += toRangeStep | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// No more rune. Repeat the last rune.
 | ||||
| 		if to == "" { | ||||
| 			toEnd = utf8.RuneError | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// Both start and end are used. Read two more runes from the to pattern.
 | ||||
| 		to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError) | ||||
| 	} | ||||
| 
 | ||||
| 	if deletion { | ||||
| 		toStart = utf8.RuneError | ||||
| 		toEnd = utf8.RuneError | ||||
| 	} else { | ||||
| 		// If from pattern is reverted, only the last rune in the to pattern will be used.
 | ||||
| 		if reverted { | ||||
| 			var size int | ||||
| 
 | ||||
| 			for len(to) > 0 { | ||||
| 				toStart, size = utf8.DecodeRuneInString(to) | ||||
| 				to = to[size:] | ||||
| 			} | ||||
| 
 | ||||
| 			toEnd = utf8.RuneError | ||||
| 		} else { | ||||
| 			to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	fromEnd = utf8.RuneError | ||||
| 
 | ||||
| 	for len(from) > 0 { | ||||
| 		from, fromStart, fromEnd, fromRangeStep = nextRuneRange(from, fromEnd) | ||||
| 
 | ||||
| 		// fromStart is a single character. Just map it with a rune in the to pattern.
 | ||||
| 		if fromRangeStep == 0 { | ||||
| 			singleRunes = tr.addRune(fromStart, toStart, singleRunes) | ||||
| 			updateRange() | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		for toEnd != utf8.RuneError && fromStart != fromEnd { | ||||
| 			// If mapped rune is a single character instead of a range, simply shift first
 | ||||
| 			// rune in the range.
 | ||||
| 			if toRangeStep == 0 { | ||||
| 				singleRunes = tr.addRune(fromStart, toStart, singleRunes) | ||||
| 				updateRange() | ||||
| 				fromStart += fromRangeStep | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			fromRangeSize = (fromEnd - fromStart) * fromRangeStep | ||||
| 			toRangeSize = (toEnd - toStart) * toRangeStep | ||||
| 
 | ||||
| 			// Not enough runes in the to pattern. Need to read more.
 | ||||
| 			if fromRangeSize > toRangeSize { | ||||
| 				fromStart, toStart = tr.addRuneRange(fromStart, fromStart+toRangeSize*fromRangeStep, toStart, toEnd, singleRunes) | ||||
| 				fromStart += fromRangeStep | ||||
| 				updateRange() | ||||
| 
 | ||||
| 				// Edge case: If fromRangeSize == toRangeSize + 1, the last fromStart value needs be considered
 | ||||
| 				// as a single rune.
 | ||||
| 				if fromStart == fromEnd { | ||||
| 					singleRunes = tr.addRune(fromStart, toStart, singleRunes) | ||||
| 					updateRange() | ||||
| 				} | ||||
| 
 | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			fromStart, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart+fromRangeSize*toRangeStep, singleRunes) | ||||
| 			updateRange() | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		if fromStart == fromEnd { | ||||
| 			fromEnd = utf8.RuneError | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		fromStart, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart, singleRunes) | ||||
| 		fromEnd = utf8.RuneError | ||||
| 	} | ||||
| 
 | ||||
| 	if fromEnd != utf8.RuneError { | ||||
| 		singleRunes = tr.addRune(fromEnd, toStart, singleRunes) | ||||
| 	} | ||||
| 
 | ||||
| 	tr.reverted = reverted | ||||
| 	tr.mappedRune = -1 | ||||
| 	tr.hasPattern = true | ||||
| 
 | ||||
| 	// Translate RuneError only if in deletion or reverted mode.
 | ||||
| 	if deletion || reverted { | ||||
| 		tr.mappedRune = toStart | ||||
| 	} | ||||
| 
 | ||||
| 	return tr | ||||
| } | ||||
| 
 | ||||
| func (tr *Translator) addRune(from, to rune, singleRunes []rune) []rune { | ||||
| 	if from <= unicode.MaxASCII { | ||||
| 		if tr.quickDict == nil { | ||||
| 			tr.quickDict = &runeDict{} | ||||
| 		} | ||||
| 
 | ||||
| 		tr.quickDict.Dict[from] = to | ||||
| 	} else { | ||||
| 		if tr.runeMap == nil { | ||||
| 			tr.runeMap = make(runeMap) | ||||
| 		} | ||||
| 
 | ||||
| 		tr.runeMap[from] = to | ||||
| 	} | ||||
| 
 | ||||
| 	singleRunes = append(singleRunes, from) | ||||
| 	return singleRunes | ||||
| } | ||||
| 
 | ||||
| func (tr *Translator) addRuneRange(fromLo, fromHi, toLo, toHi rune, singleRunes []rune) (rune, rune) { | ||||
| 	var r rune | ||||
| 	var rrm *runeRangeMap | ||||
| 
 | ||||
| 	if fromLo < fromHi { | ||||
| 		rrm = &runeRangeMap{ | ||||
| 			FromLo: fromLo, | ||||
| 			FromHi: fromHi, | ||||
| 			ToLo:   toLo, | ||||
| 			ToHi:   toHi, | ||||
| 		} | ||||
| 	} else { | ||||
| 		rrm = &runeRangeMap{ | ||||
| 			FromLo: fromHi, | ||||
| 			FromHi: fromLo, | ||||
| 			ToLo:   toHi, | ||||
| 			ToHi:   toLo, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// If there is any single rune conflicts with this rune range, clear single rune record.
 | ||||
| 	for _, r = range singleRunes { | ||||
| 		if rrm.FromLo <= r && r <= rrm.FromHi { | ||||
| 			if r <= unicode.MaxASCII { | ||||
| 				tr.quickDict.Dict[r] = 0 | ||||
| 			} else { | ||||
| 				delete(tr.runeMap, r) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	tr.ranges = append(tr.ranges, rrm) | ||||
| 	return fromHi, toHi | ||||
| } | ||||
| 
 | ||||
| func nextRuneRange(str string, last rune) (remaining string, start, end rune, rangeStep rune) { | ||||
| 	var r rune | ||||
| 	var size int | ||||
| 
 | ||||
| 	remaining = str | ||||
| 	escaping := false | ||||
| 	isRange := false | ||||
| 
 | ||||
| 	for len(remaining) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(remaining) | ||||
| 		remaining = remaining[size:] | ||||
| 
 | ||||
| 		// Parse special characters.
 | ||||
| 		if !escaping { | ||||
| 			if r == '\\' { | ||||
| 				escaping = true | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			if r == '-' { | ||||
| 				// Ignore slash at beginning of string.
 | ||||
| 				if last == utf8.RuneError { | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				start = last | ||||
| 				isRange = true | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		escaping = false | ||||
| 
 | ||||
| 		if last != utf8.RuneError { | ||||
| 			// This is a range which start and end are the same.
 | ||||
| 			// Considier it as a normal character.
 | ||||
| 			if isRange && last == r { | ||||
| 				isRange = false | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			start = last | ||||
| 			end = r | ||||
| 
 | ||||
| 			if isRange { | ||||
| 				if start < end { | ||||
| 					rangeStep = 1 | ||||
| 				} else { | ||||
| 					rangeStep = -1 | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		last = r | ||||
| 	} | ||||
| 
 | ||||
| 	start = last | ||||
| 	end = utf8.RuneError | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Translate str with a from/to pattern pair.
 | ||||
| //
 | ||||
| // See comment in Translate function for usage and samples.
 | ||||
| func (tr *Translator) Translate(str string) string { | ||||
| 	if !tr.hasPattern || str == "" { | ||||
| 		return str | ||||
| 	} | ||||
| 
 | ||||
| 	var r rune | ||||
| 	var size int | ||||
| 	var needTr bool | ||||
| 
 | ||||
| 	orig := str | ||||
| 
 | ||||
| 	var output *bytes.Buffer | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 		r, needTr = tr.TranslateRune(r) | ||||
| 
 | ||||
| 		if needTr && output == nil { | ||||
| 			output = allocBuffer(orig, str) | ||||
| 		} | ||||
| 
 | ||||
| 		if r != utf8.RuneError && output != nil { | ||||
| 			output.WriteRune(r) | ||||
| 		} | ||||
| 
 | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	// No character is translated.
 | ||||
| 	if output == nil { | ||||
| 		return orig | ||||
| 	} | ||||
| 
 | ||||
| 	return output.String() | ||||
| } | ||||
| 
 | ||||
| // TranslateRune return translated rune and true if r matches the from pattern.
 | ||||
| // If r doesn't match the pattern, original r is returned and translated is false.
 | ||||
| func (tr *Translator) TranslateRune(r rune) (result rune, translated bool) { | ||||
| 	switch { | ||||
| 	case tr.quickDict != nil: | ||||
| 		if r <= unicode.MaxASCII { | ||||
| 			result = tr.quickDict.Dict[r] | ||||
| 
 | ||||
| 			if result != 0 { | ||||
| 				translated = true | ||||
| 
 | ||||
| 				if tr.mappedRune >= 0 { | ||||
| 					result = tr.mappedRune | ||||
| 				} | ||||
| 
 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		fallthrough | ||||
| 
 | ||||
| 	case tr.runeMap != nil: | ||||
| 		var ok bool | ||||
| 
 | ||||
| 		if result, ok = tr.runeMap[r]; ok { | ||||
| 			translated = true | ||||
| 
 | ||||
| 			if tr.mappedRune >= 0 { | ||||
| 				result = tr.mappedRune | ||||
| 			} | ||||
| 
 | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		fallthrough | ||||
| 
 | ||||
| 	default: | ||||
| 		var rrm *runeRangeMap | ||||
| 		ranges := tr.ranges | ||||
| 
 | ||||
| 		for i := len(ranges) - 1; i >= 0; i-- { | ||||
| 			rrm = ranges[i] | ||||
| 
 | ||||
| 			if rrm.FromLo <= r && r <= rrm.FromHi { | ||||
| 				translated = true | ||||
| 
 | ||||
| 				if tr.mappedRune >= 0 { | ||||
| 					result = tr.mappedRune | ||||
| 					break | ||||
| 				} | ||||
| 
 | ||||
| 				if rrm.ToLo < rrm.ToHi { | ||||
| 					result = rrm.ToLo + r - rrm.FromLo | ||||
| 				} else if rrm.ToLo > rrm.ToHi { | ||||
| 					// ToHi can be smaller than ToLo if range is from higher to lower.
 | ||||
| 					result = rrm.ToLo - r + rrm.FromLo | ||||
| 				} else { | ||||
| 					result = rrm.ToLo | ||||
| 				} | ||||
| 
 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if tr.reverted { | ||||
| 		if !translated { | ||||
| 			result = tr.mappedRune | ||||
| 		} | ||||
| 
 | ||||
| 		translated = !translated | ||||
| 	} | ||||
| 
 | ||||
| 	if !translated { | ||||
| 		result = r | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // HasPattern returns true if Translator has one pattern at least.
 | ||||
| func (tr *Translator) HasPattern() bool { | ||||
| 	return tr.hasPattern | ||||
| } | ||||
| 
 | ||||
| // Translate str with the characters defined in from replaced by characters defined in to.
 | ||||
| //
 | ||||
| // From and to are patterns representing a set of characters. Pattern is defined as following.
 | ||||
| //
 | ||||
| //     * Special characters
 | ||||
| //       * '-' means a range of runes, e.g.
 | ||||
| //         * "a-z" means all characters from 'a' to 'z' inclusive;
 | ||||
| //         * "z-a" means all characters from 'z' to 'a' inclusive.
 | ||||
| //       * '^' as first character means a set of all runes excepted listed, e.g.
 | ||||
| //         * "^a-z" means all characters except 'a' to 'z' inclusive.
 | ||||
| //       * '\' escapes special characters.
 | ||||
| //     * Normal character represents itself, e.g. "abc" is a set including 'a', 'b' and 'c'.
 | ||||
| //
 | ||||
| // Translate will try to find a 1:1 mapping from from to to.
 | ||||
| // If to is smaller than from, last rune in to will be used to map "out of range" characters in from.
 | ||||
| //
 | ||||
| // Note that '^' only works in the from pattern. It will be considered as a normal character in the to pattern.
 | ||||
| //
 | ||||
| // If the to pattern is an empty string, Translate works exactly the same as Delete.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     Translate("hello", "aeiou", "12345")    => "h2ll4"
 | ||||
| //     Translate("hello", "a-z", "A-Z")        => "HELLO"
 | ||||
| //     Translate("hello", "z-a", "a-z")        => "svool"
 | ||||
| //     Translate("hello", "aeiou", "*")        => "h*ll*"
 | ||||
| //     Translate("hello", "^l", "*")           => "**ll*"
 | ||||
| //     Translate("hello ^ world", `\^lo`, "*") => "he*** * w*r*d"
 | ||||
| func Translate(str, from, to string) string { | ||||
| 	tr := NewTranslator(from, to) | ||||
| 	return tr.Translate(str) | ||||
| } | ||||
| 
 | ||||
| // Delete runes in str matching the pattern.
 | ||||
| // Pattern is defined in Translate function.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     Delete("hello", "aeiou") => "hll"
 | ||||
| //     Delete("hello", "a-k")   => "llo"
 | ||||
| //     Delete("hello", "^a-k")  => "he"
 | ||||
| func Delete(str, pattern string) string { | ||||
| 	tr := NewTranslator(pattern, "") | ||||
| 	return tr.Translate(str) | ||||
| } | ||||
| 
 | ||||
| // Count how many runes in str match the pattern.
 | ||||
| // Pattern is defined in Translate function.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     Count("hello", "aeiou") => 3
 | ||||
| //     Count("hello", "a-k")   => 3
 | ||||
| //     Count("hello", "^a-k")  => 2
 | ||||
| func Count(str, pattern string) int { | ||||
| 	if pattern == "" || str == "" { | ||||
| 		return 0 | ||||
| 	} | ||||
| 
 | ||||
| 	var r rune | ||||
| 	var size int | ||||
| 	var matched bool | ||||
| 
 | ||||
| 	tr := NewTranslator(pattern, "") | ||||
| 	cnt := 0 | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 		str = str[size:] | ||||
| 
 | ||||
| 		if _, matched = tr.TranslateRune(r); matched { | ||||
| 			cnt++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return cnt | ||||
| } | ||||
| 
 | ||||
| // Squeeze deletes adjacent repeated runes in str.
 | ||||
| // If pattern is not empty, only runes matching the pattern will be squeezed.
 | ||||
| //
 | ||||
| // Samples:
 | ||||
| //     Squeeze("hello", "")             => "helo"
 | ||||
| //     Squeeze("hello", "m-z")          => "hello"
 | ||||
| //     Squeeze("hello   world", " ")    => "hello world"
 | ||||
| func Squeeze(str, pattern string) string { | ||||
| 	var last, r rune | ||||
| 	var size int | ||||
| 	var skipSqueeze, matched bool | ||||
| 	var tr *Translator | ||||
| 	var output *bytes.Buffer | ||||
| 
 | ||||
| 	orig := str | ||||
| 	last = -1 | ||||
| 
 | ||||
| 	if len(pattern) > 0 { | ||||
| 		tr = NewTranslator(pattern, "") | ||||
| 	} | ||||
| 
 | ||||
| 	for len(str) > 0 { | ||||
| 		r, size = utf8.DecodeRuneInString(str) | ||||
| 
 | ||||
| 		// Need to squeeze the str.
 | ||||
| 		if last == r && !skipSqueeze { | ||||
| 			if tr != nil { | ||||
| 				if _, matched = tr.TranslateRune(r); !matched { | ||||
| 					skipSqueeze = true | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if output == nil { | ||||
| 				output = allocBuffer(orig, str) | ||||
| 			} | ||||
| 
 | ||||
| 			if skipSqueeze { | ||||
| 				output.WriteRune(r) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if output != nil { | ||||
| 				output.WriteRune(r) | ||||
| 			} | ||||
| 
 | ||||
| 			last = r | ||||
| 			skipSqueeze = false | ||||
| 		} | ||||
| 
 | ||||
| 		str = str[size:] | ||||
| 	} | ||||
| 
 | ||||
| 	if output == nil { | ||||
| 		return orig | ||||
| 	} | ||||
| 
 | ||||
| 	return output.String() | ||||
| } | ||||
|  | @ -237,6 +237,8 @@ github.com/hashicorp/hcl/hcl/token | |||
| github.com/hashicorp/hcl/json/parser | ||||
| github.com/hashicorp/hcl/json/scanner | ||||
| github.com/hashicorp/hcl/json/token | ||||
| # github.com/huandu/xstrings v1.3.0 | ||||
| github.com/huandu/xstrings | ||||
| # github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c | ||||
| github.com/issue9/identicon | ||||
| # github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue