diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..a43f6b4 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,157 @@ +{ + "ignoreWords": [ + "abaabaaa", + "abaabaaabaaaa", + "abbrevboth", + "acarl", + "acosh", + "addf", + "addins", + "adipiscing", + "afterwhile", + "aindent", + "aliqua", + "amet", + "araneae", + "asinh", + "assertw", + "atanh", + "awrap", + "bclear", + "behaviour", + "bmatcuk", + "bxor", + "camelcase", + "cbrt", + "colorln", + "consectetur", + "copysign", + "cosinus", + "coveooss", + "coveord", + "criticalf", + "cutset", + "debugf", + "Debugf", + "defval", + "diffmatchpatch", + "divf", + "dolore", + "DONT", + "doublestar", + "drhodes", + "eiusmod", + "elit", + "endexpr", + "Envar", + "Envars", + "erfc", + "expm", + "extensionfiles", + "fatih", + "forgeround", + "frombits", + "funcs", + "genny", + "goerrors", + "gomod", + "goquery", + "gotemplate", + "HCLVALUE", + "hexa", + "htpasswd", + "hypot", + "IHCL", + "ilogb", + "incididunt", + "infof", + "Infof", + "Infoln", + "interpretated", + "kebabcase", + "Kenobi", + "keymap", + "keypair", + "kindis", + "kindof", + "labore", + "ldexp", + "ldflags", + "lenc", + "lgamma", + "logb", + "logrus", + "lshift", + "maxf", + "Metaclass", + "minf", + "missingkey", + "modf", + "mulf", + "multilines", + "multilogger", + "munerum", + "nadipiscing", + "naliqua", + "namet", + "nconsectetur", + "ndolore", + "neiusmod", + "nelit", + "nextafter", + "nincididunt", + "nindent", + "nlabore", + "noticef", + "nsed", + "ntempor", + "otherkey", + "pickv", + "puerkito", + "pwsh", + "reutils", + "rounddown", + "rshift", + "secode", + "semicolumn", + "sergi", + "signbit", + "simplifiable", + "sincos", + "sindent", + "sirupsen", + "sjoin", + "snakecase", + "splitn", + "sprintln", + "squote", + "storer", + "stretchr", + "stripansi", + "striptcolor", + "subf", + "subselection", + "substr", + "swapcase", + "tempfile", + "tempor", + "tfvars", + "TFVARS", + "tracef", + "Tracef", + "traiecta", + "trimall", + "tsed", + "typeis", + "unmanaged", + "unmatch", + "unsplit", + "untitle", + "urlquery", + "uuidv", + "varias", + "warningf", + "worktree", + "warnf" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e37adf7..758880e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ gotemplate gotemplate.exe # End of https://www.gitignore.io/api/go,hugo,code + +.DS_Store diff --git a/README.md b/README.md index aaf3d1d..4b553f1 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,5 @@ gotemplate --var my_var2=value2 --import vars.json '{{ .my_var }} {{ .my_var2 }} ``` More examples and statement in the [documentation](https://coveooss.github.io/gotemplate/) + +Library usage [documentation](https://pkg.go.dev/github.com/coveooss/gotemplate/v3/template) diff --git a/collections/implementation/base_helper.go b/collections/implementation/base_helper.go index 5be998f..9d1a02c 100644 --- a/collections/implementation/base_helper.go +++ b/collections/implementation/base_helper.go @@ -149,17 +149,22 @@ func (bh BaseHelper) tryAsDictionary(object interface{}, strict bool) (baseIDict return result, nil } -// TryAsList tries to convert any object to IGenericList object. +// TryAsList attempts to convert the given object to a baseIList. +// If the object is a pointer, it dereferences it first. +// If the object is already a baseIList, it returns it directly. +// If the object is nil, it creates a new list. +// If the object is a slice or array, it creates a new list and populates it with the elements of the slice or array. +// If the object can be converted to a baseList, it performs the conversion. +// If the object cannot be converted to a baseIList, it returns an error. +// If the resulting list needs conversion, it creates a new list and copies the elements to it. +// +// Parameters: +// - object: The object to be converted to a baseIList. +// +// Returns: +// - baseIList: The converted list. +// - error: An error if the object cannot be converted to a baseIList. func (bh BaseHelper) TryAsList(object interface{}) (baseIList, error) { - return bh.tryAsList(object, false) -} - -// TryAsListStrict tries to convert any object to IGenericList object. -func (bh BaseHelper) TryAsListStrict(object interface{}) (baseIList, error) { - return bh.tryAsList(object, true) -} - -func (bh BaseHelper) tryAsList(object interface{}, strict bool) (baseIList, error) { if object != nil && reflect.TypeOf(object).Kind() == reflect.Ptr { object = reflect.ValueOf(object).Elem().Interface() } diff --git a/collections/implementation/generic_test.go b/collections/implementation/generic_test.go index 2ab73af..dc32cf4 100644 --- a/collections/implementation/generic_test.go +++ b/collections/implementation/generic_test.go @@ -697,7 +697,7 @@ func Test_dict_Default(t *testing.T) { {"Empty", nil, args{"Foo", "Bar"}, "Bar"}, {"Map int", dictFixture, args{"int", 1}, 123}, {"Map float", dictFixture, args{"float", 1}, 1.23}, - {"Map Non existant", dictFixture, args{"Foo", "Bar"}, "Bar"}, + {"Map Non existent", dictFixture, args{"Foo", "Bar"}, "Bar"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -722,9 +722,9 @@ func Test_dict_Delete(t *testing.T) { }{ {"Empty", nil, args{}, baseDict{}, "key not found"}, {"Map", dictFixture, args{}, dictFixture, "key not found"}, - {"Non existant key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, + {"Non existent key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, {"Map with keys", dictFixture, args{"int", []interface{}{"list"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), ""}, - {"Map with keys + non existant", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, + {"Map with keys + non existent", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -750,9 +750,9 @@ func Test_dict_Flush(t *testing.T) { }{ {"Empty", nil, nil, baseDict{}}, {"Map", dictFixture, nil, baseDict{}}, - {"Non existant key", dictFixture, []interface{}{"Test"}, dictFixture}, + {"Non existent key", dictFixture, []interface{}{"Test"}, dictFixture}, {"Map with keys", dictFixture, []interface{}{"int", "list"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, - {"Map with keys + non existant", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, + {"Map with keys + non existent", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/hcl/generated_test.go b/hcl/generated_test.go index e4ed676..43d917d 100644 --- a/hcl/generated_test.go +++ b/hcl/generated_test.go @@ -701,7 +701,7 @@ func Test_dict_Default(t *testing.T) { {"Empty", nil, args{"Foo", "Bar"}, "Bar"}, {"Map int", dictFixture, args{"int", 1}, 123}, {"Map float", dictFixture, args{"float", 1}, 1.23}, - {"Map Non existant", dictFixture, args{"Foo", "Bar"}, "Bar"}, + {"Map Non existent", dictFixture, args{"Foo", "Bar"}, "Bar"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -726,9 +726,9 @@ func Test_dict_Delete(t *testing.T) { }{ {"Empty", nil, args{}, hclDict{}, "key not found"}, {"Map", dictFixture, args{}, dictFixture, "key not found"}, - {"Non existant key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, + {"Non existent key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, {"Map with keys", dictFixture, args{"int", []interface{}{"list"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), ""}, - {"Map with keys + non existant", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, + {"Map with keys + non existent", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -754,9 +754,9 @@ func Test_dict_Flush(t *testing.T) { }{ {"Empty", nil, nil, hclDict{}}, {"Map", dictFixture, nil, hclDict{}}, - {"Non existant key", dictFixture, []interface{}{"Test"}, dictFixture}, + {"Non existent key", dictFixture, []interface{}{"Test"}, dictFixture}, {"Map with keys", dictFixture, []interface{}{"int", "list"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, - {"Map with keys + non existant", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, + {"Map with keys + non existent", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/json/generated_test.go b/json/generated_test.go index 6eb937f..5be003d 100644 --- a/json/generated_test.go +++ b/json/generated_test.go @@ -701,7 +701,7 @@ func Test_dict_Default(t *testing.T) { {"Empty", nil, args{"Foo", "Bar"}, "Bar"}, {"Map int", dictFixture, args{"int", 1}, 123}, {"Map float", dictFixture, args{"float", 1}, 1.23}, - {"Map Non existant", dictFixture, args{"Foo", "Bar"}, "Bar"}, + {"Map Non existent", dictFixture, args{"Foo", "Bar"}, "Bar"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -726,9 +726,9 @@ func Test_dict_Delete(t *testing.T) { }{ {"Empty", nil, args{}, jsonDict{}, "key not found"}, {"Map", dictFixture, args{}, dictFixture, "key not found"}, - {"Non existant key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, + {"Non existent key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, {"Map with keys", dictFixture, args{"int", []interface{}{"list"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), ""}, - {"Map with keys + non existant", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, + {"Map with keys + non existent", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -754,9 +754,9 @@ func Test_dict_Flush(t *testing.T) { }{ {"Empty", nil, nil, jsonDict{}}, {"Map", dictFixture, nil, jsonDict{}}, - {"Non existant key", dictFixture, []interface{}{"Test"}, dictFixture}, + {"Non existent key", dictFixture, []interface{}{"Test"}, dictFixture}, {"Map with keys", dictFixture, []interface{}{"int", "list"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, - {"Map with keys + non existant", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, + {"Map with keys + non existent", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/json/json.go b/json/json.go index 6d81776..6ebaa08 100644 --- a/json/json.go +++ b/json/json.go @@ -37,7 +37,7 @@ func (d jsonDict) PrettyPrint() string { func init() { collections.TypeConverters["!json"] = Unmarshal } // Unmarshal calls the native Unmarshal but transform the results -// to returns Dictionary and GenerecList instead of go native collections. +// to returns Dictionary and GenericList instead of go native collections. func Unmarshal(data []byte, out interface{}) (err error) { if err = NativeUnmarshal(data, out); err != nil { return diff --git a/main.go b/main.go index cc99fd1..389660d 100644 --- a/main.go +++ b/main.go @@ -24,8 +24,7 @@ import ( "github.com/sirupsen/logrus" ) -// Version is initialized at build time through -ldflags "-X main.Version=" -var version = "2.7.4" +var version = "major.minor.patch # locally built, should be replaced by the build process with -ldflags \"-X main.version=\"" var tempFolder = errors.Must(os.MkdirTemp("", "gotemplate-")).(string) const ( @@ -34,7 +33,7 @@ const ( const description = ` An extended template processor for go. -See: https://coveo.github.io/gotemplate for complete documentation. +See: https://coveooss.github.io/gotemplate for complete documentation. ` func runGotemplate() (exitCode int) { @@ -89,7 +88,7 @@ func runGotemplate() (exitCode int) { ignoreMissingImport = run.Flag("ignore-missing-import", "Exit with code 0 even if import does not exist").Bool() ignoreMissingSource = run.Flag("ignore-missing-source", "Exit with code 0 even if source does not exist").Bool() ignoreMissingPaths = run.Flag("ignore-missing-paths", "Exit with code 0 even if import or source do not exist").Bool() - ignoreRazor = run.Flag("ignore-razor", "Do not consider the list of excluded Razor name as razor expression").PlaceHolder("regex").Strings() + ignoreRazor = run.Flag("ignore-razor", "Do not consider the list of excluded Razor name as razor expression").PlaceHolder("regex").NoEnvar().Strings() templates = run.Arg("templates", "Template files or commands to process").Strings() list = app.Command("list", "Get detailed help on gotemplate functions").NoAutoShortcut() @@ -259,7 +258,7 @@ func runGotemplate() (exitCode int) { t.TempFolder(tempFolder) if len(*ignoreRazor) > 0 { - t.IgnoreRazorExpression(*ignoreRazor...) + t.AppendIgnoreRazorExpression(*ignoreRazor...) } if command == list.FullCommand() { diff --git a/render-doc b/render-doc index 3ecead5..4b75d1b 100755 --- a/render-doc +++ b/render-doc @@ -129,4 +129,4 @@ printf -- '---\nbookFlatSection: true\nweight: 5\n---' > $DOC_FOLDER/objects/_in # Copy README as the main page printf -- '---\ntype: docs\n---' > $CONTENT_FOLDER/_index.md -cat README.md >> $CONTENT_FOLDER/_index.md +cat README.md | grep -v "More examples and statement" >> $CONTENT_FOLDER/_index.md diff --git a/template/doc.go b/template/doc.go new file mode 100644 index 0000000..836bba5 --- /dev/null +++ b/template/doc.go @@ -0,0 +1,12 @@ +/* +Package template provides extended functionalities for the base Go template library. +It includes additional features such as context management, custom delimiters, and +environment variable configurations for template processing. + +The package imports several other packages to enhance its capabilities: +. fmt, os, filepath, reflect, strings, sync, and text/template from the standard library. +. collections and utils from github.com/coveooss/gotemplate/v3. +. multicolor from github.com/coveooss/multilogger/color. +*/ + +package template diff --git a/template/extra_math.go b/template/extra_math.go index 461b860..84dbda7 100644 --- a/template/extra_math.go +++ b/template/extra_math.go @@ -291,7 +291,7 @@ var mathFuncsHelp = descriptions{ "sincos": "Returns Sin(x), Cos(x).\nSpecial cases are:\n sincos(±0) = ±0, 1\n sincos(±Inf) = NaN, NaN\n sincos(NaN) = NaN, NaN", "sinh": "Returns the hyperbolic sine of x.\nSpecial cases are:\n sinh(±0) = ±0\n sinh(±Inf) = ±Inf\n sinh(NaN) = NaN", "sqrt": "Returns the square root of x.\nSpecial cases are:\n sqrt(+Inf) = +Inf\n sqrt(±0) = ±0\n sqrt(x < 0) = NaN\n sqrt(NaN) = NaN", - "sub": "Returns the result of the substraction of the two arguments.", + "sub": "Returns the result of the subtraction of the two arguments.", "tan": "Returns the tangent of the radian argument x.\nSpecial cases are:\n tan(±0) = ±0\n tan(±Inf) = NaN\n tan(NaN) = NaN", "tanh": "Returns the hyperbolic tangent of x.\nSpecial cases are:\n tanh(±0) = ±0\n tanh(±Inf) = ±1\n tanh(NaN) = NaN", "to": "Builds a range of integers starting with 1 by default and including the upper limit.", diff --git a/template/extra_runtime.go b/template/extra_runtime.go index f74c9e0..5c690bb 100644 --- a/template/extra_runtime.go +++ b/template/extra_runtime.go @@ -52,8 +52,8 @@ var runtimeFuncsHelp = descriptions{ "alias": "Defines an alias (go template function) using the function (exec, run, include, template). Executed in the context of the caller.", "aliases": "Returns the list of all functions that are simply an alias of another function.", "allFunctions": "Returns the list of all available functions.", - "assert": "Raises a formated error if the test condition is false.", - "assertWarning": "Issues a formated warning if the test condition is false.", + "assert": "Raises a formatted error if the test condition is false.", + "assertWarning": "Issues a formatted warning if the test condition is false.", "categories": strings.TrimSpace(collections.UnIndent(` Returns all functions group by categories. @@ -94,7 +94,7 @@ var runtimeFuncsHelp = descriptions{ This is similar to what the template action does but it allows you to capture its output in a variable. `)), "localAlias": "Defines an alias (go template function) using the function (exec, run, include, template). Executed in the context of the function it maps to.", - "raise": "Raise a formated error.", + "raise": "Raise a formatted error.", "run": "Returns the result of the shell command as string.", "substitute": "Applies the supplied regex substitute specified on the command line on the supplied string (see --substitute).", "templateNames": "Returns the list of available templates names.", @@ -365,8 +365,8 @@ func (t *Template) run(command string, args ...interface{}) (result interface{}, if len(args) == 1 { if _, err := collections.TryAsDictionary(args[0]); err == nil { - // The arguments is a dictionnary and should have been processed by t.runTemplate, then we do - // not want to invoke the shell argument with the whole dictionnary as a parameter, + // The arguments is a dictionary and should have been processed by t.runTemplate, then we do + // not want to invoke the shell argument with the whole dictionary as a parameter, args = nil } } @@ -545,7 +545,7 @@ func (t *Template) ellipsis(function string, args ...interface{}) (interface{}, convertArg(lastArg.Index(i).Interface()) } - template := fmt.Sprintf("%s %s %s %s", t.delimiters[0], function, strings.Join(argsStr, " "), t.delimiters[1]) + template := fmt.Sprintf("%s %s %s %s", t.LeftDelim(), function, strings.Join(argsStr, " "), t.RightDelim()) result, _, err := t.runTemplate(template, context) if err != nil { split := strings.SplitN(err.Error(), ">: ", 2) diff --git a/template/extra_utils.go b/template/extra_utils.go index 6b850be..cb20a9c 100644 --- a/template/extra_utils.go +++ b/template/extra_utils.go @@ -84,7 +84,7 @@ var utilsFuncsHelp = descriptions{ Color can be prefixed by: Fg: Meaning foreground (Fg is assumed if not specified) - FgHi: Meaning high intensity forgeround + FgHi: Meaning high intensity foreground Bg: Meaning background" BgHi: Meaning high intensity background `)), diff --git a/template/func_table.go b/template/func_table.go index bd2d97b..1cb2b1d 100644 --- a/template/func_table.go +++ b/template/func_table.go @@ -202,7 +202,7 @@ type dictionary = map[string]interface{} type examples = map[string][]Example type groups = map[string]string -// AddFunctions add functions to the template, but keep a detailled definition of the function added for helping purpose +// AddFunctions add functions to the template, but keep a detailed definition of the function added for helping purpose func (t *Template) AddFunctions(funcs dictionary, group string, options FuncOptions) *Template { ft := make(funcTableMap, len(funcs)) help := defval(options[FuncHelp], descriptions{}).(descriptions) diff --git a/template/razor.go b/template/razor.go index a6c4d92..c37f2c6 100644 --- a/template/razor.go +++ b/template/razor.go @@ -17,8 +17,16 @@ func (t *Template) applyRazor(content []byte) (result []byte, changed bool) { t.ensureInit() for _, ignoredExpr := range t.ignoredRazorExpr { - content = regexp.MustCompile(t.delimiters[2]+ignoredExpr).ReplaceAllFunc(content, func(match []byte) []byte { - return []byte(strings.Replace(string(match), t.delimiters[2], literalAt, 1)) + ignoredExpr = strings.TrimSpace(ignoredExpr) + if ignoredExpr == "" { + continue + } + if strings.HasSuffix(ignoredExpr, "*") || strings.HasSuffix(ignoredExpr, "+") { + ignoredExpr += `?` // Ensure that the regex is not greedy + } + ignoredExpr = t.RazorDelim() + ignoredExpr + `\b` // Stop at the end of the expression + content = regexp.MustCompile(ignoredExpr).ReplaceAllFunc(content, func(match []byte) []byte { + return []byte(strings.Replace(string(match), t.RazorDelim(), literalAt, 1)) }) } @@ -156,10 +164,10 @@ func (t *Template) ensureInit() { replacements := make([]replacement, 0, len(expressions)) for _, expr := range expressions { comment := expr[0].(string) - re := strings.Replace(expr[1].(string), "@", regexp.QuoteMeta(t.delimiters[2]), -1) - re = strings.Replace(re, "{{", regexp.QuoteMeta(t.delimiters[0]), -1) - re = strings.Replace(re, "}}", regexp.QuoteMeta(t.delimiters[1]), -1) - replace := strings.Replace(strings.Replace(strings.Replace(expr[2].(string), "{{", t.delimiters[0], -1), "}}", t.delimiters[1], -1), "@", t.delimiters[2], -1) + re := strings.Replace(expr[1].(string), "@", regexp.QuoteMeta(t.RazorDelim()), -1) + re = strings.Replace(re, "{{", regexp.QuoteMeta(t.LeftDelim()), -1) + re = strings.Replace(re, "}}", regexp.QuoteMeta(t.RightDelim()), -1) + replace := strings.Replace(strings.Replace(strings.Replace(expr[2].(string), "{{", t.LeftDelim(), -1), "}}", t.RightDelim(), -1), "@", t.RazorDelim(), -1) var exprParser replacementFunc if len(expr) >= 4 { exprParser = expr[3].(replacementFunc) diff --git a/template/razor_test.go b/template/razor_test.go index ca2e4ee..f99624a 100644 --- a/template/razor_test.go +++ b/template/razor_test.go @@ -147,37 +147,89 @@ func TestIgnoredRazorExpression(t *testing.T) { name string code string context map[string]interface{} + strict bool ignored []string want string + err error }{ { "Sha256", "Hello @var1 @sha256", map[string]interface{}{"var1": "world"}, + false, []string{"sha256"}, "Hello world @sha256", + nil, + }, + { + "Sha256 with error", + "Hello @var1 @sha256 @sha256long", + map[string]interface{}{"var1": "world"}, + false, + []string{"sha256"}, + "Hello @var1 @sha256 @sha256long", + nil, + }, + { + "Sha256 with error strict", + "Hello @var1 @sha256 @sha256long", + map[string]interface{}{"var1": "world"}, + true, + []string{"sha256"}, + "Hello world @sha256 ", + fmt.Errorf("contains undefined value"), + }, + { + "Sha256", + "Hello @sha256 @sha256long", + map[string]interface{}{"sha256long": "it works"}, + false, + []string{"sha256"}, + "Hello @sha256 it works", + nil, + }, + { + "Empty ignored", + "Hello @sha256 @sha256long", + map[string]interface{}{"sha256long": "it works"}, + false, + []string{"sha256", "", " "}, + "Hello @sha256 it works", + nil, }, { "Real Sha256", "Hello @var1 public.ecr.aws/lambda/python:3.12-arm64@sha256:335461dca279eede475193ac3cfda992d2f7e632710f8d92cbb4fb6f439abc06", map[string]interface{}{"var1": "world"}, + false, []string{"sha256"}, "Hello world public.ecr.aws/lambda/python:3.12-arm64@sha256:335461dca279eede475193ac3cfda992d2f7e632710f8d92cbb4fb6f439abc06", + nil, }, { "Regex", "Hello @var1 @this_var_is_not_a_razor_one", map[string]interface{}{"var1": "world"}, + false, []string{"\\w*not_a_razor\\w+"}, "Hello world @this_var_is_not_a_razor_one", + nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { template := MustNewTemplate(".", tt.context, "", nil) + if tt.strict { + template.SetOption(StrictErrorCheck, true) + } template.IgnoreRazorExpression(tt.ignored...) - got, _ := template.ProcessContent(tt.code, tt.name) + got, err := template.ProcessContent(tt.code, tt.name) assert.Equal(t, tt.want, string(got)) + if tt.err != nil { + assert.ErrorContains(t, err, tt.err.Error()) + } else { + assert.NoError(t, err) + } }) } } @@ -511,7 +563,7 @@ func TestMultilineStringProtect(t *testing.T) { "`{{ add 1 2 }}`", }, { - "String withing expression", + "String within expression", "@func(`@(1+2)`)", "{{ func `@(1+2)` }}", }, @@ -633,3 +685,37 @@ func ExampleTemplate_IgnoreRazorExpression() { // Neither than @thisOne or @thatOne // And this @function("text", 1) won't be invoked while 5 will be } + +func ExampleTemplate_AppendIgnoreRazorExpression() { + code := "Hello, @Name! From @Author" + + context := map[string]string{ + "Name": "There", + "Author": "Obi-Wan Kenobi", + } + + template := MustNewTemplate(".", context, "", nil) + template.IgnoreRazorExpression("Name") + template.AppendIgnoreRazorExpression("Author") + result, err := template.ProcessContent(code, "Internal example") + if err != nil { + log.Fatalf("execution failed: %s", err) + } + fmt.Println("Ignored expressions:", template.GetIgnoredRazorExpressions()) + fmt.Println(result) + + // This reset the list of ignored expressions + template.IgnoreRazorExpression() + result, err = template.ProcessContent(code, "Internal example") + if err != nil { + log.Fatalf("execution failed: %s", err) + } + fmt.Println("Ignored expressions:", template.GetIgnoredRazorExpressions()) + fmt.Println(result) + + // Output: + // Ignored expressions: [Author Name] + // Hello, @Name! From @Author + // Ignored expressions: [] + // Hello, There! From Obi-Wan Kenobi +} diff --git a/template/template.go b/template/template.go index cbef814..070ebb8 100644 --- a/template/template.go +++ b/template/template.go @@ -1,55 +1,3 @@ -// Package template provides extended functionalities for the base Go template library. -// It includes additional features such as context management, custom delimiters, and -// environment variable configurations for template processing. -// -// The package imports several other packages to enhance its capabilities: -// - fmt, os, filepath, reflect, strings, sync, and text/template from the standard library. -// - collections and utils from github.com/coveooss/gotemplate/v3. -// - multicolor from github.com/coveooss/multilogger/color. -// -// The package defines several constants for environment variables that can override default behaviors: -// - EnvAcceptNoValue: Allows the template processor to accept variables with no value without throwing an error. -// - EnvStrictErrorCheck: Enables strict error checking. -// - EnvSubstitutes: Specifies regex replacements. -// - EnvDebug: Enables debug mode. -// - EnvExtensionPath: Specifies the path for template extensions. -// - EnvInternalLogLevel: Sets the internal log level. -// - EnvLogLevel: Sets the template log level. -// -// The Template struct extends the base template functionalities and includes fields for: -// - tempFolder: Temporary folder used by the template. -// - substitutes: List of regex replacements. -// - context: Template context. -// - constantKeys: List of constant keys. -// - delimiters: List of delimiters. -// - parent: Parent template. -// - folder: Template folder. -// - children: Map of child templates. -// - aliases: Function table map for aliases. -// - functions: Function table map for functions. -// - options: Set of template options. -// - optionsEnabled: Set of enabled template options. -// - ignoredRazorExpr: List of ignored Razor expressions. -// -// The package provides several functions and methods for template management: -// - IsRazor: Determines if the supplied code contains Razor code. -// - IsCode: Determines if the supplied code contains template code. -// - NewTemplate: Creates a new Template object with default initialization. -// - MustNewTemplate: Creates a new Template object and panics if an error occurs. -// - TempFolder: Sets the temporary folder for the template. -// - GetNewContext: Returns a distinct context for each folder. -// - LeftDelim: Returns the left delimiter. -// - RightDelim: Returns the right delimiter. -// - RazorDelim: Returns the Razor delimiter. -// - SetOption: Sets a template option after initialization. -// - initExtension: Initializes template extensions. -// - init: Initializes a new template with the same attributes as the current context. -// - setConstant: Sets a constant value in the template context. -// - importTemplates: Imports templates from another template. -// - Add: Adds a value to the template context. -// - Merge: Merges multiple values into the template context. -// - Context: Returns the template context as a dictionary. -// - IgnoreRazorExpression: Ignores specified Razor expressions. package template import ( @@ -60,6 +8,7 @@ import ( "strings" "sync" "text/template" + "unicode" "github.com/coveooss/gotemplate/v3/collections" "github.com/coveooss/gotemplate/v3/utils" @@ -96,6 +45,7 @@ const ( EnvAcceptNoValue = "GOTEMPLATE_NO_VALUE" EnvStrictErrorCheck = "GOTEMPLATE_STRICT_ERROR" EnvSubstitutes = "GOTEMPLATE_SUBSTITUTES" + EnvIgnoreRazor = "GOTEMPLATE_IGNORE_RAZOR" EnvDebug = "GOTEMPLATE_DEBUG" EnvExtensionPath = "GOTEMPLATE_PATH" EnvInternalLogLevel = "GOTEMPLATE_INTERNAL_LOG_LEVEL" @@ -137,7 +87,27 @@ func IsCode(code string) bool { return IsRazor(code) || strings.Contains(code, "{{") || strings.Contains(code, "}}") } -// NewTemplate creates an Template object with default initialization. +// NewTemplate creates a new Template instance with the provided parameters. +// It initializes the template with default or provided options, context, and substitutes. +// +// Parameters: +// - folder: The folder path where the template is located. +// - context: The context data to be used within the template. +// - delimiters: Custom delimiters for the template, separated by commas. +// - options: A set of options to configure the template behavior. +// - substitutes: Optional additional substitution patterns. +// +// Returns: +// - result: A pointer to the created Template instance. +// - err: An error if the template creation fails. +// +// The function handles panics by recovering and returning an error. +// It sets up default options, context, and substitutes if not provided. +// It also processes environment variables for substitutes (GOTEMPLATE_SUBSTITUTES) and ignore razor expressions (GOTEMPLATE_IGNORE_RAZOR). +// GOTEMPLATE_IGNORE_RAZOR can be a json, yaml or hcl array of strings or a string with comma, newline or space separated values. +// GOTEMPLATE_SUBSTITUTES must be a list of substitute pattern separated by newlines. +// +// Custom delimiters can be specified, with a maximum of three comma-separated parts. func NewTemplate(folder string, context interface{}, delimiters string, options OptionsSet, substitutes ...string) (result *Template, err error) { defer func() { if rec := recover(); rec != nil { @@ -170,6 +140,23 @@ func NewTemplate(folder string, context interface{}, delimiters string, options } t.substitutes = utils.InitReplacers(append(baseSubstitutesRegex, substitutes...)...) + if ignoreRazorFromEnv := os.Getenv(EnvIgnoreRazor); ignoreRazorFromEnv != "" { + var list []string + if unicode.IsLetter(rune(ignoreRazorFromEnv[0])) { + ignoreRazorFromEnv = strings.ReplaceAll(ignoreRazorFromEnv, "\r\n", ",") + ignoreRazorFromEnv = strings.ReplaceAll(ignoreRazorFromEnv, "\n", ",") + ignoreRazorFromEnv = strings.ReplaceAll(ignoreRazorFromEnv, " ", ",") + list = strings.Split(ignoreRazorFromEnv, ",") + } else { + var data []interface{} + if err := collections.ConvertData(ignoreRazorFromEnv, &data); err != nil { + return nil, fmt.Errorf("invalid value for %s: %v", EnvIgnoreRazor, ignoreRazorFromEnv) + } + list = collections.ToStrings(data) + } + t.AppendIgnoreRazorExpression(list...) + } + if t.options[Extension] { t.initExtension() } @@ -314,7 +301,7 @@ func (t *Template) init(folder string) { } t.addFuncs() t.children = make(map[string]*Template) - t.Delims(t.delimiters[0], t.delimiters[1]) + t.Delims(t.LeftDelim(), t.RightDelim()) t.setConstant(false, "\n", "NL", "CR", "NEWLINE") t.setConstant(false, true, "true") t.setConstant(false, false, "false") @@ -381,3 +368,19 @@ func (t *Template) Context() (result collections.IDictionary) { func (t *Template) IgnoreRazorExpression(expr ...string) { t.ignoredRazorExpr = expr } + +// AppendIgnoreRazorExpression appends one or more Razor expressions to the list of ignored Razor expressions. +// This allows the template to bypass processing for the specified expressions. +// +// Parameters: +// +// expr: A variadic parameter representing one or more Razor expressions to be ignored. +func (t *Template) AppendIgnoreRazorExpression(expr ...string) { + t.ignoredRazorExpr = append(expr, t.ignoredRazorExpr...) +} + +// GetIgnoredRazorExpressions returns a slice of strings containing the ignored Razor expressions. +// These expressions are not processed by the template engine. +func (t *Template) GetIgnoredRazorExpressions() []string { + return t.ignoredRazorExpr +} diff --git a/template/template_error_handler.go b/template/template_error_handler.go index 5213603..67241fc 100644 --- a/template/template_error_handler.go +++ b/template/template_error_handler.go @@ -10,7 +10,7 @@ import ( "github.com/fatih/color" ) -// TemplateErrorHandler handle errors occuring during template evaluation and try to mitigate the error +// TemplateErrorHandler handle errors occurring during template evaluation and try to mitigate the error // and continuing evaluation in order to return all potential errors instead of stopping after the first one type errorHandler struct { *Template diff --git a/template/template_handler.go b/template/template_handler.go index b76fdf2..f94357c 100644 --- a/template/template_handler.go +++ b/template/template_handler.go @@ -68,7 +68,7 @@ func (t *Template) processTemplate(template, sourceFolder, targetFolder string, return } - // Avoid addind an extra blank line if the result already ends with a newline + // Avoid adding an extra blank line if the result already ends with a newline if !strings.HasSuffix(result, "\n") { Println(result) } else { diff --git a/utils/exec.go b/utils/exec.go index 5418fa7..b9750dd 100644 --- a/utils/exec.go +++ b/utils/exec.go @@ -18,7 +18,7 @@ func IsShebangScript(content string) bool { return len(matches) > 0 && matches[1] != "" } -// ScriptParts splits up the supplied content into program, subprogram and source if the content matches Shebang defintion +// ScriptParts splits up the supplied content into program, subprogram and source if the content matches Shebang definition func ScriptParts(content string) (program, subprogram, source string) { matches := shebang.FindStringSubmatch(strings.TrimSpace(content)) if len(matches) > 0 { @@ -50,7 +50,7 @@ func GetCommandFromFile(filename string, args ...interface{}) (cmd *exec.Cmd, er } } else if _, errPath := exec.LookPath(command); errPath != nil { if strings.Contains(command, " ") { - // The command is a string that should be splitted up into several parts + // The command is a string that should be split up into several parts split := strings.Split(command, " ") command = split[0] strArgs = append(split[1:], strArgs...) diff --git a/xml/generated_test.go b/xml/generated_test.go index 459f022..b89dc81 100644 --- a/xml/generated_test.go +++ b/xml/generated_test.go @@ -701,7 +701,7 @@ func Test_dict_Default(t *testing.T) { {"Empty", nil, args{"Foo", "Bar"}, "Bar"}, {"Map int", dictFixture, args{"int", 1}, 123}, {"Map float", dictFixture, args{"float", 1}, 1.23}, - {"Map Non existant", dictFixture, args{"Foo", "Bar"}, "Bar"}, + {"Map Non existent", dictFixture, args{"Foo", "Bar"}, "Bar"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -726,9 +726,9 @@ func Test_dict_Delete(t *testing.T) { }{ {"Empty", nil, args{}, xmlDict{}, "key not found"}, {"Map", dictFixture, args{}, dictFixture, "key not found"}, - {"Non existant key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, + {"Non existent key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, {"Map with keys", dictFixture, args{"int", []interface{}{"list"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), ""}, - {"Map with keys + non existant", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, + {"Map with keys + non existent", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -754,9 +754,9 @@ func Test_dict_Flush(t *testing.T) { }{ {"Empty", nil, nil, xmlDict{}}, {"Map", dictFixture, nil, xmlDict{}}, - {"Non existant key", dictFixture, []interface{}{"Test"}, dictFixture}, + {"Non existent key", dictFixture, []interface{}{"Test"}, dictFixture}, {"Map with keys", dictFixture, []interface{}{"int", "list"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, - {"Map with keys + non existant", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, + {"Map with keys + non existent", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/xml/xml.go b/xml/xml.go index 3bdf87a..8cd2afe 100644 --- a/xml/xml.go +++ b/xml/xml.go @@ -36,7 +36,7 @@ func (d xmlDict) PrettyPrint() string { // func init() { collections.TypeConverters["xml"] = Unmarshal } // Unmarshal calls the native Unmarshal but transform the results -// to returns Dictionary and GenerecList instead of go native collections. +// to returns Dictionary and GenericList instead of go native collections. func Unmarshal(data []byte, out interface{}) (err error) { if err = NativeUnmarshal(data, out); err != nil { return diff --git a/yaml/generated_test.go b/yaml/generated_test.go index f655484..654f4ce 100644 --- a/yaml/generated_test.go +++ b/yaml/generated_test.go @@ -701,7 +701,7 @@ func Test_dict_Default(t *testing.T) { {"Empty", nil, args{"Foo", "Bar"}, "Bar"}, {"Map int", dictFixture, args{"int", 1}, 123}, {"Map float", dictFixture, args{"float", 1}, 1.23}, - {"Map Non existant", dictFixture, args{"Foo", "Bar"}, "Bar"}, + {"Map Non existent", dictFixture, args{"Foo", "Bar"}, "Bar"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -726,9 +726,9 @@ func Test_dict_Delete(t *testing.T) { }{ {"Empty", nil, args{}, yamlDict{}, "key not found"}, {"Map", dictFixture, args{}, dictFixture, "key not found"}, - {"Non existant key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, + {"Non existent key", dictFixture, args{"Test", nil}, dictFixture, "key Test not found"}, {"Map with keys", dictFixture, args{"int", []interface{}{"list"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), ""}, - {"Map with keys + non existant", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, + {"Map with keys + non existent", dictFixture, args{"int", []interface{}{"list", "Test"}}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt"), "key Test not found"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -754,9 +754,9 @@ func Test_dict_Flush(t *testing.T) { }{ {"Empty", nil, nil, yamlDict{}}, {"Map", dictFixture, nil, yamlDict{}}, - {"Non existant key", dictFixture, []interface{}{"Test"}, dictFixture}, + {"Non existent key", dictFixture, []interface{}{"Test"}, dictFixture}, {"Map with keys", dictFixture, []interface{}{"int", "list"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, - {"Map with keys + non existant", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, + {"Map with keys + non existent", dictFixture, []interface{}{"int", "list", "Test"}, dictFixture.Clone("float", "string", "listInt", "map", "mapInt")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {