Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
113
tools/inflector/inflector.go
Normal file
113
tools/inflector/inflector.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package inflector
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var columnifyRemoveRegex = regexp.MustCompile(`[^\w\.\*\-\_\@\#]+`)
|
||||
var snakecaseSplitRegex = regexp.MustCompile(`[\W_]+`)
|
||||
|
||||
// UcFirst converts the first character of a string into uppercase.
|
||||
func UcFirst(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
s := []rune(str)
|
||||
|
||||
return string(unicode.ToUpper(s[0])) + string(s[1:])
|
||||
}
|
||||
|
||||
// Columnify strips invalid db identifier characters.
|
||||
func Columnify(str string) string {
|
||||
return columnifyRemoveRegex.ReplaceAllString(str, "")
|
||||
}
|
||||
|
||||
// Sentenize converts and normalizes string into a sentence.
|
||||
func Sentenize(str string) string {
|
||||
str = strings.TrimSpace(str)
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
str = UcFirst(str)
|
||||
|
||||
lastChar := str[len(str)-1:]
|
||||
if lastChar != "." && lastChar != "?" && lastChar != "!" {
|
||||
return str + "."
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Sanitize sanitizes `str` by removing all characters satisfying `removePattern`.
|
||||
// Returns an error if the pattern is not valid regex string.
|
||||
func Sanitize(str string, removePattern string) (string, error) {
|
||||
exp, err := regexp.Compile(removePattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return exp.ReplaceAllString(str, ""), nil
|
||||
}
|
||||
|
||||
// Snakecase removes all non word characters and converts any english text into a snakecase.
|
||||
// "ABBREVIATIONS" are preserved, eg. "myTestDB" will become "my_test_db".
|
||||
func Snakecase(str string) string {
|
||||
var result strings.Builder
|
||||
|
||||
// split at any non word character and underscore
|
||||
words := snakecaseSplitRegex.Split(str, -1)
|
||||
|
||||
for _, word := range words {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if result.Len() > 0 {
|
||||
result.WriteString("_")
|
||||
}
|
||||
|
||||
for i, c := range word {
|
||||
if unicode.IsUpper(c) && i > 0 &&
|
||||
// is not a following uppercase character
|
||||
!unicode.IsUpper(rune(word[i-1])) {
|
||||
result.WriteString("_")
|
||||
}
|
||||
|
||||
result.WriteRune(c)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// Camelize converts the provided string to its "CamelCased" version
|
||||
// (non alphanumeric characters are removed).
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// inflector.Camelize("send_email") // "SendEmail"
|
||||
func Camelize(str string) string {
|
||||
var result strings.Builder
|
||||
|
||||
var isPrevSpecial bool
|
||||
|
||||
for _, c := range str {
|
||||
if !unicode.IsLetter(c) && !unicode.IsNumber(c) {
|
||||
isPrevSpecial = true
|
||||
continue
|
||||
}
|
||||
|
||||
if isPrevSpecial || result.Len() == 0 {
|
||||
isPrevSpecial = false
|
||||
result.WriteRune(unicode.ToUpper(c))
|
||||
} else {
|
||||
result.WriteRune(c)
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
175
tools/inflector/inflector_test.go
Normal file
175
tools/inflector/inflector_test.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package inflector_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
)
|
||||
|
||||
func TestUcFirst(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{" ", " "},
|
||||
{"Test", "Test"},
|
||||
{"test", "Test"},
|
||||
{"test test2", "Test test2"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result := inflector.UcFirst(s.val)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnify(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
{"123", "123"},
|
||||
{"Test.", "Test."},
|
||||
{" test ", "test"},
|
||||
{"test1.test2", "test1.test2"},
|
||||
{"@test!abc", "@testabc"},
|
||||
{"#test?abc", "#testabc"},
|
||||
{"123test(123)#", "123test123#"},
|
||||
{"test1--test2", "test1--test2"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result := inflector.Columnify(s.val)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSentenize(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
{".", "."},
|
||||
{"?", "?"},
|
||||
{"!", "!"},
|
||||
{"Test", "Test."},
|
||||
{" test ", "Test."},
|
||||
{"hello world", "Hello world."},
|
||||
{"hello world.", "Hello world."},
|
||||
{"hello world!", "Hello world!"},
|
||||
{"hello world?", "Hello world?"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result := inflector.Sentenize(s.val)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitize(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
pattern string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{"", ``, "", false},
|
||||
{" ", ``, " ", false},
|
||||
{" ", ` `, "", false},
|
||||
{"", `[A-Z]`, "", false},
|
||||
{"abcABC", `[A-Z]`, "abc", false},
|
||||
{"abcABC", `[A-Z`, "", true}, // invalid pattern
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result, err := inflector.Sanitize(s.val, s.pattern)
|
||||
hasErr := err != nil
|
||||
|
||||
if s.expectErr != hasErr {
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectErr, hasErr, err)
|
||||
}
|
||||
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnakecase(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
{"!@#$%^", ""},
|
||||
{"...", ""},
|
||||
{"_", ""},
|
||||
{"John Doe", "john_doe"},
|
||||
{"John_Doe", "john_doe"},
|
||||
{".a!b@c#d$e%123. ", "a_b_c_d_e_123"},
|
||||
{"HelloWorld", "hello_world"},
|
||||
{"HelloWorld1HelloWorld2", "hello_world1_hello_world2"},
|
||||
{"TEST", "test"},
|
||||
{"testABR", "test_abr"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result := inflector.Snakecase(s.val)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCamelize(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
{"Test", "Test"},
|
||||
{"test", "Test"},
|
||||
{"testTest2", "TestTest2"},
|
||||
{"TestTest2", "TestTest2"},
|
||||
{"test test2", "TestTest2"},
|
||||
{"test-test2", "TestTest2"},
|
||||
{"test'test2", "TestTest2"},
|
||||
{"test1test2", "Test1test2"},
|
||||
{"1test-test2", "1testTest2"},
|
||||
{"123", "123"},
|
||||
{"123a", "123a"},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
result := inflector.Camelize(s.val)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
89
tools/inflector/singularize.go
Normal file
89
tools/inflector/singularize.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package inflector
|
||||
|
||||
import (
|
||||
"log"
|
||||
"regexp"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
)
|
||||
|
||||
var compiledPatterns = store.New[string, *regexp.Regexp](nil)
|
||||
|
||||
// note: the patterns are extracted from popular Ruby/PHP/Node.js inflector packages
|
||||
var singularRules = []struct {
|
||||
pattern string // lazily compiled
|
||||
replacement string
|
||||
}{
|
||||
{"(?i)([nrlm]ese|deer|fish|sheep|measles|ois|pox|media|ss)$", "${1}"},
|
||||
{"(?i)^(sea[- ]bass)$", "${1}"},
|
||||
{"(?i)(s)tatuses$", "${1}tatus"},
|
||||
{"(?i)(f)eet$", "${1}oot"},
|
||||
{"(?i)(t)eeth$", "${1}ooth"},
|
||||
{"(?i)^(.*)(menu)s$", "${1}${2}"},
|
||||
{"(?i)(quiz)zes$", "${1}"},
|
||||
{"(?i)(matr)ices$", "${1}ix"},
|
||||
{"(?i)(vert|ind)ices$", "${1}ex"},
|
||||
{"(?i)^(ox)en", "${1}"},
|
||||
{"(?i)(alias)es$", "${1}"},
|
||||
{"(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$", "${1}us"},
|
||||
{"(?i)([ftw]ax)es", "${1}"},
|
||||
{"(?i)(cris|ax|test)es$", "${1}is"},
|
||||
{"(?i)(shoe)s$", "${1}"},
|
||||
{"(?i)(o)es$", "${1}"},
|
||||
{"(?i)ouses$", "ouse"},
|
||||
{"(?i)([^a])uses$", "${1}us"},
|
||||
{"(?i)([m|l])ice$", "${1}ouse"},
|
||||
{"(?i)(x|ch|ss|sh)es$", "${1}"},
|
||||
{"(?i)(m)ovies$", "${1}ovie"},
|
||||
{"(?i)(s)eries$", "${1}eries"},
|
||||
{"(?i)([^aeiouy]|qu)ies$", "${1}y"},
|
||||
{"(?i)([lr])ves$", "${1}f"},
|
||||
{"(?i)(tive)s$", "${1}"},
|
||||
{"(?i)(hive)s$", "${1}"},
|
||||
{"(?i)(drive)s$", "${1}"},
|
||||
{"(?i)([^fo])ves$", "${1}fe"},
|
||||
{"(?i)(^analy)ses$", "${1}sis"},
|
||||
{"(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "${1}${2}sis"},
|
||||
{"(?i)([ti])a$", "${1}um"},
|
||||
{"(?i)(p)eople$", "${1}erson"},
|
||||
{"(?i)(m)en$", "${1}an"},
|
||||
{"(?i)(c)hildren$", "${1}hild"},
|
||||
{"(?i)(n)ews$", "${1}ews"},
|
||||
{"(?i)(n)etherlands$", "${1}etherlands"},
|
||||
{"(?i)eaus$", "eau"},
|
||||
{"(?i)(currenc)ies$", "${1}y"},
|
||||
{"(?i)^(.*us)$", "${1}"},
|
||||
{"(?i)s$", ""},
|
||||
}
|
||||
|
||||
// Singularize converts the specified word into its singular version.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// inflector.Singularize("people") // "person"
|
||||
func Singularize(word string) string {
|
||||
if word == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, rule := range singularRules {
|
||||
re := compiledPatterns.GetOrSet(rule.pattern, func() *regexp.Regexp {
|
||||
re, err := regexp.Compile(rule.pattern)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return re
|
||||
})
|
||||
if re == nil {
|
||||
// log only for debug purposes
|
||||
log.Println("[Singularize] failed to retrieve/compile rule pattern " + rule.pattern)
|
||||
continue
|
||||
}
|
||||
|
||||
if re.MatchString(word) {
|
||||
return re.ReplaceAllString(word, rule.replacement)
|
||||
}
|
||||
}
|
||||
|
||||
return word
|
||||
}
|
76
tools/inflector/singularize_test.go
Normal file
76
tools/inflector/singularize_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package inflector_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
)
|
||||
|
||||
func TestSingularize(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
word string
|
||||
expected string
|
||||
}{
|
||||
{"abcnese", "abcnese"},
|
||||
{"deer", "deer"},
|
||||
{"sheep", "sheep"},
|
||||
{"measles", "measles"},
|
||||
{"pox", "pox"},
|
||||
{"media", "media"},
|
||||
{"bliss", "bliss"},
|
||||
{"sea-bass", "sea-bass"},
|
||||
{"Statuses", "Status"},
|
||||
{"Feet", "Foot"},
|
||||
{"Teeth", "Tooth"},
|
||||
{"abcmenus", "abcmenu"},
|
||||
{"Quizzes", "Quiz"},
|
||||
{"Matrices", "Matrix"},
|
||||
{"Vertices", "Vertex"},
|
||||
{"Indices", "Index"},
|
||||
{"Aliases", "Alias"},
|
||||
{"Alumni", "Alumnus"},
|
||||
{"Bacilli", "Bacillus"},
|
||||
{"Cacti", "Cactus"},
|
||||
{"Fungi", "Fungus"},
|
||||
{"Nuclei", "Nucleus"},
|
||||
{"Radii", "Radius"},
|
||||
{"Stimuli", "Stimulus"},
|
||||
{"Syllabi", "Syllabus"},
|
||||
{"Termini", "Terminus"},
|
||||
{"Viri", "Virus"},
|
||||
{"Faxes", "Fax"},
|
||||
{"Crises", "Crisis"},
|
||||
{"Axes", "Axis"},
|
||||
{"Shoes", "Shoe"},
|
||||
{"abcoes", "abco"},
|
||||
{"Houses", "House"},
|
||||
{"Mice", "Mouse"},
|
||||
{"abcxes", "abcx"},
|
||||
{"Movies", "Movie"},
|
||||
{"Series", "Series"},
|
||||
{"abcquies", "abcquy"},
|
||||
{"Relatives", "Relative"},
|
||||
{"Drives", "Drive"},
|
||||
{"aardwolves", "aardwolf"},
|
||||
{"Analyses", "Analysis"},
|
||||
{"Diagnoses", "Diagnosis"},
|
||||
{"People", "Person"},
|
||||
{"Men", "Man"},
|
||||
{"Children", "Child"},
|
||||
{"News", "News"},
|
||||
{"Netherlands", "Netherlands"},
|
||||
{"Tableaus", "Tableau"},
|
||||
{"Currencies", "Currency"},
|
||||
{"abcs", "abc"},
|
||||
{"abc", "abc"},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.word, func(t *testing.T) {
|
||||
result := inflector.Singularize(s.word)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %q, got %q", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue