mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
555dfa207a
This commit moves to a forked version go-radix (fork source has not had any updates in 3 years.), whith 2 notable changes:
* It's generic (using Go generics) and thus removes a lot of type conversions/assertions.
* It allows nodes to be replaced during walk, which allows to partition the tree for parallel processing without worrying about locking.
For this repo, this means:
* The assembly step now processes nested sections in parallel, which gives a speedup for deep content trees with a slight allocation penalty (see benchmarks below).
* Nodes that needs to be reinserted are inserted directly.
* Also, there are some drive-by fixes of some allocation issues, e.g. avoid wrapping mutexes in returned anonomous functions, a common source of hidden allocations.
```
│ master.bench │ perf-p3.bench │
│ sec/op │ sec/op vs base │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10 6.958m ± 3% 7.015m ± 3% ~ (p=0.589 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10 14.25m ± 1% 14.56m ± 8% ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10 48.07m ± 3% 49.23m ± 3% ~ (p=0.394 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10 66.66m ± 4% 66.47m ± 6% ~ (p=0.485 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10 59.57m ± 4% 50.73m ± 5% -14.85% (p=0.002 n=6)
geomean 28.54m 27.92m -2.18%
│ master.bench │ perf-p3.bench │
│ B/op │ B/op vs base │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10 4.513Mi ± 0% 4.527Mi ± 0% +0.33% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10 15.35Mi ± 0% 15.49Mi ± 0% +0.94% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10 62.50Mi ± 0% 63.19Mi ± 0% +1.10% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10 86.78Mi ± 0% 87.73Mi ± 0% +1.09% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10 62.96Mi ± 0% 63.66Mi ± 0% +1.12% (p=0.002 n=6)
geomean 29.84Mi 30.11Mi +0.92%
│ master.bench │ perf-p3.bench │
│ allocs/op │ allocs/op vs base │
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=1/pagesPerSection=50-10 60.44k ± 0% 60.97k ± 0% +0.87% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=100-10 205.8k ± 0% 211.4k ± 0% +2.70% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=1/sectionsPerLevel=6/pagesPerSection=500-10 831.1k ± 0% 858.3k ± 0% +3.27% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=2/sectionsPerLevel=6/pagesPerSection=100-10 1.157M ± 0% 1.197M ± 0% +3.41% (p=0.002 n=6)
AssembleDeepSiteWithManySections/depth=4/sectionsPerLevel=2/pagesPerSection=100-10 839.9k ± 0% 867.8k ± 0% +3.31% (p=0.002 n=6)
geomean 398.5k 409.3k +2.71%
```
311 lines
7.3 KiB
Go
311 lines
7.3 KiB
Go
package tplimpl
|
|
|
|
import (
|
|
"io"
|
|
"iter"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
"github.com/gohugoio/hugo/tpl"
|
|
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
|
)
|
|
|
|
func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
|
|
if err := func() error {
|
|
meta := templ.Fi.Meta()
|
|
f, err := meta.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
b, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templ.content = removeLeadingBOM(string(b))
|
|
if !templ.noBaseOf {
|
|
templ.noBaseOf = !needsBaseTemplate(templ.content)
|
|
}
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The tweet and twitter shortcodes were deprecated in favor of the x shortcode
|
|
// in v0.141.0. We can remove these aliases in v0.155.0 or later.
|
|
var embeddedTemplatesAliases = map[string][]string{
|
|
"_shortcodes/twitter.html": {"_shortcodes/tweet.html"},
|
|
}
|
|
|
|
func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
|
|
err := s.tns.doParseTemplate(ti, replace)
|
|
if err != nil {
|
|
return s.addFileContext(ti, "parse of template failed", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
|
|
if !ti.noBaseOf || ti.category == CategoryBaseof {
|
|
// Delay parsing until we have the base template.
|
|
return nil
|
|
}
|
|
pi := ti.PathInfo
|
|
name := pi.PathNoLeadingSlash()
|
|
|
|
var (
|
|
templ tpl.Template
|
|
err error
|
|
)
|
|
|
|
if ti.D.IsPlainText {
|
|
prototype := t.parseText
|
|
if !replace && prototype.Lookup(name) != nil {
|
|
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
|
|
}
|
|
templ, err = prototype.New(name).Parse(ti.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
prototype := t.parseHTML
|
|
if !replace && prototype.Lookup(name) != nil {
|
|
name += "-" + strconv.FormatUint(t.nameCounter.Add(1), 10)
|
|
}
|
|
templ, err = prototype.New(name).Parse(ti.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ti.subCategory == SubCategoryEmbedded {
|
|
// In Hugo 0.146.0 we moved the internal templates around.
|
|
// For the "_internal/twitter_cards.html" style templates, they
|
|
// were moved to the _partials directory.
|
|
// But we need to make them accessible from the old path for a while.
|
|
if pi.Type() == paths.TypePartial {
|
|
aliasName := strings.TrimPrefix(name, "_partials/")
|
|
aliasName = "_internal/" + aliasName
|
|
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// This was also possible before Hugo 0.146.0, but this should be deprecated.
|
|
if pi.Type() == paths.TypeShortcode {
|
|
aliasName := strings.TrimPrefix(name, "_shortcodes/")
|
|
aliasName = "_internal/shortcodes/" + aliasName
|
|
_, err = prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Issue #13599.
|
|
if ti.category == CategoryPartial && ti.Fi != nil && ti.Fi.Meta().PathInfo.Section() == "partials" {
|
|
aliasName := strings.TrimPrefix(name, "_")
|
|
if _, err := prototype.AddParseTree(aliasName, templ.(*htmltemplate.Template).Tree); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
ti.Template = templ
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTemplateInfo) error {
|
|
tb := &TemplWithBaseApplied{
|
|
Overlay: overlay,
|
|
Base: base.Info,
|
|
}
|
|
|
|
base.Info.overlays = append(base.Info.overlays, overlay)
|
|
|
|
var templ tpl.Template
|
|
if overlay.D.IsPlainText {
|
|
tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
|
|
var err error
|
|
tt, err = tt.Parse(base.Info.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tt, err = tt.Parse(overlay.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templ = tt
|
|
t.baseofTextClones = append(t.baseofTextClones, tt)
|
|
} else {
|
|
tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
|
|
var err error
|
|
tt, err = tt.Parse(base.Info.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tt, err = tt.Parse(overlay.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templ = tt
|
|
|
|
t.baseofHtmlClones = append(t.baseofHtmlClones, tt)
|
|
|
|
}
|
|
|
|
tb.Template = &TemplInfo{
|
|
Template: templ,
|
|
base: base.Info,
|
|
PathInfo: overlay.PathInfo,
|
|
Fi: overlay.Fi,
|
|
D: overlay.D,
|
|
matrix: overlay.matrix,
|
|
noBaseOf: true,
|
|
}
|
|
|
|
variants := overlay.baseVariants.Get(base.Key)
|
|
if variants == nil {
|
|
variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
|
|
overlay.baseVariants.Insert(base.Key, variants)
|
|
}
|
|
variants[base.Info.D] = tb
|
|
return nil
|
|
}
|
|
|
|
func (t *templateNamespace) templatesIn(in tpl.Template) iter.Seq[tpl.Template] {
|
|
return func(yield func(t tpl.Template) bool) {
|
|
switch in := in.(type) {
|
|
case *htmltemplate.Template:
|
|
for t := range in.All() {
|
|
if !yield(t) {
|
|
return
|
|
}
|
|
}
|
|
|
|
case *texttemplate.Template:
|
|
for t := range in.All() {
|
|
if !yield(t) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var baseTemplateDefineRe = regexp.MustCompile(`^{{-?\s*define`)
|
|
|
|
// needsBaseTemplate returns true if the first non-comment template block is a
|
|
// define block.
|
|
func needsBaseTemplate(templ string) bool {
|
|
idx := -1
|
|
inComment := false
|
|
for i := 0; i < len(templ); {
|
|
if !inComment && strings.HasPrefix(templ[i:], "{{/*") {
|
|
inComment = true
|
|
i += 4
|
|
} else if !inComment && strings.HasPrefix(templ[i:], "{{- /*") {
|
|
inComment = true
|
|
i += 6
|
|
} else if inComment && strings.HasPrefix(templ[i:], "*/}}") {
|
|
inComment = false
|
|
i += 4
|
|
} else if inComment && strings.HasPrefix(templ[i:], "*/ -}}") {
|
|
inComment = false
|
|
i += 6
|
|
} else {
|
|
r, size := utf8.DecodeRuneInString(templ[i:])
|
|
if !inComment {
|
|
if strings.HasPrefix(templ[i:], "{{") {
|
|
idx = i
|
|
break
|
|
} else if !unicode.IsSpace(r) {
|
|
break
|
|
}
|
|
}
|
|
i += size
|
|
}
|
|
}
|
|
|
|
if idx == -1 {
|
|
return false
|
|
}
|
|
|
|
return baseTemplateDefineRe.MatchString(templ[idx:])
|
|
}
|
|
|
|
func removeLeadingBOM(s string) string {
|
|
const bom = '\ufeff'
|
|
|
|
for i, r := range s {
|
|
if i == 0 && r != bom {
|
|
return s
|
|
}
|
|
if i > 0 {
|
|
return s[i:]
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
type templateNamespace struct {
|
|
parseText *texttemplate.Template
|
|
parseHTML *htmltemplate.Template
|
|
prototypeText *texttemplate.Template
|
|
prototypeHTML *htmltemplate.Template
|
|
|
|
nameCounter atomic.Uint64
|
|
|
|
standaloneText *texttemplate.Template
|
|
|
|
baseofTextClones []*texttemplate.Template
|
|
baseofHtmlClones []*htmltemplate.Template
|
|
}
|
|
|
|
func (t *templateNamespace) createPrototypesParse() error {
|
|
if t.prototypeHTML == nil {
|
|
panic("prototypeHTML not set")
|
|
}
|
|
t.parseHTML = htmltemplate.Must(t.prototypeHTML.Clone())
|
|
t.parseText = texttemplate.Must(t.prototypeText.Clone())
|
|
return nil
|
|
}
|
|
|
|
func (t *templateNamespace) createPrototypes(init bool) error {
|
|
if init {
|
|
t.prototypeHTML = htmltemplate.Must(t.parseHTML.Clone())
|
|
t.prototypeText = texttemplate.Must(t.parseText.Clone())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
|
|
return &templateNamespace{
|
|
parseHTML: htmltemplate.New("").Funcs(funcs),
|
|
parseText: texttemplate.New("").Funcs(funcs),
|
|
standaloneText: texttemplate.New("").Funcs(funcs),
|
|
}
|
|
}
|
|
|
|
func isText(t tpl.Template) bool {
|
|
switch t.(type) {
|
|
case *texttemplate.Template:
|
|
return true
|
|
case *htmltemplate.Template:
|
|
return false
|
|
default:
|
|
panic("unknown template type")
|
|
}
|
|
}
|