Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 5bea058bad tpl: Improve the return keyword in templates
Replace the partial return template rewriting with a sentinel error
trapped in the template executor:

* return now works in any template, not just partials.
* return can be used anywhere in the template, e.g. inside if/range;
  it stops execution of the current template, so a bare return in a
  block or template include ends just that template.
* {{ return <value> }} sets the return value of the enclosing partial;
  using it outside a partial is now an error (it was silently ignored).

The fork changes are limited to hugo_template.go plus one mechanical
rename (walkTemplate -> walkTemplateOld) mirrored in the fork script.

Closes #15212

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 11:28:18 +02:00
96 changed files with 553 additions and 963 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22700.20000
- image: bepsays/ci-hugoreleaser:1.22600.20500
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22700.20000
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20500
steps:
- *restore-cache
- &attach-workspace
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.27.x]
go-version: [1.26.x]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
+2 -2
View File
@@ -2,8 +2,8 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.27"
ARG ALPINE_VERSION="3.24"
ARG GO_VERSION="1.26"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
+1 -1
View File
@@ -116,7 +116,7 @@ func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
cmd.Long = `Display project configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for (default is the default content language)")
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`)
_ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions)
applyLocalFlagsBuildConfig(cmd, c.r)
+3 -3
View File
@@ -81,9 +81,9 @@ func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
for i, v := range slices.Backward(s.items) {
if predicate(v) {
items = append(items, v)
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1)
}
}
-11
View File
@@ -178,14 +178,3 @@ 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
}
+2 -1
View File
@@ -131,13 +131,14 @@ func (c *Scratch) DeleteInMap(key string, mapKey string) string {
// GetSortedMapValues returns a sorted map previously filled with SetInMap.
func (c *Scratch) GetSortedMapValues(key string) any {
c.mu.RLock()
defer c.mu.RUnlock()
if c.values[key] == nil {
c.mu.RUnlock()
return nil
}
unsortedMap := c.values[key].(map[string]any)
c.mu.RUnlock()
var keys []string
for mapKey := range unsortedMap {
keys = append(keys, mapKey)
-24
View File
@@ -207,30 +207,6 @@ func TestScratchGetSortedMapValues(t *testing.T) {
}
}
func TestScratchGetSortedMapValuesConcurrentWithSetInMap(t *testing.T) {
t.Parallel()
scratch := NewScratch()
scratch.SetInMap("key", "initial", "initial")
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := range 1000 {
scratch.SetInMap("key", "value", i)
}
}()
go func() {
defer wg.Done()
for range 1000 {
scratch.GetSortedMapValues("key")
}
}()
wg.Wait()
}
func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch()
scratch.Add("A", 1)
-3
View File
@@ -82,9 +82,6 @@ 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
}
+22 -23
View File
@@ -1005,30 +1005,29 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
func newDefaultConfig() *Config {
return &Config{
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
Environment: hugo.EnvironmentProduction,
TitleCaseStyle: "AP",
PluralizeListTitles: true,
CapitalizeListTitles: true,
StaticDir: []string{"static"},
SummaryLength: 70,
Timeout: "60s",
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
RootConfig: RootConfig{
Environment: hugo.EnvironmentProduction,
TitleCaseStyle: "AP",
PluralizeListTitles: true,
CapitalizeListTitles: true,
StaticDir: []string{"static"},
SummaryLength: 70,
Timeout: "60s",
//lint:ignore SA1019 Keep as adapter for now.
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
//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",
CommonDirs: config.CommonDirs{
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
AssetDir: "assets",
LayoutDir: "layouts",
I18nDir: "i18n",
DataDir: "data",
},
},
}
}
+1 -1
View File
@@ -317,7 +317,7 @@ func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string {
b, err := afero.ReadFile(fs, filename)
if err != nil {
// Print some debug info
root, _, _ := strings.Cut(filename, helpers.FilePathSeparator)
root := strings.Split(filename, helpers.FilePathSeparator)[0]
afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() {
fmt.Println(" ", path)
+2 -2
View File
@@ -459,7 +459,7 @@ func (c TestConfig) IsZero() bool {
// BuildState are state used during a build.
type BuildState struct {
counter atomic.Uint64
counter uint64
// Tracks invocations of the Build method.
BuildCounter atomic.Uint64
@@ -538,5 +538,5 @@ func (b *BuildState) GetFilenamesWithPostPrefix() []string {
}
func (b *BuildState) Incr() int {
return int(b.counter.Add(uint64(1)))
return int(atomic.AddUint64(&b.counter, uint64(1)))
}
+1 -1
View File
@@ -186,4 +186,4 @@ require (
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
go 1.27.0
go 1.26.0
+2 -2
View File
@@ -14,6 +14,7 @@
package hugolib
import (
"cmp"
"context"
"fmt"
"path"
@@ -22,7 +23,6 @@ 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 := herrors.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
if err := cmp.Or(a.doCreatePages("", 0), a.g.Wait()); err != nil {
return err
}
if err := a.pwRoot.WalkContext.HandleEventsAndHooks(); err != nil {
+2 -3
View File
@@ -16,7 +16,6 @@ package doctree
import (
"fmt"
"iter"
"slices"
"strings"
"sync"
@@ -252,8 +251,8 @@ func (ctx *WalkContext[T]) HandleEvents() error {
// Loop the event handlers in reverse order so
// that events created by the handlers themselves will
// be picked up further up the tree.
for _, v := range slices.Backward(ctx.eventHandlers[event.Name]) {
v(event)
for i := len(ctx.eventHandlers[event.Name]) - 1; i >= 0; i-- {
ctx.eventHandlers[event.Name][i](event)
if event.stopPropagation {
break
}
+19 -8
View File
@@ -924,9 +924,8 @@ func (s *IntegrationTestBuilder) initBuilder() error {
if s.Cfg.Running {
flags.Set("internal", hmaps.Params{
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
"fastRenderMode": s.Cfg.FastRenderMode,
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
})
} else if s.Cfg.Watching {
flags.Set("internal", hmaps.Params{
@@ -1180,7 +1179,23 @@ func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filen
t.Helper()
filename = filepath.Clean(filename)
b, err := afero.ReadFile(fs, filename)
s.Assert(err, qt.IsNil)
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)
}
return string(b)
}
@@ -1217,10 +1232,6 @@ type IntegrationTestConfig struct {
// Whether to simulate server mode.
Running bool
// Whether to simulate the server's fast render mode.
// Only used when Running is set.
FastRenderMode bool
// Watch for changes.
// This is (currently) always set to true when Running is set.
// Note that the CLI for the server does allow for --watch=false, but that is not used in these test.
+20 -12
View File
@@ -332,8 +332,10 @@ func (ps *pageState) RegularPagesRecursive() page.Pages {
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
Recursive: true,
},
)
@@ -352,8 +354,10 @@ func (ps *pageState) RegularPages() page.Pages {
case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
},
},
)
case kinds.KindTerm:
@@ -375,11 +379,13 @@ func (ps *pageState) Pages() page.Pages {
case kinds.KindSection, kinds.KindHome:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: ps.Path(),
KeyPart: "page-section",
Include: pagePredicates.ShouldListLocal.And(
pagePredicates.KindPage.Or(pagePredicates.KindSection),
).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "page-section",
Include: pagePredicates.ShouldListLocal.And(
pagePredicates.KindPage.Or(pagePredicates.KindSection),
).BoolFunc(),
},
},
)
case kinds.KindTerm:
@@ -391,9 +397,11 @@ func (ps *pageState) Pages() page.Pages {
case kinds.KindTaxonomy:
return ps.s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: ps.Path(),
KeyPart: "term",
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: ps.Path(),
KeyPart: "term",
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
},
Recursive: true,
},
)
+4 -2
View File
@@ -941,8 +941,10 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
if pageparser.HasShortcode(contentToRender) {
ct := contentTableOfContents{
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
source: []byte(contentToRender),
sourceInfo: sourceInfo{
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
source: []byte(contentToRender),
},
}
ct.contentToRender = ct.source
// String contains a shortcode.
+7 -20
View File
@@ -104,34 +104,21 @@ func (pco *pageContentOutput) Reset() {
pco.renderHooks = &renderHooks{}
}
func (pco *pageContentOutput) Render(ctx context.Context, args ...any) (template.HTML, error) {
if len(args) == 0 {
return "", errors.New("no view given")
func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
if len(layout) == 0 {
return "", errors.New("no layout given")
}
if len(args) > 2 {
return "", errors.New("too many arguments, expected VIEW [CONTEXT]")
}
view, err := cast.ToStringE(args[0])
if err != nil {
return "", fmt.Errorf("failed to convert view argument to string: %w", err)
}
// Make sure to send the *pageState and not the *pageContentOutput to the template.
var data any = pco.po.p
if len(args) == 2 {
data = args[1]
}
templ, found, err := pco.po.p.resolveTemplate(view)
templ, found, err := pco.po.p.resolveTemplate(layout...)
if err != nil {
return "", pco.po.p.wrapError(err)
}
if !found {
return "", fmt.Errorf("template %q not found", view)
return "", fmt.Errorf("template %q not found", layout[0])
}
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, data)
// Make sure to send the *pageState and not the *pageContentOutput to the template.
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
if err != nil {
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
}
-50
View File
@@ -1913,56 +1913,6 @@ func TestRenderWithoutArgument(t *testing.T) {
b.Assert(err, qt.IsNotNil)
}
// See issue 15077.
func TestRenderWithContext(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- content/p1.md --
---
title: "P1"
---
-- layouts/page.html --
{{ .Render "li" }}|{{ .Render "li" (dict "Title" "Custom") }}
-- layouts/li.html --
Title: {{ .Title }}{{- /**/ -}}
`
b := Test(t, files)
b.AssertFileContent("public/p1/index.html", "Title: P1|Title: Custom")
}
// See issue 15077.
func TestRenderWithContextErrors(t *testing.T) {
t.Parallel()
filesTemplate := `
-- hugo.toml --
-- content/p1.md --
---
title: "P1"
---
-- layouts/li.html --
li
-- layouts/page.html --
RENDER
`
for _, test := range []struct {
render string
message string
}{
{`{{ .Render "li" "foo" "bar" }}`, `(?s).*too many arguments, expected VIEW \[CONTEXT\].*`},
{`{{ .Render (slice "li") }}`, `(?s).*failed to convert view argument to string: unable to cast \[\]string{"li"} of type \[\]string to string.*`},
} {
files := strings.ReplaceAll(filesTemplate, "RENDER", test.render)
b, err := TestE(t, files)
b.Assert(err, qt.ErrorMatches, test.message)
}
}
// Issue #13021
func TestAllStores(t *testing.T) {
t.Parallel()
-127
View File
@@ -2211,130 +2211,3 @@ objects: ["o1"]
b.AssertFileContent("public/objects/index.html", "Objects|taxonomy|<p>Objects content edited.</p>")
b.AssertFileContent("public/objects/o1/index.html", "O1|term|")
}
func TestRebuildEditMountedAssetOnlyRelPermalinkUsed(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/mytext.txt --
mytext v1
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
// Simulate the periodic data refresh.
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
// An unrelated edit to a home bundle resource must not revert the
// published data to an older version.
b.EditFileReplaceAll("content/mytext.txt", "mytext v1", "mytext v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
// One more refresh round.
b.EditFileReplaceAll("out/data.json", "v2", "v3").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v3"}`)
}
func TestRebuildEditMountedAssetContentUsed(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/mytext.txt --
mytext v1
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|{{ $data.Content | safeHTML }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
b.AssertFileContent("public/index.html", `{"version":"v2"}`)
b.EditFileReplaceAll("content/mytext.txt", "mytext v1", "mytext v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
b.EditFileReplaceAll("out/data.json", "v2", "v3").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v3"}`)
b.AssertFileContent("public/index.html", `{"version":"v3"}`)
}
func TestRebuildFastRenderEditMountedAssetNotRecentlyVisited(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "rss", "sitemap", "section"]
[module]
[[module.mounts]]
source = "assets"
target = "assets"
[[module.mounts]]
source = "out"
target = "assets/out"
-- out/data.json --
{"version": "v1"}
-- content/_index.md --
---
title: "Home"
---
-- content/p1.md --
---
title: "P1"
---
-- layouts/home.html --
{{ $data := resources.Get "out/data.json" | minify }}
Data: {{ $data.RelPermalink }}|
-- layouts/single.html --
Single: {{ .Title }}|
`
recentlyVisited := types.NewEvictingQueue[string](20).Add("/p1/")
b := TestRunning(t, files, func(cfg *IntegrationTestConfig) {
cfg.FastRenderMode = true
cfg.BuildCfg = BuildCfg{RecentlyTouched: recentlyVisited}
})
b.AssertFileContent("public/index.html", "Data: /out/data.min.json|")
b.AssertFileContent("public/out/data.min.json", `{"version":"v1"}`)
// Simulate the periodic data refresh.
b.EditFileReplaceAll("out/data.json", "v1", "v2").Build()
b.AssertFileContent("public/out/data.min.json", `{"version":"v2"}`)
}
+24 -14
View File
@@ -759,9 +759,11 @@ func (s *Site) Pages() page.Pages {
s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
},
Recursive: true,
IncludeSelf: true,
},
@@ -774,9 +776,11 @@ func (s *Site) RegularPages() page.Pages {
s.CheckReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "global",
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
},
Recursive: true,
},
)
@@ -918,9 +922,11 @@ func (s *Site) prepareInits() {
sections := s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
Path: "",
KeyPart: "sectionorhome",
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
Path: "",
KeyPart: "sectionorhome",
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
},
IncludeSelf: true,
Recursive: true,
},
@@ -1458,10 +1464,12 @@ func (s *Site) assembleMenus() (navigation.Menus, error) {
return false, nil
}
me := navigation.MenuEntry{
Identifier: id,
Name: p.LinkTitle(),
Weight: p.Weight(),
Page: p,
MenuConfig: navigation.MenuConfig{
Identifier: id,
Name: p.LinkTitle(),
Weight: p.Weight(),
},
Page: p,
}
navigation.SetPageValues(&me, p)
@@ -1500,7 +1508,9 @@ func (s *Site) assembleMenus() (navigation.Menus, error) {
if !ok {
// if parent does not exist, create one without a URL
flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{
Name: p.EntryName,
MenuConfig: navigation.MenuConfig{
Name: p.EntryName,
},
}
}
flat[twoD{p.MenuName, p.EntryName}].Children = childmenu
+13 -8
View File
@@ -14,6 +14,7 @@
package sitesmatrix
import (
"cmp"
"fmt"
"iter"
"maps"
@@ -779,7 +780,7 @@ func (b *IntSetsBuilder) Build() *IntSets {
}
func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
applyFilter := func(what string, values []string, matcher ConfiguredDimension) *hmaps.OrderedIntSet {
applyFilter := func(what string, values []string, matcher ConfiguredDimension) (*hmaps.OrderedIntSet, error) {
var result *hmaps.OrderedIntSet
if len(values) == 0 {
@@ -799,16 +800,16 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
}
}
return result
return result, nil
}
filter, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(values, matcher.ResolveIndex, hglob.GetGlobDot)
if err != nil {
panic(fmt.Errorf("failed to create filter for %s: %w", what, err))
return nil, fmt.Errorf("failed to create filter for %s: %w", what, err)
}
iter, err := matcher.IndexMatch(filter)
if err != nil {
panic(fmt.Errorf("failed to match %s %q: %w", what, values, err))
return nil, fmt.Errorf("failed to match %s %q: %w", what, values, err)
}
for i := range iter {
if result == nil {
@@ -817,12 +818,16 @@ func (b *IntSetsBuilder) WithConfig(cfg IntSetsConfig) *IntSetsBuilder {
result.Set(i)
}
return result
return result, nil
}
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)
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))
}
b.GlobFilterMisses = Bools{
len(cfg.Globs.Languages) > 0 && l == nil,
+2 -2
View File
@@ -252,11 +252,11 @@ type IsRebuildProvider interface {
// IncrementByOne implements Incrementer adding 1 every time Incr is called.
type IncrementByOne struct {
counter atomic.Uint64
counter uint64
}
func (c *IncrementByOne) Incr() int {
return int(c.counter.Add(uint64(1)))
return int(atomic.AddUint64(&c.counter, uint64(1)))
}
// Incrementer increments and returns the value.
+78 -76
View File
@@ -528,94 +528,96 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
}
jsOpts := Options{
ExternalOptions: externalOptions,
DependencyManager: b.dependencyManager,
Splitting: true,
ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string {
var importContextPath string
if args.Kind == api.ResolveEntryPoint {
importContextPath = args.Path
} else {
importContextPath = args.Importer
}
importContext, importContextFound := state.importerImportContext.Get(importContextPath)
ExternalOptions: externalOptions,
InternalOptions: InternalOptions{
DependencyManager: b.dependencyManager,
Splitting: true,
ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string {
var importContextPath string
if args.Kind == api.ResolveEntryPoint {
importContextPath = args.Path
} else {
importContextPath = args.Importer
}
importContext, importContextFound := state.importerImportContext.Get(importContextPath)
// We want to track the dependencies closest to where they're used.
dm := b.dependencyManager
if importContextFound {
dm = importContext.dm
}
// We want to track the dependencies closest to where they're used.
dm := b.dependencyManager
if importContextFound {
dm = importContext.dm
}
if r, found := state.importResource.Get(imp); found {
dm.AddIdentity(identity.FirstIdentity(r))
return imp
}
if importContext.resourceGetter != nil {
resolved := ResolveResource(imp, importContext.resourceGetter)
if resolved != nil {
resolvePath := resources.InternalResourceTargetPath(resolved)
dm.AddIdentity(identity.FirstIdentity(resolved))
imp := PrefixHugoVirtual + resolvePath
state.importResource.Set(imp, resolved)
state.importerImportContext.Set(imp, importContext)
if r, found := state.importResource.Get(imp); found {
dm.AddIdentity(identity.FirstIdentity(r))
return imp
}
}
return ""
},
ImportOnLoadFunc: func(args api.OnLoadArgs) (string, error) {
imp := args.Path
if r, found := state.importResource.Get(imp); found {
content, err := resources.InternalResourceSourceContent(ctx, r)
if err != nil {
return "", fmt.Errorf("failed to read import %q: %w", resources.InternalResourceSourcePathBestEffort(r), err)
}
return content, nil
}
return "", nil
},
ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage {
if importContext, found := state.importerImportContext.Get(args.Path); found {
if !importContext.scriptOptions.IsZero() {
return importContext.scriptOptions.Params
}
}
return nil
},
ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved {
if loc := args.Location; loc != nil {
path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":")
if r, found := state.importResource.Get(path); found {
sourcePath := resources.InternalResourceSourcePathBestEffort(r)
if importContext.resourceGetter != nil {
resolved := ResolveResource(imp, importContext.resourceGetter)
if resolved != nil {
resolvePath := resources.InternalResourceTargetPath(resolved)
dm.AddIdentity(identity.FirstIdentity(resolved))
imp := PrefixHugoVirtual + resolvePath
state.importResource.Set(imp, resolved)
state.importerImportContext.Set(imp, importContext)
return imp
var contentr hugio.ReadSeekCloser
if cp, ok := r.(hugio.ReadSeekCloserProvider); ok {
contentr, _ = cp.ReadSeekCloser()
}
return &ErrorMessageResolved{
Content: contentr,
Path: sourcePath,
Message: args.Text,
}
return ""
},
ImportOnLoadFunc: func(args api.OnLoadArgs) (string, error) {
imp := args.Path
if r, found := state.importResource.Get(imp); found {
content, err := resources.InternalResourceSourceContent(ctx, r)
if err != nil {
return "", fmt.Errorf("failed to read import %q: %w", resources.InternalResourceSourcePathBestEffort(r), err)
}
return content, nil
}
return "", nil
},
ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage {
if importContext, found := state.importerImportContext.Get(args.Path); found {
if !importContext.scriptOptions.IsZero() {
return importContext.scriptOptions.Params
}
}
return nil
},
ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved {
if loc := args.Location; loc != nil {
path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":")
if r, found := state.importResource.Get(path); found {
sourcePath := resources.InternalResourceSourcePathBestEffort(r)
var contentr hugio.ReadSeekCloser
if cp, ok := r.(hugio.ReadSeekCloserProvider); ok {
contentr, _ = cp.ReadSeekCloser()
}
return &ErrorMessageResolved{
Content: contentr,
Path: sourcePath,
Message: args.Text,
}
}
}
}
return nil
},
ResolveSourceMapSource: func(s string) string {
if r, found := state.importResource.Get(s); found {
if ss := resources.InternalResourceSourcePath(r); ss != "" {
return ss
return nil
},
ResolveSourceMapSource: func(s string) string {
if r, found := state.importResource.Get(s); found {
if ss := resources.InternalResourceSourcePath(r); ss != "" {
return ss
}
return PrefixHugoMemory + s
}
return PrefixHugoMemory + s
}
return ""
return ""
},
EntryPoints: entryPoints,
},
EntryPoints: entryPoints,
}
result, err := b.client.buildClient.Build(jsOpts)
+60 -28
View File
@@ -27,8 +27,10 @@ func TestToBuildOptions(t *testing.T) {
c := qt.New(t)
opts := Options{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -44,12 +46,16 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
Target: []string{"es2018"},
Format: "cjs",
Minify: true,
AvoidTDZ: true,
MediaType: media.Builtin.JavascriptType,
Stdin: true,
ExternalOptions: ExternalOptions{
Target: []string{"es2018"},
Format: "cjs",
Minify: true,
AvoidTDZ: true,
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -68,10 +74,14 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -91,10 +101,14 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "inline",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -114,10 +128,14 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "external",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
ExternalOptions: ExternalOptions{
Target: []string{"es2018"}, Format: "cjs", Minify: true,
SourceMap: "external",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -137,9 +155,13 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
JSX: "automatic", JSXImportSource: "preact",
MediaType: media.Builtin.JavascriptType,
Stdin: true,
ExternalOptions: ExternalOptions{
JSX: "automatic", JSXImportSource: "preact",
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
Stdin: true,
},
}
c.Assert(opts.compile(), qt.IsNil)
@@ -157,18 +179,24 @@ func TestToBuildOptions(t *testing.T) {
})
opts = Options{
Drop: "console",
ExternalOptions: ExternalOptions{
Drop: "console",
},
}
c.Assert(opts.compile(), qt.IsNil)
c.Assert(opts.compiled.Drop, qt.Equals, api.DropConsole)
opts = Options{
Drop: "debugger",
ExternalOptions: ExternalOptions{
Drop: "debugger",
},
}
c.Assert(opts.compile(), qt.IsNil)
c.Assert(opts.compiled.Drop, qt.Equals, api.DropDebugger)
opts = Options{
Drop: "adsfadsf",
ExternalOptions: ExternalOptions{
Drop: "adsfadsf",
},
}
c.Assert(opts.compile(), qt.ErrorMatches, `unsupported drop type: "adsfadsf"`)
}
@@ -194,8 +222,12 @@ func TestToBuildOptionsTarget(t *testing.T) {
} {
c.Run(test.target, func(c *qt.C) {
opts := Options{
Target: []string{test.target},
MediaType: media.Builtin.JavascriptType,
ExternalOptions: ExternalOptions{
Target: []string{test.target},
},
InternalOptions: InternalOptions{
MediaType: media.Builtin.JavascriptType,
},
}
c.Assert(opts.compile(), qt.IsNil)
+1 -8
View File
@@ -582,9 +582,6 @@ 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()
@@ -613,8 +610,6 @@ 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
@@ -771,9 +766,7 @@ func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) {
}
for _, d := range dp.dispatchers {
// 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 {
if err := d.inGroup.Wait(); err != nil {
return err
}
}
+3 -1
View File
@@ -326,7 +326,9 @@ func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
renderer.WithNodeRenderers(
util.Prioritized(&hugoContextRenderer{
logger: a.logger,
Writer: html.DefaultWriter,
Config: html.Config{
Writer: html.DefaultWriter,
},
}, 50),
),
)
+12 -8
View File
@@ -36,7 +36,9 @@ var _ renderer.SetOptioner = (*hookedRenderer)(nil)
func newLinkRenderer(cfg goldmark_config.Config) renderer.NodeRenderer {
r := &hookedRenderer{
linkifyProtocol: []byte(cfg.Extensions.LinkifyProtocol),
Writer: html.DefaultWriter,
Config: html.Config{
Writer: html.DefaultWriter,
},
}
return r
}
@@ -167,13 +169,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
ctx.RenderContext().Ctx,
w,
imageLinkContext{
BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.HTML(text),
plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
isBlock: isBlock,
linkContext: linkContext{
BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.HTML(text),
plainText: render.TextPlain(n, source),
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
},
isBlock: isBlock,
},
)
+2 -2
View File
@@ -70,7 +70,7 @@ type SuffixInfo struct {
// If http.DetectContentType resolves to application/octet-stream, a zero Type is returned.
// If http.DetectContentType resolves to text/plain or application/xml, we try to get more specific using types and ext.
func FromContent(types Types, extensionHints []string, content []byte) Type {
t, _, _ := strings.Cut(http.DetectContentType(content), ";")
t := strings.Split(http.DetectContentType(content), ";")[0]
if t == "application/octet-stream" {
return zero
}
@@ -143,7 +143,7 @@ func FromString(t string) (Type, error) {
mainType := parts[0]
subParts := strings.Split(parts[1], "+")
subType, _, _ := strings.Cut(subParts[0], ";")
subType := strings.Split(subParts[0], ";")[0]
var suffix string
+2 -16
View File
@@ -178,29 +178,15 @@ func (s *Store) WriteMetrics(w io.Writer) {
}
sort.Sort(bySum(results))
for _, v := range results {
if s.calculateHints {
fmt.Fprintf(w, " %15s %12s %12s %9d %7.f %6d %5d %s\n", formatDuration(v.sum), formatDuration(v.avg), formatDuration(v.max), v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
fmt.Fprintf(w, " %15s %12s %12s %9d %7.f %6d %5d %s\n", v.sum, v.avg, v.max, v.cacheFactor, float64(v.cacheCount)/float64(v.count)*100, v.cacheCount, v.count, v.key)
} else {
fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", formatDuration(v.sum), formatDuration(v.avg), formatDuration(v.max), v.count, v.key)
fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", v.sum, v.avg, v.max, v.count, v.key)
}
}
}
func formatDuration(d time.Duration) string {
switch {
case d >= time.Second:
return fmt.Sprintf("%.2f s", float64(d)/float64(time.Second)) // additional spacing between value and unit
case d >= time.Millisecond:
return fmt.Sprintf("%.2f ms", float64(d)/float64(time.Millisecond))
case d >= time.Microsecond:
return fmt.Sprintf("%.2f µs", float64(d)/float64(time.Microsecond))
default:
return fmt.Sprintf("%.2f ns", float64(d)/float64(time.Nanosecond))
}
}
// A result represents the calculated results for a given metric.
type result struct {
key string
-19
View File
@@ -17,7 +17,6 @@ import (
"html/template"
"strings"
"testing"
"time"
"github.com/gohugoio/hugo/resources/page"
@@ -67,21 +66,3 @@ func BenchmarkHowSimilar(b *testing.B) {
howSimilar(s1, s2)
}
}
func TestFormatDuration(t *testing.T) {
c := qt.New(t)
tests := []struct {
duration time.Duration
want string
}{
{4*time.Second + 342*time.Millisecond, "4.34 s"}, // additional spacing between value and unit
{170*time.Millisecond + 289*time.Microsecond, "170.29 ms"},
{16*time.Microsecond + 90*time.Nanosecond, "16.09 µs"},
{147 * time.Nanosecond, "147.00 ns"},
}
for _, tt := range tests {
got := formatDuration(tt.duration)
c.Assert(got, qt.Equals, tt.want)
}
}
+1 -1
View File
@@ -615,7 +615,7 @@ func (c *Client) writeHugoDirectSum(mods Modules) error {
continue
}
if m.IsGoMod() && m.VersionQuery() != "" {
sums = append(sums, modSum{path: m.Path(), version: m.Version(), sum: m.Sum()})
sums = append(sums, modSum{pathVersionKey: pathVersionKey{path: m.Path(), version: m.Version()}, sum: m.Sum()})
}
}
+5 -18
View File
@@ -195,28 +195,15 @@ func Pack(sourceFs, assetsWithDuplicatesPreservedFs afero.Fs, mods modules.Modul
}
}
// Stable defaults; mark the workspace private so npm never publishes it.
// Preserve any hand-set values from an existing file.
name, version, private := "hugoautogen", "0.1.0", true
if data, err := afero.ReadFile(sourceFs, workspacePackageJSON); err == nil {
var existing map[string]any
if err := json.Unmarshal(data, &existing); err == nil {
if s, ok := existing["name"].(string); ok && s != "" {
name = s
}
if s, ok := existing["version"].(string); ok && s != "" {
version = s
}
if b, ok := existing["private"].(bool); ok {
private = b
}
}
name := "project"
rfi, err := sourceFs.Stat("")
if err == nil {
name = rfi.Name()
}
autoGenPkg := map[string]any{
"name": name,
"version": version,
"private": private,
"version": "0.1.0",
dependenciesKey: moduleDeps,
devDependenciesKey: moduleDevDeps,
}
@@ -92,40 +92,6 @@ func TestPackageBuilder(t *testing.T) {
b.Assert(string(meta2), qt.Equals, string(meta1))
}
// The generated workspace package.json should get a stable name and
// "private": true, and preserve hand-set name/version/private on regeneration.
// See issue 15245.
func TestPackStableNameAndPrivate(t *testing.T) {
files := getPackageBuilderTestFiles()
b := hugolib.Test(t, files)
sourceFs := b.H.BaseFs.ProjectSourceFs
assetsFs := b.H.BaseFs.AssetsWithDuplicatesPreserved.Fs
mods := b.H.Configs.Modules
b.Assert(npm.Pack(sourceFs, assetsFs, mods), qt.IsNil)
pkg, err := afero.ReadFile(sourceFs, "packages/hugoautogen/package.json")
b.Assert(err, qt.IsNil)
b.Assert(string(pkg), qt.Contains, `"name": "hugoautogen"`)
b.Assert(string(pkg), qt.Contains, `"private": true`)
b.Assert(string(pkg), qt.Contains, `"version": "0.1.0"`)
// Hand-set fields survive a re-pack.
edited := strings.NewReplacer(
`"name": "hugoautogen"`, `"name": "@foo/hugoautogen"`,
`"version": "0.1.0"`, `"version": "1.2.3"`,
`"private": true`, `"private": false`,
).Replace(string(pkg))
b.Assert(afero.WriteFile(sourceFs, "packages/hugoautogen/package.json", []byte(edited), 0o666), qt.IsNil)
b.Assert(npm.Pack(sourceFs, assetsFs, mods), qt.IsNil)
pkg, err = afero.ReadFile(sourceFs, "packages/hugoautogen/package.json")
b.Assert(err, qt.IsNil)
b.Assert(string(pkg), qt.Contains, `"name": "@foo/hugoautogen"`)
b.Assert(string(pkg), qt.Contains, `"version": "1.2.3"`)
b.Assert(string(pkg), qt.Contains, `"private": false`)
}
func BenchmarkPackageFilesSum(b *testing.B) {
files := getPackageBuilderTestFiles()
bb := hugolib.Test(b, files)
+4 -4
View File
@@ -40,8 +40,8 @@ func TestMenuCache(t *testing.T) {
m[0].MenuConfig.Title = "changed"
}
var o1 atomic.Uint64
var o2 atomic.Uint64
var o1 uint64
var o2 uint64
var wg sync.WaitGroup
@@ -59,7 +59,7 @@ func TestMenuCache(t *testing.T) {
for k, menu := range testMenuSets {
l1.Lock()
m, ca := c1.get("k1", nil, menu)
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(uint64(k), uint64(k+1)))
c.Assert(ca, qt.Equals, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
l1.Unlock()
m2, c2 := c1.get("k1", nil, m)
c.Assert(c2, qt.Equals, true)
@@ -69,7 +69,7 @@ func TestMenuCache(t *testing.T) {
l2.Lock()
m3, c3 := c1.get("k2", changeFirst, menu)
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
c.Assert(c3, qt.Equals, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
l2.Unlock()
c.Assert(m3, qt.Not(qt.IsNil))
c.Assert("changed", qt.Equals, m3[0].Title)
+10 -8
View File
@@ -72,14 +72,16 @@ type Config struct {
// can be set if position of first shortcode is known
func newPageLexer(input []byte, stateStart stateFunc, cfg Config) *pageLexer {
lexer := &pageLexer{
input: input,
stateStart: stateStart,
summaryDivider: summaryDivider,
cfg: cfg,
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
openShortcodes: make(map[unique.Handle[string]]bool),
items: make([]Item, 0, 5),
input: input,
stateStart: stateStart,
summaryDivider: summaryDivider,
cfg: cfg,
lexerShortcodeState: lexerShortcodeState{
currLeftDelimItem: tLeftDelimScNoMarkup,
currRightDelimItem: tRightDelimScNoMarkup,
openShortcodes: make(map[unique.Handle[string]]bool),
},
items: make([]Item, 0, 5),
}
lexer.sectionHandlers = createSectionHandlers(lexer)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+1 -1
View File
@@ -162,7 +162,7 @@ func NewHugoInfo(opts HugoInfoOptions) HugoInfo {
BuildDate: opts.BuildDate,
GoVersion: opts.GoVersion,
HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider,
hugoInfoProviders: hugoInfoProviders{HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider},
opts: opts,
store: hstore.NewScratch(),
+2 -3
View File
@@ -327,9 +327,8 @@ type PageMetaInternalProvider interface {
// PageRenderProvider provides a way for a Page to render content.
type PageRenderProvider interface {
// Render renders the given view (a layout template) with this Page as
// the data context, or, if given, the second argument CONTEXT.
Render(ctx context.Context, args ...any) (template.HTML, error)
// Render renders the given layout with this Page as context.
Render(ctx context.Context, layout ...string) (template.HTML, error)
// RenderString renders the first value in args with the content renderer defined
// for this Page.
// It takes an optional map as a second argument:
+2 -2
View File
@@ -117,8 +117,8 @@ func (lcp *LazyContentProvider) Len(ctx context.Context) int {
return lcp.init.Value(ctx).Len(ctx)
}
func (lcp *LazyContentProvider) Render(ctx context.Context, args ...any) (template.HTML, error) {
return lcp.init.Value(ctx).Render(ctx, args...)
func (lcp *LazyContentProvider) Render(ctx context.Context, layout ...string) (template.HTML, error) {
return lcp.init.Value(ctx).Render(ctx, layout...)
}
func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
+1 -1
View File
@@ -399,7 +399,7 @@ func (p *nopPage) RelRef(argsm map[string]any) (string, error) {
return "", nil
}
func (p *nopPage) Render(ctx context.Context, args ...any) (template.HTML, error) {
func (p *nopPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
return "", nil
}
-5
View File
@@ -130,11 +130,6 @@ func (pages Pages) ProbablyEq(other any) bool {
return true
}
// IndexOf returns the index of page in p, or -1 if not found.
func (p Pages) IndexOf(page Page) int {
return searchPage(page, p)
}
// PagesFactory somehow creates some Pages.
// We do a lot of lazy Pages initialization in Hugo, so we need a type.
type PagesFactory func() Pages
+4 -4
View File
@@ -31,8 +31,8 @@ func TestPageCache(t *testing.T) {
p[0].(*testPage).description = "changed"
}
var o1 atomic.Uint64
var o2 atomic.Uint64
var o1 uint64
var o2 uint64
var wg sync.WaitGroup
@@ -50,7 +50,7 @@ func TestPageCache(t *testing.T) {
for k, pages := range testPageSets {
l1.Lock()
p, ca := c1.get("k1", nil, pages)
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(uint64(k), uint64(k+1)))
c.Assert(ca, qt.Equals, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
l1.Unlock()
p2, c2 := c1.get("k1", nil, p)
c.Assert(c2, qt.Equals, true)
@@ -60,7 +60,7 @@ func TestPageCache(t *testing.T) {
l2.Lock()
p3, c3 := c1.get("k2", changeFirst, pages)
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
c.Assert(c3, qt.Equals, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
l2.Unlock()
c.Assert(p3, qt.Not(qt.IsNil))
c.Assert("changed", qt.Equals, p3[0].(*testPage).description)
-12
View File
@@ -70,15 +70,3 @@ func TestToPages(t *testing.T) {
_, err := ToPages("not a page")
c.Assert(err, qt.Not(qt.IsNil))
}
func TestIndexOf(t *testing.T) {
c := qt.New(t)
p1, p2, p3, p4 := &testPage{title: "p1"}, &testPage{title: "p2"}, &testPage{title: "p3"}, &testPage{title: "p4"}
pages := Pages{p1, p2, p3}
c.Assert(pages.IndexOf(p1), qt.Equals, 0)
c.Assert(pages.IndexOf(p2), qt.Equals, 1)
c.Assert(pages.IndexOf(p3), qt.Equals, 2)
c.Assert(pages.IndexOf(p4), qt.Equals, -1)
}
+1 -1
View File
@@ -483,7 +483,7 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error)
return "", nil
}
func (p *testPage) Render(ctx context.Context, args ...any) (template.HTML, error) {
func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
panic("testpage: not implemented")
}
+5 -1
View File
@@ -26,6 +26,10 @@ 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) {
return postpub.NewPostPublishResource(spec.incr.Incr(), r), nil
result := postpub.NewPostPublishResource(spec.incr.Incr(), r)
if result == nil {
panic("got nil result")
}
return result, nil
})
}
+19 -54
View File
@@ -15,17 +15,8 @@ import (
)
func main() {
/*
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.
*/
// The current is built with 2dc996f71b0ebafb77e64433e58333e049488a3c go1.26.3
// TODO(bep) preserve the staticcheck.conf file.
fmt.Println("Forking ...")
defer fmt.Println("Done ...")
@@ -34,7 +25,7 @@ func main() {
htmlRoot := filepath.Join(forkRoot, "htmltemplate")
for _, pkg := range goPackages {
copyGoPackage(pkg.dstPkg, pkg.srcPkg, pkg.skip)
copyGoPackage(pkg.dstPkg, pkg.srcPkg)
}
for _, pkg := range goPackages {
@@ -46,6 +37,7 @@ func main() {
}
const (
// TODO(bep)
goSource = "/Users/bep/dev/go/misc/go/src"
forkRoot = "../../tpl/internal/go_templates"
)
@@ -55,7 +47,6 @@ type goPackage struct {
dstPkg string
replacer func(name, content string) string
rewriter func(name string)
skip func(name string) bool
}
var (
@@ -64,14 +55,12 @@ 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) {",
)
@@ -85,26 +74,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", `//go:build !windows
content = strings.Replace(content, "package template\n", `// +build go1.13,!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
@@ -122,17 +111,6 @@ 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)
@@ -152,11 +130,6 @@ 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"`)
@@ -167,18 +140,10 @@ 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 {
return nil
if !info.IsDir() && len(path) > 10 && !strings.Contains(path, "hugo") {
must(fs.Remove(path))
}
if keepRe.MatchString(info.Name()) {
return nil
}
must(fs.Remove(path))
return nil
}))
}
@@ -189,11 +154,11 @@ func must(err error, what ...string) {
}
}
func copyGoPackage(dst, src string, skip func(name string) bool) {
func copyGoPackage(dst, src string) {
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 skip == nil || !skip(s) }))
must(hugio.CopyDir(fs, from, to, func(s string) bool { return true }))
}
func doWithGoFiles(dir string,
+5 -10
View File
@@ -119,8 +119,7 @@ usePackageJSON="auto"
"strip-ansi": "7.2.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm",
"version": "0.1.0"
}
-- golden1/package.json --
@@ -150,8 +149,7 @@ usePackageJSON="auto"
"strip-ansi": "7.2.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm",
"version": "0.1.0"
}
-- golden3/packages/hugoautogen/package.json --
@@ -170,8 +168,7 @@ usePackageJSON="auto"
"strip-ansi": "7.2.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm",
"version": "0.1.0"
}
-- golden4/packages/hugoautogen/package.json --
@@ -191,8 +188,7 @@ usePackageJSON="auto"
"strip-ansi": "7.2.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm",
"version": "0.1.0"
}
-- golden5/packages/hugoautogen/package.json --
@@ -210,8 +206,7 @@ usePackageJSON="auto"
"strip-ansi": "7.2.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm",
"version": "0.1.0"
}
-- go.mod --
@@ -42,8 +42,7 @@ go 1.20
"strip-ansi": "7.0.0",
"to-pascal-case": "1.0.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm__moduleorder",
"version": "0.1.0"
}
-- golden1/packages/hugoautogen/hugo_packagemeta.json --
@@ -37,8 +37,7 @@ path="github.com/gohugoio/hugoTestModule2"
"@babel/preset-env": "7.9.5",
"postcss-cli": "7.1.0"
},
"name": "hugoautogen",
"private": true,
"name": "script-mod_npm_withexisting",
"version": "0.1.0"
}
-- golden/package.json --
+2 -2
View File
@@ -53,8 +53,8 @@ func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error)
collator := langs.GetCollator1(ns.deps.Conf.Language().(*langs.Language))
// Create a list of pairs that will be used to do the sort
p := pairList{Collator: collator, sortComp: ns.sortComp, SortAsc: true, SliceType: sliceType,
Pairs: make([]pair, seqv.Len())}
p := pairList{Collator: collator, sortComp: ns.sortComp, SortAsc: true, SliceType: sliceType}
p.Pairs = make([]pair, seqv.Len())
var sortByField string
for i, l := range args {
@@ -22,9 +22,8 @@ const _attr_name = "attrNoneattrScriptattrScriptTypeattrStyleattrURLattrSrcsetat
var _attr_index = [...]uint8{0, 8, 18, 32, 41, 48, 58, 73}
func (i attr) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_attr_index)-1 {
if i >= attr(len(_attr_index)-1) {
return "attr(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _attr_name[_attr_index[idx]:_attr_index[idx+1]]
return _attr_name[_attr_index[i]:_attr_index[i+1]]
}
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -427,7 +428,7 @@ func TestStringer(t *testing.T) {
if err := tmpl.Execute(b, s); err != nil {
t.Fatal(err)
}
var expect = "string=3"
expect := "string=3"
if b.String() != expect {
t.Errorf("expected %q got %q", expect, b.String())
}
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -19,9 +19,8 @@ const _delim_name = "delimNonedelimDoubleQuotedelimSingleQuotedelimSpaceOrTagEnd
var _delim_index = [...]uint8{0, 9, 25, 41, 59}
func (i delim) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_delim_index)-1 {
if i >= delim(len(_delim_index)-1) {
return "delim(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _delim_name[_delim_index[idx]:_delim_index[idx+1]]
return _delim_name[_delim_index[i]:_delim_index[i+1]]
}
@@ -21,9 +21,8 @@ const _element_name = "elementNoneelementScriptelementStyleelementTextareaelemen
var _element_index = [...]uint8{0, 11, 24, 36, 51, 63, 74}
func (i element) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_element_index)-1 {
if i >= element(len(_element_index)-1) {
return "element(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _element_name[_element_index[idx]:_element_index[idx+1]]
return _element_name[_element_index[i]:_element_index[i+1]]
}
@@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"html"
//"internal/godebug"
"io"
"maps"
"regexp"
@@ -164,7 +165,9 @@ func (e *escaper) escape(c context, n parse.Node) context {
panic("escaping " + n.String() + " is unimplemented")
}
var htmlmetacontenturlescape = true
//var debugAllowActionJSTmpl = godebug.New("jstmpllitinterp")
var htmlmetacontenturlescape = true //godebug.New("htmlmetacontenturlescape")
// escapeAction escapes an action template node.
func (e *escaper) escapeAction(c context, n *parse.ActionNode) context {
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -1865,7 +1866,7 @@ func TestEscapeText(t *testing.T) {
},
{
"<script>function f() {`${ function f() { `${1}` } }`}",
context{state: stateJS, element: elementScript, jsCtx: jsCtxRegexp},
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
},
{
"<script>`${ { `` }",
@@ -2265,55 +2266,3 @@ func TestAliasedParseTreeDoesNotOverescape(t *testing.T) {
t.Fatalf(`Template "foo" and "bar" rendered %q and %q respectively, expected equal values`, got1, got2)
}
}
func TestCVE202656858(t *testing.T) {
tests := []struct {
name string
tmpl string
input string
want string
}{
{
name: "regexp after open brace in if block",
tmpl: `<script>if(true){/{{.}}/g.test("x")}</script>`,
input: "a.b",
want: `<script>if(true){/a\.b/g.test("x")}</script>`,
},
{
name: "regexp after close brace",
tmpl: `<script>if(true){x=1}/{{.}}/g.test("x")</script>`,
input: "a.b",
want: `<script>if(true){x=1}/a\.b/g.test("x")</script>`,
},
{
name: "regexp pathological attacker input",
tmpl: `<script>if(true){/{{.}}/g.test("x")}</script>`,
input: `./;alert(1);var q=/.`,
want: `<script>if(true){/\.\/;alert\(1\);var q=\/\./g.test("x")}</script>`,
},
{
name: "regexp after open brace in template literal",
tmpl: "<script>`${ (function(){/{{.}}/g.test(x)}) }`</script>",
input: "a.b",
want: "<script>`${ (function(){/a\\.b/g.test(x)}) }`</script>",
},
{
name: "regexp after close brace in template literal",
tmpl: "<script>`${ (function(){}/{{.}}/g.test(x)) }`</script>",
input: "a.b",
want: "<script>`${ (function(){}/a\\.b/g.test(x)) }`</script>",
},
}
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)
}
})
}
}
@@ -2,6 +2,9 @@
// 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 (
@@ -2,6 +2,9 @@
// 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 (
@@ -5,6 +5,7 @@
// Tests for template execution, copied from text/template.
//go:build !windows
// +build !windows
package template
@@ -324,12 +325,16 @@ 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
@@ -376,15 +381,21 @@ var execTests = []execTest{
{".Method3(nil constant)", "-{{.Method3 nil}}-", "-Method3: &lt;nil&gt;-", tVal, true},
{".Method3(nil value)", "-{{.Method3 .MXI.unset}}-", "-Method3: &lt;nil&gt;-", 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},
@@ -470,10 +481,14 @@ 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>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;nil&gt;", tVal, true}, // NOTE: "&lt;no value&gt;" in text/template
@@ -839,7 +854,7 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
var value = struct{ Str string }{hello}
value := struct{ Str string }{hello}
for i := 0; i < len(delimPairs); i += 2 {
text := ".Str"
left := delimPairs[i+0]
@@ -862,7 +877,7 @@ func TestDelims(t *testing.T) {
if err != nil {
t.Fatalf("delim %q text %q parse err %s", left, text, err)
}
var b = new(strings.Builder)
b := new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -963,7 +978,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
var tree = &Tree{
tree := &Tree{
1,
&Tree{
2, &Tree{
@@ -1214,7 +1229,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
var cmpStruct = struct {
cmpStruct := struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -220,7 +221,8 @@ func TestJSStrEscaper(t *testing.T) {
{"<!--", `\u003c!--`},
{"-->", `--\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
@@ -18,9 +18,8 @@ const _jsCtx_name = "jsCtxRegexpjsCtxDivOpjsCtxUnknown"
var _jsCtx_index = [...]uint8{0, 11, 21, 33}
func (i jsCtx) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_jsCtx_index)-1 {
if i >= jsCtx(len(_jsCtx_index)-1) {
return "jsCtx(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _jsCtx_name[_jsCtx_index[idx]:_jsCtx_index[idx+1]]
return _jsCtx_name[_jsCtx_index[i]:_jsCtx_index[i+1]]
}
@@ -5,6 +5,7 @@
// Tests for multiple-template execution, copied from text/template.
//go:build !windows
// +build !windows
package template
@@ -267,7 +268,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.
var inlined = map[string]string{
inlined := map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`,
}
@@ -46,9 +46,8 @@ 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 {
idx := int(i) - 0
if i < 0 || idx >= len(_state_index)-1 {
if i >= state(len(_state_index)-1) {
return "state(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _state_name[_state_index[idx]:_state_index[idx+1]]
return _state_name[_state_index[i]:_state_index[i+1]]
}
@@ -26,8 +26,7 @@ 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
// after the first execution.
// The underlying template's parse tree, updated to be HTML-safe.
Tree *parse.Tree
*nameSpace // common to all associated templates
}
@@ -333,12 +332,10 @@ func (t *Template) Name() string {
type FuncMap = template.FuncMap
// Funcs adds the elements of the argument map to the template's function map.
// 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 must be called before the template is parsed.
// It panics if a value in the map is not a function with appropriate return
// type. The return value is the template, so calls can be chained.
// type. However, it is legal to overwrite elements of the map. 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
@@ -2,6 +2,9 @@
// 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 (
@@ -336,14 +336,11 @@ 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
@@ -352,7 +349,6 @@ 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]
@@ -430,7 +426,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
// If "</script" appears in a regex literal, the '/' should not
// close the regex literal, and it will later be escaped to
// "\x3C/script" in escapeText.
if i > 0 && i+7 <= len(s) && bytes.EqualFold(s[i-1:i+7], []byte("</script")) {
if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
i++
} else if !inCharset {
c.state, c.jsCtx = stateJS, jsCtxDivOp
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -42,7 +43,6 @@ func TestFindEndTag(t *testing.T) {
}
func BenchmarkTemplateSpecialTags(b *testing.B) {
r := struct {
Name, Gift string
}{"Aunt Mildred", "bone china tea set"}
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -19,9 +19,8 @@ const _urlPart_name = "urlPartNoneurlPartPreQueryurlPartQueryOrFragurlPartUnknow
var _urlPart_index = [...]uint8{0, 11, 26, 44, 58}
func (i urlPart) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_urlPart_index)-1 {
if i >= urlPart(len(_urlPart_index)-1) {
return "urlPart(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _urlPart_name[_urlPart_index[idx]:_urlPart_index[idx+1]]
return _urlPart_name[_urlPart_index[i]:_urlPart_index[i+1]]
}
@@ -7,8 +7,6 @@
package testenv
import (
"errors"
"io/fs"
"syscall"
)
@@ -21,22 +19,5 @@ func syscallIsNotSupported(err error) bool {
return false
}
if errno, ok := errors.AsType[syscall.Errno](err); ok {
switch errno {
case syscall.EPERM, syscall.EROFS:
// User lacks permission: either the call requires root permission and the
// user is not root, or the call is denied by a container security policy.
return true
case syscall.EINVAL:
// Some containers return EINVAL instead of EPERM if a system call is
// denied by security policy.
return true
}
}
if errors.Is(err, fs.ErrPermission) || errors.Is(err, errors.ErrUnsupported) {
return true
}
return false
}
@@ -2,6 +2,9 @@
// 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 (
@@ -2,6 +2,9 @@
// 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 (
@@ -2,6 +2,9 @@
// 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 (
@@ -54,7 +57,7 @@ Output 2: {{printf "%q" . | title}}
}
// This example demonstrates registering two custom template functions
// and how to overwrite one of the functions after the template has been
// and how to overwite one of the functions after the template has been
// parsed. Overwriting can be used, for example, to alter the operation
// of cloned templates.
func ExampleTemplate_funcs() {
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -336,12 +337,16 @@ 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},
@@ -388,15 +393,21 @@ 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},
@@ -489,10 +500,14 @@ 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>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true},
{
"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
"&lt;script&gt;alert(&#34;XSS&#34;);&lt;/script&gt;", nil, true,
},
{"html", `{{html .PS}}`, "a string", tVal, true},
{"html typed nil", `{{html .NIL}}`, "&lt;nil&gt;", tVal, true},
{"html untyped nil", `{{html .Empty0}}`, "&lt;no value&gt;", tVal, true},
@@ -927,8 +942,9 @@ var delimPairs = []string{
func TestDelims(t *testing.T) {
const hello = "Hello, world"
var value = struct{ Str string }{hello}
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]
@@ -939,23 +955,17 @@ func TestDelims(t *testing.T) {
if right == "" { // default case
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
text = trueLeft + text + trueRight
// Now add a comment
text += trueLeft + "/*comment*/" + trueRight
// Now add an action containing a string.
text += trueLeft + `"` + trueLeft + `"` + trueRight
// 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)
}
// 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)
b := new(strings.Builder)
err = tmpl.Execute(b, value)
if err != nil {
t.Fatalf("delim %q exec err %s", left, err)
@@ -1015,23 +1025,6 @@ 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
@@ -1077,7 +1070,7 @@ const treeTemplate = `
`
func TestTree(t *testing.T) {
var tree = &Tree{
tree := &Tree{
1,
&Tree{
2, &Tree{
@@ -1330,7 +1323,7 @@ var cmpTests = []cmpTest{
func TestComparison(t *testing.T) {
b := new(strings.Builder)
var cmpStruct = struct {
cmpStruct := struct {
Uthree, Ufour uint
NegOne, Three int
Ptr, NilPtr *int
@@ -1843,12 +1836,13 @@ 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}}",
@@ -2,6 +2,9 @@
// 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 (
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
//go:build !windows
// +build !windows
package template
@@ -10,10 +11,11 @@ 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 (
@@ -30,22 +32,32 @@ 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,
nil},
},
{
"malformed name", `{{define "foo}} FOO `, hasError,
nil,
nil,
},
}
func TestMultiParse(t *testing.T) {
@@ -442,7 +454,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.
var inlined = map[string]string{
inlined := map[string]string{
"stylesheet": `{{define "stylesheet"}}stylesheet{{end}}`,
"xhtml": `{{block "stylesheet" .}}{{end}}`,
}
@@ -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 = defaultLeftDelim
left = leftDelim
}
if right == "" {
right = defaultRightDelim
right = rightDelim
}
l := &lexer{
name: name,
@@ -260,10 +260,10 @@ func lex(name, input, left, right string) *lexer {
// state functions
const (
defaultLeftDelim = "{{"
defaultRightDelim = "}}"
leftComment = "/*"
rightComment = "*/"
leftDelim = "{{"
rightDelim = "}}"
leftComment = "/*"
rightComment = "*/"
)
// lexText scans until an opening action delimiter, "{{".
@@ -2,6 +2,9 @@
// 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 (
@@ -171,9 +171,9 @@ func (c *CommentNode) String() string {
}
func (c *CommentNode) writeTo(sb *strings.Builder) {
sb.WriteString(c.tr.leftDelim)
sb.WriteString("{{")
sb.WriteString(c.Text)
sb.WriteString(c.tr.rightDelim)
sb.WriteString("}}")
}
func (c *CommentNode) tree() *Tree {
@@ -277,9 +277,9 @@ func (a *ActionNode) String() string {
}
func (a *ActionNode) writeTo(sb *strings.Builder) {
sb.WriteString(a.tr.leftDelim)
sb.WriteString("{{")
a.Pipe.writeTo(sb)
sb.WriteString(a.tr.rightDelim)
sb.WriteString("}}")
}
func (a *ActionNode) tree() *Tree {
@@ -793,7 +793,7 @@ func (t *Tree) newEnd(pos Pos) *endNode {
}
func (e *endNode) String() string {
return e.tr.leftDelim + "end" + e.tr.rightDelim
return "{{end}}"
}
func (e *endNode) writeTo(sb *strings.Builder) {
@@ -825,7 +825,7 @@ func (e *elseNode) Type() NodeType {
}
func (e *elseNode) String() string {
return e.tr.leftDelim + "else" + e.tr.rightDelim
return "{{else}}"
}
func (e *elseNode) writeTo(sb *strings.Builder) {
@@ -869,21 +869,17 @@ func (b *BranchNode) writeTo(sb *strings.Builder) {
default:
panic("unknown branch type")
}
sb.WriteString(b.tr.leftDelim)
sb.WriteString("{{")
sb.WriteString(name)
sb.WriteByte(' ')
b.Pipe.writeTo(sb)
sb.WriteString(b.tr.rightDelim)
sb.WriteString("}}")
b.List.writeTo(sb)
if b.ElseList != nil {
sb.WriteString(b.tr.leftDelim)
sb.WriteString("else")
sb.WriteString(b.tr.rightDelim)
sb.WriteString("{{else}}")
b.ElseList.writeTo(sb)
}
sb.WriteString(b.tr.leftDelim)
sb.WriteString("end")
sb.WriteString(b.tr.rightDelim)
sb.WriteString("{{end}}")
}
func (b *BranchNode) tree() *Tree {
@@ -929,9 +925,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 b.tr.leftDelim + "break" + b.tr.rightDelim }
func (b *BreakNode) String() string { return "{{break}}" }
func (b *BreakNode) tree() *Tree { return b.tr }
func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString(b.String()) }
func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") }
// ContinueNode represents a {{continue}} action.
type ContinueNode struct {
@@ -946,9 +942,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 c.tr.leftDelim + "continue" + c.tr.rightDelim }
func (c *ContinueNode) String() string { return "{{continue}}" }
func (c *ContinueNode) tree() *Tree { return c.tr }
func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString(c.String()) }
func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") }
// RangeNode represents a {{range}} action and its commands.
type RangeNode struct {
@@ -997,14 +993,13 @@ func (t *TemplateNode) String() string {
}
func (t *TemplateNode) writeTo(sb *strings.Builder) {
sb.WriteString(t.tr.leftDelim)
sb.WriteString("template ")
sb.WriteString("{{template ")
sb.WriteString(strconv.Quote(t.Name))
if t.Pipe != nil {
sb.WriteByte(' ')
t.Pipe.writeTo(sb)
}
sb.WriteString(t.tr.rightDelim)
sb.WriteString("}}")
}
func (t *TemplateNode) tree() *Tree {
@@ -33,9 +33,6 @@ 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.
@@ -63,12 +60,10 @@ func (t *Tree) Copy() *Tree {
return nil
}
return &Tree{
Name: t.Name,
ParseName: t.ParseName,
Root: t.Root.CopyList(),
text: t.text,
leftDelim: t.leftDelim,
rightDelim: t.rightDelim,
Name: t.Name,
ParseName: t.ParseName,
Root: t.Root.CopyList(),
text: t.text,
}
}
@@ -263,15 +258,7 @@ 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
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)
lexer := lex(t.Name, text, leftDelim, rightDelim)
t.startParse(funcs, lexer, treeSet)
t.text = text
t.parse()
@@ -331,8 +318,6 @@ 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()
@@ -561,7 +546,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",
@@ -665,8 +650,6 @@ 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
@@ -2,6 +2,9 @@
// 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 (
@@ -412,36 +415,6 @@ 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,
@@ -167,13 +167,11 @@ func (t *Template) Delims(left, right string) *Template {
}
// Funcs adds the elements of the argument map to the template's function map.
// 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 must be called before the template is parsed.
// 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.
// The return value is the template, so calls can be chained.
// It is legal to overwrite elements of the map. The return value is the template,
// so calls can be chained.
func (t *Template) Funcs(funcMap FuncMap) *Template {
t.init()
t.muFuncs.Lock()
+2 -2
View File
@@ -170,8 +170,8 @@ D1
got := buf.String()
// Get rid of all the durations, including the space and unit, they are never the same.
durationRe := regexp.MustCompile(`\b[\.\d]*\s*(ms|ns|µs|s)\b`)
// Get rid of all the durations, they are never the same.
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
normalize := func(s string) string {
s = durationRe.ReplaceAllString(s, "")
-5
View File
@@ -90,11 +90,6 @@ func init() {
[][2]string{},
)
ns.AddMethodMapping(ctx.Publish,
nil,
[][2]string{},
)
ns.AddMethodMapping(ctx.PostProcess,
nil,
[][2]string{},
-12
View File
@@ -296,18 +296,6 @@ func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource,
return ns.minifyClient.Minify(r)
}
// Publish publishes r to the destination and returns it.
func (ns *Namespace) Publish(r resource.Resource) (resource.Resource, error) {
s, ok := r.(resource.Source)
if !ok {
return nil, fmt.Errorf("%T can not be published", r)
}
if err := s.Publish(); err != nil {
return nil, err
}
return r, nil
}
// PostProcess processes r after the build.
//
// Deprecated: Use templates.Defer instead.
@@ -278,26 +278,6 @@ disableKinds = ['page','section','rss','sitemap','taxonomy','term']
b.AssertLogContains("! WARN Dart Sass: hugo:vars")
}
// See issue 15208.
func TestPublish(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "section", "RSS", "sitemap", "robotsTXT", "404"]
-- assets/js/main.js --
let foo;
-- layouts/home.html --
{{ $r := resources.Get "js/main.js" | minify | resources.Publish }}
Name: {{ $r.Name }}|
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html", "Name: /js/main.js|")
b.AssertFileExists("public/js/main.min.js", true)
}
// See issue 15086.
func TestPostProcessDeprecated(t *testing.T) {
t.Parallel()
+2 -3
View File
@@ -18,7 +18,6 @@ import (
"html"
"html/template"
"regexp"
"slices"
"strings"
"unicode"
"unicode/utf8"
@@ -133,8 +132,8 @@ func (ns *Namespace) Truncate(s any, options ...any) (template.HTML, error) {
out.WriteString(ellipsis)
// Close out any open HTML tags
var currentTag *htmlTag
for _, tag := range slices.Backward(tags) {
for i := len(tags) - 1; i >= 0; i-- {
tag := tags[i]
if tag.pos >= endTextPos || currentTag != nil {
if currentTag != nil && currentTag.name == tag.name {
currentTag = nil