Repository: tableauio/tableau (go)

Issue:
strcase: support custom acronyms for ToSnake, ToCamel and ToLowerCamel
refer [strcase: Custom Acronyms for ToCamel && ToLowerCamel](https://github.com/iancoleman/strcase?tab=readme-ov-file#custom-acronyms-for-tocamel--tolowercamel)

Problem case:
<img width="500" alt="in_k_8_s" src="https://github.com/user-attachments/assets/77377c9b-62e5-4913-9ef5-2982f4e14c72">


Pull request:
strcase: support custom acronyms
- close #145 

Patch:
diff --git a/go.mod b/go.mod
index 2c397f7..a307e23 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.20
 require (
 	github.com/davecgh/go-spew v1.1.1
 	github.com/emirpasic/gods v1.18.1
-	github.com/iancoleman/strcase v0.3.0
 	github.com/jhump/protoreflect v1.16.0
 	github.com/pkg/errors v0.9.1
 	github.com/protocolbuffers/txtpbfmt v0.0.0-20240820135758-21b1d9897dc7
diff --git a/internal/confgen/parser.go b/internal/confgen/parser.go
index 9d47331..57eb28d 100644
--- a/internal/confgen/parser.go
+++ b/internal/confgen/parser.go
@@ -8,12 +8,12 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/iancoleman/strcase"
 	"github.com/tableauio/tableau/format"
 	"github.com/tableauio/tableau/internal/confgen/prop"
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/importer"
 	"github.com/tableauio/tableau/internal/importer/book"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/options"
diff --git a/internal/confgen/util.go b/internal/confgen/util.go
index 78b5305..e948a3a 100644
--- a/internal/confgen/util.go
+++ b/internal/confgen/util.go
@@ -5,10 +5,10 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/iancoleman/strcase"
 	"github.com/tableauio/tableau/format"
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/importer"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
diff --git a/internal/importer/book/cell.go b/internal/importer/book/cell.go
index 8351cbe..01417ee 100644
--- a/internal/importer/book/cell.go
+++ b/internal/importer/book/cell.go
@@ -8,8 +8,8 @@ import (
 	"sync"
 	"unicode"
 
-	"github.com/tableauio/tableau/internal/camelcase"
 	"github.com/tableauio/tableau/internal/excel"
+	"github.com/tableauio/tableau/internal/strcase/camelcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/log"
 	"github.com/tableauio/tableau/xerrors"
diff --git a/internal/importer/xml.go b/internal/importer/xml.go
index 2cca61d..88d1e55 100644
--- a/internal/importer/xml.go
+++ b/internal/importer/xml.go
@@ -485,7 +485,7 @@ func parseXMLAttribute(bnode *book.Node, attrName, attrValue string, isFirstAttr
 						Children: []*book.Node{
 							{
 								Name:  book.KeywordType,
-								Value: fmt.Sprintf("[%s]", desc.ColumnType),
+								Value: attrValue,
 							},
 							{
 								Name:  book.KeywordVariable,
diff --git a/internal/protogen/document_parser.go b/internal/protogen/document_parser.go
index 2e3d5d6..027a858 100644
--- a/internal/protogen/document_parser.go
+++ b/internal/protogen/document_parser.go
@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/iancoleman/strcase"
 	"github.com/pkg/errors"
 	"github.com/tableauio/tableau/internal/importer/book"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"github.com/tableauio/tableau/xerrors"
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 8d76035..57c7f15 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -5,11 +5,11 @@ import (
 	"strings"
 
 	"github.com/emirpasic/gods/sets/treeset"
-	"github.com/iancoleman/strcase"
 	"github.com/pkg/errors"
 	"github.com/rogpeppe/go-internal/lockedfile"
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/printer"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
diff --git a/internal/protogen/parser.go b/internal/protogen/parser.go
index d1af3a4..91f1063 100644
--- a/internal/protogen/parser.go
+++ b/internal/protogen/parser.go
@@ -5,10 +5,10 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/iancoleman/strcase"
 	"github.com/tableauio/tableau/internal/fs"
 	"github.com/tableauio/tableau/internal/importer/book"
 	"github.com/tableauio/tableau/internal/protogen/parseroptions"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
diff --git a/internal/protogen/protogen.go b/internal/protogen/protogen.go
index f2ecd9f..29232b5 100644
--- a/internal/protogen/protogen.go
+++ b/internal/protogen/protogen.go
@@ -15,6 +15,7 @@ import (
 	"github.com/tableauio/tableau/internal/importer"
 	"github.com/tableauio/tableau/internal/importer/book"
 	"github.com/tableauio/tableau/internal/protogen/parseroptions"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/internal/xproto"
 	"github.com/tableauio/tableau/log"
@@ -66,6 +67,10 @@ func NewGeneratorWithOptions(protoPackage, indir, outdir string, opts *options.O
 		book.SetMetasheetName(opts.Proto.Input.MetasheetName)
 	}
 
+	for key, val := range opts.Acronyms {
+		strcase.ConfigureAcronym(key, val)
+	}
+
 	// parse custom imported proto files
 	protofiles, err := xproto.ParseProtos(
 		g.InputOpt.ProtoPaths,
diff --git a/internal/strcase/README.md b/internal/strcase/README.md
new file mode 100644
index 0000000..81c9cd1
--- /dev/null
+++ b/internal/strcase/README.md
@@ -0,0 +1,38 @@
+# strcase
+
+> NOTE: we learned a lot from package [strcase](https://github.com/iancoleman/strcase)
+
+strcase is a go package for converting string case to various cases (e.g. [snake case](https://en.wikipedia.org/wiki/Snake_case) or [camel case](https://en.wikipedia.org/wiki/CamelCase)) to see the full conversion table below.
+
+## Example
+
+```go
+s := "AnyKind of_string"
+```
+
+| Function                                  | Result               |
+| ----------------------------------------- | -------------------- |
+| `ToSnake(s)`                              | `any_kind_of_string` |
+| `ToSnakeWithIgnore(s, '.')`               | `any_kind.of_string` |
+| `ToScreamingSnake(s)`                     | `ANY_KIND_OF_STRING` |
+| `ToKebab(s)`                              | `any-kind-of-string` |
+| `ToScreamingKebab(s)`                     | `ANY-KIND-OF-STRING` |
+| `ToDelimited(s, '.')`                     | `any.kind.of.string` |
+| `ToScreamingDelimited(s, '.', '', true)`  | `ANY.KIND.OF.STRING` |
+| `ToScreamingDelimited(s, '.', ' ', true)` | `ANY.KIND OF.STRING` |
+| `ToCamel(s)`                              | `AnyKindOfString`    |
+| `ToLowerCamel(s)`                         | `anyKindOfString`    |
+
+## Custom Acronyms
+
+Sometimes, text may contain specific acronyms which need to be handled in a certain way.
+
+To configure your custom acronyms globally you can use the following before running any conversion.
+
+```go
+// For "WebAPIV3Spec":
+//  - ToCamel: WebApiv3Spec
+//  - ToLowerCamel: webApiv3Spec
+//  - ToSnake: web_apiv3_spec
+strcase.ConfigureAcronym("APIV3", "apiv3")
+```
diff --git a/internal/strcase/acronym.go b/internal/strcase/acronym.go
new file mode 100644
index 0000000..59e21df
--- /dev/null
+++ b/internal/strcase/acronym.go
@@ -0,0 +1,19 @@
+package strcase
+
+import (
+	"sync"
+)
+
+var uppercaseAcronym = sync.Map{}
+
+// ConfigureAcronym allows you to add additional words which will be considered
+// as acronyms.
+//
+// Examples:
+//
+//	ConfigureAcronym("API", "api")
+//	ConfigureAcronym("K8s", "k8s")
+//	ConfigureAcronym("3D", "3d")
+func ConfigureAcronym(key, val string) {
+	uppercaseAcronym.Store(key, val)
+}
diff --git a/internal/strcase/camel.go b/internal/strcase/camel.go
new file mode 100644
index 0000000..e1db6c8
--- /dev/null
+++ b/internal/strcase/camel.go
@@ -0,0 +1,82 @@
+package strcase
+
+import (
+	"strings"
+)
+
+// Converts a string to camelCase/CamelCase. The first word starting with
+// initial uppercase or lowercase letter.
+func toCamelInitCase(s string, initUppercase bool) string {
+	s = strings.TrimSpace(s)
+	if s == "" {
+		return s
+	}
+
+	n := strings.Builder{}
+	n.Grow(len(s))
+	upperNext := initUppercase
+	prevIsUpper := false
+
+	bytes := []byte(s)
+	for i := 0; i < len(bytes); i++ {
+		// treat acronyms as words, e.g.: for JSONData -> JSON is a whole word
+		acronymFound := false
+		uppercaseAcronym.Range(func(key, value any) bool {
+			remain := string(bytes[i:])
+			if strings.HasPrefix(remain, key.(string)) {
+				val := value.(string)
+				if i > 0 || upperNext {
+					val = upperFirst(val)
+				} else {
+					val = lowerFirst(val)
+				}
+				n.WriteString(val)
+				i += len(key.(string)) - 1
+				upperNext = true
+				acronymFound = true
+				return false
+			}
+			return true
+		})
+		if acronymFound {
+			continue
+		}
+
+		v := bytes[i]
+		vIsUpper := isUpper(v)
+		vIsLower := isLower(v)
+		if upperNext {
+			if vIsLower {
+				v = toUpper(v)
+			}
+		} else if i == 0 {
+			if vIsUpper {
+				v = toLower(v)
+			}
+		} else if prevIsUpper && vIsUpper {
+			v = toLower(v)
+		}
+		prevIsUpper = vIsUpper
+
+		if vIsUpper || vIsLower {
+			n.WriteByte(v)
+			upperNext = false
+		} else if isDigit(v) {
+			n.WriteByte(v)
+			upperNext = true
+		} else {
+			upperNext = isSeparator(v)
+		}
+	}
+	return n.String()
+}
+
+// ToCamel converts a string to CamelCase
+func ToCamel(s string) string {
+	return toCamelInitCase(s, true)
+}
+
+// ToLowerCamel converts a string to lowerCamelCase
+func ToLowerCamel(s string) string {
+	return toCamelInitCase(s, false)
+}
diff --git a/internal/strcase/doc.go b/internal/strcase/doc.go
new file mode 100644
index 0000000..605f8e9
--- /dev/null
+++ b/internal/strcase/doc.go
@@ -0,0 +1,13 @@
+// Package strcase converts strings to various cases. See the conversion table below:
+//
+//	| Function                        | Result             |
+//	|---------------------------------|--------------------|
+//	| ToSnake(s)                      | any_kind_of_string |
+//	| ToScreamingSnake(s)             | ANY_KIND_OF_STRING |
+//	| ToKebab(s)                      | any-kind-of-string |
+//	| ToScreamingKebab(s)             | ANY-KIND-OF-STRING |
+//	| ToDelimited(s, '.')             | any.kind.of.string |
+//	| ToScreamingDelimited(s, '.')    | ANY.KIND.OF.STRING |
+//	| ToCamel(s)                      | AnyKindOfString    |
+//	| ToLowerCamel(s)                 | anyKindOfString    |
+package strcase
diff --git a/internal/strcase/snake.go b/internal/strcase/snake.go
new file mode 100644
index 0000000..828725d
--- /dev/null
+++ b/internal/strcase/snake.go
@@ -0,0 +1,115 @@
+package strcase
+
+import (
+	"strings"
+)
+
+// ToSnake converts a string to snake_case
+func ToSnake(s string) string {
+	return ToDelimited(s, '_')
+}
+
+func ToSnakeWithIgnore(s string, ignore string) string {
+	return ToScreamingDelimited(s, '_', ignore, false)
+}
+
+// ToScreamingSnake converts a string to SCREAMING_SNAKE_CASE
+func ToScreamingSnake(s string) string {
+	return ToScreamingDelimited(s, '_', "", true)
+}
+
+// ToKebab converts a string to kebab-case
+func ToKebab(s string) string {
+	return ToDelimited(s, '-')
+}
+
+// ToScreamingKebab converts a string to SCREAMING-KEBAB-CASE
+func ToScreamingKebab(s string) string {
+	return ToScreamingDelimited(s, '-', "", true)
+}
+
+// ToDelimited converts a string to delimited.snake.case
+// (in this case `delimiter = '.'`)
+func ToDelimited(s string, delimiter uint8) string {
+	return ToScreamingDelimited(s, delimiter, "", false)
+}
+
+// ToScreamingDelimited converts a string to SCREAMING.DELIMITED.SNAKE.CASE
+// (in this case `delimiter = '.'; screaming = true`)
+// or delimited.snake.case
+// (in this case `delimiter = '.'; screaming = false`)
+func ToScreamingDelimited(s string, delimiter uint8, ignore string, screaming bool) string {
+	n := strings.Builder{}
+	n.Grow(len(s) + 2) // nominal 2 bytes of extra space for inserted delimiters
+
+	s = strings.TrimSpace(s)
+	bytes := []byte(s)
+	for i := 0; i < len(bytes); i++ {
+		// treat acronyms as words, e.g.: for JSONData -> JSON is a whole word
+		acronymFound := false
+		uppercaseAcronym.Range(func(key, value any) bool {
+			remain := string(bytes[i:])
+			if strings.HasPrefix(remain, key.(string)) {
+				n.WriteString(value.(string))
+				i += len(key.(string)) - 1
+				if i+1 < len(bytes) {
+					next := bytes[i+1]
+					if belong(next, Upper, Lower, Digit) && !strings.ContainsAny(string(next), ignore) {
+						n.WriteByte(delimiter)
+					}
+				}
+				acronymFound = true
+				return false
+			}
+			return true
+		})
+		if acronymFound {
+			continue
+		}
+
+		v := bytes[i]
+		vIsUpper := isUpper(v)
+		vIsLow := isLower(v)
+		if vIsLow && screaming {
+			v = toUpper(v)
+		} else if vIsUpper && !screaming {
+			v = toLower(v)
+		}
+
+		// treat acronyms as words, eg for JSONData -> JSON is a whole word
+		if i+1 < len(s) {
+			next := s[i+1]
+			vIsDigit := isDigit(v)
+			nextIsUpper := isUpper(next)
+			nextIsLower := isLower(next)
+			nextIsDigit := isDigit(next)
+			// add underscore if next letter case type is changed
+			if (vIsUpper && (nextIsLower || nextIsDigit)) ||
+				(vIsLow && (nextIsUpper || nextIsDigit)) ||
+				(vIsDigit && (nextIsUpper || nextIsLower)) {
+				prevIgnore := ignore != "" && i > 0 && strings.ContainsAny(string(s[i-1]), ignore)
+				if !prevIgnore {
+					if vIsUpper && nextIsLower {
+						if prevIsCap := i > 0 && isUpper(s[i-1]); prevIsCap {
+							n.WriteByte(delimiter)
+						}
+					}
+					n.WriteByte(v)
+					if vIsLow || vIsDigit || nextIsDigit {
+						n.WriteByte(delimiter)
+					}
+					continue
+				}
+			}
+		}
+
+		if isSeparator(v) && !strings.ContainsAny(string(v), ignore) {
+			// replace space/underscore/hyphen/dot with delimiter
+			n.WriteByte(delimiter)
+		} else {
+			n.WriteByte(v)
+		}
+	}
+
+	return n.String()
+}
diff --git a/internal/strcase/util.go b/internal/strcase/util.go
new file mode 100644
index 0000000..19c82b1
--- /dev/null
+++ b/internal/strcase/util.go
@@ -0,0 +1,79 @@
+package strcase
+
+import "unicode"
+
+type byteType int
+
+const (
+	Other byteType = iota
+	Upper
+	Lower
+	Separator
+	Digit
+)
+
+func getType(v byte) byteType {
+	switch {
+	case v >= 'A' && v <= 'Z':
+		return Upper
+	case v >= 'a' && v <= 'z':
+		return Lower
+	case v >= '0' && v <= '9':
+		return Digit
+	case v == ' ' || v == '_' || v == '-' || v == '.':
+		// space/underscore/hyphen/dot
+		return Separator
+	default:
+		return Other
+	}
+}
+
+func belong(v byte, types ...byteType) bool {
+	t := getType(v)
+	for _, typ := range types {
+		if t == typ {
+			return true
+		}
+	}
+	return false
+}
+
+func isUpper(v byte) bool {
+	return getType(v) == Upper
+}
+
+func isLower(v byte) bool {
+	return getType(v) == Lower
+}
+
+func isDigit(v byte) bool {
+	return getType(v) == Digit
+}
+
+func isSeparator(v byte) bool {
+	return getType(v) == Separator
+}
+
+func toUpper(v byte) byte {
+	return byte(unicode.ToUpper(rune(v)))
+}
+
+func toLower(v byte) byte {
+	return byte(unicode.ToLower(rune(v)))
+}
+
+// upperFirst converts the first character of a string to uppercase.
+func upperFirst(s string) string {
+	if len(s) == 0 {
+		return s
+	}
+	return string(unicode.ToUpper(rune(s[0]))) + s[1:]
+}
+
+// lowerFirst converts the first character of a string to lowercase.
+func lowerFirst(s string) string {
+	if len(s) == 0 {
+		return s
+	}
+	return string(unicode.ToLower(rune(s[0]))) + s[1:]
+}
diff --git a/internal/xlsxgen/xlsxgen.go b/internal/xlsxgen/xlsxgen.go
index 9409963..638ead3 100644
--- a/internal/xlsxgen/xlsxgen.go
+++ b/internal/xlsxgen/xlsxgen.go
@@ -11,8 +11,8 @@ import (
 	"time"
 	"unicode"
 
-	"github.com/iancoleman/strcase"
 	"github.com/pkg/errors"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/log"
 	"github.com/tableauio/tableau/options"
diff --git a/internal/xproto/union.go b/internal/xproto/union.go
index 2d60780..868e88f 100644
--- a/internal/xproto/union.go
+++ b/internal/xproto/union.go
@@ -1,7 +1,7 @@
 package xproto
 
 import (
-	"github.com/iancoleman/strcase"
+	"github.com/tableauio/tableau/internal/strcase"
 	"github.com/tableauio/tableau/proto/tableaupb"
 	"google.golang.org/protobuf/encoding/protowire"
 	"google.golang.org/protobuf/proto"
diff --git a/options/options.go b/options/options.go
index 691d456..135a974 100644
--- a/options/options.go
+++ b/options/options.go
@@ -24,6 +24,12 @@ type Options struct {
 	// Default: "Local".
 	LocationName string `yaml:"locationName"`
 
+	// Configure your custom acronyms. Out of the box, "ID" -> "id" is auto configured.
+	//
+	// For example, if you configure K8s -> k8s, then the field name in PascalCase "InK8s"
+	// will be converted to snake_case "in_k8s" but not "in_k_8_s".
+	Acronyms map[string]string `yaml:"acronyms"`
+
 	Log *log.Options // Log options.
 
 	Proto *ProtoOption `yaml:"proto"` // Proto generation options.
@@ -281,6 +287,13 @@ func Lang(o string) Option {
 	}
 }
 
+// Acronyms configures your custom acronyms globally in protogen.
+func Acronyms(o map[string]string) Option {
+	return func(opts *Options) {
+		opts.Acronyms = o
+	}
+}
+
 // Log sets log options.
 func Log(o *log.Options) Option {
 	return func(opts *Options) {


Test patch:
diff --git a/internal/strcase/camel_test.go b/internal/strcase/camel_test.go
new file mode 100644
index 0000000..3eb4392
--- /dev/null
+++ b/internal/strcase/camel_test.go
@@ -0,0 +1,166 @@
+package strcase
+
+import "testing"
+
+func toCamel(tb testing.TB) {
+	cases := [][]string{
+		{"test_case", "TestCase"},
+		{"test.case", "TestCase"},
+		{"test", "Test"},
+		{"TestCase", "TestCase"},
+		{" test  case ", "TestCase"},
+		{"", ""},
+		{"many_many_words", "ManyManyWords"},
+		{"AnyKind of_string", "AnyKindOfString"},
+		{"odd-fix", "OddFix"},
+		{"numbers2And55with000", "Numbers2And55With000"},
+		{"ID", "Id"},
+		{"CONSTANT_CASE", "ConstantCase"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToCamel(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToCamel(t *testing.T) {
+	toCamel(t)
+}
+
+func BenchmarkToCamel(b *testing.B) {
+	benchmarkCamelTest(b, toCamel)
+}
+
+func toLowerCamel(tb testing.TB) {
+	cases := [][]string{
+		{"foo-bar", "fooBar"},
+		{"TestCase", "testCase"},
+		{"", ""},
+		{"AnyKind of_string", "anyKindOfString"},
+		{"AnyKind.of-string", "anyKindOfString"},
+		{"ID", "id"},
+		{"some string", "someString"},
+		{" some string", "someString"},
+		{"CONSTANT_CASE", "constantCase"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToLowerCamel(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToLowerCamel(t *testing.T) {
+	toLowerCamel(t)
+}
+
+func TestCustomAcronymToCamel(t *testing.T) {
+	tests := []struct {
+		name         string
+		acronymKey   string
+		acronymValue string
+		value        string
+		expected     string
+	}{
+		{
+			name:         "CCTV Custom Acronym",
+			acronymKey:   "CCTV",
+			acronymValue: "cctv",
+			value:        "CCTVChannel",
+			expected:     "CctvChannel",
+		},
+		{
+			name:         "ABCDACME Custom Acroynm",
+			acronymKey:   "ABCDACME",
+			acronymValue: "AbcdAcme",
+			value:        "ABCDACMEAlias",
+			expected:     "AbcdAcmeAlias",
+		},
+		{
+			name:         "PostgreSQL Custom Acronym",
+			acronymKey:   "PostgreSQL",
+			acronymValue: "postgreSQL",
+			value:        "powerfulPostgreSQLDatabase",
+			expected:     "PowerfulPostgreSQLDatabase",
+		},
+		{
+			name:         "APIV3 Custom Acronym",
+			acronymKey:   "APIV3",
+			acronymValue: "apiv3",
+			value:        "WebAPIV3Spec",
+			expected:     "WebApiv3Spec",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ConfigureAcronym(test.acronymKey, test.acronymValue)
+			if result := ToCamel(test.value); result != test.expected {
+				t.Errorf("expected custom acronym result %s, got %s", test.expected, result)
+			}
+		})
+	}
+}
+
+func TestCustomAcronymToLowerCamel(t *testing.T) {
+	tests := []struct {
+		name         string
+		acronymKey   string
+		acronymValue string
+		value        string
+		expected     string
+	}{
+		{
+			name:         "CCTV Custom Acronym",
+			acronymKey:   "CCTV",
+			acronymValue: "cctv",
+			value:        "CCTVChannel",
+			expected:     "cctvChannel",
+		},
+		{
+			name:         "ABCDACME Custom Acroynm",
+			acronymKey:   "ABCDACME",
+			acronymValue: "AbcdAcme",
+			value:        "ABCDACMEAlias",
+			expected:     "abcdAcmeAlias",
+		},
+		{
+			name:         "PostgreSQL Custom Acronym",
+			acronymKey:   "PostgreSQL",
+			acronymValue: "postgreSQL",
+			value:        "PowerfulPostgreSQLDatabase",
+			expected:     "powerfulPostgreSQLDatabase",
+		},
+		{
+			name:         "APIV3 Custom Acronym",
+			acronymKey:   "APIV3",
+			acronymValue: "apiv3",
+			value:        "WebAPIV3Spec",
+			expected:     "webApiv3Spec",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ConfigureAcronym(test.acronymKey, test.acronymValue)
+			if result := ToLowerCamel(test.value); result != test.expected {
+				t.Errorf("expected custom acronym result %s, got %s", test.expected, result)
+			}
+		})
+	}
+}
+
+func BenchmarkToLowerCamel(b *testing.B) {
+	benchmarkCamelTest(b, toLowerCamel)
+}
+
+func benchmarkCamelTest(b *testing.B, fn func(testing.TB)) {
+	for n := 0; n < b.N; n++ {
+		fn(b)
+	}
+}
diff --git a/internal/camelcase/README.md b/internal/strcase/camelcase/README.md
similarity index 100%
rename from internal/camelcase/README.md
rename to internal/strcase/camelcase/README.md
diff --git a/internal/camelcase/camelcase.go b/internal/strcase/camelcase/camelcase.go
similarity index 100%
rename from internal/camelcase/camelcase.go
rename to internal/strcase/camelcase/camelcase.go
diff --git a/internal/strcase/concurrency_test.go b/internal/strcase/concurrency_test.go
new file mode 100644
index 0000000..f99ebbf
--- /dev/null
+++ b/internal/strcase/concurrency_test.go
@@ -0,0 +1,46 @@
+package strcase
+
+import (
+	"testing"
+)
+
+func TestConcurrency(t *testing.T) {
+	for i := 0; i < 10000; i++ {
+		go doConvert()
+	}
+
+}
+
+func doConvert() {
+	var snakes = []string{"code", "exchange", "pe_ratio", "profit_margin", "updated_date"}
+	for _, v := range snakes {
+		_ = convertDatabaseNameToCamelCase(v)
+	}
+}
+
+func convertDatabaseNameToCamelCase(d string) (s string) {
+	ConfigureAcronym("price_book_mrq", "PriceBookMRQ")
+	ConfigureAcronym("pe_ratio", "PERatio")
+	ConfigureAcronym("peg_ratio", "PEGRatio")
+	ConfigureAcronym("eps_estimate_current_year", "EPSEstimateCurrentYear")
+	ConfigureAcronym("eps_estimate_next_year", "EPSEstimateNextYear")
+	ConfigureAcronym("eps_estimate_next_quarter", "EPSNextQuarter")
+	ConfigureAcronym("eps_estimate_current_quarter", "EPSEstimateCurrentQuarter")
+	ConfigureAcronym("ebitda", "EBITDA")
+	ConfigureAcronym("operating_margin_ttm", "OperatingMarginTTM")
+	ConfigureAcronym("return_on_assets_ttm", "ReturnOnAssetsTTM")
+	ConfigureAcronym("return_on_equity_ttm", "ReturnOnEquityTTM")
+	ConfigureAcronym("revenue_ttm", "RevenueTTM")
+	ConfigureAcronym("revenue_per_share_ttm", "RevenuePerShareTTM")
+	ConfigureAcronym("quarterly_revenue_growth_yoy", "QuarterlyRevenueGrowthYOY")
+	ConfigureAcronym("gross_profit_ttm", "GrossProfitTTM")
+	ConfigureAcronym("diluted_eps_ttm", "DilutedEpsTTM")
+	ConfigureAcronym("quarterly_earnings_growth_yoy", "QuarterlyEarningsGrowthYOY")
+	ConfigureAcronym("two_hundred_day_ma", "TwoHundredDayMA")
+	ConfigureAcronym("trailing_pe", "TrailingPE")
+	ConfigureAcronym("forward_pe", "ForwardPE")
+	ConfigureAcronym("price_sales_ttm", "PriceSalesTTM")
+	ConfigureAcronym("price_book_mrq", "PriceBookMRQ")
+	s = ToCamel(d)
+	return
+}
diff --git a/internal/strcase/snake_test.go b/internal/strcase/snake_test.go
new file mode 100644
index 0000000..09c5eb4
--- /dev/null
+++ b/internal/strcase/snake_test.go
@@ -0,0 +1,297 @@
+package strcase
+
+import "testing"
+
+func toSnake(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "test_case"},
+		{"TestCase", "test_case"},
+		{"Test Case", "test_case"},
+		{" Test Case", "test_case"},
+		{"Test Case ", "test_case"},
+		{" Test Case ", "test_case"},
+		{"test", "test"},
+		{"test_case", "test_case"},
+		{"Test", "test"},
+		{"", ""},
+		{"ManyManyWords", "many_many_words"},
+		{"manyManyWords", "many_many_words"},
+		{"AnyKind of_string", "any_kind_of_string"},
+		{"numbers2and55with000", "numbers_2_and_55_with_000"},
+		{"JSONData", "json_data"},
+		{"userID", "user_id"},
+		{"AAAbbb", "aa_abbb"},
+		{"1A2", "1_a_2"},
+		{"A1B", "a_1_b"},
+		{"A1A2A3", "a_1_a_2_a_3"},
+		{"A1 A2 A3", "a_1_a_2_a_3"},
+		{"AB1AB2AB3", "ab_1_ab_2_ab_3"},
+		{"AB1 AB2 AB3", "ab_1_ab_2_ab_3"},
+		{"some string", "some_string"},
+		{" some string", "some_string"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToSnake(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToSnake(t *testing.T) { toSnake(t) }
+
+func BenchmarkToSnake(b *testing.B) {
+	benchmarkSnakeTest(b, toSnake)
+}
+
+func toSnakeWithIgnore(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "test_case"},
+		{"TestCase", "test_case"},
+		{"Test Case", "test_case"},
+		{" Test Case", "test_case"},
+		{"Test Case ", "test_case"},
+		{" Test Case ", "test_case"},
+		{"test", "test"},
+		{"test_case", "test_case"},
+		{"Test", "test"},
+		{"", ""},
+		{"ManyManyWords", "many_many_words"},
+		{"manyManyWords", "many_many_words"},
+		{"AnyKind of_string", "any_kind_of_string"},
+		{"numbers2and55with000", "numbers_2_and_55_with_000"},
+		{"JSONData", "json_data"},
+		{"AwesomeActivity.UserID", "awesome_activity.user_id", "."},
+		{"AwesomeActivity.User.Id", "awesome_activity.user.id", "."},
+		{"AwesomeUsername@Awesome.Com", "awesome_username@awesome.com", ".@"},
+		{"lets-ignore all.of dots-and-dashes", "lets-ignore_all.of_dots-and-dashes", ".-"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		var ignore string
+		ignore = ""
+		if len(i) == 3 {
+			ignore = i[2]
+		}
+		result := ToSnakeWithIgnore(in, ignore)
+		if result != out {
+			istr := ""
+			if len(i) == 3 {
+				istr = " ignoring '" + i[2] + "'"
+			}
+			tb.Errorf("%q (%q != %q%s)", in, result, out, istr)
+		}
+	}
+}
+
+func TestToSnakeWithIgnore(t *testing.T) { toSnakeWithIgnore(t) }
+
+func BenchmarkToSnakeWithIgnore(b *testing.B) {
+	benchmarkSnakeTest(b, toSnakeWithIgnore)
+}
+
+func TestCustomAcronymsToSnake(t *testing.T) {
+	tests := []struct {
+		name         string
+		acronymKey   string
+		acronymValue string
+		value        string
+		expected     string
+	}{
+		// TODO: API and APIV3, find the longest acronym
+		// {
+		// 	name:         "API Custom Acronym",
+		// 	acronymKey:   "API",
+		// 	acronymValue: "api",
+		// 	expected:     "Api",
+		// },
+		{
+			name:         "APIV3 Custom Acronym",
+			acronymKey:   "APIV3",
+			acronymValue: "apiv3",
+			value:        "WebAPIV3Spec",
+			expected:     "web_apiv3_spec",
+		},
+		{
+			name:         "APIV3 Custom Acronym with spaces",
+			acronymKey:   "APIV3",
+			acronymValue: "apiv3",
+			value:        "APIV3 Spec ",
+			expected:     "apiv3_spec",
+		},
+		{
+			name:         "K8s Custom Acroynm",
+			acronymKey:   "K8s",
+			acronymValue: "k8s",
+			value:        "InK8s",
+			expected:     "in_k8s",
+		},
+		{
+			name:         "K8s Custom Acroynm with spaces",
+			acronymKey:   "K8s",
+			acronymValue: "k8s",
+			value:        " InK8s  XX",
+			expected:     "in_k8s__xx",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			ConfigureAcronym(test.acronymKey, test.acronymValue)
+			if result := ToSnake(test.value); result != test.expected {
+				t.Errorf("expected custom acronym result %s, got %s", test.expected, result)
+			}
+		})
+	}
+}
+
+func toDelimited(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "test@case"},
+		{"TestCase", "test@case"},
+		{"Test Case", "test@case"},
+		{" Test Case", "test@case"},
+		{"Test Case ", "test@case"},
+		{" Test Case ", "test@case"},
+		{"test", "test"},
+		{"test_case", "test@case"},
+		{"Test", "test"},
+		{"", ""},
+		{"ManyManyWords", "many@many@words"},
+		{"manyManyWords", "many@many@words"},
+		{"AnyKind of_string", "any@kind@of@string"},
+		{"numbers2and55with000", "numbers@2@and@55@with@000"},
+		{"JSONData", "json@data"},
+		{"userID", "user@id"},
+		{"AAAbbb", "aa@abbb"},
+		{"test-case", "test@case"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToDelimited(in, '@')
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToDelimited(t *testing.T) { toDelimited(t) }
+
+func BenchmarkToDelimited(b *testing.B) {
+	benchmarkSnakeTest(b, toDelimited)
+}
+
+func toScreamingSnake(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "TEST_CASE"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToScreamingSnake(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToScreamingSnake(t *testing.T) { toScreamingSnake(t) }
+
+func BenchmarkToScreamingSnake(b *testing.B) {
+	benchmarkSnakeTest(b, toScreamingSnake)
+}
+
+func toKebab(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "test-case"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToKebab(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToKebab(t *testing.T) { toKebab(t) }
+
+func BenchmarkToKebab(b *testing.B) {
+	benchmarkSnakeTest(b, toKebab)
+}
+
+func toScreamingKebab(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "TEST-CASE"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToScreamingKebab(in)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToScreamingKebab(t *testing.T) { toScreamingKebab(t) }
+
+func BenchmarkToScreamingKebab(b *testing.B) {
+	benchmarkSnakeTest(b, toScreamingKebab)
+}
+
+func toScreamingDelimited(tb testing.TB) {
+	cases := [][]string{
+		{"testCase", "TEST.CASE"},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		result := ToScreamingDelimited(in, '.', "", true)
+		if result != out {
+			tb.Errorf("%q (%q != %q)", in, result, out)
+		}
+	}
+}
+
+func TestToScreamingDelimited(t *testing.T) { toScreamingDelimited(t) }
+
+func BenchmarkToScreamingDelimited(b *testing.B) {
+	benchmarkSnakeTest(b, toScreamingDelimited)
+}
+
+func toScreamingDelimitedWithIgnore(tb testing.TB) {
+	cases := [][]string{
+		{"AnyKind of_string", "ANY.KIND OF.STRING", ".", " "},
+	}
+	for _, i := range cases {
+		in := i[0]
+		out := i[1]
+		delimiter := i[2][0]
+		ignore := i[3][0]
+		result := ToScreamingDelimited(in, delimiter, string(ignore), true)
+		if result != out {
+			istr := ""
+			if len(i) == 4 {
+				istr = " ignoring '" + i[3] + "'"
+			}
+			tb.Errorf("%q (%q != %q%s)", in, result, out, istr)
+		}
+	}
+}
+
+func TestToScreamingDelimitedWithIgnore(t *testing.T) { toScreamingDelimitedWithIgnore(t) }
+
+func BenchmarkToScreamingDelimitedWithIgnore(b *testing.B) {
+	benchmarkSnakeTest(b, toScreamingDelimitedWithIgnore)
+}
+
+func benchmarkSnakeTest(b *testing.B, fn func(testing.TB)) {
+	for n := 0; n < b.N; n++ {
+		fn(b)
+	}
+}
diff --git a/test/functest/conf/YamlAcronymConf.json b/test/functest/conf/YamlAcronymConf.json
new file mode 100644
index 0000000..77b9d27
--- /dev/null
+++ b/test/functest/conf/YamlAcronymConf.json
@@ -0,0 +1,4 @@
+{
+    "inK8s": null,
+    "apiv3Spec": "https://openai.com/"
+}
\ No newline at end of file
diff --git a/test/functest/conf/base/XmlPatchConf.json b/test/functest/conf/base/XmlPatchConf.json
index c48d573..51afdbf 100644
--- a/test/functest/conf/base/XmlPatchConf.json
+++ b/test/functest/conf/base/XmlPatchConf.json
@@ -11,6 +11,11 @@
         2,
         3
     ],
+    "incellScalarListList": [
+        4,
+        5,
+        6
+    ],
     "structListList": [
         {
             "id": 1,
diff --git a/test/functest/conf/test/XmlPatchConf.json b/test/functest/conf/test/XmlPatchConf.json
index 62422d3..e3346ad 100644
--- a/test/functest/conf/test/XmlPatchConf.json
+++ b/test/functest/conf/test/XmlPatchConf.json
@@ -5,6 +5,11 @@
         20,
         30
     ],
+    "incellScalarListList": [
+        40,
+        50,
+        60
+    ],
     "structListList": [
         {
             "id": 10,
diff --git a/test/functest/proto/xml__patch__base__env.proto b/test/functest/proto/xml__patch__base__env.proto
index aa4cfa7..16cb157 100644
--- a/test/functest/proto/xml__patch__base__env.proto
+++ b/test/functest/proto/xml__patch__base__env.proto
@@ -24,13 +24,14 @@ message XmlPatchConf {
     google.protobuf.Duration expiry = 4 [(tableau.field) = {name:"Expiry"}];
   }
   repeated int32 scalar_list_list = 3 [(tableau.field) = {name:"ScalarList" prop:{patch:PATCH_REPLACE}}];
-  repeated Animal struct_list_list = 4 [(tableau.field) = {name:"StructList"}];
+  repeated int32 incell_scalar_list_list = 4 [(tableau.field) = {name:"IncellScalarList" layout:LAYOUT_INCELL prop:{patch:PATCH_REPLACE}}];
+  repeated Animal struct_list_list = 5 [(tableau.field) = {name:"StructList"}];
   message Animal {
     uint32 id = 1 [(tableau.field) = {name:"ID"}];
     string name = 2 [(tableau.field) = {name:"Name"}];
   }
-  map<uint32, string> scalar_map_map = 5 [(tableau.field) = {name:"ScalarMap" layout:LAYOUT_INCELL prop:{patch:PATCH_REPLACE}}];
-  map<uint32, Item> struct_map_map = 6 [(tableau.field) = {name:"StructMap" key:"key"}];
+  map<uint32, string> scalar_map_map = 6 [(tableau.field) = {name:"ScalarMap" layout:LAYOUT_INCELL prop:{patch:PATCH_REPLACE}}];
+  map<uint32, Item> struct_map_map = 7 [(tableau.field) = {name:"StructMap" key:"key"}];
   message Item {
     uint32 key = 1 [(tableau.field) = {name:"key"}];
     string name = 2 [(tableau.field) = {name:"Name"}];
diff --git a/test/functest/proto/yaml__misc__acronym.proto b/test/functest/proto/yaml__misc__acronym.proto
new file mode 100644
index 0000000..3db1f43
--- /dev/null
+++ b/test/functest/proto/yaml__misc__acronym.proto
@@ -0,0 +1,20 @@
+// Code generated by tableau (protogen v0.6.1). DO NOT EDIT.
+// clang-format off
+
+syntax = "proto3";
+
+package protoconf;
+
+import "tableau/protobuf/tableau.proto";
+
+option go_package = "github.com/tableauio/tableau/test/functest/protoconf";
+option (tableau.workbook) = {name:"yaml/misc/Acronym.yaml"};
+
+message YamlAcronymConf {
+  option (tableau.worksheet) = {name:"YamlAcronymConf"};
+
+  book in_k8s = 1 [(tableau.field) = {name:"InK8s"}];
+  message book {
+  }
+  string apiv3_spec = 2 [(tableau.field) = {name:"APIV3Spec"}];
+}
diff --git a/test/functest/testdata/xml/patch/base/Env.xml b/test/functest/testdata/xml/patch/base/Env.xml
index 5517b50..378d3cb 100644
--- a/test/functest/testdata/xml/patch/base/Env.xml
+++ b/test/functest/testdata/xml/patch/base/Env.xml
@@ -7,6 +7,7 @@
   <Env>string</Env>
   <StrictStruct ID="{Fruit}uint32" Name="string" Start="datetime" Expiry="duration" />
   <ScalarList>[int32]|{patch:PATCH_REPLACE}</ScalarList>
+  <IncellScalarList>[]int32|{patch:PATCH_REPLACE}</IncellScalarList>
   <StructList ID="[Animal]uint32" Name="string" />
   <ScalarMap>map<uint32, string>|{patch:PATCH_REPLACE}</ScalarMap>
   <StructMap key="map<uint32, Item>" Name="string" Num="int32" />
@@ -19,6 +20,7 @@
   <ScalarList>1</ScalarList>
   <ScalarList>2</ScalarList>
   <ScalarList>3</ScalarList>
+  <IncellScalarList>4,5,6</IncellScalarList>
   <StructList ID="1" Name="dog" />
   <StructList ID="2" Name="bird" />
   <ScalarMap>1:dog,2:bird,3:cat</ScalarMap>
diff --git a/test/functest/testdata/xml/patch/overlays/test/Env.xml b/test/functest/testdata/xml/patch/overlays/test/Env.xml
index aa939c3..1e8f972 100644
--- a/test/functest/testdata/xml/patch/overlays/test/Env.xml
+++ b/test/functest/testdata/xml/patch/overlays/test/Env.xml
@@ -4,6 +4,7 @@
   <ScalarList>10</ScalarList>
   <ScalarList>20</ScalarList>
   <ScalarList>30</ScalarList>
+  <IncellScalarList>40,50,60</IncellScalarList>
   <StructList ID="10" Name="snake" />
   <StructList ID="20" Name="python" />
 </XmlPatchConf>
diff --git a/test/functest/testdata/yaml/misc/Acronym.yaml b/test/functest/testdata/yaml/misc/Acronym.yaml
new file mode 100644
index 0000000..61694b3
--- /dev/null
+++ b/test/functest/testdata/yaml/misc/Acronym.yaml
@@ -0,0 +1,11 @@
+# define metasheet: generate all sheets
+"@sheet": "@TABLEAU"
+---
+# define schema
+"@sheet": "@YamlAcronymConf"
+InK8s: book
+APIV3Spec: string
+---
+"@sheet": YamlAcronymConf
+InK8s: true
+APIV3Spec: https://openai.com/
diff --git a/test/functest/util.go b/test/functest/util.go
index bf6472d..9f92cf3 100644
--- a/test/functest/util.go
+++ b/test/functest/util.go
@@ -81,6 +81,10 @@ func genProto(logLevel string) error {
 				Mode:  "FULL",
 			},
 		),
+		options.Acronyms(map[string]string{
+			"K8s":   "k8s",
+			"APIV3": "apiv3",
+		}),
 		// options.Lang("zh"),
 	)
 }