diff --git a/.circleci/config.yml b/.circleci/config.yml index fedba9ea4..ed9f399d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ parameters: defaults: &defaults resource_class: large docker: - - image: bepsays/ci-hugoreleaser:1.22600.20500 + - image: bepsays/ci-hugoreleaser:1.22700.20000 environment: &buildenv GOMODCACHE: /root/project/gomodcache version: 2 @@ -58,7 +58,7 @@ jobs: environment: <<: [*buildenv] docker: - - image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20500 + - image: bepsays/ci-hugoreleaser-linux-arm64:1.22700.20000 steps: - *restore-cache - &attach-workspace diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c104cf5e..8646415e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: test: strategy: matrix: - go-version: [1.26.x] + go-version: [1.27.x] os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/Dockerfile b/Dockerfile index ee9f9cc55..dc8926f26 100755 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,8 @@ # Twitter: https://twitter.com/gohugoio # Website: https://gohugo.io/ -ARG GO_VERSION="1.26" -ARG ALPINE_VERSION="3.22" +ARG GO_VERSION="1.27" +ARG ALPINE_VERSION="3.24" ARG DART_SASS_VERSION="1.79.3" FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx diff --git a/common/herrors/errors.go b/common/herrors/errors.go index 5e223f42f..551e74401 100644 --- a/common/herrors/errors.go +++ b/common/herrors/errors.go @@ -178,3 +178,14 @@ func improveIfNilPointerMsg(inErr error) string { s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field) return nilPointerErrRe.ReplaceAllString(inErr.Error(), s) } + +// Or returns the first non-nil error from the given list of errors. +// If all errors are nil, it returns nil. +func Or(errs ...error) error { + for _, err := range errs { + if err != nil { + return err + } + } + return nil +} diff --git a/common/hugio/copy.go b/common/hugio/copy.go index 31d679dfc..18c2000d3 100644 --- a/common/hugio/copy.go +++ b/common/hugio/copy.go @@ -82,6 +82,9 @@ func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool return err } } else { + if shouldCopy != nil && !shouldCopy(fromFilename) { + continue + } if err := CopyFile(fs, fromFilename, toFilename); err != nil { return err } diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 5613b177c..1a941ba1b 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -1017,15 +1017,20 @@ func newDefaultConfig() *Config { Timeout: "60s", CommonDirs: config.CommonDirs{ + //lint:ignore SA1019 Keep as adapter for now. ArcheTypeDir: "archetypes", ContentDir: "content", ResourceDir: "resources", PublishDir: "public", ThemesDir: "themes", - AssetDir: "assets", - LayoutDir: "layouts", - I18nDir: "i18n", - DataDir: "data", + //lint:ignore SA1019 Keep as adapter for now. + AssetDir: "assets", + //lint:ignore SA1019 Keep as adapter for now. + LayoutDir: "layouts", + //lint:ignore SA1019 Keep as adapter for now. + I18nDir: "i18n", + //lint:ignore SA1019 Keep as adapter for now. + DataDir: "data", }, }, } diff --git a/go.mod b/go.mod index e10b249d2..db3f24bcf 100644 --- a/go.mod +++ b/go.mod @@ -186,4 +186,4 @@ require ( software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect ) -go 1.26.0 +go 1.27.0 diff --git a/hugolib/content_map_page_assembler.go b/hugolib/content_map_page_assembler.go index 54cff0fc3..8fb0fbd34 100644 --- a/hugolib/content_map_page_assembler.go +++ b/hugolib/content_map_page_assembler.go @@ -14,7 +14,6 @@ package hugolib import ( - "cmp" "context" "fmt" "path" @@ -23,6 +22,7 @@ import ( "github.com/bep/helpers/maphelpers" "github.com/gohugoio/go-radix" + "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs/files" @@ -134,7 +134,7 @@ func (a *allPagesAssembler) createAllPages() error { }() } - if err := cmp.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil { + if err := herrors.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil { return err } if err := a.pwRoot.WalkContext.HandleEventsAndHooks(); err != nil { diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index 6898e825b..b7ecbe25a 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -1179,23 +1179,7 @@ func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filen t.Helper() filename = filepath.Clean(filename) b, err := afero.ReadFile(fs, filename) - if err != nil { - // Print some debug info - hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator) - start := 0 - if hadSlash { - start = 1 - } - end := start + 1 - - parts := strings.Split(filename, helpers.FilePathSeparator) - if parts[start] == "work" { - end++ - } - - s.Assert(err, qt.IsNil) - - } + s.Assert(err, qt.IsNil) return string(b) } diff --git a/hugolib/sitesmatrix/vectorstores.go b/hugolib/sitesmatrix/vectorstores.go index b865bea65..574afcdf3 100644 --- a/hugolib/sitesmatrix/vectorstores.go +++ b/hugolib/sitesmatrix/vectorstores.go @@ -14,7 +14,6 @@ package sitesmatrix import ( - "cmp" "fmt" "iter" "maps" @@ -780,7 +779,7 @@ func (b *IntSetsBuilder) Build() *IntSets { } func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder { - applyFilter := func(what string, values []string, matcher ConfiguredDimension) (*hmaps.OrderedIntSet, error) { + applyFilter := func(what string, values []string, matcher ConfiguredDimension) *hmaps.OrderedIntSet { var result *hmaps.OrderedIntSet if len(values) == 0 { @@ -800,16 +799,16 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder { } } - return result, nil + return result } filter, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(values, matcher.ResolveIndex, hglob.GetGlobDot) if err != nil { - return nil, fmt.Errorf("failed to create filter for %s: %w", what, err) + panic(fmt.Errorf("failed to create filter for %s: %w", what, err)) } iter, err := matcher.IndexMatch(filter) if err != nil { - return nil, fmt.Errorf("failed to match %s %q: %w", what, values, err) + panic(fmt.Errorf("failed to match %s %q: %w", what, values, err)) } for i := range iter { if result == nil { @@ -818,16 +817,12 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder { result.Set(i) } - return result, nil + return result } - l, err1 := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages) - v, err2 := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions) - r, err3 := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles) - - if err := cmp.Or(err1, err2, err3); err != nil { - panic(fmt.Errorf("failed to apply filters: %w", err)) - } + l := applyFilter("languages", cfg.Globs.Languages, b.cfg.ConfiguredLanguages) + v := applyFilter("versions", cfg.Globs.Versions, b.cfg.ConfiguredVersions) + r := applyFilter("roles", cfg.Globs.Roles, b.cfg.ConfiguredRoles) b.GlobFilterMisses = Bools{ len(cfg.Globs.Languages) > 0 && l == nil, diff --git a/internal/warpc/warpc.go b/internal/warpc/warpc.go index be8e48bb5..59318eb1d 100644 --- a/internal/warpc/warpc.go +++ b/internal/warpc/warpc.go @@ -582,6 +582,9 @@ func (p *dispatcherPool[Q, R]) Err() error { } } +// Workaround for data race, see https://github.com/wazero/wazero/issues/2532 +var wazeroCacheMu sync.Mutex + func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { if opts.Ctx == nil { opts.Ctx = context.Background() @@ -610,6 +613,8 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { runtimeConfig = runtimeConfig.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesThreads) if opts.CompilationCacheDir != "" { + wazeroCacheMu.Lock() + defer wazeroCacheMu.Unlock() compilationCache, err := wazero.NewCompilationCacheWithDir(opts.CompilationCacheDir) if err != nil { return nil, err @@ -766,7 +771,9 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { } for _, d := range dp.dispatchers { - if err := d.inGroup.Wait(); err != nil { + // ErrShutdown is expected here; since Go 1.27 (json/v2), Decode + // surfaces the read error from the pipes we just closed. + if err := d.inGroup.Wait(); err != nil && err != ErrShutdown { return err } } diff --git a/resources/images/testdata/images_golden/filters/mask/transparant.png b/resources/images/testdata/images_golden/filters/mask/transparant.png index c8f45165e..5d103ea5f 100644 Binary files a/resources/images/testdata/images_golden/filters/mask/transparant.png and b/resources/images/testdata/images_golden/filters/mask/transparant.png differ diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png index 44af25303..1c1c980f2 100644 Binary files a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x-fc03ec.png differ diff --git a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png index 9bca47b14..5f18b8f17 100644 Binary files a/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png and b/resources/images/testdata/images_golden/methods/resize-gopherpng-100x.png differ diff --git a/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png index ec5042e67..b821ece6e 100644 Binary files a/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png and b/resources/images/testdata/images_golden/process/misc/resize-100x100-r180.png differ diff --git a/resources/images/testdata/images_golden/process/webp/png.png b/resources/images/testdata/images_golden/process/webp/png.png index b1f5eb89b..04d591b08 100644 Binary files a/resources/images/testdata/images_golden/process/webp/png.png and b/resources/images/testdata/images_golden/process/webp/png.png differ diff --git a/resources/post_publish.go b/resources/post_publish.go index 848755782..c8e59a612 100644 --- a/resources/post_publish.go +++ b/resources/post_publish.go @@ -26,10 +26,6 @@ type transformationKeyer interface { func (spec *Spec) PostProcess(r resource.Resource) (postpub.PostPublishedResource, error) { key := r.(transformationKeyer).TransformationKey() return spec.PostProcessResources.GetOrCreate(key, func() (postpub.PostPublishedResource, error) { - result := postpub.NewPostPublishResource(spec.incr.Incr(), r) - if result == nil { - panic("got nil result") - } - return result, nil + return postpub.NewPostPublishResource(spec.incr.Incr(), r), nil }) } diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index b1af8621a..68890636d 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -15,8 +15,17 @@ import ( ) func main() { - // The current is built with 2dc996f71b0ebafb77e64433e58333e049488a3c go1.26.3 - // TODO(bep) preserve the staticcheck.conf file. + /* + Previously: with 2dc996f71b0ebafb77e64433e58333e049488a3c go1.26.3 + Current: 8af21751f0 [release-branch.go1.27] go1.27.0 + + Note that the upgrade here is mostly automatic, but: + + * testenv.go is a stubbed Hugo version and is never overwritten; if the template tests start using new helpers from it, stub them in by hand. + * Some of the replacements below match exact upstream source; if upstream drifts, the build breaks and they need updating. + * Some test code depends on Go internals we don't fork; remove or stub these out to make it build. + * We're only patching the execution part of the template packages, so it's also good to check the execution package's Git history to check for valuable changes in our patched files. + */ fmt.Println("Forking ...") defer fmt.Println("Done ...") @@ -25,7 +34,7 @@ func main() { htmlRoot := filepath.Join(forkRoot, "htmltemplate") for _, pkg := range goPackages { - copyGoPackage(pkg.dstPkg, pkg.srcPkg) + copyGoPackage(pkg.dstPkg, pkg.srcPkg, pkg.skip) } for _, pkg := range goPackages { @@ -37,7 +46,6 @@ func main() { } const ( - // TODO(bep) goSource = "/Users/bep/dev/go/misc/go/src" forkRoot = "../../tpl/internal/go_templates" ) @@ -47,6 +55,7 @@ type goPackage struct { dstPkg string replacer func(name, content string) string rewriter func(name string) + skip func(name string) bool } var ( @@ -55,12 +64,14 @@ var ( `"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`, `"internal/testenv"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`, "TestLinkerGC", "_TestLinkerGC", + "{new(int), true},", "//{new(int), true}, // Commented out for Hugo. We have a slightly different view on ... the truth.", // Rename types and function that we want to overload. "type state struct", "type stateOld struct", "func (s *state) evalFunction", "func (s *state) evalFunctionOld", "func (s *state) evalField(", "func (s *state) evalFieldOld(", "func (s *state) evalCall(", "func (s *state) evalCallOld(", "func (s *state) walkTemplate(", "func (s *state) walkTemplateOld(", + "func (s *state) validateType(", "func (s *state) _validateType(", "func isTrue(val reflect.Value) (truth, ok bool) {", "func isTrueOld(val reflect.Value) (truth, ok bool) {", ) @@ -74,26 +85,26 @@ var ( "\"text/template\"\n", "template \"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate\"\n", `"html/template"`, `htmltemplate "html/template"`, `"fmt"`, `htmltemplate "html/template"`, + `"internal/testenv"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`, + // Renamed so hugo_template.go can wrap it. + "func indirect(", "func doIndirect(", `t.Skip("this test currently fails with -race; see issue #39807")`, `// t.Skip("this test currently fails with -race; see issue #39807")`, ) + + // We don't want the internal/godebug dependency; meta content URL escaping is always on. + escapeGodebugReplacers = strings.NewReplacer( + `var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")`, ``, + `var htmlmetacontenturlescape = godebug.New("htmlmetacontenturlescape")`, `var htmlmetacontenturlescape = true`, + `if htmlmetacontenturlescape.Value() != "0" {`, `if htmlmetacontenturlescape {`, + ) ) func commonReplace(name, content string) string { if strings.HasSuffix(name, "_test.go") { - content = strings.Replace(content, "package template\n", `// +build go1.13,!windows + content = strings.Replace(content, "package template\n", `//go:build !windows package template `, 1) - content = strings.Replace(content, "package template_test\n", `// +build go1.13 - -package template_test -`, 1) - - content = strings.Replace(content, "package parse\n", `// +build go1.13 - -package parse -`, 1) - } return content @@ -111,6 +122,17 @@ var goPackages = []goPackage{ content = removeAll(`(?s)// Strings of content.*?\)\n`, content) } + if strings.HasSuffix(name, "escape.go") { + content = escapeGodebugReplacers.Replace(content) + // Drop the else branch calling IncNonDefault; goimports removes the then-unused godebug import. + content = removeAll(` else \{\n(?:\t*//.*\n)*\t*htmlmetacontenturlescape\.IncNonDefault\(\)\n\t*\}`, content) + } + + if strings.HasSuffix(name, "escape_test.go") { + // Tests the GODEBUG=htmlmetacontenturlescape=0 path, which we hard code to on. + content = removeAll(`(?s)func TestMetaContentEscapeGODEBUG.*?\n\}\n`, content) + } + content = commonReplace(name, content) return htmlTemplateReplacers.Replace(content) @@ -130,6 +152,11 @@ var goPackages = []goPackage{ replacer: func(name, content string) string { return testEnvReplacers.Replace(content) }, rewriter: func(name string) { rewrite(name, `"internal/testenv" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"`) }, + // testenv.go is a heavily stubbed Hugo version; keep it. The tests test the stubbed away parts. + skip: func(name string) bool { + base := filepath.Base(name) + return base == "testenv.go" || base == "testenv_test.go" + }, }, {srcPkg: "internal/cfg", dstPkg: "cfg", rewriter: func(name string) { rewrite(name, `"internal/cfg" -> "github.com/gohugoio/hugo/tpl/internal/go_templates/cfg"`) @@ -140,10 +167,18 @@ var fs = afero.NewOsFs() // Removes all non-Hugo files in the go_templates folder. func cleanFork() { + keepRe := regexp.MustCompile(`(?i)hugo|staticcheck\.conf|^testenv\.go$`) must(filepath.Walk(filepath.Join(forkRoot), func(path string, info os.FileInfo, err error) error { - if !info.IsDir() && len(path) > 10 && !strings.Contains(path, "hugo") { - must(fs.Remove(path)) + if info.IsDir() || len(path) <= 10 { + return nil } + + if keepRe.MatchString(info.Name()) { + return nil + } + + must(fs.Remove(path)) + return nil })) } @@ -154,11 +189,11 @@ func must(err error, what ...string) { } } -func copyGoPackage(dst, src string) { +func copyGoPackage(dst, src string, skip func(name string) bool) { from := filepath.Join(goSource, src) to := filepath.Join(forkRoot, dst) fmt.Println("Copy", from, "to", to) - must(hugio.CopyDir(fs, from, to, func(s string) bool { return true })) + must(hugio.CopyDir(fs, from, to, func(s string) bool { return skip == nil || !skip(s) })) } func doWithGoFiles(dir string, diff --git a/tpl/internal/go_templates/htmltemplate/attr_string.go b/tpl/internal/go_templates/htmltemplate/attr_string.go index 7159fa9cb..009458f42 100644 --- a/tpl/internal/go_templates/htmltemplate/attr_string.go +++ b/tpl/internal/go_templates/htmltemplate/attr_string.go @@ -22,8 +22,9 @@ const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcsetat var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58, 73} func (i attr) String() string { - if i >= attr(len(_attr_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_attr_index)-1 { return "attr(" + strconv.FormatInt(int64(i), 10) + ")" } - return _attr_name[_attr_index[i]:_attr_index[i+1]] + return _attr_name[_attr_index[idx]:_attr_index[idx+1]] } diff --git a/tpl/internal/go_templates/htmltemplate/clone_test.go b/tpl/internal/go_templates/htmltemplate/clone_test.go index e42177a6b..a3d61b3ab 100644 --- a/tpl/internal/go_templates/htmltemplate/clone_test.go +++ b/tpl/internal/go_templates/htmltemplate/clone_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template diff --git a/tpl/internal/go_templates/htmltemplate/content_test.go b/tpl/internal/go_templates/htmltemplate/content_test.go index e886ee8ff..60648ec30 100644 --- a/tpl/internal/go_templates/htmltemplate/content_test.go +++ b/tpl/internal/go_templates/htmltemplate/content_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template @@ -428,7 +427,7 @@ func TestStringer(t *testing.T) { if err := tmpl.Execute(b, s); err != nil { t.Fatal(err) } - expect := "string=3" + var expect = "string=3" if b.String() != expect { t.Errorf("expected %q got %q", expect, b.String()) } diff --git a/tpl/internal/go_templates/htmltemplate/css_test.go b/tpl/internal/go_templates/htmltemplate/css_test.go index f2b2add3a..6626bdda5 100644 --- a/tpl/internal/go_templates/htmltemplate/css_test.go +++ b/tpl/internal/go_templates/htmltemplate/css_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template diff --git a/tpl/internal/go_templates/htmltemplate/delim_string.go b/tpl/internal/go_templates/htmltemplate/delim_string.go index 8d8285022..eb0c3922e 100644 --- a/tpl/internal/go_templates/htmltemplate/delim_string.go +++ b/tpl/internal/go_templates/htmltemplate/delim_string.go @@ -19,8 +19,9 @@ const _delim_name = "delimNonedelimDoubleQuotedelimSingleQuotedelimSpaceOrTagEnd var _delim_index = [...]uint8{0, 9, 25, 41, 59} func (i delim) String() string { - if i >= delim(len(_delim_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_delim_index)-1 { return "delim(" + strconv.FormatInt(int64(i), 10) + ")" } - return _delim_name[_delim_index[i]:_delim_index[i+1]] + return _delim_name[_delim_index[idx]:_delim_index[idx+1]] } diff --git a/tpl/internal/go_templates/htmltemplate/element_string.go b/tpl/internal/go_templates/htmltemplate/element_string.go index bdf9da7b9..3ccaebcdf 100644 --- a/tpl/internal/go_templates/htmltemplate/element_string.go +++ b/tpl/internal/go_templates/htmltemplate/element_string.go @@ -21,8 +21,9 @@ const _element_name = "elementNoneelementScriptelementStyleelementTextareaelemen var _element_index = [...]uint8{0, 11, 24, 36, 51, 63, 74} func (i element) String() string { - if i >= element(len(_element_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_element_index)-1 { return "element(" + strconv.FormatInt(int64(i), 10) + ")" } - return _element_name[_element_index[i]:_element_index[i+1]] + return _element_name[_element_index[idx]:_element_index[idx+1]] } diff --git a/tpl/internal/go_templates/htmltemplate/escape.go b/tpl/internal/go_templates/htmltemplate/escape.go index a0b6dd79a..5710fa7b5 100644 --- a/tpl/internal/go_templates/htmltemplate/escape.go +++ b/tpl/internal/go_templates/htmltemplate/escape.go @@ -8,7 +8,6 @@ import ( "bytes" "fmt" "html" - //"internal/godebug" "io" "maps" "regexp" @@ -165,9 +164,7 @@ func (e *escaper) escape(c context, n parse.Node) context { panic("escaping " + n.String() + " is unimplemented") } -//var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp") - -var htmlmetacontenturlescape = true //godebug.New("htmlmetacontenturlescape") +var htmlmetacontenturlescape = true // escapeAction escapes an action template node. func (e *escaper) escapeAction(c context, n *parse.ActionNode) context { diff --git a/tpl/internal/go_templates/htmltemplate/escape_test.go b/tpl/internal/go_templates/htmltemplate/escape_test.go index de75a6981..7759d060f 100644 --- a/tpl/internal/go_templates/htmltemplate/escape_test.go +++ b/tpl/internal/go_templates/htmltemplate/escape_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template @@ -1866,7 +1865,7 @@ func TestEscapeText(t *testing.T) { }, { "`, + input: "a.b", + want: ``, + }, + { + name: "regexp after close brace", + tmpl: ``, + input: "a.b", + want: ``, + }, + { + name: "regexp pathological attacker input", + tmpl: ``, + input: `./;alert(1);var q=/.`, + want: ``, + }, + { + name: "regexp after open brace in template literal", + tmpl: "", + input: "a.b", + want: "", + }, + { + name: "regexp after close brace in template literal", + tmpl: "", + input: "a.b", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpl := Must(New("test").Parse(tt.tmpl)) + var buf bytes.Buffer + if err := tmpl.Execute(&buf, tt.input); err != nil { + t.Fatalf("Execute: %v", err) + } + if got := buf.String(); got != tt.want { + t.Errorf("got: %s\nwant: %s", got, tt.want) + } + }) + } +} diff --git a/tpl/internal/go_templates/htmltemplate/example_test.go b/tpl/internal/go_templates/htmltemplate/example_test.go index 6485c7cfb..1fb06c633 100644 --- a/tpl/internal/go_templates/htmltemplate/example_test.go +++ b/tpl/internal/go_templates/htmltemplate/example_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package template_test import ( diff --git a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go index 43cc3bf01..24b22d984 100644 --- a/tpl/internal/go_templates/htmltemplate/examplefiles_test.go +++ b/tpl/internal/go_templates/htmltemplate/examplefiles_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package template_test import ( diff --git a/tpl/internal/go_templates/htmltemplate/exec_test.go b/tpl/internal/go_templates/htmltemplate/exec_test.go index e01813e68..4f6c4d1c1 100644 --- a/tpl/internal/go_templates/htmltemplate/exec_test.go +++ b/tpl/internal/go_templates/htmltemplate/exec_test.go @@ -5,7 +5,6 @@ // Tests for template execution, copied from text/template. //go:build !windows -// +build !windows package template @@ -325,16 +324,12 @@ var execTests = []execTest{ {"$.U.V", "{{$.U.V}}", "v", tVal, true}, {"declare in action", "{{$x := $.U.V}}{{$x}}", "v", tVal, true}, {"simple assignment", "{{$x := 2}}{{$x = 3}}{{$x}}", "3", tVal, true}, - { - "nested assignment", + {"nested assignment", "{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{$x}}", - "3", tVal, true, - }, - { - "nested assignment changes the last declaration", + "3", tVal, true}, + {"nested assignment changes the last declaration", "{{$x := 1}}{{if true}}{{$x := 2}}{{if true}}{{$x = 3}}{{end}}{{end}}{{$x}}", - "1", tVal, true, - }, + "1", tVal, true}, // Type with String method. {"V{6666}.String()", "-{{.V0}}-", "-{6666}-", tVal, true}, // NOTE: -<6666>- in text/template @@ -381,21 +376,15 @@ var execTests = []execTest{ {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: <nil>-", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: <nil>-", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, - { - "method on chained var", + {"method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true, - }, - { - "chained method", + "true", tVal, true}, + {"chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true, - }, - { - "chained method on variable", + "true", tVal, true}, + {"chained method on variable", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", - "true", tVal, true, - }, + "true", tVal, true}, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, @@ -481,14 +470,10 @@ var execTests = []execTest{ {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, // HTML. - { - "html", `{{html ""}}`, - "<script>alert("XSS");</script>", nil, true, - }, - { - "html pipeline", `{{printf "" | html}}`, - "<script>alert("XSS");</script>", nil, true, - }, + {"html", `{{html ""}}`, + "<script>alert("XSS");</script>", nil, true}, + {"html pipeline", `{{printf "" | html}}`, + "<script>alert("XSS");</script>", nil, true}, {"html", `{{html .PS}}`, "a string", tVal, true}, {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "<nil>", tVal, true}, // NOTE: "<no value>" in text/template @@ -854,7 +839,7 @@ var delimPairs = []string{ func TestDelims(t *testing.T) { const hello = "Hello, world" - value := struct{ Str string }{hello} + var value = struct{ Str string }{hello} for i := 0; i < len(delimPairs); i += 2 { text := ".Str" left := delimPairs[i+0] @@ -877,7 +862,7 @@ func TestDelims(t *testing.T) { if err != nil { t.Fatalf("delim %q text %q parse err %s", left, text, err) } - b := new(strings.Builder) + var b = new(strings.Builder) err = tmpl.Execute(b, value) if err != nil { t.Fatalf("delim %q exec err %s", left, err) @@ -978,7 +963,7 @@ const treeTemplate = ` ` func TestTree(t *testing.T) { - tree := &Tree{ + var tree = &Tree{ 1, &Tree{ 2, &Tree{ @@ -1229,7 +1214,7 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(strings.Builder) - cmpStruct := struct { + var cmpStruct = struct { Uthree, Ufour uint NegOne, Three int Ptr, NilPtr *int diff --git a/tpl/internal/go_templates/htmltemplate/html_test.go b/tpl/internal/go_templates/htmltemplate/html_test.go index bea668ef9..5983c592b 100644 --- a/tpl/internal/go_templates/htmltemplate/html_test.go +++ b/tpl/internal/go_templates/htmltemplate/html_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template diff --git a/tpl/internal/go_templates/htmltemplate/js_test.go b/tpl/internal/go_templates/htmltemplate/js_test.go index 22ebdcfed..c4f89e5dd 100644 --- a/tpl/internal/go_templates/htmltemplate/js_test.go +++ b/tpl/internal/go_templates/htmltemplate/js_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template @@ -221,8 +220,7 @@ func TestJSStrEscaper(t *testing.T) { {"", `--\u003e`}, // From https://code.google.com/p/doctype/wiki/ArticleUtf7 - { - "+ADw-script+AD4-alert(1)+ADw-/script+AD4-", + {"+ADw-script+AD4-alert(1)+ADw-/script+AD4-", `\u002bADw-script\u002bAD4-alert(1)\u002bADw-\/script\u002bAD4-`, }, // Invalid UTF-8 sequence diff --git a/tpl/internal/go_templates/htmltemplate/jsctx_string.go b/tpl/internal/go_templates/htmltemplate/jsctx_string.go index 23948934c..103d5559f 100644 --- a/tpl/internal/go_templates/htmltemplate/jsctx_string.go +++ b/tpl/internal/go_templates/htmltemplate/jsctx_string.go @@ -18,8 +18,9 @@ const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown" var _jsCtx_index = [...]uint8{0, 11, 21, 33} func (i jsCtx) String() string { - if i >= jsCtx(len(_jsCtx_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_jsCtx_index)-1 { return "jsCtx(" + strconv.FormatInt(int64(i), 10) + ")" } - return _jsCtx_name[_jsCtx_index[i]:_jsCtx_index[i+1]] + return _jsCtx_name[_jsCtx_index[idx]:_jsCtx_index[idx+1]] } diff --git a/tpl/internal/go_templates/htmltemplate/multi_test.go b/tpl/internal/go_templates/htmltemplate/multi_test.go index c320c4353..9107b3611 100644 --- a/tpl/internal/go_templates/htmltemplate/multi_test.go +++ b/tpl/internal/go_templates/htmltemplate/multi_test.go @@ -5,7 +5,6 @@ // Tests for multiple-template execution, copied from text/template. //go:build !windows -// +build !windows package template @@ -268,7 +267,7 @@ func TestIssue19294(t *testing.T) { // by the contents of "stylesheet", but if the internal map associating // names with templates is built in the wrong order, the empty block // looks non-empty and this doesn't happen. - inlined := map[string]string{ + var inlined = map[string]string{ "stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`, "xhtml": `{{block "stylesheet" .}}{{end}}`, } diff --git a/tpl/internal/go_templates/htmltemplate/state_string.go b/tpl/internal/go_templates/htmltemplate/state_string.go index f5a70b223..407064e8d 100644 --- a/tpl/internal/go_templates/htmltemplate/state_string.go +++ b/tpl/internal/go_templates/htmltemplate/state_string.go @@ -46,8 +46,9 @@ const _state_name = "stateTextstateTagstateAttrNamestateAfterNamestateBeforeValu var _state_index = [...]uint16{0, 9, 17, 30, 44, 60, 72, 83, 92, 100, 111, 118, 130, 142, 156, 169, 184, 198, 216, 235, 243, 256, 269, 282, 295, 306, 322, 337, 347, 363, 382, 391} func (i state) String() string { - if i >= state(len(_state_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_state_index)-1 { return "state(" + strconv.FormatInt(int64(i), 10) + ")" } - return _state_name[_state_index[i]:_state_index[i+1]] + return _state_name[_state_index[idx]:_state_index[idx+1]] } diff --git a/tpl/internal/go_templates/htmltemplate/template.go b/tpl/internal/go_templates/htmltemplate/template.go index 4582ddd5f..f6be5d4c2 100644 --- a/tpl/internal/go_templates/htmltemplate/template.go +++ b/tpl/internal/go_templates/htmltemplate/template.go @@ -26,7 +26,8 @@ type Template struct { // we need to keep our version of the name space and the underlying // template's in sync. text *template.Template - // The underlying template's parse tree, updated to be HTML-safe. + // The underlying template's parse tree, updated to be HTML-safe + // after the first execution. Tree *parse.Tree *nameSpace // common to all associated templates } @@ -332,10 +333,12 @@ func (t *Template) Name() string { type FuncMap = template.FuncMap // Funcs adds the elements of the argument map to the template's function map. -// It must be called before the template is parsed. +// Any function used in the template must be added before the template is +// parsed. Funcs may be called more than once, including after parsing (for +// example, after [Template.Clone]), to replace a function of the same name; +// the replacement is used when the template is executed. // It panics if a value in the map is not a function with appropriate return -// type. However, it is legal to overwrite elements of the map. The return -// value is the template, so calls can be chained. +// type. The return value is the template, so calls can be chained. func (t *Template) Funcs(funcMap FuncMap) *Template { t.text.Funcs(template.FuncMap(funcMap)) return t diff --git a/tpl/internal/go_templates/htmltemplate/template_test.go b/tpl/internal/go_templates/htmltemplate/template_test.go index c5a5df437..b348825de 100644 --- a/tpl/internal/go_templates/htmltemplate/template_test.go +++ b/tpl/internal/go_templates/htmltemplate/template_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package template_test import ( diff --git a/tpl/internal/go_templates/htmltemplate/transition.go b/tpl/internal/go_templates/htmltemplate/transition.go index ea4b272cc..d9d4f63be 100644 --- a/tpl/internal/go_templates/htmltemplate/transition.go +++ b/tpl/internal/go_templates/htmltemplate/transition.go @@ -336,11 +336,14 @@ func tJS(c context, s []byte) (context, int) { // We only care about tracking brace depth if we are inside of a // template literal. if len(c.jsBraceDepth) == 0 { + c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx) return c, i + 1 } c.jsBraceDepth[len(c.jsBraceDepth)-1]++ + c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx) case '}': if len(c.jsBraceDepth) == 0 { + c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx) return c, i + 1 } // There are no cases where a brace can be escaped in the JS context @@ -349,6 +352,7 @@ func tJS(c context, s []byte) (context, int) { // fully fledged parsers will just fail anyway. c.jsBraceDepth[len(c.jsBraceDepth)-1]-- if c.jsBraceDepth[len(c.jsBraceDepth)-1] >= 0 { + c.jsCtx = nextJSCtx(s[i:i+1], c.jsCtx) return c, i + 1 } c.jsBraceDepth = c.jsBraceDepth[:len(c.jsBraceDepth)-1] @@ -426,7 +430,7 @@ func tJSDelimited(c context, s []byte) (context, int) { // If " 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte(" 0 && i+7 <= len(s) && bytes.EqualFold(s[i-1:i+7], []byte("-", tVal, true}, @@ -393,21 +388,15 @@ var execTests = []execTest{ {".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: -", tVal, true}, {".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: -", tVal, true}, {"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true}, - { - "method on chained var", + {"method on chained var", "{{range .MSIone}}{{if $.U.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true, - }, - { - "chained method", + "true", tVal, true}, + {"chained method", "{{range .MSIone}}{{if $.GetU.TrueFalse $.True}}{{$.U.TrueFalse $.True}}{{else}}WRONG{{end}}{{end}}", - "true", tVal, true, - }, - { - "chained method on variable", + "true", tVal, true}, + {"chained method on variable", "{{with $x := .}}{{with .SI}}{{$.GetU.TrueFalse $.True}}{{end}}{{end}}", - "true", tVal, true, - }, + "true", tVal, true}, {".NilOKFunc not nil", "{{call .NilOKFunc .PI}}", "false", tVal, true}, {".NilOKFunc nil", "{{call .NilOKFunc nil}}", "true", tVal, true}, {"method on nil value from slice", "-{{range .}}{{.Method1 1234}}{{end}}-", "-1234-", tSliceOfNil, true}, @@ -500,14 +489,10 @@ var execTests = []execTest{ {"printf lots", `{{printf "%d %s %g %s" 127 "hello" 7-3i .Method0}}`, "127 hello (7-3i) M0", tVal, true}, // HTML. - { - "html", `{{html ""}}`, - "<script>alert("XSS");</script>", nil, true, - }, - { - "html pipeline", `{{printf "" | html}}`, - "<script>alert("XSS");</script>", nil, true, - }, + {"html", `{{html ""}}`, + "<script>alert("XSS");</script>", nil, true}, + {"html pipeline", `{{printf "" | html}}`, + "<script>alert("XSS");</script>", nil, true}, {"html", `{{html .PS}}`, "a string", tVal, true}, {"html typed nil", `{{html .NIL}}`, "<nil>", tVal, true}, {"html untyped nil", `{{html .Empty0}}`, "<no value>", tVal, true}, @@ -942,9 +927,8 @@ var delimPairs = []string{ func TestDelims(t *testing.T) { const hello = "Hello, world" - value := struct{ Str string }{hello} + var value = struct{ Str string }{hello} for i := 0; i < len(delimPairs); i += 2 { - text := ".Str" left := delimPairs[i+0] trueLeft := left right := delimPairs[i+1] @@ -955,17 +939,23 @@ func TestDelims(t *testing.T) { if right == "" { // default case trueRight = "}}" } - text = trueLeft + text + trueRight - // Now add a comment - text += trueLeft + "/*comment*/" + trueRight - // Now add an action containing a string. - text += trueLeft + `"` + trueLeft + `"` + trueRight + action := trueLeft + ".Str" + trueRight + // A comment, which is not preserved in the parse tree. + comment := trueLeft + "/*comment*/" + trueRight + // An action containing a string that looks like the left delimiter. + strAction := trueLeft + `"` + trueLeft + `"` + trueRight + text := action + comment + strAction // At this point text looks like `{{.Str}}{{/*comment*/}}{{"{{"}}`. tmpl, err := New("delims").Delims(left, right).Parse(text) if err != nil { t.Fatalf("delim %q text %q parse err %s", left, text, err) } - b := new(strings.Builder) + // The parse tree's String form should roundtrip back to the input, + // using the custom delimiters, modulo the dropped comment. + if got, want := tmpl.Root.String(), action+strAction; got != want { + t.Errorf("delim %q: String() = %q, want %q", left, got, want) + } + var b = new(strings.Builder) err = tmpl.Execute(b, value) if err != nil { t.Fatalf("delim %q exec err %s", left, err) @@ -1025,6 +1015,23 @@ type CustomError struct{} func (*CustomError) Error() string { return "heyo !" } +// Check that a custom error can be returned. +func TestExecError_CustomError(t *testing.T) { + failingFunc := func() (string, error) { + return "", &CustomError{} + } + tmpl := Must(New("top").Funcs(FuncMap{ + "err": failingFunc, + }).Parse("{{ err }}")) + + var b bytes.Buffer + err := tmpl.Execute(&b, nil) + + if _, ok := errors.AsType[*CustomError](err); !ok { + t.Fatalf("expected custom error; got %s", err) + } +} + func TestJSEscaping(t *testing.T) { testCases := []struct { in, exp string @@ -1070,7 +1077,7 @@ const treeTemplate = ` ` func TestTree(t *testing.T) { - tree := &Tree{ + var tree = &Tree{ 1, &Tree{ 2, &Tree{ @@ -1323,7 +1330,7 @@ var cmpTests = []cmpTest{ func TestComparison(t *testing.T) { b := new(strings.Builder) - cmpStruct := struct { + var cmpStruct = struct { Uthree, Ufour uint NegOne, Three int Ptr, NilPtr *int @@ -1836,13 +1843,12 @@ func TestFunctionCheckDuringCall(t *testing.T) { input string data any wantErr string - }{ - { - name: "call nothing", - input: `{{call}}`, - data: tVal, - wantErr: "wrong number of args for call: want at least 1 got 0", - }, + }{{ + name: "call nothing", + input: `{{call}}`, + data: tVal, + wantErr: "wrong number of args for call: want at least 1 got 0", + }, { name: "call non-function", input: "{{call .True}}", diff --git a/tpl/internal/go_templates/texttemplate/link_test.go b/tpl/internal/go_templates/texttemplate/link_test.go index 23f6a31fa..0dabb65f4 100644 --- a/tpl/internal/go_templates/texttemplate/link_test.go +++ b/tpl/internal/go_templates/texttemplate/link_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package template_test import ( diff --git a/tpl/internal/go_templates/texttemplate/multi_test.go b/tpl/internal/go_templates/texttemplate/multi_test.go index 8fda90d74..b2c22c440 100644 --- a/tpl/internal/go_templates/texttemplate/multi_test.go +++ b/tpl/internal/go_templates/texttemplate/multi_test.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. //go:build !windows -// +build !windows package template @@ -11,11 +10,10 @@ package template import ( "fmt" + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "os" "strings" "testing" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" ) const ( @@ -32,32 +30,22 @@ type multiParseTest struct { } var multiParseTests = []multiParseTest{ - { - "empty", "", noError, + {"empty", "", noError, nil, - nil, - }, - { - "one", `{{define "foo"}} FOO {{end}}`, noError, + nil}, + {"one", `{{define "foo"}} FOO {{end}}`, noError, []string{"foo"}, - []string{" FOO "}, - }, - { - "two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError, + []string{" FOO "}}, + {"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError, []string{"foo", "bar"}, - []string{" FOO ", " BAR "}, - }, + []string{" FOO ", " BAR "}}, // errors - { - "missing end", `{{define "foo"}} FOO `, hasError, + {"missing end", `{{define "foo"}} FOO `, hasError, nil, + nil}, + {"malformed name", `{{define "foo}} FOO `, hasError, nil, - }, - { - "malformed name", `{{define "foo}} FOO `, hasError, - nil, - nil, - }, + nil}, } func TestMultiParse(t *testing.T) { @@ -454,7 +442,7 @@ func TestIssue19294(t *testing.T) { // by the contents of "stylesheet", but if the internal map associating // names with templates is built in the wrong order, the empty block // looks non-empty and this doesn't happen. - inlined := map[string]string{ + var inlined = map[string]string{ "stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`, "xhtml": `{{block "stylesheet" .}}{{end}}`, } diff --git a/tpl/internal/go_templates/texttemplate/parse/lex.go b/tpl/internal/go_templates/texttemplate/parse/lex.go index a00f48e65..fa9eaf5a3 100644 --- a/tpl/internal/go_templates/texttemplate/parse/lex.go +++ b/tpl/internal/go_templates/texttemplate/parse/lex.go @@ -240,10 +240,10 @@ func (l *lexer) nextItem() item { // lex creates a new scanner for the input string. func lex(name, input, left, right string) *lexer { if left == "" { - left = leftDelim + left = defaultLeftDelim } if right == "" { - right = rightDelim + right = defaultRightDelim } l := &lexer{ name: name, @@ -260,10 +260,10 @@ func lex(name, input, left, right string) *lexer { // state functions const ( - leftDelim = "{{" - rightDelim = "}}" - leftComment = "/*" - rightComment = "*/" + defaultLeftDelim = "{{" + defaultRightDelim = "}}" + leftComment = "/*" + rightComment = "*/" ) // lexText scans until an opening action delimiter, "{{". diff --git a/tpl/internal/go_templates/texttemplate/parse/lex_test.go b/tpl/internal/go_templates/texttemplate/parse/lex_test.go index 633ffb055..bd535c4ce 100644 --- a/tpl/internal/go_templates/texttemplate/parse/lex_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/lex_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package parse import ( diff --git a/tpl/internal/go_templates/texttemplate/parse/node.go b/tpl/internal/go_templates/texttemplate/parse/node.go index a31309874..31d663c8d 100644 --- a/tpl/internal/go_templates/texttemplate/parse/node.go +++ b/tpl/internal/go_templates/texttemplate/parse/node.go @@ -171,9 +171,9 @@ func (c *CommentNode) String() string { } func (c *CommentNode) writeTo(sb *strings.Builder) { - sb.WriteString("{{") + sb.WriteString(c.tr.leftDelim) sb.WriteString(c.Text) - sb.WriteString("}}") + sb.WriteString(c.tr.rightDelim) } func (c *CommentNode) tree() *Tree { @@ -277,9 +277,9 @@ func (a *ActionNode) String() string { } func (a *ActionNode) writeTo(sb *strings.Builder) { - sb.WriteString("{{") + sb.WriteString(a.tr.leftDelim) a.Pipe.writeTo(sb) - sb.WriteString("}}") + sb.WriteString(a.tr.rightDelim) } func (a *ActionNode) tree() *Tree { @@ -793,7 +793,7 @@ func (t *Tree) newEnd(pos Pos) *endNode { } func (e *endNode) String() string { - return "{{end}}" + return e.tr.leftDelim + "end" + e.tr.rightDelim } func (e *endNode) writeTo(sb *strings.Builder) { @@ -825,7 +825,7 @@ func (e *elseNode) Type() NodeType { } func (e *elseNode) String() string { - return "{{else}}" + return e.tr.leftDelim + "else" + e.tr.rightDelim } func (e *elseNode) writeTo(sb *strings.Builder) { @@ -869,17 +869,21 @@ func (b *BranchNode) writeTo(sb *strings.Builder) { default: panic("unknown branch type") } - sb.WriteString("{{") + sb.WriteString(b.tr.leftDelim) sb.WriteString(name) sb.WriteByte(' ') b.Pipe.writeTo(sb) - sb.WriteString("}}") + sb.WriteString(b.tr.rightDelim) b.List.writeTo(sb) if b.ElseList != nil { - sb.WriteString("{{else}}") + sb.WriteString(b.tr.leftDelim) + sb.WriteString("else") + sb.WriteString(b.tr.rightDelim) b.ElseList.writeTo(sb) } - sb.WriteString("{{end}}") + sb.WriteString(b.tr.leftDelim) + sb.WriteString("end") + sb.WriteString(b.tr.rightDelim) } func (b *BranchNode) tree() *Tree { @@ -925,9 +929,9 @@ func (t *Tree) newBreak(pos Pos, line int) *BreakNode { } func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) } -func (b *BreakNode) String() string { return "{{break}}" } +func (b *BreakNode) String() string { return b.tr.leftDelim + "break" + b.tr.rightDelim } func (b *BreakNode) tree() *Tree { return b.tr } -func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") } +func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString(b.String()) } // ContinueNode represents a {{continue}} action. type ContinueNode struct { @@ -942,9 +946,9 @@ func (t *Tree) newContinue(pos Pos, line int) *ContinueNode { } func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) } -func (c *ContinueNode) String() string { return "{{continue}}" } +func (c *ContinueNode) String() string { return c.tr.leftDelim + "continue" + c.tr.rightDelim } func (c *ContinueNode) tree() *Tree { return c.tr } -func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") } +func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString(c.String()) } // RangeNode represents a {{range}} action and its commands. type RangeNode struct { @@ -993,13 +997,14 @@ func (t *TemplateNode) String() string { } func (t *TemplateNode) writeTo(sb *strings.Builder) { - sb.WriteString("{{template ") + sb.WriteString(t.tr.leftDelim) + sb.WriteString("template ") sb.WriteString(strconv.Quote(t.Name)) if t.Pipe != nil { sb.WriteByte(' ') t.Pipe.writeTo(sb) } - sb.WriteString("}}") + sb.WriteString(t.tr.rightDelim) } func (t *TemplateNode) tree() *Tree { diff --git a/tpl/internal/go_templates/texttemplate/parse/parse.go b/tpl/internal/go_templates/texttemplate/parse/parse.go index b74dfb7f4..ae81be249 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse.go @@ -33,6 +33,9 @@ type Tree struct { actionLine int // line of left delim starting action rangeDepth int stackDepth int // depth of nested parenthesized expressions + + leftDelim string + rightDelim string } // A Mode value is a set of flags (or 0). Modes control parser behavior. @@ -60,10 +63,12 @@ func (t *Tree) Copy() *Tree { return nil } return &Tree{ - Name: t.Name, - ParseName: t.ParseName, - Root: t.Root.CopyList(), - text: t.text, + Name: t.Name, + ParseName: t.ParseName, + Root: t.Root.CopyList(), + text: t.text, + leftDelim: t.leftDelim, + rightDelim: t.rightDelim, } } @@ -258,7 +263,15 @@ func (t *Tree) stopParse() { func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) { defer t.recover(&err) t.ParseName = t.Name - lexer := lex(t.Name, text, leftDelim, rightDelim) + t.leftDelim = leftDelim + if t.leftDelim == "" { + t.leftDelim = defaultLeftDelim + } + t.rightDelim = rightDelim + if t.rightDelim == "" { + t.rightDelim = defaultRightDelim + } + lexer := lex(t.Name, text, t.leftDelim, t.rightDelim) t.startParse(funcs, lexer, treeSet) t.text = text t.parse() @@ -318,6 +331,8 @@ func (t *Tree) parse() { newT := New("definition") // name will be updated once we know it. newT.text = t.text newT.Mode = t.Mode + newT.leftDelim = t.leftDelim + newT.rightDelim = t.rightDelim newT.ParseName = t.ParseName newT.startParse(t.funcs, t.lex, t.treeSet) newT.parseDefinition() @@ -546,7 +561,7 @@ func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, t.rangeDepth-- } switch next.Type() { - case nodeEnd: //done + case nodeEnd: // done case nodeElse: // Special case for "else if" and "else with". // If the "else" is followed immediately by an "if" or "with", @@ -650,6 +665,8 @@ func (t *Tree) blockControl() Node { block := New(name) // name will be updated once we know it. block.text = t.text block.Mode = t.Mode + block.leftDelim = t.leftDelim + block.rightDelim = t.rightDelim block.ParseName = t.ParseName block.startParse(t.funcs, t.lex, t.treeSet) var end Node diff --git a/tpl/internal/go_templates/texttemplate/parse/parse_test.go b/tpl/internal/go_templates/texttemplate/parse/parse_test.go index 90ec9852b..8269e6800 100644 --- a/tpl/internal/go_templates/texttemplate/parse/parse_test.go +++ b/tpl/internal/go_templates/texttemplate/parse/parse_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.13 -// +build go1.13 - package parse import ( @@ -415,6 +412,36 @@ func TestParseWithComments(t *testing.T) { } } +func TestDelimsStringRoundtrip(t *testing.T) { + // Each input is already in canonical String form, so parsing it with the + // custom delimiters and printing the resulting tree must reproduce the + // input exactly. This exercises the String/writeTo methods of every node + // type that emits delimiters. + const ( + left = "[[" + right = "]]" + ) + for _, input := range []string{ + `[[.X]]`, // ActionNode + `[[/* a comment */]]`, // CommentNode + `[[if .X]]y[[else]]z[[end]]`, // BranchNode (if), with else and end + `[[range .X]][[break]][[continue]][[end]]`, // RangeNode, BreakNode, ContinueNode + `[[with .X]]y[[end]]`, // BranchNode (with) + `[[template "name" .]]`, // TemplateNode + } { + tr := New("test") + tr.Mode = ParseComments + tmpl, err := tr.Parse(input, left, right, make(map[string]*Tree)) + if err != nil { + t.Errorf("%q: unexpected parse error: %v", input, err) + continue + } + if got := tmpl.Root.String(); got != input { + t.Errorf("got\n\t%q\nexpected\n\t%q", got, input) + } + } +} + func TestKeywordsAndFuncs(t *testing.T) { // Check collisions between functions and new keywords like 'break'. When a // break function is provided, the parser should treat 'break' as a function, diff --git a/tpl/internal/go_templates/texttemplate/template.go b/tpl/internal/go_templates/texttemplate/template.go index e66c01fd4..43f5cd416 100644 --- a/tpl/internal/go_templates/texttemplate/template.go +++ b/tpl/internal/go_templates/texttemplate/template.go @@ -167,11 +167,13 @@ func (t *Template) Delims(left, right string) *Template { } // Funcs adds the elements of the argument map to the template's function map. -// It must be called before the template is parsed. +// Any function used in the template must be added before the template is +// parsed. Funcs may be called more than once, including after parsing (for +// example, after [Template.Clone]), to replace a function of the same name; +// the replacement is used when the template is executed. // It panics if a value in the map is not a function with appropriate return // type or if the name cannot be used syntactically as a function in a template. -// It is legal to overwrite elements of the map. The return value is the template, -// so calls can be chained. +// The return value is the template, so calls can be chained. func (t *Template) Funcs(funcMap FuncMap) *Template { t.init() t.muFuncs.Lock()