Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a mechanism to ignore some patterns in gotemplate/razor #259

Merged
merged 9 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
if: github.ref == 'refs/heads/master'
uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3
with:
hugo-version: "0.82.0"
hugo-version: "0.135.0"
extended: true

- name: Build Documentation
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Hugo
uses: peaceiris/actions-hugo@75d2e84710de30f6ff7268e08f310b60ef14033f # v3
with:
hugo-version: "0.82.0"
hugo-version: "0.135.0"
extended: true

- name: Test Documentation
Expand Down
Empty file added docs/.hugo_build.lock
Empty file.
2 changes: 1 addition & 1 deletion docs/themes/book
Submodule book updated 233 files
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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()
templates = run.Arg("templates", "Template files or commands to process").Strings()

list = app.Command("list", "Get detailed help on gotemplate functions").NoAutoShortcut()
Expand Down Expand Up @@ -257,6 +258,10 @@ func runGotemplate() (exitCode int) {
}
t.TempFolder(tempFolder)

if len(*ignoreRazor) > 0 {
t.IgnoreRazorExpression(*ignoreRazor...)
}

if command == list.FullCommand() {
if !(*listFunctions || *listTemplates) {
// If neither list functions or templates is selected, we default to list functions
Expand Down
1 change: 1 addition & 0 deletions render-doc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ function cleanup {
trap cleanup EXIT
go build -o gotemplate

export GOTEMPLATE_NO_STDIN=1
CONTENT_FOLDER="docs/content"
DOC_FOLDER="$CONTENT_FOLDER/docs"
export COLUMNS=108
Expand Down
6 changes: 6 additions & 0 deletions template/razor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ 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))
})
}

for _, r := range replacementsInit[fmt.Sprint(t.delimiters)] {
printDebugInfo(r, string(content))
if r.parser == nil {
Expand Down
74 changes: 74 additions & 0 deletions template/razor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,47 @@ func TestBase(t *testing.T) {
}
}

func TestIgnoredRazorExpression(t *testing.T) {
t.Parallel()
tests := []struct {
name string
code string
context map[string]interface{}
ignored []string
want string
}{
{
"Sha256",
"Hello @var1 @sha256",
map[string]interface{}{"var1": "world"},
[]string{"sha256"},
"Hello world @sha256",
},
{
"Real Sha256",
"Hello @var1 public.ecr.aws/lambda/python:3.12-arm64@sha256:335461dca279eede475193ac3cfda992d2f7e632710f8d92cbb4fb6f439abc06",
map[string]interface{}{"var1": "world"},
[]string{"sha256"},
"Hello world public.ecr.aws/lambda/python:3.12-arm64@sha256:335461dca279eede475193ac3cfda992d2f7e632710f8d92cbb4fb6f439abc06",
},
{
"Regex",
"Hello @var1 @this_var_is_not_a_razor_one",
map[string]interface{}{"var1": "world"},
[]string{"\\w*not_a_razor\\w+"},
"Hello world @this_var_is_not_a_razor_one",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
template := MustNewTemplate(".", tt.context, "", nil)
template.IgnoreRazorExpression(tt.ignored...)
got, _ := template.ProcessContent(tt.code, tt.name)
assert.Equal(t, tt.want, string(got))
})
}
}

func TestInvocation(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -559,3 +600,36 @@ func TestReservedKeywords(t *testing.T) {
})
}
}

var log = multilogger.New("example")

func ExampleTemplate_IgnoreRazorExpression() {
code := []string{
"Hello, @Name! From @Author",
"This @variable should not be changed.",
"Neither than @thisOne or @thatOne",
`And this @function("text", 1) won't be invoked while @add(2, 3) will be`,
}

context := map[string]string{
"Name": "There",
"Author": "Obi-Wan Kenobi",
}
template := MustNewTemplate(".", context, "", nil)
template.IgnoreRazorExpression(
"variable", // Work with full variable name
`th(is|at)One`, // or with a regular expression
`function`, // or with a function
)
result, err := template.ProcessContent(strings.Join(code, "\n"), "Internal example")
if err != nil {
log.Fatalf("execution failed: %s", err)
}
fmt.Println(result)

// Output:
// Hello, There! From Obi-Wan Kenobi
// This @variable should not be changed.
// Neither than @thisOne or @thatOne
// And this @function("text", 1) won't be invoked while 5 will be
}
111 changes: 92 additions & 19 deletions template/template.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,55 @@
// 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 (
Expand All @@ -22,21 +74,24 @@ var templateMutex sync.Mutex
// Template let us extend the functionalities of base go template library.
type Template struct {
*template.Template
tempFolder string
substitutes []utils.RegexReplacer
context interface{}
constantKeys []interface{}
delimiters []string
parent *Template
folder string
children map[string]*Template
aliases funcTableMap
functions funcTableMap
options OptionsSet
optionsEnabled OptionsSet
tempFolder string
substitutes []utils.RegexReplacer
context interface{}
constantKeys []interface{}
delimiters []string
parent *Template
folder string
children map[string]*Template
aliases funcTableMap
functions funcTableMap
options OptionsSet
optionsEnabled OptionsSet
ignoredRazorExpr []string
}

// Environment variables that could be defined to override default behaviors.
// EnvAcceptNoValue is an environment variable that, when set, allows the template
// processor to accept variables with no value without throwing an error.
const (
EnvAcceptNoValue = "GOTEMPLATE_NO_VALUE"
EnvStrictErrorCheck = "GOTEMPLATE_STRICT_ERROR"
Expand Down Expand Up @@ -186,16 +241,22 @@ func (t *Template) IsRazor(code string) bool {
return strings.Contains(code, t.RazorDelim()) && !strings.Contains(code, noGoTemplate) && !strings.Contains(code, noRazor)
}

// LeftDelim returns the left delimiter.
// LeftDelim returns the left delimiter used in the template.
// It retrieves the first element from the delimiters slice.
func (t *Template) LeftDelim() string { return t.delimiters[0] }

// RightDelim returns the right delimiter.
// RightDelim returns the right delimiter used in the template.
// It retrieves the second element from the delimiters slice.
func (t *Template) RightDelim() string { return t.delimiters[1] }

// RazorDelim returns the razor delimiter.
// RazorDelim returns the third delimiter from the Template's delimiters slice.
// This delimiter is typically used for Razor-style templating.
func (t *Template) RazorDelim() string { return t.delimiters[2] }

// SetOption allows setting of template option after initialization.
// SetOption sets the specified option to the given boolean value in the Template's options map.
// Parameters:
// - option: The option to be set.
// - value: The boolean value to set for the specified option.
func (t *Template) SetOption(option Options, value bool) { t.options[option] = value }

func (t *Template) isTemplate(file string) bool {
Expand Down Expand Up @@ -290,21 +351,33 @@ func (t *Template) importTemplates(source *Template) {
}

// Add allows adding a value to the template context.
// The context must be a dictionnary to use that function, otherwise, it will panic.
// The context must be a dictionary to use that function, otherwise, it will panic.
func (t *Template) Add(key string, value interface{}) {
collections.AsDictionary(t.context).Add(key, value)
}

// Merge allows adding multiple values to the template context.
// The context and values must both be dictionnary to use that function, otherwise, it will panic.
// The context and values must both be dictionary to use that function, otherwise, it will panic.
func (t *Template) Merge(values interface{}) {
collections.AsDictionary(t.context).Add(key, collections.AsDictionary(values))
}

// Context returns the template context as a dictionnary if possible, otherwise, it returns null.
// Context returns the context of the Template as a collections.IDictionary.
// It provides access to the underlying data structure that holds the template's context.
func (t *Template) Context() (result collections.IDictionary) {
if result, _ = collections.TryAsDictionary(t.context); result == nil {
result = collections.CreateDictionary()
}
return
}

// IgnoreRazorExpression sets the list of Razor expressions to be ignored by the template.
// This method accepts a variadic number of string arguments, allowing multiple expressions
// to be specified at once.
//
// Parameters:
//
// expr: A variadic list of strings representing the Razor expressions to ignore (can also be a regular expression).
func (t *Template) IgnoreRazorExpression(expr ...string) {
t.ignoredRazorExpr = expr
}