Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a25af7facf | |||
| 723579ff54 | |||
| 85ad5e48eb | |||
| 7b5199fdef | |||
| e31ff547d2 | |||
| 87260e4a60 | |||
| 8405b802cf | |||
| bf05832d14 | |||
| a05736cb9f | |||
| 49dceb19f5 | |||
| 5e7099256e | |||
| 0805c734a4 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. Defaults to the first language defined.")
|
||||
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for (default is the default content language)")
|
||||
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)
|
||||
|
||||
@@ -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 := len(s.items) - 1; i >= 0; i-- {
|
||||
if predicate(s.items[i]) {
|
||||
items = append(items, s.items[i])
|
||||
for i, v := range slices.Backward(s.items) {
|
||||
if predicate(v) {
|
||||
items = append(items, v)
|
||||
s.items = slices.Delete(s.items, i, i+1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
|
||||
// This should be the only one.
|
||||
var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 165,
|
||||
Minor: 166,
|
||||
PatchLevel: 0,
|
||||
Suffix: "",
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -1005,29 +1005,30 @@ 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"},
|
||||
RootConfig: RootConfig{
|
||||
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"},
|
||||
Environment: hugo.EnvironmentProduction,
|
||||
TitleCaseStyle: "AP",
|
||||
PluralizeListTitles: true,
|
||||
CapitalizeListTitles: true,
|
||||
StaticDir: []string{"static"},
|
||||
SummaryLength: 70,
|
||||
Timeout: "60s",
|
||||
|
||||
CommonDirs: config.CommonDirs{
|
||||
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.
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.Split(filename, helpers.FilePathSeparator)[0]
|
||||
root, _, _ := strings.Cut(filename, helpers.FilePathSeparator)
|
||||
afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error {
|
||||
if info != nil && !info.IsDir() {
|
||||
fmt.Println(" ", path)
|
||||
|
||||
@@ -459,7 +459,7 @@ func (c TestConfig) IsZero() bool {
|
||||
|
||||
// BuildState are state used during a build.
|
||||
type BuildState struct {
|
||||
counter uint64
|
||||
counter atomic.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(atomic.AddUint64(&b.counter, uint64(1)))
|
||||
return int(b.counter.Add(uint64(1)))
|
||||
}
|
||||
|
||||
@@ -186,4 +186,4 @@ require (
|
||||
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
|
||||
)
|
||||
|
||||
go 1.26.0
|
||||
go 1.27.0
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,7 +25,7 @@ type contentNodeShifter struct {
|
||||
conf config.AllProvider // Used for logging/debugging.
|
||||
}
|
||||
|
||||
func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (contentNode, bool, bool) {
|
||||
func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (contentNode, contentNode, bool, bool) {
|
||||
switch v := n.(type) {
|
||||
case contentNodesMap:
|
||||
deleted, wasDeleted := v[vec]
|
||||
@@ -33,49 +33,53 @@ func (s *contentNodeShifter) Delete(n contentNode, vec sitesmatrix.Vector) (cont
|
||||
delete(v, vec)
|
||||
resource.MarkStale(deleted)
|
||||
}
|
||||
return deleted, wasDeleted, len(v) == 0
|
||||
return v, deleted, wasDeleted, len(v) == 0
|
||||
case contentNodeForSite:
|
||||
if v.siteVector() != vec {
|
||||
return nil, false, false
|
||||
return v, nil, false, false
|
||||
}
|
||||
resource.MarkStale(v)
|
||||
return v, true, true
|
||||
return nil, v, true, true
|
||||
case contentNodes:
|
||||
var deleted contentNodes
|
||||
for i, nn := range v {
|
||||
if vv, ok, _ := s.Delete(nn, vec); ok {
|
||||
var deleted, remaining contentNodes
|
||||
for _, nn := range v {
|
||||
updated, vv, ok, isEmpty := s.Delete(nn, vec)
|
||||
if ok {
|
||||
deleted = append(deleted, vv)
|
||||
v = append(v[:i], v[i+1:]...)
|
||||
}
|
||||
if !isEmpty {
|
||||
remaining = append(remaining, updated)
|
||||
}
|
||||
}
|
||||
if len(deleted) == 0 {
|
||||
return nil, false, false
|
||||
return v, nil, false, false
|
||||
}
|
||||
return deleted, true, len(v) == 0
|
||||
return remaining, deleted, true, len(remaining) == 0
|
||||
default:
|
||||
v = v.(contentNodeSingle) // Ensure single node.
|
||||
resource.MarkStale(v)
|
||||
return v, true, true
|
||||
|
||||
vv := v.(contentNodeSingle) // Ensure single node.
|
||||
resource.MarkStale(vv)
|
||||
return nil, vv, true, true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) bool) bool {
|
||||
func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) bool) (contentNode, bool) {
|
||||
switch ss := v.(type) {
|
||||
case contentNodeSingle:
|
||||
if f(ss) {
|
||||
resource.MarkStale(ss)
|
||||
return true
|
||||
return nil, true
|
||||
}
|
||||
return false
|
||||
return ss, false
|
||||
case contentNodes:
|
||||
for i, n := range ss {
|
||||
var remaining contentNodes
|
||||
for _, n := range ss {
|
||||
if f(n) {
|
||||
resource.MarkStale(n)
|
||||
ss = append(ss[:i], ss[i+1:]...)
|
||||
} else {
|
||||
remaining = append(remaining, n)
|
||||
}
|
||||
}
|
||||
return len(ss) == 0
|
||||
return remaining, len(remaining) == 0
|
||||
case contentNodesMap:
|
||||
for k, n := range ss {
|
||||
if f(n) {
|
||||
@@ -83,7 +87,7 @@ func (s *contentNodeShifter) DeleteFunc(v contentNode, f func(n contentNode) boo
|
||||
delete(ss, k)
|
||||
}
|
||||
}
|
||||
return len(ss) == 0
|
||||
return ss, len(ss) == 0
|
||||
default:
|
||||
panic(fmt.Sprintf("DeleteFunc: unknown type %T", v))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2025 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
)
|
||||
|
||||
type testContentNodeForSite struct {
|
||||
vec sitesmatrix.Vector
|
||||
}
|
||||
|
||||
func (n *testContentNodeForSite) Path() string { return "/test" }
|
||||
func (n *testContentNodeForSite) nodeCategorySingle() {}
|
||||
func (n *testContentNodeForSite) siteVector() sitesmatrix.Vector { return n.vec }
|
||||
func (n *testContentNodeForSite) forEeachContentNode(f func(sitesmatrix.Vector, contentNode) bool) bool {
|
||||
return f(n.vec, n)
|
||||
}
|
||||
|
||||
// See issue 15207.
|
||||
func TestContentNodeShifterDeleteMultipleFromNodes(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
s := &contentNodeShifter{}
|
||||
vec := sitesmatrix.Vector{2, 0, 0}
|
||||
other := sitesmatrix.Vector{1, 0, 0}
|
||||
|
||||
newNodes := func() contentNodes {
|
||||
return contentNodes{
|
||||
&testContentNodeForSite{vec: other},
|
||||
&testContentNodeForSite{vec: vec},
|
||||
&testContentNodeForSite{vec: other},
|
||||
&testContentNodeForSite{vec: other},
|
||||
&testContentNodeForSite{vec: vec},
|
||||
}
|
||||
}
|
||||
|
||||
updated, deleted, wasDeleted, isEmpty := s.Delete(newNodes(), vec)
|
||||
c.Assert(wasDeleted, qt.IsTrue)
|
||||
c.Assert(isEmpty, qt.IsFalse)
|
||||
c.Assert(deleted.(contentNodes), qt.HasLen, 2)
|
||||
remaining := updated.(contentNodes)
|
||||
c.Assert(remaining, qt.HasLen, 3)
|
||||
for _, n := range remaining {
|
||||
c.Assert(n.(contentNodeForSite).siteVector(), qt.Equals, other)
|
||||
}
|
||||
|
||||
// Delete all.
|
||||
updated, deleted, wasDeleted, isEmpty = s.Delete(contentNodes{
|
||||
&testContentNodeForSite{vec: vec},
|
||||
&testContentNodeForSite{vec: vec},
|
||||
}, vec)
|
||||
c.Assert(wasDeleted, qt.IsTrue)
|
||||
c.Assert(isEmpty, qt.IsTrue)
|
||||
c.Assert(deleted.(contentNodes), qt.HasLen, 2)
|
||||
c.Assert(updated, qt.IsNil)
|
||||
|
||||
// Delete none.
|
||||
nodes := contentNodes{&testContentNodeForSite{vec: other}}
|
||||
updated, deleted, wasDeleted, isEmpty = s.Delete(nodes, vec)
|
||||
c.Assert(wasDeleted, qt.IsFalse)
|
||||
c.Assert(isEmpty, qt.IsFalse)
|
||||
c.Assert(deleted, qt.IsNil)
|
||||
c.Assert(updated.(contentNodes), qt.HasLen, 1)
|
||||
}
|
||||
@@ -258,12 +258,15 @@ func (s *testShifter) Insert(old, new *testValue) (*testValue, *testValue, bool)
|
||||
return new, old, true
|
||||
}
|
||||
|
||||
func (s *testShifter) Delete(n *testValue, dimension sitesmatrix.Vector) (*testValue, bool, bool) {
|
||||
return nil, true, true
|
||||
func (s *testShifter) Delete(n *testValue, dimension sitesmatrix.Vector) (*testValue, *testValue, bool, bool) {
|
||||
return nil, n, true, true
|
||||
}
|
||||
|
||||
func (s *testShifter) DeleteFunc(v *testValue, f func(*testValue) bool) bool {
|
||||
return f(v)
|
||||
func (s *testShifter) DeleteFunc(v *testValue, f func(*testValue) bool) (*testValue, bool) {
|
||||
if f(v) {
|
||||
return nil, true
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
func (s *testShifter) Shift(n *testValue, dimension sitesmatrix.Vector, fallback bool) (v *testValue, ok bool) {
|
||||
|
||||
@@ -49,12 +49,14 @@ type (
|
||||
// and a bool indicating if an existing record is updated.
|
||||
Insert(old, new T) (T, T, bool)
|
||||
|
||||
// Delete deletes T from the given dimension and returns the deleted T and whether the dimension was deleted and if it's empty after the delete.
|
||||
Delete(v T, dimension sitesmatrix.Vector) (T, bool, bool)
|
||||
// Delete deletes T from the given dimension.
|
||||
// It returns the updated T, the deleted T, whether anything was deleted
|
||||
// and whether T is empty after the delete.
|
||||
Delete(v T, dimension sitesmatrix.Vector) (T, T, bool, bool)
|
||||
|
||||
// DeleteFunc deletes nodes in v from the tree where the given function returns true.
|
||||
// It returns true if it's empty after the delete.
|
||||
DeleteFunc(v T, f func(n T) bool) bool
|
||||
// DeleteFunc deletes nodes in v where the given function returns true.
|
||||
// It returns the updated T and whether it's empty after the delete.
|
||||
DeleteFunc(v T, f func(n T) bool) (T, bool)
|
||||
|
||||
// Shift shifts v into the given dimension,
|
||||
// if fallback is true, it will fall back a fallback match if found.
|
||||
@@ -118,7 +120,7 @@ func (r *NodeShiftTree[T]) DeleteFuncRaw(key string, f func(T) bool) (T, int) {
|
||||
return lastDeleted, count
|
||||
}
|
||||
|
||||
isEmpty := r.shifter.DeleteFunc(v, func(n T) bool {
|
||||
updated, isEmpty := r.shifter.DeleteFunc(v, func(n T) bool {
|
||||
if f(n) {
|
||||
count++
|
||||
lastDeleted = n
|
||||
@@ -129,6 +131,8 @@ func (r *NodeShiftTree[T]) DeleteFuncRaw(key string, f func(T) bool) (T, int) {
|
||||
|
||||
if isEmpty {
|
||||
r.tree.Delete(key)
|
||||
} else if count > 0 {
|
||||
r.tree.Insert(key, updated)
|
||||
}
|
||||
|
||||
return lastDeleted, count
|
||||
@@ -161,10 +165,15 @@ func (r *NodeShiftTree[T]) delete(key string) (T, bool) {
|
||||
var wasDeleted bool
|
||||
var deleted T
|
||||
if v, ok := r.tree.Get(key); ok {
|
||||
var isEmpty bool
|
||||
deleted, wasDeleted, isEmpty = r.shifter.Delete(v, r.siteVector)
|
||||
var (
|
||||
updated T
|
||||
isEmpty bool
|
||||
)
|
||||
updated, deleted, wasDeleted, isEmpty = r.shifter.Delete(v, r.siteVector)
|
||||
if isEmpty {
|
||||
r.tree.Delete(key)
|
||||
} else if wasDeleted {
|
||||
r.tree.Insert(key, updated)
|
||||
}
|
||||
}
|
||||
return deleted, wasDeleted
|
||||
|
||||
@@ -16,6 +16,7 @@ package doctree
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -251,8 +252,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 i := len(ctx.eventHandlers[event.Name]) - 1; i >= 0; i-- {
|
||||
ctx.eventHandlers[event.Name][i](event)
|
||||
for _, v := range slices.Backward(ctx.eventHandlers[event.Name]) {
|
||||
v(event)
|
||||
if event.stopPropagation {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -924,8 +924,9 @@ func (s *IntegrationTestBuilder) initBuilder() error {
|
||||
|
||||
if s.Cfg.Running {
|
||||
flags.Set("internal", hmaps.Params{
|
||||
"running": s.Cfg.Running,
|
||||
"watch": s.Cfg.Running,
|
||||
"running": s.Cfg.Running,
|
||||
"watch": s.Cfg.Running,
|
||||
"fastRenderMode": s.Cfg.FastRenderMode,
|
||||
})
|
||||
} else if s.Cfg.Watching {
|
||||
flags.Set("internal", hmaps.Params{
|
||||
@@ -1179,23 +1180,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)
|
||||
}
|
||||
|
||||
@@ -1232,6 +1217,10 @@ 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.
|
||||
|
||||
@@ -332,10 +332,8 @@ func (ps *pageState) RegularPagesRecursive() page.Pages {
|
||||
case kinds.KindSection, kinds.KindHome:
|
||||
return ps.s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: ps.Path(),
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
},
|
||||
Path: ps.Path(),
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
Recursive: true,
|
||||
},
|
||||
)
|
||||
@@ -354,10 +352,8 @@ func (ps *pageState) RegularPages() page.Pages {
|
||||
case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy:
|
||||
return ps.s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: ps.Path(),
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
},
|
||||
Path: ps.Path(),
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
},
|
||||
)
|
||||
case kinds.KindTerm:
|
||||
@@ -379,13 +375,11 @@ func (ps *pageState) Pages() page.Pages {
|
||||
case kinds.KindSection, kinds.KindHome:
|
||||
return ps.s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: ps.Path(),
|
||||
KeyPart: "page-section",
|
||||
Include: pagePredicates.ShouldListLocal.And(
|
||||
pagePredicates.KindPage.Or(pagePredicates.KindSection),
|
||||
).BoolFunc(),
|
||||
},
|
||||
Path: ps.Path(),
|
||||
KeyPart: "page-section",
|
||||
Include: pagePredicates.ShouldListLocal.And(
|
||||
pagePredicates.KindPage.Or(pagePredicates.KindSection),
|
||||
).BoolFunc(),
|
||||
},
|
||||
)
|
||||
case kinds.KindTerm:
|
||||
@@ -397,11 +391,9 @@ func (ps *pageState) Pages() page.Pages {
|
||||
case kinds.KindTaxonomy:
|
||||
return ps.s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: ps.Path(),
|
||||
KeyPart: "term",
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
|
||||
},
|
||||
Path: ps.Path(),
|
||||
KeyPart: "term",
|
||||
Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm).BoolFunc(),
|
||||
Recursive: true,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -807,13 +807,13 @@ func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlai
|
||||
|
||||
// TODO(bep) is set in a test. Fix that.
|
||||
if result.fuzzyWordCount == 0 {
|
||||
result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100
|
||||
result.fuzzyWordCount = (result.wordCount + 99) / 100 * 100
|
||||
}
|
||||
|
||||
if isCJKLanguage {
|
||||
result.readingTime = (result.wordCount + 500) / 501
|
||||
result.readingTime = (result.wordCount + 499) / 500
|
||||
} else {
|
||||
result.readingTime = (result.wordCount + 212) / 213
|
||||
result.readingTime = (result.wordCount + 211) / 212
|
||||
}
|
||||
|
||||
rs.Value = result
|
||||
@@ -941,10 +941,8 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
|
||||
|
||||
if pageparser.HasShortcode(contentToRender) {
|
||||
ct := contentTableOfContents{
|
||||
sourceInfo: sourceInfo{
|
||||
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
|
||||
source: []byte(contentToRender),
|
||||
},
|
||||
filename: pco.po.p.pathOrTitle() + " (rendered from string)",
|
||||
source: []byte(contentToRender),
|
||||
}
|
||||
ct.contentToRender = ct.source
|
||||
// String contains a shortcode.
|
||||
|
||||
@@ -412,7 +412,8 @@ baseURL = "http://example.com/"
|
||||
p := b.H.Sites[0].RegularPages()[0]
|
||||
|
||||
b.Assert(p.Summary(context.Background()), qt.Equals, template.HTML(
|
||||
"<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>"))
|
||||
"<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>",
|
||||
))
|
||||
|
||||
cnt := content(p)
|
||||
b.Assert(cnt, qt.Equals, "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>Many people say so. <a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">↩︎</a></p>\n</li>\n</ol>\n</div>")
|
||||
@@ -730,7 +731,8 @@ This is **content**.
|
||||
Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|
|
||||
Content: {{ .Content }}|
|
||||
|
||||
`).AssertFileContent("public/simple/index.html",
|
||||
`).AssertFileContent(
|
||||
"public/simple/index.html",
|
||||
"Summary: <p>This is <strong>summary</strong>.</p>|",
|
||||
"Truncated: true|",
|
||||
"Content: <p>This is <strong>summary</strong>.</p>\n<p>This is <strong>content</strong>.</p>|",
|
||||
@@ -1286,13 +1288,15 @@ AllTranslations: {{ range .AllTranslations }}{{ .Language.Lang }}|{{ end }}|
|
||||
|
||||
`
|
||||
b := Test(t, files)
|
||||
b.AssertFileContent("public/en/sect/p1/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/en/sect/p1/index.html",
|
||||
"TranslationKey: adfasdf|",
|
||||
"AllTranslations: en|nn||",
|
||||
"Translations: nn||",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/nn/sect/p1/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/nn/sect/p1/index.html",
|
||||
"TranslationKey: adfasdf|",
|
||||
"Translations: en||",
|
||||
"AllTranslations: en|nn||",
|
||||
@@ -1378,12 +1382,14 @@ Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ .Content }}|{{ end }}|
|
||||
|
||||
`
|
||||
b := Test(t, files)
|
||||
b.AssertFileContent("public/en/sect/mybundle_en/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/en/sect/mybundle_en/index.html",
|
||||
"TranslationKey: adfasdf|",
|
||||
"Resources: /en/sect/mybundle_en/f1.txt|f1.en|/en/sect/mybundle_en/f2.txt|f2.en||",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/nn/sect/mybundle_nn/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/nn/sect/mybundle_nn/index.html",
|
||||
"TranslationKey: adfasdf|",
|
||||
"Title: mybundle nn|TranslationKey: adfasdf|\nResources: /en/sect/mybundle_en/f1.txt|f1.en|/nn/sect/mybundle_nn/f2.nn.txt|f2.nn||",
|
||||
)
|
||||
@@ -1497,31 +1503,37 @@ CONTENT:{{ .Content }}
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/page-md-shortcode/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-md-shortcode/index.html",
|
||||
"SUMMARY:<p>This is a a shortcode.</p>:END",
|
||||
"CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.</p>\n",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/page-md-shortcode-same-line/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-md-shortcode-same-line/index.html",
|
||||
"SUMMARY:<p>This is a a shortcode</p>:END",
|
||||
"CONTENT:<p>This is a a shortcode</p>\n\n<p>Same line.</p>\n",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-md-shortcode-same-line-after/index.html",
|
||||
"SUMMARY:<p>Summary</p>:END",
|
||||
"CONTENT:<p>Summary</p>\n\na shortcode",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/page-org-shortcode/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-org-shortcode/index.html",
|
||||
"SUMMARY:<p>\nThis is a a shortcode.\n</p>:END",
|
||||
"CONTENT:<p>\nThis is a a shortcode.\n</p>\n<p>\nContent.\t\n</p>\n",
|
||||
)
|
||||
b.AssertFileContent("public/page-org-variant1/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-org-variant1/index.html",
|
||||
"SUMMARY:<p>\nSummary.\n</p>:END",
|
||||
"CONTENT:<p>\nSummary.\n</p>\n<p>\nContent.\t\n</p>\n",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/page-md-only-shortcode/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/page-md-only-shortcode/index.html",
|
||||
"SUMMARY:a shortcode:END",
|
||||
"CONTENT:a shortcode\n\na shortcode\n",
|
||||
)
|
||||
@@ -1731,7 +1743,8 @@ c: {{ .Scratch.Get "c" }}
|
||||
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/index.html",
|
||||
".Scratch eq .Store: true",
|
||||
"a: b",
|
||||
"c: d",
|
||||
@@ -1928,7 +1941,8 @@ Site: {{ site.Store.Get "Site" }}|
|
||||
|
||||
b := TestRunning(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/index.html",
|
||||
`
|
||||
Shortcode: sh-Home|
|
||||
Page: p-Home|
|
||||
@@ -1939,7 +1953,8 @@ Hugo: h-Home|
|
||||
|
||||
b.EditFileReplaceAll("content/_index.md", "Home", "Homer").Build()
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/index.html",
|
||||
`
|
||||
Shortcode: sh-Homer|
|
||||
Page: p-Homer|
|
||||
@@ -2149,7 +2164,8 @@ EF
|
||||
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/s1/p1/index.html",
|
||||
b.AssertFileContent(
|
||||
"public/s1/p1/index.html",
|
||||
"ab|ab|ab",
|
||||
"cD|cD|cD",
|
||||
"EF|EF|EF",
|
||||
@@ -2368,3 +2384,77 @@ All.
|
||||
myothersection, _ := s.GetPage("myothersection") // backed by a content file.
|
||||
check(myothersection, true)
|
||||
}
|
||||
|
||||
// See issue 15206.
|
||||
func TestReadingTimeAndFuzzyWordCountBoundaries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["home", "section", "taxonomy", "term", "rss", "sitemap"]
|
||||
-- content/p99.md --
|
||||
---
|
||||
title: p99
|
||||
---
|
||||
` + strings.Repeat("word ", 99) + `
|
||||
-- content/p100.md --
|
||||
---
|
||||
title: p100
|
||||
---
|
||||
` + strings.Repeat("word ", 100) + `
|
||||
-- content/p101.md --
|
||||
---
|
||||
title: p101
|
||||
---
|
||||
` + strings.Repeat("word ", 101) + `
|
||||
-- content/p211.md --
|
||||
---
|
||||
title: p211
|
||||
---
|
||||
` + strings.Repeat("word ", 211) + `
|
||||
-- content/p212.md --
|
||||
---
|
||||
title: p212
|
||||
---
|
||||
` + strings.Repeat("word ", 212) + `
|
||||
-- content/p213.md --
|
||||
---
|
||||
title: p213
|
||||
---
|
||||
` + strings.Repeat("word ", 213) + `
|
||||
-- content/p499.md --
|
||||
---
|
||||
title: p499
|
||||
isCJKLanguage: true
|
||||
---
|
||||
` + strings.Repeat("你", 499) + `
|
||||
-- content/p500.md --
|
||||
---
|
||||
title: p500
|
||||
isCJKLanguage: true
|
||||
---
|
||||
` + strings.Repeat("你", 500) + `
|
||||
-- content/p501.md --
|
||||
---
|
||||
title: p501
|
||||
isCJKLanguage: true
|
||||
---
|
||||
` + strings.Repeat("你", 501) + `
|
||||
-- layouts/page.html --
|
||||
{{ .WordCount }}|{{ .FuzzyWordCount }}|{{ .ReadingTime }}
|
||||
`
|
||||
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p99/index.html", "99|100|1")
|
||||
b.AssertFileContent("public/p100/index.html", "100|100|1")
|
||||
b.AssertFileContent("public/p101/index.html", "101|200|1")
|
||||
|
||||
b.AssertFileContent("public/p211/index.html", "211|300|1")
|
||||
b.AssertFileContent("public/p212/index.html", "212|300|1")
|
||||
b.AssertFileContent("public/p213/index.html", "213|300|2")
|
||||
|
||||
b.AssertFileContent("public/p499/index.html", "499|500|1")
|
||||
b.AssertFileContent("public/p500/index.html", "500|500|1")
|
||||
b.AssertFileContent("public/p501/index.html", "501|600|2")
|
||||
}
|
||||
|
||||
@@ -2211,3 +2211,130 @@ 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"}`)
|
||||
}
|
||||
|
||||
@@ -759,11 +759,9 @@ func (s *Site) Pages() page.Pages {
|
||||
s.CheckReady()
|
||||
return s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: "",
|
||||
KeyPart: "global",
|
||||
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
|
||||
},
|
||||
Path: "",
|
||||
KeyPart: "global",
|
||||
Include: pagePredicates.ShouldListGlobal.BoolFunc(),
|
||||
Recursive: true,
|
||||
IncludeSelf: true,
|
||||
},
|
||||
@@ -776,11 +774,9 @@ func (s *Site) RegularPages() page.Pages {
|
||||
s.CheckReady()
|
||||
return s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: "",
|
||||
KeyPart: "global",
|
||||
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
},
|
||||
Path: "",
|
||||
KeyPart: "global",
|
||||
Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage).BoolFunc(),
|
||||
Recursive: true,
|
||||
},
|
||||
)
|
||||
@@ -922,11 +918,9 @@ func (s *Site) prepareInits() {
|
||||
|
||||
sections := s.pageMap.getPagesInSection(
|
||||
pageMapQueryPagesInSection{
|
||||
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
|
||||
Path: "",
|
||||
KeyPart: "sectionorhome",
|
||||
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
|
||||
},
|
||||
Path: "",
|
||||
KeyPart: "sectionorhome",
|
||||
Include: pagePredicates.KindSection.Or(pagePredicates.KindHome).BoolFunc(),
|
||||
IncludeSelf: true,
|
||||
Recursive: true,
|
||||
},
|
||||
@@ -1464,12 +1458,10 @@ func (s *Site) assembleMenus() (navigation.Menus, error) {
|
||||
return false, nil
|
||||
}
|
||||
me := navigation.MenuEntry{
|
||||
MenuConfig: navigation.MenuConfig{
|
||||
Identifier: id,
|
||||
Name: p.LinkTitle(),
|
||||
Weight: p.Weight(),
|
||||
},
|
||||
Page: p,
|
||||
Identifier: id,
|
||||
Name: p.LinkTitle(),
|
||||
Weight: p.Weight(),
|
||||
Page: p,
|
||||
}
|
||||
|
||||
navigation.SetPageValues(&me, p)
|
||||
@@ -1508,9 +1500,7 @@ 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{
|
||||
MenuConfig: navigation.MenuConfig{
|
||||
Name: p.EntryName,
|
||||
},
|
||||
Name: p.EntryName,
|
||||
}
|
||||
}
|
||||
flat[twoD{p.MenuName, p.EntryName}].Children = childmenu
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/4895
|
||||
@@ -314,6 +316,70 @@ X123X
|
||||
`)
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestTemplateReturnEarly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "http://example.com/"
|
||||
-- layouts/home.html --
|
||||
home-start|{{ if true }}{{ return }}{{ end }}home-end
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "home-start|", "! home-end")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestTemplateReturnEarlyFromBlock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "http://example.com/"
|
||||
-- layouts/baseof.html --
|
||||
header|{{ block "main" . }}{{ end }}|footer
|
||||
-- layouts/home.html --
|
||||
{{ define "main" }}main-start|{{ return }}main-end{{ end }}
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "header|main-start||footer", "! main-end")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestTemplateReturnEarlyFromTemplateInclude(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "http://example.com/"
|
||||
-- layouts/home.html --
|
||||
{{ template "foo" . }}|after
|
||||
{{ define "foo" }}foo-start|{{ return }}foo-end{{ end }}
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "foo-start||after", "! foo-end")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestTemplateReturnWithValueOutsidePartialFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "http://example.com/"
|
||||
-- layouts/home.html --
|
||||
{{ return 32 }}
|
||||
`
|
||||
b, err := TestE(t, files)
|
||||
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "return with a value is only supported in partials")
|
||||
}
|
||||
|
||||
func TestPartialCached(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Release env.
|
||||
# These will be replaced by script before release.
|
||||
HUGORELEASER_TAG=v0.164.0
|
||||
HUGORELEASER_COMMITISH=ce2470e7012b5ab5fc4e10ebe4027e9f8d9e00dc
|
||||
HUGORELEASER_TAG=v0.165.0
|
||||
HUGORELEASER_COMMITISH=76a5e1880ab46688155b02e99bab9be2a6134492
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -252,11 +252,11 @@ type IsRebuildProvider interface {
|
||||
|
||||
// IncrementByOne implements Incrementer adding 1 every time Incr is called.
|
||||
type IncrementByOne struct {
|
||||
counter uint64
|
||||
counter atomic.Uint64
|
||||
}
|
||||
|
||||
func (c *IncrementByOne) Incr() int {
|
||||
return int(atomic.AddUint64(&c.counter, uint64(1)))
|
||||
return int(c.counter.Add(uint64(1)))
|
||||
}
|
||||
|
||||
// Incrementer increments and returns the value.
|
||||
|
||||
@@ -528,96 +528,94 @@ func (b *batcher) doBuild(ctx context.Context) (*Package, error) {
|
||||
}
|
||||
|
||||
jsOpts := Options{
|
||||
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)
|
||||
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)
|
||||
|
||||
// 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))
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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 PrefixHugoMemory + s
|
||||
}
|
||||
return ""
|
||||
},
|
||||
EntryPoints: entryPoints,
|
||||
}
|
||||
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 PrefixHugoMemory + s
|
||||
}
|
||||
return ""
|
||||
},
|
||||
EntryPoints: entryPoints,
|
||||
}
|
||||
|
||||
result, err := b.client.buildClient.Build(jsOpts)
|
||||
|
||||
@@ -27,10 +27,8 @@ func TestToBuildOptions(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
opts := Options{
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -46,16 +44,12 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Target: []string{"es2018"},
|
||||
Format: "cjs",
|
||||
Minify: true,
|
||||
AvoidTDZ: true,
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
Target: []string{"es2018"},
|
||||
Format: "cjs",
|
||||
Minify: true,
|
||||
AvoidTDZ: true,
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -74,14 +68,10 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "inline",
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "inline",
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -101,14 +91,10 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "inline",
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "inline",
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -128,14 +114,10 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "external",
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
Target: []string{"es2018"}, Format: "cjs", Minify: true,
|
||||
SourceMap: "external",
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -155,13 +137,9 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
JSX: "automatic", JSXImportSource: "preact",
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
},
|
||||
JSX: "automatic", JSXImportSource: "preact",
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
Stdin: true,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
@@ -179,24 +157,18 @@ func TestToBuildOptions(t *testing.T) {
|
||||
})
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Drop: "console",
|
||||
},
|
||||
Drop: "console",
|
||||
}
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
c.Assert(opts.compiled.Drop, qt.Equals, api.DropConsole)
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Drop: "debugger",
|
||||
},
|
||||
Drop: "debugger",
|
||||
}
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
c.Assert(opts.compiled.Drop, qt.Equals, api.DropDebugger)
|
||||
|
||||
opts = Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Drop: "adsfadsf",
|
||||
},
|
||||
Drop: "adsfadsf",
|
||||
}
|
||||
c.Assert(opts.compile(), qt.ErrorMatches, `unsupported drop type: "adsfadsf"`)
|
||||
}
|
||||
@@ -222,12 +194,8 @@ func TestToBuildOptionsTarget(t *testing.T) {
|
||||
} {
|
||||
c.Run(test.target, func(c *qt.C) {
|
||||
opts := Options{
|
||||
ExternalOptions: ExternalOptions{
|
||||
Target: []string{test.target},
|
||||
},
|
||||
InternalOptions: InternalOptions{
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
},
|
||||
Target: []string{test.target},
|
||||
MediaType: media.Builtin.JavascriptType,
|
||||
}
|
||||
|
||||
c.Assert(opts.compile(), qt.IsNil)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +326,7 @@ func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
|
||||
renderer.WithNodeRenderers(
|
||||
util.Prioritized(&hugoContextRenderer{
|
||||
logger: a.logger,
|
||||
Config: html.Config{
|
||||
Writer: html.DefaultWriter,
|
||||
},
|
||||
Writer: html.DefaultWriter,
|
||||
}, 50),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -36,9 +36,7 @@ var _ renderer.SetOptioner = (*hookedRenderer)(nil)
|
||||
func newLinkRenderer(cfg goldmark_config.Config) renderer.NodeRenderer {
|
||||
r := &hookedRenderer{
|
||||
linkifyProtocol: []byte(cfg.Extensions.LinkifyProtocol),
|
||||
Config: html.Config{
|
||||
Writer: html.DefaultWriter,
|
||||
},
|
||||
Writer: html.DefaultWriter,
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -169,15 +167,13 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
imageLinkContext{
|
||||
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,
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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.Split(http.DetectContentType(content), ";")[0]
|
||||
t, _, _ := strings.Cut(http.DetectContentType(content), ";")
|
||||
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.Split(subParts[0], ";")[0]
|
||||
subType, _, _ := strings.Cut(subParts[0], ";")
|
||||
|
||||
var suffix string
|
||||
|
||||
|
||||
@@ -178,15 +178,29 @@ 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", v.sum, v.avg, 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", formatDuration(v.sum), formatDuration(v.avg), formatDuration(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", v.sum, v.avg, v.max, v.count, v.key)
|
||||
fmt.Fprintf(w, " %15s %12s %12s %5d %s\n", formatDuration(v.sum), formatDuration(v.avg), formatDuration(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
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
|
||||
@@ -66,3 +67,21 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ func (c *Client) writeHugoDirectSum(mods Modules) error {
|
||||
continue
|
||||
}
|
||||
if m.IsGoMod() && m.VersionQuery() != "" {
|
||||
sums = append(sums, modSum{pathVersionKey: pathVersionKey{path: m.Path(), version: m.Version()}, sum: m.Sum()})
|
||||
sums = append(sums, modSum{path: m.Path(), version: m.Version(), sum: m.Sum()})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ func TestMenuCache(t *testing.T) {
|
||||
m[0].MenuConfig.Title = "changed"
|
||||
}
|
||||
|
||||
var o1 uint64
|
||||
var o2 uint64
|
||||
var o1 atomic.Uint64
|
||||
var o2 atomic.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, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
|
||||
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(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, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
|
||||
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
|
||||
l2.Unlock()
|
||||
c.Assert(m3, qt.Not(qt.IsNil))
|
||||
c.Assert("changed", qt.Equals, m3[0].Title)
|
||||
|
||||
@@ -72,16 +72,14 @@ 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,
|
||||
lexerShortcodeState: lexerShortcodeState{
|
||||
currLeftDelimItem: tLeftDelimScNoMarkup,
|
||||
currRightDelimItem: tRightDelimScNoMarkup,
|
||||
openShortcodes: make(map[unique.Handle[string]]bool),
|
||||
},
|
||||
items: make([]Item, 0, 5),
|
||||
input: input,
|
||||
stateStart: stateStart,
|
||||
summaryDivider: summaryDivider,
|
||||
cfg: cfg,
|
||||
currLeftDelimItem: tLeftDelimScNoMarkup,
|
||||
currRightDelimItem: tRightDelimScNoMarkup,
|
||||
openShortcodes: make(map[unique.Handle[string]]bool),
|
||||
items: make([]Item, 0, 5),
|
||||
}
|
||||
|
||||
lexer.sectionHandlers = createSectionHandlers(lexer)
|
||||
|
||||
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
@@ -162,7 +162,7 @@ func NewHugoInfo(opts HugoInfoOptions) HugoInfo {
|
||||
BuildDate: opts.BuildDate,
|
||||
GoVersion: opts.GoVersion,
|
||||
|
||||
hugoInfoProviders: hugoInfoProviders{HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider},
|
||||
HugoInfoHugoSitesProvider: opts.HugoInfoHugoSitesProvider,
|
||||
|
||||
opts: opts,
|
||||
store: hstore.NewScratch(),
|
||||
|
||||
@@ -130,6 +130,11 @@ 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
|
||||
|
||||
@@ -31,8 +31,8 @@ func TestPageCache(t *testing.T) {
|
||||
p[0].(*testPage).description = "changed"
|
||||
}
|
||||
|
||||
var o1 uint64
|
||||
var o2 uint64
|
||||
var o1 atomic.Uint64
|
||||
var o2 atomic.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, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)))
|
||||
c.Assert(ca, qt.Equals, !o1.CompareAndSwap(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, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)))
|
||||
c.Assert(c3, qt.Equals, !o2.CompareAndSwap(uint64(k), uint64(k+1)))
|
||||
l2.Unlock()
|
||||
c.Assert(p3, qt.Not(qt.IsNil))
|
||||
c.Assert("changed", qt.Equals, p3[0].(*testPage).description)
|
||||
|
||||
@@ -70,3 +70,15 @@ 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,11 +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) {",
|
||||
)
|
||||
|
||||
@@ -73,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
|
||||
@@ -110,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)
|
||||
@@ -129,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"`)
|
||||
@@ -139,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
|
||||
}))
|
||||
}
|
||||
@@ -153,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,
|
||||
|
||||
@@ -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}
|
||||
p.Pairs = make([]pair, seqv.Len())
|
||||
p := pairList{Collator: collator, sortComp: ns.sortComp, SortAsc: true, SliceType: sliceType,
|
||||
Pairs: make([]pair, seqv.Len())}
|
||||
|
||||
var sortByField string
|
||||
for i, l := range args {
|
||||
|
||||
@@ -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]]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
|
||||
@@ -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]]
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
},
|
||||
{
|
||||
"<script>function f() {`${ function f() { `${1}` } }`}",
|
||||
context{state: stateJS, element: elementScript, jsCtx: jsCtxDivOp},
|
||||
context{state: stateJS, element: elementScript, jsCtx: jsCtxRegexp},
|
||||
},
|
||||
{
|
||||
"<script>`${ { `` }",
|
||||
@@ -2266,3 +2265,55 @@ 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,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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{
|
||||
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | 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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
|
||||
@@ -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) {
|
||||
{"<!--", `\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,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]]
|
||||
}
|
||||
|
||||
@@ -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}}`,
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 "</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.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
|
||||
if i > 0 && i+7 <= len(s) && bytes.EqualFold(s[i-1:i+7], []byte("</script")) {
|
||||
i++
|
||||
} else if !inCharset {
|
||||
c.state, c.jsCtx = stateJS, jsCtxDivOp
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
@@ -43,6 +42,7 @@ func TestFindEndTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkTemplateSpecialTags(b *testing.B) {
|
||||
|
||||
r := struct {
|
||||
Name, Gift string
|
||||
}{"Aunt Mildred", "bone china tea set"}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ const _urlPart_name = "urlPartNoneurlPartPreQueryurlPartQueryOrFragurlPartUnknow
|
||||
var _urlPart_index = [...]uint8{0, 11, 26, 44, 58}
|
||||
|
||||
func (i urlPart) String() string {
|
||||
if i >= urlPart(len(_urlPart_index)-1) {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_urlPart_index)-1 {
|
||||
return "urlPart(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _urlPart_name[_urlPart_index[i]:_urlPart_index[i+1]]
|
||||
return _urlPart_name[_urlPart_index[idx]:_urlPart_index[idx+1]]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -19,5 +21,22 @@ 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,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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
@@ -57,7 +54,7 @@ Output 2: {{printf "%q" . | title}}
|
||||
}
|
||||
|
||||
// This example demonstrates registering two custom template functions
|
||||
// and how to overwite one of the functions after the template has been
|
||||
// and how to overwrite 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() {
|
||||
|
||||
@@ -495,7 +495,7 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
|
||||
func (s *state) walkTemplateOld(dot reflect.Value, t *parse.TemplateNode) {
|
||||
s.at(t)
|
||||
tmpl := s.tmpl.Lookup(t.Name)
|
||||
if tmpl == nil {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
@@ -337,16 +336,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},
|
||||
@@ -393,21 +388,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},
|
||||
@@ -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>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{
|
||||
"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | html}}`,
|
||||
"<script>alert("XSS");</script>", nil, true,
|
||||
},
|
||||
{"html", `{{html "<script>alert(\"XSS\");</script>"}}`,
|
||||
"<script>alert("XSS");</script>", nil, true},
|
||||
{"html pipeline", `{{printf "<script>alert(\"XSS\");</script>" | 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}}",
|
||||
|
||||
@@ -98,8 +98,33 @@ func (t *Template) Prepare() (*Template, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ReturnError is the sentinel used by the return keyword
|
||||
// to signal an early exit from a template.
|
||||
// A bare {{ return }} is trapped at the nearest template boundary,
|
||||
// a {{ return <value> }} must be trapped by a partial invocation.
|
||||
type ReturnError struct {
|
||||
Value any
|
||||
HasValue bool
|
||||
}
|
||||
|
||||
func (r *ReturnError) Error() string {
|
||||
return "return with a value is only supported in partials"
|
||||
}
|
||||
|
||||
func (t *Template) executeWithState(state *state, value reflect.Value) (err error) {
|
||||
defer errRecover(&err)
|
||||
// Added for Hugo. Trap the return sentinel.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rerr, ok := r.(*ReturnError); ok {
|
||||
if rerr.HasValue {
|
||||
err = rerr
|
||||
}
|
||||
return
|
||||
}
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
if t.Tree == nil || t.Root == nil {
|
||||
state.errorf("%q is an incomplete or empty template", t.Name())
|
||||
}
|
||||
@@ -123,10 +148,63 @@ type state struct {
|
||||
depth int // the height of the stack of executing templates.
|
||||
}
|
||||
|
||||
// newReturnError evaluates any return value and creates the return sentinel.
|
||||
// Everything after the return keyword is evaluated as a command,
|
||||
// so both {{ return $v }} and {{ return add . 42 }} work.
|
||||
func (s *state) newReturnError(dot reflect.Value, cmd parse.Node, args []parse.Node, final reflect.Value) *ReturnError {
|
||||
rerr := &ReturnError{}
|
||||
|
||||
toValue := func(v reflect.Value) any {
|
||||
if v.IsValid() && v.Type() == reflectValueType {
|
||||
v = v.Interface().(reflect.Value)
|
||||
}
|
||||
if !v.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(args) > 1:
|
||||
rerr.HasValue = true
|
||||
if _, ok := args[1].(*parse.NilNode); ok && len(args) == 2 && isMissing(final) {
|
||||
// {{ return nil }}
|
||||
break
|
||||
}
|
||||
c := *(cmd.(*parse.CommandNode))
|
||||
c.Args = args[1:]
|
||||
rerr.Value = toValue(s.evalCommand(dot, &c, final))
|
||||
case !isMissing(final):
|
||||
rerr.HasValue = true
|
||||
rerr.Value = toValue(final)
|
||||
}
|
||||
|
||||
return rerr
|
||||
}
|
||||
|
||||
// walkTemplate traps any bare return so it only ends the execution of the
|
||||
// included template.
|
||||
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if rerr, ok := r.(*ReturnError); ok && !rerr.HasValue {
|
||||
return
|
||||
}
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
s.walkTemplateOld(dot, t)
|
||||
}
|
||||
|
||||
func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
|
||||
s.at(node)
|
||||
name := node.Ident
|
||||
|
||||
// Added for Hugo.
|
||||
if name == "return" {
|
||||
panic(s.newReturnError(dot, cmd, args, final))
|
||||
}
|
||||
|
||||
var function reflect.Value
|
||||
// Added for Hugo.
|
||||
var first reflect.Value
|
||||
@@ -296,6 +374,9 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
if name == "try" {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(*ReturnError); ok {
|
||||
panic(r)
|
||||
}
|
||||
// Cause: herrors.Cause(err)
|
||||
if err, ok := r.(error); ok {
|
||||
val = reflect.ValueOf(TryValue{Value: nil, Err: newErrorWithCause(err)})
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}}`,
|
||||
}
|
||||
|
||||
@@ -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, "{{".
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -43,9 +43,9 @@ func init() {
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
// TODO(bep) we need the return to be a valid identifiers, but
|
||||
// should consider another way of adding it.
|
||||
ns.AddMethodMapping(func() string { return "" },
|
||||
// The return keyword is intercepted in the template executor,
|
||||
// but it needs to resolve to a function.
|
||||
ns.AddMethodMapping(func(v ...any) any { return nil },
|
||||
[]string{"return"},
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
@@ -17,9 +17,9 @@ package partials
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -91,18 +91,6 @@ type Namespace struct {
|
||||
cachedPartials *partialCache
|
||||
}
|
||||
|
||||
// contextWrapper makes room for a return value in a partial invocation.
|
||||
type contextWrapper struct {
|
||||
Arg any
|
||||
Result any
|
||||
}
|
||||
|
||||
// Set sets the return value and returns an empty string.
|
||||
func (c *contextWrapper) Set(in any) string {
|
||||
c.Result = in
|
||||
return ""
|
||||
}
|
||||
|
||||
// Include executes the named partial.
|
||||
// If the partial contains a return statement, that value will be returned.
|
||||
// Else, the rendered output will be returned:
|
||||
@@ -159,39 +147,27 @@ func (ns *Namespace) doInclude(ctx context.Context, key string, templ *tplimpl.T
|
||||
data = dataList[0]
|
||||
}
|
||||
|
||||
info := templ.ParseInfo
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
|
||||
var w io.Writer
|
||||
|
||||
if info.HasReturn {
|
||||
|
||||
// Wrap the context sent to the template to capture the return value.
|
||||
// Note that the template is rewritten to make sure that the dot (".")
|
||||
// and the $ variable points to Arg.
|
||||
data = &contextWrapper{
|
||||
Arg: data,
|
||||
if err := ns.deps.GetTemplateStore().ExecuteWithContextAndKey(ctx, key, templ, b, data); err != nil {
|
||||
var rerr *texttemplate.ReturnError
|
||||
if !errors.As(err, &rerr) {
|
||||
return includeResult{err: err}
|
||||
}
|
||||
// The partial has a {{ return <value> }}; any rendered output is discarded.
|
||||
return includeResult{
|
||||
name: templ.Name(),
|
||||
result: rerr.Value,
|
||||
}
|
||||
|
||||
// We don't care about any template output.
|
||||
w = io.Discard
|
||||
} else {
|
||||
b := bp.GetBuffer()
|
||||
defer bp.PutBuffer(b)
|
||||
w = b
|
||||
}
|
||||
|
||||
if err := ns.deps.GetTemplateStore().ExecuteWithContextAndKey(ctx, key, templ, w, data); err != nil {
|
||||
return includeResult{err: err}
|
||||
}
|
||||
|
||||
var result any
|
||||
|
||||
if ctx, ok := data.(*contextWrapper); ok {
|
||||
result = ctx.Result
|
||||
} else if _, ok := templ.Template.(*texttemplate.Template); ok {
|
||||
result = w.(fmt.Stringer).String()
|
||||
if _, ok := templ.Template.(*texttemplate.Template); ok {
|
||||
result = b.String()
|
||||
} else {
|
||||
result = template.HTML(w.(fmt.Stringer).String())
|
||||
result = template.HTML(b.String())
|
||||
}
|
||||
|
||||
return includeResult{
|
||||
|
||||
@@ -170,8 +170,8 @@ D1
|
||||
|
||||
got := buf.String()
|
||||
|
||||
// Get rid of all the durations, they are never the same.
|
||||
durationRe := regexp.MustCompile(`\b[\.\d]*(ms|ns|µs|s)\b`)
|
||||
// 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`)
|
||||
|
||||
normalize := func(s string) string {
|
||||
s = durationRe.ReplaceAllString(s, "")
|
||||
@@ -335,3 +335,97 @@ BAR
|
||||
|
||||
b.AssertFileContent("public/index.html", "OO:BAR")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestPartialReturnConditional(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
1:{{ partial "parity.html" 1 }}|2:{{ partial "parity.html" 2 }}|
|
||||
-- layouts/_partials/parity.html --
|
||||
{{ if math.ModBool . 2 }}
|
||||
{{ return "even" }}
|
||||
{{ end }}
|
||||
{{ return "odd" }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "1:odd|2:even|")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestPartialReturnFromRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
{{ partial "find.html" (slice 1 2 3) }}|
|
||||
-- layouts/_partials/find.html --
|
||||
{{ range . }}{{ if eq . 2 }}{{ return . }}{{ end }}{{ end }}{{ return "notfound" }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "2|")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestPartialReturnBareStopsEarly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
{{ partial "p.html" . }}
|
||||
-- layouts/_partials/p.html --
|
||||
partial-start|{{ if true }}{{ return }}{{ end }}partial-end
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "partial-start|", "! partial-end")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestPartialReturnNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
{{ if eq (partial "p.html" .) nil }}NIL{{ end }}
|
||||
-- layouts/_partials/p.html --
|
||||
{{ return nil }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "NIL")
|
||||
}
|
||||
|
||||
// See issue 15212.
|
||||
func TestPartialReturnValueFromTemplateInclude(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
{{ partial "p.html" . }}|
|
||||
-- layouts/_partials/p.html --
|
||||
{{ template "p-helper" . }}
|
||||
{{ define "p-helper" }}{{ return 42 }}{{ end }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "42|")
|
||||
}
|
||||
|
||||
@@ -90,6 +90,11 @@ func init() {
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
ns.AddMethodMapping(ctx.Publish,
|
||||
nil,
|
||||
[][2]string{},
|
||||
)
|
||||
|
||||
ns.AddMethodMapping(ctx.PostProcess,
|
||||
nil,
|
||||
[][2]string{},
|
||||
|
||||
@@ -296,6 +296,18 @@ 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,6 +278,26 @@ 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()
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"html"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -132,8 +133,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 i := len(tags) - 1; i >= 0; i-- {
|
||||
tag := tags[i]
|
||||
for _, tag := range slices.Backward(tags) {
|
||||
|
||||
if tag.pos >= endTextPos || currentTag != nil {
|
||||
if currentTag != nil && currentTag.name == tag.name {
|
||||
currentTag = nil
|
||||
|
||||
@@ -277,7 +277,7 @@ P1.
|
||||
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "wrong number of args for string: want 1 got 0")
|
||||
b.Assert(err.Error(), qt.Contains, "return must be the last command in a pipeline")
|
||||
}
|
||||
|
||||
func TestPartialWithoutSuffixIssue13601(t *testing.T) {
|
||||
|
||||
@@ -24,9 +24,6 @@ type ParseInfo struct {
|
||||
// Set for partial templates with any {{ inner }} or {{ templates.Inner }}
|
||||
HasPartialInner bool
|
||||
|
||||
// Set for partials with a return statement.
|
||||
HasReturn bool
|
||||
|
||||
// Config extracted from template.
|
||||
Config ParseConfig
|
||||
}
|
||||
|
||||
@@ -32,9 +32,6 @@ type templateTransformContext struct {
|
||||
configChecked bool
|
||||
|
||||
t *TemplInfo
|
||||
|
||||
// Store away the return node in partials.
|
||||
returnNode *parse.CommandNode
|
||||
}
|
||||
|
||||
func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
|
||||
@@ -84,7 +81,7 @@ func applyTemplateTransformers(
|
||||
panic(fmt.Errorf("template %s not parsed", t))
|
||||
}
|
||||
|
||||
if err := c.applyTransformationsAndSetReturnWrapper(tree); err != nil {
|
||||
if err := c.applyTransformations(tree.Root); err != nil {
|
||||
return c, fmt.Errorf("failed to transform template %q: %w", t.Name(), err)
|
||||
}
|
||||
|
||||
@@ -99,12 +96,6 @@ func getParseTree(templ tpl.Template) *parse.Tree {
|
||||
}
|
||||
|
||||
const (
|
||||
// We parse this template and modify the nodes in order to assign
|
||||
// the return value of a partial to a contextWrapper via Set. We use
|
||||
// "range" over a one-element slice so we can shift dot to the
|
||||
// partial's argument, Arg, while allowing Arg to be falsy.
|
||||
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
|
||||
|
||||
doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
|
||||
|
||||
// _pushPartialDecorator is always falsy.
|
||||
@@ -117,7 +108,6 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
partialReturnWrapper *parse.ListNode
|
||||
doDefer *parse.ListNode
|
||||
popPartialDecorator *parse.ListNode
|
||||
pushPartialDecorator *parse.ListNode
|
||||
@@ -125,13 +115,7 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
partialReturnWrapper = templ.Tree.Root
|
||||
|
||||
templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
|
||||
templ, err := texttemplate.New("").Funcs(texttemplate.FuncMap{"doDefer": func(string, string) string { return "" }}).Parse(doDeferTempl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -156,39 +140,8 @@ func init() {
|
||||
popPartialDecoratorElse = templ.Tree.Root
|
||||
}
|
||||
|
||||
// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
|
||||
// predefined partial return wrapper to insert those of a user-defined partial.
|
||||
func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
|
||||
wrapper := partialReturnWrapper.CopyList()
|
||||
rangeNode := wrapper.Nodes[2].(*parse.RangeNode)
|
||||
retn := rangeNode.List.Nodes[0]
|
||||
setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0]
|
||||
setPipe := setCmd.Args[1].(*parse.PipeNode)
|
||||
// Replace PLACEHOLDER with the real return value.
|
||||
// Note that this is a PipeNode, so it will be wrapped in parens.
|
||||
setPipe.Cmds = []*parse.CommandNode{c.returnNode}
|
||||
rangeNode.List.Nodes = append(n.Nodes, retn)
|
||||
|
||||
return wrapper
|
||||
}
|
||||
|
||||
func (c *templateTransformContext) applyTransformationsAndSetReturnWrapper(tree *parse.Tree) error {
|
||||
_, err := c.applyTransformations(tree.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.returnNode != nil {
|
||||
// This is a partial with a return statement.
|
||||
c.t.ParseInfo.HasReturn = true
|
||||
tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyTransformations does 2 things:
|
||||
// 1) Parses partial return statement.
|
||||
// 2) Tracks template (partial) dependencies and some other info.
|
||||
func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
|
||||
// applyTransformations tracks template (partial) dependencies and some other info.
|
||||
func (c *templateTransformContext) applyTransformations(n parse.Node) error {
|
||||
switch x := n.(type) {
|
||||
case *parse.ListNode:
|
||||
if x != nil {
|
||||
@@ -211,19 +164,22 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
|
||||
case *parse.PipeNode:
|
||||
c.collectConfig(x)
|
||||
for i, cmd := range x.Cmds {
|
||||
keep, _ := c.applyTransformations(cmd)
|
||||
if !keep {
|
||||
x.Cmds = slices.Delete(x.Cmds, i, i+1)
|
||||
// A return in any other position would silently discard the rest of the pipeline.
|
||||
if i < len(x.Cmds)-1 && len(cmd.Args) > 0 {
|
||||
if id, ok := cmd.Args[0].(*parse.IdentifierNode); ok && id.Ident == "return" {
|
||||
c.err = errors.New("return must be the last command in a pipeline")
|
||||
return c.err
|
||||
}
|
||||
}
|
||||
c.applyTransformations(cmd)
|
||||
}
|
||||
|
||||
case *parse.CommandNode:
|
||||
if x == nil {
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
c.collectInnerInShortcode(x)
|
||||
c.collectInnerInPartial(x)
|
||||
keep := c.collectReturnNode(x)
|
||||
|
||||
for _, elem := range x.Args {
|
||||
switch an := elem.(type) {
|
||||
@@ -231,10 +187,9 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
|
||||
c.applyTransformations(an)
|
||||
}
|
||||
}
|
||||
return keep, c.err
|
||||
}
|
||||
|
||||
return true, c.err
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *templateTransformContext) isWithPartial(args []parse.Node) bool {
|
||||
@@ -355,7 +310,7 @@ func (c *templateTransformContext) handleWithPartial(withNode *parse.WithNode) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := cc.applyTransformationsAndSetReturnWrapper(tree); err != nil {
|
||||
if err := cc.applyTransformations(tree.Root); err != nil {
|
||||
c.err = fmt.Errorf("failed to transform internal partial decorator template %q: %w", internalPartialName, err)
|
||||
return
|
||||
}
|
||||
@@ -565,24 +520,3 @@ func (c *templateTransformContext) collectInnerInPartial(n *parse.CommandNode) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
|
||||
if c.t.category != CategoryPartial || c.returnNode != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(n.Args) < 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
ident, ok := n.Args[0].(*parse.IdentifierNode)
|
||||
if !ok || ident.Ident != "return" {
|
||||
return true
|
||||
}
|
||||
|
||||
c.returnNode = n
|
||||
// Remove the "return" identifiers
|
||||
c.returnNode.Args = c.returnNode.Args[1:]
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||