mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-26 16:28:52 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bea058bad | |||
| 49dceb19f5 | |||
| 5e7099256e | |||
| 0805c734a4 |
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+106
-16
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ var (
|
||||
"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 isTrue(val reflect.Value) (truth, ok bool) {", "func isTrueOld(val reflect.Value) (truth, ok bool) {",
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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{},
|
||||
)
|
||||
|
||||
+15
-39
@@ -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{
|
||||
|
||||
@@ -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|")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user