mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
Create pages from _content.gotmpl
Closes #12427 Closes #12485 Closes #6310 Closes #5074
This commit is contained in:
331
hugolib/pagesfromdata/pagesfromgotmpl.go
Normal file
331
hugolib/pagesfromdata/pagesfromgotmpl.go
Normal file
@@ -0,0 +1,331 @@
|
||||
// Copyright 2024 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 pagesfromdata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type PagesFromDataTemplateContext interface {
|
||||
// AddPage adds a new page to the site.
|
||||
// The first return value will always be an empty string.
|
||||
AddPage(any) (string, error)
|
||||
|
||||
// AddResource adds a new resource to the site.
|
||||
// The first return value will always be an empty string.
|
||||
AddResource(any) (string, error)
|
||||
|
||||
// The site to which the pages will be added.
|
||||
Site() page.Site
|
||||
|
||||
// The same template may be executed multiple times for multiple languages.
|
||||
// The Store can be used to store state between these invocations.
|
||||
Store() *maps.Scratch
|
||||
|
||||
// By default, the template will be executed for the language
|
||||
// defined by the _content.gotmpl file (e.g. its mount definition).
|
||||
// This method can be used to activate the template for all languages.
|
||||
// The return value will always be an empty string.
|
||||
EnableAllLanguages() string
|
||||
}
|
||||
|
||||
var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil)
|
||||
|
||||
type pagesFromDataTemplateContext struct {
|
||||
p *PagesFromTemplate
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) {
|
||||
m, err := maps.ToStringMapE(v)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
pathv, ok := m["path"]
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("path not set")
|
||||
}
|
||||
path, err := cast.ToStringE(pathv)
|
||||
if err != nil || path == "" {
|
||||
return "", nil, fmt.Errorf("invalid path %q", path)
|
||||
}
|
||||
return path, m, nil
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
|
||||
path, m, err := p.toPathMap(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
pd := pagemeta.DefaultPageConfig
|
||||
pd.IsFromContentAdapter = true
|
||||
|
||||
if err := mapstructure.WeakDecode(m, &pd); err != nil {
|
||||
return "", fmt.Errorf("failed to decode page map: %w", err)
|
||||
}
|
||||
|
||||
p.p.buildState.NumPagesAdded++
|
||||
|
||||
if err := pd.Validate(true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", p.p.HandlePage(p.p, &pd)
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) {
|
||||
path, m, err := p.toPathMap(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var rd pagemeta.ResourceConfig
|
||||
if err := mapstructure.WeakDecode(m, &rd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
p.p.buildState.NumResourcesAdded++
|
||||
|
||||
if err := rd.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", p.p.HandleResource(p.p, &rd)
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) Site() page.Site {
|
||||
return p.p.Site
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) Store() *maps.Scratch {
|
||||
return p.p.store
|
||||
}
|
||||
|
||||
func (p *pagesFromDataTemplateContext) EnableAllLanguages() string {
|
||||
p.p.buildState.EnableAllLanguages = true
|
||||
return ""
|
||||
}
|
||||
|
||||
func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate {
|
||||
return &PagesFromTemplate{
|
||||
PagesFromTemplateOptions: opts,
|
||||
PagesFromTemplateDeps: opts.DepsFromSite(opts.Site),
|
||||
buildState: &BuildState{
|
||||
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
|
||||
},
|
||||
store: maps.NewScratch(),
|
||||
}
|
||||
}
|
||||
|
||||
type PagesFromTemplateOptions struct {
|
||||
Site page.Site
|
||||
DepsFromSite func(page.Site) PagesFromTemplateDeps
|
||||
|
||||
DependencyManager identity.Manager
|
||||
|
||||
Watching bool
|
||||
|
||||
HandlePage func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error
|
||||
HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error
|
||||
|
||||
GoTmplFi hugofs.FileMetaInfo
|
||||
}
|
||||
|
||||
type PagesFromTemplateDeps struct {
|
||||
TmplFinder tpl.TemplateParseFinder
|
||||
TmplExec tpl.TemplateExecutor
|
||||
}
|
||||
|
||||
var _ resource.Staler = (*PagesFromTemplate)(nil)
|
||||
|
||||
type PagesFromTemplate struct {
|
||||
PagesFromTemplateOptions
|
||||
PagesFromTemplateDeps
|
||||
buildState *BuildState
|
||||
store *maps.Scratch
|
||||
}
|
||||
|
||||
func (b *PagesFromTemplate) AddChange(id identity.Identity) {
|
||||
b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id)
|
||||
}
|
||||
|
||||
func (b *PagesFromTemplate) MarkStale() {
|
||||
b.buildState.StaleVersion++
|
||||
}
|
||||
|
||||
func (b *PagesFromTemplate) StaleVersion() uint32 {
|
||||
return b.buildState.StaleVersion
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
NumPagesAdded uint64
|
||||
NumResourcesAdded uint64
|
||||
EnableAllLanguages bool
|
||||
ChangedIdentities []identity.Identity
|
||||
DeletedPaths []string
|
||||
Path *paths.Path
|
||||
}
|
||||
|
||||
type BuildState struct {
|
||||
StaleVersion uint32
|
||||
|
||||
EnableAllLanguages bool
|
||||
|
||||
// Paths deleted in the current build.
|
||||
DeletedPaths []string
|
||||
|
||||
// Changed identities in the current build.
|
||||
ChangedIdentities []identity.Identity
|
||||
|
||||
NumPagesAdded uint64
|
||||
NumResourcesAdded uint64
|
||||
|
||||
sourceInfosCurrent *maps.Cache[string, *sourceInfo]
|
||||
sourceInfosPrevious *maps.Cache[string, *sourceInfo]
|
||||
}
|
||||
|
||||
func (b *BuildState) hash(v any) uint64 {
|
||||
return identity.HashUint64(v)
|
||||
}
|
||||
|
||||
func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool {
|
||||
h := b.hash(v)
|
||||
si, found := b.sourceInfosPrevious.Get(changedPath)
|
||||
if found {
|
||||
b.sourceInfosCurrent.Set(changedPath, si)
|
||||
if si.hash == h {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
si = &sourceInfo{}
|
||||
b.sourceInfosCurrent.Set(changedPath, si)
|
||||
}
|
||||
si.hash = h
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BuildState) resolveDeletedPaths() {
|
||||
if b.sourceInfosPrevious == nil {
|
||||
b.DeletedPaths = nil
|
||||
return
|
||||
}
|
||||
var paths []string
|
||||
b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) {
|
||||
if _, found := b.sourceInfosCurrent.Get(k); !found {
|
||||
paths = append(paths, k)
|
||||
}
|
||||
})
|
||||
|
||||
b.DeletedPaths = paths
|
||||
}
|
||||
|
||||
func (b *BuildState) PrepareNextBuild() {
|
||||
b.sourceInfosPrevious = b.sourceInfosCurrent
|
||||
b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]()
|
||||
b.StaleVersion = 0
|
||||
b.DeletedPaths = nil
|
||||
b.ChangedIdentities = nil
|
||||
b.NumPagesAdded = 0
|
||||
b.NumResourcesAdded = 0
|
||||
}
|
||||
|
||||
type sourceInfo struct {
|
||||
hash uint64
|
||||
}
|
||||
|
||||
func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate {
|
||||
// We deliberately make them share the same DepenencyManager and Store.
|
||||
p.PagesFromTemplateOptions.Site = s
|
||||
p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s)
|
||||
p.buildState = &BuildState{
|
||||
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
|
||||
}
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate {
|
||||
p.PagesFromTemplateOptions.GoTmplFi = fi
|
||||
return &p
|
||||
}
|
||||
|
||||
func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager {
|
||||
return p.DependencyManager
|
||||
}
|
||||
|
||||
func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
|
||||
defer func() {
|
||||
p.buildState.PrepareNextBuild()
|
||||
}()
|
||||
|
||||
f, err := p.GoTmplFi.Meta().Open()
|
||||
if err != nil {
|
||||
return BuildInfo{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
|
||||
if err != nil {
|
||||
return BuildInfo{}, err
|
||||
}
|
||||
|
||||
data := &pagesFromDataTemplateContext{
|
||||
p: p,
|
||||
}
|
||||
|
||||
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
|
||||
|
||||
if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
|
||||
return BuildInfo{}, err
|
||||
}
|
||||
|
||||
if p.Watching {
|
||||
p.buildState.resolveDeletedPaths()
|
||||
}
|
||||
|
||||
bi := BuildInfo{
|
||||
NumPagesAdded: p.buildState.NumPagesAdded,
|
||||
NumResourcesAdded: p.buildState.NumResourcesAdded,
|
||||
EnableAllLanguages: p.buildState.EnableAllLanguages,
|
||||
ChangedIdentities: p.buildState.ChangedIdentities,
|
||||
DeletedPaths: p.buildState.DeletedPaths,
|
||||
Path: p.GoTmplFi.Meta().PathInfo,
|
||||
}
|
||||
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
//////////////
|
479
hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
Normal file
479
hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright 2024 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 pagesfromdata_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/markup/asciidocext"
|
||||
"github.com/gohugoio/hugo/markup/pandoc"
|
||||
"github.com/gohugoio/hugo/markup/rst"
|
||||
)
|
||||
|
||||
const filesPagesFromDataTempleBasic = `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
disableLiveReload = true
|
||||
-- assets/a/pixel.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- assets/mydata.yaml --
|
||||
p1: "p1"
|
||||
draft: false
|
||||
-- layouts/partials/get-value.html --
|
||||
{{ $val := "p1" }}
|
||||
{{ return $val }}
|
||||
-- layouts/_default/single.html --
|
||||
Single: {{ .Title }}|{{ .Content }}|Params: {{ .Params.param1 }}|Path: {{ .Path }}|
|
||||
Dates: Date: {{ .Date.Format "2006-01-02" }}|Lastmod: {{ .Lastmod.Format "2006-01-02" }}|PublishDate: {{ .PublishDate.Format "2006-01-02" }}|ExpiryDate: {{ .ExpiryDate.Format "2006-01-02" }}|
|
||||
Len Resources: {{ .Resources | len }}
|
||||
Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|{{ end }}$
|
||||
{{ with .Resources.Get "featured.png" }}
|
||||
Featured Image: {{ .RelPermalink }}|{{ .Name }}|
|
||||
{{ with .Resize "10x10" }}
|
||||
Resized Featured Image: {{ .RelPermalink }}|{{ .Width }}|
|
||||
{{ end}}
|
||||
{{ end }}
|
||||
-- layouts/_default/list.html --
|
||||
List: {{ .Title }}|{{ .Content }}|
|
||||
RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Title }}:{{ .Path }}|{{ end }}$
|
||||
Sections: {{ range .Sections }}{{ .Title }}:{{ .Path }}|{{ end }}$
|
||||
-- content/docs/pfile.md --
|
||||
---
|
||||
title: "pfile"
|
||||
date: 2023-03-01
|
||||
---
|
||||
Pfile Content
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $pixel := resources.Get "a/pixel.png" }}
|
||||
{{ $dataResource := resources.Get "mydata.yaml" }}
|
||||
{{ $data := $dataResource | transform.Unmarshal }}
|
||||
{{ $pd := $data.p1 }}
|
||||
{{ $pp := partial "get-value.html" }}
|
||||
{{ $title := printf "%s:%s" $pd $pp }}
|
||||
{{ $date := "2023-03-01" | time.AsTime }}
|
||||
{{ $dates := dict "date" $date }}
|
||||
{{ $contentMarkdown := dict "value" "**Hello World**" "mediaType" "text/markdown" }}
|
||||
{{ $contentMarkdownDefault := dict "value" "**Hello World Default**" }}
|
||||
{{ $contentHTML := dict "value" "<b>Hello World!</b> No **markdown** here." "mediaType" "text/html" }}
|
||||
{{ $.AddPage (dict "kind" "page" "path" "P1" "title" $title "dates" $dates "content" $contentMarkdown "params" (dict "param1" "param1v" ) ) }}
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p3" "title" "p3title" "dates" $dates "content" $contentMarkdownDefault "draft" false ) }}
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p4" "title" "p4title" "dates" $dates "content" $contentMarkdownDefault "draft" $data.draft ) }}
|
||||
|
||||
|
||||
{{ $resourceContent := dict "value" $dataResource }}
|
||||
{{ $.AddResource (dict "path" "p1/data1.yaml" "content" $resourceContent) }}
|
||||
{{ $.AddResource (dict "path" "p1/mytext.txt" "content" (dict "value" "some text") "name" "textresource" "title" "My Text Resource" "params" (dict "param1" "param1v") )}}
|
||||
{{ $.AddResource (dict "path" "p1/sub/mytex2.txt" "content" (dict "value" "some text") "title" "My Text Sub Resource" ) }}
|
||||
{{ $.AddResource (dict "path" "P1/Sub/MyMixCaseText2.txt" "content" (dict "value" "some text") "title" "My Text Sub Mixed Case Path Resource" ) }}
|
||||
{{ $.AddResource (dict "path" "p1/sub/data1.yaml" "content" $resourceContent "title" "Sub data") }}
|
||||
{{ $resourceParams := dict "data2ParaM1" "data2Param1v" }}
|
||||
{{ $.AddResource (dict "path" "p1/data2.yaml" "name" "data2.yaml" "title" "My data 2" "params" $resourceParams "content" $resourceContent) }}
|
||||
{{ $.AddResource (dict "path" "p1/featuredimage.png" "name" "featured.png" "title" "My Featured Image" "params" $resourceParams "content" (dict "value" $pixel ))}}
|
||||
`
|
||||
|
||||
func TestPagesFromGoTmplMisc(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.Test(t, filesPagesFromDataTempleBasic)
|
||||
b.AssertPublishDir(`
|
||||
docs/p1/mytext.txt
|
||||
docs/p1/sub/mytex2.tx
|
||||
docs/p1/sub/mymixcasetext2.txt
|
||||
`)
|
||||
|
||||
// Page from markdown file.
|
||||
b.AssertFileContent("public/docs/pfile/index.html", "Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|")
|
||||
// Pages from gotmpl.
|
||||
b.AssertFileContent("public/docs/p1/index.html",
|
||||
"Single: p1:p1|",
|
||||
"Path: /docs/p1|",
|
||||
"<strong>Hello World</strong>",
|
||||
"Params: param1v|",
|
||||
"Len Resources: 7",
|
||||
"RelPermalink: /mydata.yaml|Name: data1.yaml|Title: data1.yaml|Params: map[]|",
|
||||
"RelPermalink: /mydata.yaml|Name: data2.yaml|Title: My data 2|Params: map[data2param1:data2Param1v]|",
|
||||
"RelPermalink: /a/pixel.png|Name: featured.png|Title: My Featured Image|Params: map[data2param1:data2Param1v]|",
|
||||
"RelPermalink: /docs/p1/sub/mytex2.txt|Name: sub/mytex2.txt|",
|
||||
"RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: sub/mymixcasetext2.txt|",
|
||||
"RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|",
|
||||
"Featured Image: /a/pixel.png|featured.png|",
|
||||
"Resized Featured Image: /a/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_10x10_resize_box_3.png|10|",
|
||||
// Resource from string
|
||||
"RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
|
||||
// Dates
|
||||
"Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|",
|
||||
)
|
||||
b.AssertFileContent("public/docs/p2/index.html", "Single: p2title|", "<b>Hello World!</b> No **markdown** here.")
|
||||
b.AssertFileContent("public/docs/p3/index.html", "<strong>Hello World Default</strong>")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplAsciidocAndSimilar(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
[security]
|
||||
[security.exec]
|
||||
allow = ['asciidoctor', 'pandoc','rst2html', 'python']
|
||||
-- layouts/_default/single.html --
|
||||
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $.AddPage (dict "path" "asciidoc" "content" (dict "value" "Mark my words, #automation is essential#." "mediaType" "text/asciidoc" )) }}
|
||||
{{ $.AddPage (dict "path" "pandoc" "content" (dict "value" "This ~~is deleted text.~~" "mediaType" "text/pandoc" )) }}
|
||||
{{ $.AddPage (dict "path" "rst" "content" (dict "value" "This is *bold*." "mediaType" "text/rst" )) }}
|
||||
{{ $.AddPage (dict "path" "org" "content" (dict "value" "the ability to use +strikethrough+ is a plus" "mediaType" "text/org" )) }}
|
||||
{{ $.AddPage (dict "path" "nocontent" "title" "No Content" ) }}
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
if asciidocext.Supports() {
|
||||
b.AssertFileContent("public/docs/asciidoc/index.html",
|
||||
"Mark my words, <mark>automation is essential</mark>",
|
||||
"Path: /docs/asciidoc|",
|
||||
)
|
||||
}
|
||||
if pandoc.Supports() {
|
||||
b.AssertFileContent("public/docs/pandoc/index.html",
|
||||
"This <del>is deleted text.</del>",
|
||||
"Path: /docs/pandoc|",
|
||||
)
|
||||
}
|
||||
|
||||
if rst.Supports() {
|
||||
b.AssertFileContent("public/docs/rst/index.html",
|
||||
"This is <em>bold</em>",
|
||||
"Path: /docs/rst|",
|
||||
)
|
||||
}
|
||||
|
||||
b.AssertFileContent("public/docs/org/index.html",
|
||||
"the ability to use <del>strikethrough</del> is a plus",
|
||||
"Path: /docs/org|",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/docs/nocontent/index.html", "|Content: |Title: No Content|Path: /docs/nocontent|")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplAddPageErrors(t *testing.T) {
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $.AddPage DICT }}
|
||||
`
|
||||
|
||||
t.Run("AddPage, missing Path", func(t *testing.T) {
|
||||
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1")`)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
|
||||
b.Assert(err.Error(), qt.Contains, "error calling AddPage: path not set")
|
||||
})
|
||||
|
||||
t.Run("AddPage, path starting with slash", func(t *testing.T) {
|
||||
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1" "path" "/foo")`)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, `path "/foo" must not start with a /`)
|
||||
})
|
||||
|
||||
t.Run("AddPage, lang set", func(t *testing.T) {
|
||||
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "path" "p1" "lang" "en")`)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
|
||||
b.Assert(err.Error(), qt.Contains, "error calling AddPage: lang must not be set")
|
||||
})
|
||||
|
||||
t.Run("Site methods not ready", func(t *testing.T) {
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ .Site.METHOD }}
|
||||
`
|
||||
|
||||
for _, method := range []string{"RegularPages", "Pages", "AllPages", "AllRegularPages", "Home", "Sections", "GetPage", "Menus", "MainSections", "Taxonomies"} {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
files := strings.ReplaceAll(filesTemplate, "METHOD", method)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, fmt.Sprintf("error calling %s: this method cannot be called before the site is fully initialized", method))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplAddResourceErrors(t *testing.T) {
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $.AddResource DICT }}
|
||||
`
|
||||
|
||||
t.Run("missing Path", func(t *testing.T) {
|
||||
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "name" "r1")`)
|
||||
b, err := hugolib.TestE(t, files)
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "error calling AddResource: path not set")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplEditGoTmpl(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"title" "p2title"`, `"title" "p2titleedited"`).Build()
|
||||
b.AssertFileContent("public/docs/p2/index.html", "Single: p2titleedited|")
|
||||
b.AssertFileContent("public/docs/index.html", "p2titleedited")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplEditDataResource(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.AssertRenderCountPage(7)
|
||||
b.EditFileReplaceAll("assets/mydata.yaml", "p1: \"p1\"", "p1: \"p1edited\"").Build()
|
||||
b.AssertFileContent("public/docs/p1/index.html", "Single: p1edited:p1|")
|
||||
b.AssertFileContent("public/docs/index.html", "p1edited")
|
||||
b.AssertRenderCountPage(3)
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplEditPartial(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.EditFileReplaceAll("layouts/partials/get-value.html", "p1", "p1edited").Build()
|
||||
b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1edited|")
|
||||
b.AssertFileContent("public/docs/index.html", "p1edited")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplRemovePage(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.EditFileReplaceAll("content/docs/_content.gotmpl", `{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}`, "").Build()
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplDraftPage(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"draft" false`, `"draft" true`).Build()
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplDraftFlagFromResource(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.EditFileReplaceAll("assets/mydata.yaml", `draft: false`, `draft: true`).Build()
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|pfile:/docs/pfile|$")
|
||||
b.EditFileReplaceAll("assets/mydata.yaml", `draft: true`, `draft: false`).Build()
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplMovePage(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"path" "p2"`, `"path" "p2moved"`).Build()
|
||||
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2moved|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplRemoveGoTmpl(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
|
||||
b.AssertFileContent("public/index.html",
|
||||
"RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$",
|
||||
"Sections: Docs:/docs|",
|
||||
)
|
||||
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
|
||||
b.RemoveFiles("content/docs/_content.gotmpl").Build()
|
||||
// One regular page left.
|
||||
b.AssertFileContent("public/index.html",
|
||||
"RegularPagesRecursive: pfile:/docs/pfile|$",
|
||||
"Sections: Docs:/docs|",
|
||||
)
|
||||
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: pfile:/docs/pfile|$")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplLanguagePerFile(t *testing.T) {
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
title = "Title"
|
||||
[languages.fr]
|
||||
weight = 2
|
||||
title = "Titre"
|
||||
disabled = DISABLE
|
||||
-- layouts/_default/single.html --
|
||||
Single: {{ .Title }}|{{ .Content }}|
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Title" ) }}
|
||||
-- content/docs/_content.fr.gotmpl --
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Titre" ) }}
|
||||
`
|
||||
|
||||
for _, disable := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
|
||||
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
|
||||
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title||")
|
||||
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
|
||||
if !disable {
|
||||
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre||")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplEnableAllLanguages(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
title = "Title"
|
||||
[languages.fr]
|
||||
title = "Titre"
|
||||
weight = 2
|
||||
disabled = DISABLE
|
||||
-- i18n/en.yaml --
|
||||
title: Title
|
||||
-- i18n/fr.yaml --
|
||||
title: Titre
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ .EnableAllLanguages }}
|
||||
{{ $titleFromStore := .Store.Get "title" }}
|
||||
{{ if not $titleFromStore }}
|
||||
{{ $titleFromStore = "notfound"}}
|
||||
{{ .Store.Set "title" site.Title }}
|
||||
{{ end }}
|
||||
{{ $title := printf "%s:%s:%s" site.Title (i18n "title") $titleFromStore }}
|
||||
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title ) }}
|
||||
-- layouts/_default/single.html --
|
||||
Single: {{ .Title }}|{{ .Content }}|
|
||||
|
||||
`
|
||||
|
||||
for _, disable := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
|
||||
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
|
||||
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
|
||||
if !disable {
|
||||
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title:Title:notfound||")
|
||||
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre:Titre:Title||")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplMarkdownify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- layouts/_default/single.html --
|
||||
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $content := "**Hello World**" | markdownify }}
|
||||
{{ $.AddPage (dict "path" "p1" "content" (dict "value" $content "mediaType" "text/html" )) }}
|
||||
`
|
||||
|
||||
b, err := hugolib.TestE(t, files)
|
||||
|
||||
// This currently fails. We should fix this, but that is not a trivial task, so do it later.
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
b.Assert(err.Error(), qt.Contains, "error calling markdownify: this method cannot be called before the site is fully initialized")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplResourceWithoutExtensionWithMediaTypeProvided(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- layouts/_default/single.html --
|
||||
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
|
||||
{{ range .Resources }}
|
||||
|RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|MediaType: {{ .MediaType }}|
|
||||
{{ end }}
|
||||
-- content/docs/_content.gotmpl --
|
||||
{{ $.AddPage (dict "path" "p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
|
||||
{{ $.AddResource (dict "path" "p1/myresource" "content" (dict "value" "abcde" "mediaType" "text/plain" )) }}
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/docs/p1/index.html", "RelPermalink: /docs/p1/myresource|Name: myresource|Title: myresource|Params: map[]|MediaType: text/plain|")
|
||||
}
|
||||
|
||||
func TestPagesFromGoTmplCascade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- layouts/_default/single.html --
|
||||
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
|
||||
-- content/_content.gotmpl --
|
||||
{{ $cascade := dict "params" (dict "cascadeparam1" "cascadeparam1value" ) }}
|
||||
{{ $.AddPage (dict "path" "docs" "kind" "section" "cascade" $cascade ) }}
|
||||
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/docs/p1/index.html", "|Path: /docs/p1|Params: map[cascadeparam1:cascadeparam1value")
|
||||
}
|
||||
|
||||
func TestPagesFromGoBuildOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
|
||||
baseURL = "https://example.com"
|
||||
-- layouts/_default/single.html --
|
||||
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
|
||||
-- content/_content.gotmpl --
|
||||
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
|
||||
{{ $never := dict "list" "never" "publishResources" false "render" "never" }}
|
||||
{{ $.AddPage (dict "path" "docs/p2" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" ) "build" $never ) }}
|
||||
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileExists("public/docs/p1/index.html", true)
|
||||
b.AssertFileExists("public/docs/p2/index.html", false)
|
||||
}
|
32
hugolib/pagesfromdata/pagesfromgotmpl_test.go
Normal file
32
hugolib/pagesfromdata/pagesfromgotmpl_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright 2024 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 pagesfromdata
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkHash(b *testing.B) {
|
||||
m := map[string]any{
|
||||
"foo": "bar",
|
||||
"bar": "foo",
|
||||
"stringSlice": []any{"a", "b", "c"},
|
||||
"intSlice": []any{1, 2, 3},
|
||||
"largeText": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.",
|
||||
}
|
||||
|
||||
bs := BuildState{}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
bs.hash(m)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user