* Super expansion * Explain which features are in 1.11 vs 1.12 * Move imports Signed-off-by: jolheiser <john.olheiser@gmail.com>tags/v1.10.5
--- | --- | ||||
date: "2019-11-28:00:00+02:00" | 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 | toc: true | ||||
draft: false | draft: false | ||||
menu: | menu: | ||||
sidebar: | 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. | 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. | 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. | ||||
In any file matched by the above globs, certain variables will be expanded. | 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}` | 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 | | |||||
github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 | github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 | ||||
github.com/google/go-github/v24 v24.0.1 | github.com/google/go-github/v24 v24.0.1 | ||||
github.com/gorilla/context v1.1.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/assert v1.3.2 // indirect | ||||
github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c | github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c | ||||
github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d |
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | 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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | 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/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 h1:IaTa37u4m1fUuTH9K9ldO5IONKVDXjLiUO1T9vj0OF0= | ||||
github.com/issue9/assert v1.3.2/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio= | github.com/issue9/assert v1.3.2/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio= |
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/git" | "code.gitea.io/gitea/modules/git" | ||||
"code.gitea.io/gitea/modules/log" | "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 { | func generateExpansion(src string, templateRepo, generateRepo *models.Repository) string { | ||||
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 { | 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 | |||||
if expansion, ok := expansionMap[key]; ok { | |||||
return expansion | |||||
} | } | ||||
return key | |||||
}) | }) | ||||
} | } | ||||
return fmt.Errorf("checkGiteaTemplate: %v", err) | return fmt.Errorf("checkGiteaTemplate: %v", err) | ||||
} | } | ||||
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 gt != nil { | |||||
if err := os.Remove(gt.Path); err != nil { | |||||
return fmt.Errorf("remove .giteatemplate: %v", err) | |||||
} | |||||
if info.IsDir() { | |||||
return nil | |||||
} | |||||
// 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 | |||||
} | |||||
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 info.IsDir() { | |||||
return nil | |||||
} | |||||
if err := ioutil.WriteFile(path, | |||||
[]byte(generateExpansion(string(content), templateRepo, generateRepo)), | |||||
0644); err != nil { | |||||
return err | |||||
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 | |||||
} | } | ||||
break | |||||
} | } | ||||
return nil | |||||
}); err != nil { | |||||
return err | |||||
} | } | ||||
return nil | |||||
}); err != nil { | |||||
return err | |||||
} | } | ||||
} | } | ||||
# 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 |
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' |
# 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. |
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. | |||||
# xstrings # | |||||
[![Build Status](https://travis-ci.org/huandu/xstrings.svg?branch=master)](https://travis-ci.org/huandu/xstrings) | |||||
[![GoDoc](https://godoc.org/github.com/huandu/xstrings?status.svg)](https://godoc.org/github.com/huandu/xstrings) | |||||
[![Go Report](https://goreportcard.com/badge/github.com/huandu/xstrings)](https://goreportcard.com/report/github.com/huandu/xstrings) | |||||
[![Coverage Status](https://coveralls.io/repos/github/huandu/xstrings/badge.svg?branch=master)](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. |
// 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 | |||||
} |
// 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) | |||||
} |
// 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 | |||||
} |
// 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 |
// 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:] | |||||
} | |||||
} | |||||
} |
module github.com/huandu/xstrings | |||||
go 1.12 |
// 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 | |||||
} |
// 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() | |||||
} |
github.com/hashicorp/hcl/json/parser | github.com/hashicorp/hcl/json/parser | ||||
github.com/hashicorp/hcl/json/scanner | github.com/hashicorp/hcl/json/scanner | ||||
github.com/hashicorp/hcl/json/token | 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 v0.0.0-20160320065130-d36b54562f4c | ||||
github.com/issue9/identicon | github.com/issue9/identicon | ||||
# github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d | # github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d |