mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-31 22:41:53 +02:00
Add Page.RenderShortcodes
A layouts/shortcodes/include.html shortcode may look like this: ```html {{ $p := site.GetPage (.Get 0) }} {{ $p.RenderShortcodes }} ``` Fixes #7297
This commit is contained in:
@@ -114,6 +114,10 @@ func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) {
|
||||
}
|
||||
|
||||
type pageState struct {
|
||||
// Incremented for each new page created.
|
||||
// Note that this will change between builds for a given Page.
|
||||
id int
|
||||
|
||||
// This slice will be of same length as the number of global slice of output
|
||||
// formats (for all sites).
|
||||
pageOutputs []*pageOutput
|
||||
@@ -772,7 +776,7 @@ Loop:
|
||||
currShortcode.pos = it.Pos()
|
||||
currShortcode.length = iter.Current().Pos() - it.Pos()
|
||||
if currShortcode.placeholder == "" {
|
||||
currShortcode.placeholder = createShortcodePlaceholder("s", currShortcode.ordinal)
|
||||
currShortcode.placeholder = createShortcodePlaceholder("s", p.id, currShortcode.ordinal)
|
||||
}
|
||||
|
||||
if currShortcode.name != "" {
|
||||
@@ -784,7 +788,7 @@ Loop:
|
||||
currShortcode.params = s
|
||||
}
|
||||
|
||||
currShortcode.placeholder = createShortcodePlaceholder("s", ordinal)
|
||||
currShortcode.placeholder = createShortcodePlaceholder("s", p.id, ordinal)
|
||||
ordinal++
|
||||
s.shortcodes = append(s.shortcodes, currShortcode)
|
||||
|
||||
|
@@ -31,14 +31,19 @@ import (
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
)
|
||||
|
||||
var pageIdCounter atomic.Int64
|
||||
|
||||
func newPageBase(metaProvider *pageMeta) (*pageState, error) {
|
||||
if metaProvider.s == nil {
|
||||
panic("must provide a Site")
|
||||
}
|
||||
|
||||
id := int(pageIdCounter.Add(1))
|
||||
|
||||
s := metaProvider.s
|
||||
|
||||
ps := &pageState{
|
||||
id: id,
|
||||
pageOutput: nopPageOutput,
|
||||
pageOutputTemplateVariationsState: atomic.NewUint32(0),
|
||||
pageCommon: &pageCommon{
|
||||
|
@@ -86,6 +86,7 @@ type pageOutput struct {
|
||||
page.ContentProvider
|
||||
page.PageRenderProvider
|
||||
page.TableOfContentsProvider
|
||||
page.RenderShortcodesProvider
|
||||
|
||||
// May be nil.
|
||||
cp *pageContentOutput
|
||||
@@ -99,6 +100,7 @@ func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
|
||||
p.ContentProvider = cp
|
||||
p.PageRenderProvider = cp
|
||||
p.TableOfContentsProvider = cp
|
||||
p.RenderShortcodesProvider = cp
|
||||
p.cp = cp
|
||||
|
||||
}
|
||||
|
@@ -103,6 +103,30 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
|
||||
return err
|
||||
}
|
||||
|
||||
ctxCallback := func(cp2 *pageContentOutput) {
|
||||
cp.p.cmap.hasNonMarkdownShortcode = cp.p.cmap.hasNonMarkdownShortcode || cp2.p.cmap.hasNonMarkdownShortcode
|
||||
// Merge content placeholders
|
||||
for k, v := range cp2.contentPlaceholders {
|
||||
cp.contentPlaceholders[k] = v
|
||||
}
|
||||
|
||||
if p.s.watching() {
|
||||
for _, s := range cp2.p.shortcodeState.shortcodes {
|
||||
for _, templ := range s.templs {
|
||||
dependencyTracker.Add(templ.(identity.Manager))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer shortcode names so HasShortcode works for shortcodes from included pages.
|
||||
cp.p.shortcodeState.transferNames(cp2.p.shortcodeState)
|
||||
if cp2.p.pageOutputTemplateVariationsState.Load() == 2 {
|
||||
cp.p.pageOutputTemplateVariationsState.Store(2)
|
||||
}
|
||||
}
|
||||
|
||||
ctx = tpl.SetCallbackFunctionInContext(ctx, ctxCallback)
|
||||
|
||||
var hasVariants bool
|
||||
cp.workContent, hasVariants, err = p.contentToRender(ctx, p.source.parsed, p.cmap, cp.contentPlaceholders)
|
||||
if err != nil {
|
||||
@@ -350,6 +374,63 @@ func (p *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Frag
|
||||
return p.tableOfContents
|
||||
}
|
||||
|
||||
func (p *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
|
||||
p.p.s.initInit(ctx, p.initToC, p.p)
|
||||
source := p.p.source.parsed.Input()
|
||||
renderedShortcodes := p.contentPlaceholders
|
||||
var insertPlaceholders bool
|
||||
var hasVariants bool
|
||||
var cb func(*pageContentOutput)
|
||||
if v := tpl.GetCallbackFunctionFromContext(ctx); v != nil {
|
||||
if fn, ok := v.(func(*pageContentOutput)); ok {
|
||||
insertPlaceholders = true
|
||||
cb = fn
|
||||
}
|
||||
}
|
||||
c := make([]byte, 0, len(source)+(len(source)/10))
|
||||
for _, it := range p.p.cmap.items {
|
||||
switch v := it.(type) {
|
||||
case pageparser.Item:
|
||||
c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
|
||||
case pageContentReplacement:
|
||||
// Ignore.
|
||||
case *shortcode:
|
||||
if !insertPlaceholders || !v.insertPlaceholder() {
|
||||
// Insert the rendered shortcode.
|
||||
renderedShortcode, found := renderedShortcodes[v.placeholder]
|
||||
if !found {
|
||||
// This should never happen.
|
||||
panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
|
||||
}
|
||||
|
||||
b, more, err := renderedShortcode.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to render shortcode: %w", err)
|
||||
}
|
||||
hasVariants = hasVariants || more
|
||||
c = append(c, []byte(b)...)
|
||||
|
||||
} else {
|
||||
// Insert the placeholder so we can insert the content after
|
||||
// markdown processing.
|
||||
c = append(c, []byte(v.placeholder)...)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown item type %T", it))
|
||||
}
|
||||
}
|
||||
|
||||
if hasVariants {
|
||||
p.p.pageOutputTemplateVariationsState.Store(2)
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
cb(p)
|
||||
}
|
||||
|
||||
return helpers.BytesToHTML(c), nil
|
||||
}
|
||||
|
||||
func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
|
||||
p.p.s.initInit(ctx, p.initToC, p.p)
|
||||
return p.tableOfContentsHTML
|
||||
|
@@ -1998,7 +1998,6 @@ func TestRenderWithoutArgument(t *testing.T) {
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).BuildE()
|
||||
|
||||
|
232
hugolib/rendershortcodes_test.go
Normal file
232
hugolib/rendershortcodes_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2023 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 (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderShortcodesBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["home", "taxonomy", "term"]
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
## p1-h1
|
||||
{{% include "p2" %}}
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "p2"
|
||||
---
|
||||
### p2-h1
|
||||
{{< withhtml >}}
|
||||
### p2-h2
|
||||
{{% withmarkdown %}}
|
||||
### p2-h3
|
||||
{{% include "p3" %}}
|
||||
-- content/p3.md --
|
||||
---
|
||||
title: "p3"
|
||||
---
|
||||
### p3-h1
|
||||
{{< withhtml >}}
|
||||
### p3-h2
|
||||
{{% withmarkdown %}}
|
||||
{{< level3 >}}
|
||||
-- layouts/shortcodes/include.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
{{ $p.RenderShortcodes }}
|
||||
-- layouts/shortcodes/withhtml.html --
|
||||
<div>{{ .Page.Title }} withhtml</div>
|
||||
-- layouts/shortcodes/withmarkdown.html --
|
||||
#### {{ .Page.Title }} withmarkdown
|
||||
-- layouts/shortcodes/level3.html --
|
||||
Level 3: {{ .Page.Title }}
|
||||
-- layouts/_default/single.html --
|
||||
Fragments: {{ .Fragments.Identifiers }}|
|
||||
HasShortcode Level 1: {{ .HasShortcode "include" }}|
|
||||
HasShortcode Level 2: {{ .HasShortcode "withmarkdown" }}|
|
||||
HasShortcode Level 3: {{ .HasShortcode "level3" }}|
|
||||
HasSHortcode not found: {{ .HasShortcode "notfound" }}|
|
||||
Content: {{ .Content }}|
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"Fragments: [p1-h1 p2-h1 p2-h2 p2-h3 p2-withmarkdown p3-h1 p3-h2 p3-withmarkdown]|",
|
||||
"HasShortcode Level 1: true|",
|
||||
"HasShortcode Level 2: true|",
|
||||
"HasShortcode Level 3: true|",
|
||||
"HasSHortcode not found: false|",
|
||||
)
|
||||
|
||||
// TODO1 more assertions.
|
||||
|
||||
}
|
||||
|
||||
func TestRenderShortcodesNestedMultipleOutputFormatTemplates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
|
||||
[outputs]
|
||||
page = ["html", "json"]
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
## p1-h1
|
||||
{{% include "p2" %}}
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "p2"
|
||||
---
|
||||
### p2-h1
|
||||
{{% myshort %}}
|
||||
-- layouts/shortcodes/include.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
{{ $p.RenderShortcodes }}
|
||||
-- layouts/shortcodes/myshort.html --
|
||||
Myshort HTML.
|
||||
-- layouts/shortcodes/myshort.json --
|
||||
Myshort JSON.
|
||||
-- layouts/_default/single.html --
|
||||
HTML: {{ .Content }}
|
||||
-- layouts/_default/single.json --
|
||||
JSON: {{ .Content }}
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "Myshort HTML")
|
||||
b.AssertFileContent("public/p1/index.json", "Myshort JSON")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderShortcodesEditNested(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableLiveReload = true
|
||||
disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
## p1-h1
|
||||
{{% include "p2" %}}
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "p2"
|
||||
---
|
||||
### p2-h1
|
||||
{{% myshort %}}
|
||||
-- layouts/shortcodes/include.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
{{ $p.RenderShortcodes }}
|
||||
-- layouts/shortcodes/myshort.html --
|
||||
Myshort Original.
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Content }}
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "Myshort Original.")
|
||||
|
||||
b.EditFileReplace("layouts/shortcodes/myshort.html", func(s string) string {
|
||||
return "Myshort Edited."
|
||||
})
|
||||
b.Build()
|
||||
b.AssertFileContent("public/p1/index.html", "Myshort Edited.")
|
||||
|
||||
}
|
||||
|
||||
func TestRenderShortcodesEditIncludedPage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableLiveReload = true
|
||||
disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"]
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
## p1-h1
|
||||
{{% include "p2" %}}
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "p2"
|
||||
---
|
||||
### Original
|
||||
{{% myshort %}}
|
||||
-- layouts/shortcodes/include.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
{{ $p.RenderShortcodes }}
|
||||
-- layouts/shortcodes/myshort.html --
|
||||
Myshort Original.
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Content }}
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "Original")
|
||||
|
||||
b.EditFileReplace("content/p2.md", func(s string) string {
|
||||
return strings.Replace(s, "Original", "Edited", 1)
|
||||
})
|
||||
b.Build()
|
||||
b.AssertFileContent("public/p1/index.html", "Edited")
|
||||
|
||||
}
|
@@ -185,8 +185,8 @@ func (scp *ShortcodeWithPage) page() page.Page {
|
||||
// Note - this value must not contain any markup syntax
|
||||
const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE"
|
||||
|
||||
func createShortcodePlaceholder(id string, ordinal int) string {
|
||||
return shortcodePlaceholderPrefix + id + strconv.Itoa(ordinal) + "HBHB"
|
||||
func createShortcodePlaceholder(sid string, id, ordinal int) string {
|
||||
return shortcodePlaceholderPrefix + strconv.Itoa(id) + sid + strconv.Itoa(ordinal) + "HBHB"
|
||||
}
|
||||
|
||||
type shortcode struct {
|
||||
|
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// A placeholder for the TableOfContents markup. This is what we pass to the Goldmark etc. renderers.
|
||||
var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0)
|
||||
var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0, 0)
|
||||
|
||||
// shortcodeRenderer is typically used to delay rendering of inner shortcodes
|
||||
// marked with placeholders in the content.
|
||||
|
@@ -948,7 +948,6 @@ title: "p1"
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -991,7 +990,6 @@ title: "p1"
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1023,7 +1021,6 @@ echo "foo";
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1061,7 +1058,6 @@ title: "p1"
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1098,8 +1094,8 @@ Title: {{ .Get "title" | safeHTML }}
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
Verbose: true,
|
||||
|
||||
Verbose: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1191,8 +1187,8 @@ C'est un test
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
Verbose: true,
|
||||
|
||||
Verbose: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1229,8 +1225,8 @@ InnerDeindent: {{ .Get 0 }}: {{ len .InnerDeindent }}
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
Verbose: true,
|
||||
|
||||
Verbose: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
@@ -1269,8 +1265,8 @@ Inner: {{ .Get 0 }}: {{ len .Inner }}
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
Verbose: true,
|
||||
|
||||
Verbose: true,
|
||||
},
|
||||
).BuildE()
|
||||
|
||||
@@ -1306,8 +1302,8 @@ Hello.
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
Verbose: true,
|
||||
|
||||
Verbose: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
|
Reference in New Issue
Block a user