1
0
Fork 0

Adding upstream version 0.28.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 10:57:38 +02:00
parent 88f1d47ab6
commit e28c88ef14
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
933 changed files with 194711 additions and 0 deletions

View 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()
}

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}