summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/content/doc/usage/template-repositories.md (renamed from docs/content/doc/features/gitea-directory.md)64
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--modules/repository/generate.go138
-rw-r--r--vendor/github.com/huandu/xstrings/.gitignore24
-rw-r--r--vendor/github.com/huandu/xstrings/.travis.yml7
-rw-r--r--vendor/github.com/huandu/xstrings/CONTRIBUTING.md23
-rw-r--r--vendor/github.com/huandu/xstrings/LICENSE22
-rw-r--r--vendor/github.com/huandu/xstrings/README.md117
-rw-r--r--vendor/github.com/huandu/xstrings/common.go25
-rw-r--r--vendor/github.com/huandu/xstrings/convert.go404
-rw-r--r--vendor/github.com/huandu/xstrings/count.go120
-rw-r--r--vendor/github.com/huandu/xstrings/doc.go8
-rw-r--r--vendor/github.com/huandu/xstrings/format.go170
-rw-r--r--vendor/github.com/huandu/xstrings/go.mod3
-rw-r--r--vendor/github.com/huandu/xstrings/manipulate.go217
-rw-r--r--vendor/github.com/huandu/xstrings/translate.go547
-rw-r--r--vendor/modules.txt2
18 files changed, 1814 insertions, 80 deletions
diff --git a/docs/content/doc/features/gitea-directory.md b/docs/content/doc/usage/template-repositories.md
index e598969bcd..fe3fb86192 100644
--- a/docs/content/doc/features/gitea-directory.md
+++ b/docs/content/doc/usage/template-repositories.md
@@ -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 |
+
diff --git a/go.mod b/go.mod
index 943eb2a792..cb1dca4b5d 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index 6f9548acf4..13ffa77502 100644
--- a/go.sum
+++ b/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=
diff --git a/modules/repository/generate.go b/modules/repository/generate.go
index 96ce25e59f..52502e8eb3 100644
--- a/modules/repository/generate.go
+++ b/modules/repository/generate.go
@@ -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 {
+ 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 {
- 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
})
}
@@ -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)
- }
-
- // 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
}
}
diff --git a/vendor/github.com/huandu/xstrings/.gitignore b/vendor/github.com/huandu/xstrings/.gitignore
new file mode 100644
index 0000000000..daf913b1b3
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/.gitignore
@@ -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
diff --git a/vendor/github.com/huandu/xstrings/.travis.yml b/vendor/github.com/huandu/xstrings/.travis.yml
new file mode 100644
index 0000000000..d6460be411
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/.travis.yml
@@ -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'
diff --git a/vendor/github.com/huandu/xstrings/CONTRIBUTING.md b/vendor/github.com/huandu/xstrings/CONTRIBUTING.md
new file mode 100644
index 0000000000..d7b4b8d584
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/CONTRIBUTING.md
@@ -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.
diff --git a/vendor/github.com/huandu/xstrings/LICENSE b/vendor/github.com/huandu/xstrings/LICENSE
new file mode 100644
index 0000000000..2701772593
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/LICENSE
@@ -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.
+
diff --git a/vendor/github.com/huandu/xstrings/README.md b/vendor/github.com/huandu/xstrings/README.md
new file mode 100644
index 0000000000..292bf2f39e
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/README.md
@@ -0,0 +1,117 @@
+# 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.
diff --git a/vendor/github.com/huandu/xstrings/common.go b/vendor/github.com/huandu/xstrings/common.go
new file mode 100644
index 0000000000..2aff57aab4
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/common.go
@@ -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
+}
diff --git a/vendor/github.com/huandu/xstrings/convert.go b/vendor/github.com/huandu/xstrings/convert.go
new file mode 100644
index 0000000000..3686780d23
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/convert.go
@@ -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)
+}
diff --git a/vendor/github.com/huandu/xstrings/count.go b/vendor/github.com/huandu/xstrings/count.go
new file mode 100644
index 0000000000..f96e38703a
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/count.go
@@ -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
+}
diff --git a/vendor/github.com/huandu/xstrings/doc.go b/vendor/github.com/huandu/xstrings/doc.go
new file mode 100644
index 0000000000..1a6ef069f6
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/doc.go
@@ -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
diff --git a/vendor/github.com/huandu/xstrings/format.go b/vendor/github.com/huandu/xstrings/format.go
new file mode 100644
index 0000000000..2d02df1c04
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/format.go
@@ -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:]
+ }
+ }
+}
diff --git a/vendor/github.com/huandu/xstrings/go.mod b/vendor/github.com/huandu/xstrings/go.mod
new file mode 100644
index 0000000000..3982c204ca
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/go.mod
@@ -0,0 +1,3 @@
+module github.com/huandu/xstrings
+
+go 1.12
diff --git a/vendor/github.com/huandu/xstrings/manipulate.go b/vendor/github.com/huandu/xstrings/manipulate.go
new file mode 100644
index 0000000000..0eefb43ed7
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/manipulate.go
@@ -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
+}
diff --git a/vendor/github.com/huandu/xstrings/translate.go b/vendor/github.com/huandu/xstrings/translate.go
new file mode 100644
index 0000000000..66e23f86d0
--- /dev/null
+++ b/vendor/github.com/huandu/xstrings/translate.go
@@ -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()
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 6b2546886f..5203c24e4a 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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