summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2024-04-11 17:46:18 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2024-04-15 09:49:57 +0200
commitdf11327ba90179747be2b25574ac48c2f336b298 (patch)
tree70f7487cee6815109fc325ed0619130bb29834ca
parenta18e2bcb9a2c556e03dc7e790b0343c83163877a (diff)
Pass .RenderShortcodes' Page to render hooks as .PageInner
The main use case for this is to resolve links and resources (e.g. images) relative to the included `Page`. A typical `include` would similar to this: ```handlebars {{ with site.GetPage (.Get 0) }} {{ .RenderShortcodes }} {{ end }} ``` And when used in a Markdown file: ```markdown {{% include "/posts/p1" %}} ``` Any render hook triggered while rendering `/posts/p1` will get `/posts/p1` when calling `.PageInner`. Note that * This is only relevant for shortcodes included with `{{%` that calls `.RenderShortcodes`. * `.PageInner` is available in all render hooks that, before this commit, received `.Page`. * `.PageInner` will fall back to the value of `.Page` if not relevant and will always have a value. Fixes #12356
-rw-r--r--hugolib/content_map_page.go2
-rw-r--r--hugolib/page__content.go5
-rw-r--r--hugolib/page__meta.go24
-rw-r--r--hugolib/page__per_output.go17
-rw-r--r--hugolib/rendershortcodes_test.go96
-rw-r--r--hugolib/shortcode.go6
-rw-r--r--markup/converter/converter.go9
-rw-r--r--markup/converter/hooks/hooks.go20
-rw-r--r--markup/goldmark/codeblocks/render.go27
-rw-r--r--markup/goldmark/convert.go2
-rw-r--r--markup/goldmark/hugocontext/hugocontext.go165
-rw-r--r--markup/goldmark/hugocontext/hugocontext_test.go34
-rw-r--r--markup/goldmark/internal/render/context.go25
-rw-r--r--markup/goldmark/render_hooks.go26
-rw-r--r--resources/page/pagemeta/page_frontmatter.go3
-rw-r--r--tpl/template.go2
-rw-r--r--tpl/tplimpl/embedded/templates/_default/_markup/render-image.html2
-rw-r--r--tpl/tplimpl/embedded/templates/_default/_markup/render-link.html6
18 files changed, 443 insertions, 28 deletions
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index 336fe197c..aa32b5320 100644
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -1603,7 +1603,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
targetPaths := ps.targetPaths()
baseTarget := targetPaths.SubResourceBaseTarget
duplicateResourceFiles := true
- if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.pageConfig.Markup) {
+ if ps.m.pageConfig.IsGoldmark {
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
}
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index 799fc89b6..f10c25d7b 100644
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -522,6 +522,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
if err != nil {
return nil, err
}
+
if !ok {
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
}
@@ -626,8 +627,10 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return nil, err
}
- // Callback called from above (e.g. in .RenderString)
+ // Callback called from below (e.g. in .RenderString)
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
+ cp.otherOutputs[cp2.po.p.pid] = cp2
+
// Merge content placeholders
for k, v := range ct2.contentPlaceholders {
ct.contentPlaceholders[k] = v
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
index ebf57f3b3..d8203fe75 100644
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -737,6 +737,8 @@ func (p *pageMeta) applyDefaultValues() error {
}
}
+ p.pageConfig.IsGoldmark = p.s.ContentSpec.Converters.IsGoldmark(p.pageConfig.Markup)
+
if p.pageConfig.Title == "" && p.f == nil {
switch p.Kind() {
case kinds.KindHome:
@@ -794,12 +796,26 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
path = p.Path()
}
+ doc := newPageForRenderHook(ps)
+
+ documentLookup := func(id uint64) any {
+ if id == ps.pid {
+ // This prevents infinite recursion in some cases.
+ return doc
+ }
+ if v, ok := ps.pageOutput.pco.otherOutputs[id]; ok {
+ return v.po.p
+ }
+ return nil
+ }
+
cpp, err := cp.New(
converter.DocumentContext{
- Document: newPageForRenderHook(ps),
- DocumentID: id,
- DocumentName: path,
- Filename: filename,
+ Document: doc,
+ DocumentLookup: documentLookup,
+ DocumentID: id,
+ DocumentName: path,
+ Filename: filename,
},
)
if err != nil {
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index fac719ea9..4ff67c074 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -30,6 +30,7 @@ import (
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
@@ -68,8 +69,9 @@ var (
func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
cp := &pageContentOutput{
- po: po,
- renderHooks: &renderHooks{},
+ po: po,
+ renderHooks: &renderHooks{},
+ otherOutputs: make(map[uint64]*pageContentOutput),
}
return cp, nil
}
@@ -83,6 +85,10 @@ type renderHooks struct {
type pageContentOutput struct {
po *pageOutput
+ // Other pages involved in rendering of this page,
+ // typically included with .RenderShortcodes.
+ otherOutputs map[uint64]*pageContentOutput
+
contentRenderedVersion int // Incremented on reset.
contentRendered bool // Set on content render.
@@ -165,6 +171,13 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
cb(pco, ct)
}
+ if tpl.Context.IsInGoldmark.Get(ctx) {
+ // This content will be parsed and rendered by Goldmark.
+ // Wrap it in a special Hugo markup to assign the correct Page from
+ // the stack.
+ c = hugocontext.Wrap(c, pco.po.p.pid)
+ }
+
return helpers.BytesToHTML(c), nil
}
diff --git a/hugolib/rendershortcodes_test.go b/hugolib/rendershortcodes_test.go
index 696ed8f41..67b1a85ef 100644
--- a/hugolib/rendershortcodes_test.go
+++ b/hugolib/rendershortcodes_test.go
@@ -200,3 +200,99 @@ Myshort Original.
b.Build()
b.AssertFileContent("public/p1/index.html", "Edited")
}
+
+func TestRenderShortcodesNestedPageContextIssue12356(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"]
+-- layouts/_default/_markup/render-image.html --
+{{- with .PageInner.Resources.Get .Destination -}}Image: {{ .RelPermalink }}|{{- end -}}
+-- layouts/_default/_markup/render-link.html --
+{{- with .PageInner.GetPage .Destination -}}Link: {{ .RelPermalink }}|{{- end -}}
+-- layouts/_default/_markup/render-heading.html --
+Heading: {{ .PageInner.Title }}: {{ .PlainText }}|
+-- layouts/_default/_markup/render-codeblock.html --
+CodeBlock: {{ .PageInner.Title }}: {{ .Type }}|
+-- layouts/_default/list.html --
+Content:{{ .Content }}|
+Fragments: {{ with .Fragments }}{{.Identifiers }}{{ end }}|
+-- layouts/_default/single.html --
+Content:{{ .Content }}|
+-- layouts/shortcodes/include.html --
+{{ with site.GetPage (.Get 0) }}
+ {{ .RenderShortcodes }}
+{{ end }}
+-- content/markdown/_index.md --
+---
+title: "Markdown"
+---
+# H1
+|{{% include "/posts/p1" %}}|
+![kitten](pixel3.png "Pixel 3")
+
+§§§go
+fmt.Println("Hello")
+§§§
+
+-- content/markdown2/_index.md --
+---
+title: "Markdown 2"
+---
+|{{< include "/posts/p1" >}}|
+-- content/html/_index.html --
+---
+title: "HTML"
+---
+|{{% include "/posts/p1" %}}|
+
+-- content/posts/p1/index.md --
+---
+title: "p1"
+---
+## H2-p1
+![kitten](pixel1.png "Pixel 1")
+![kitten](pixel2.png "Pixel 2")
+[p2](p2)
+
+§§§bash
+echo "Hello"
+§§§
+
+-- content/posts/p2/index.md --
+---
+title: "p2"
+---
+-- content/posts/p1/pixel1.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+-- content/posts/p1/pixel2.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+-- content/markdown/pixel3.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+-- content/html/pixel4.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+
+`
+
+ b := Test(t, files)
+
+ b.AssertFileContent("public/markdown/index.html",
+ // Images.
+ "Image: /posts/p1/pixel1.png|\nImage: /posts/p1/pixel2.png|\n|\nImage: /markdown/pixel3.png|</p>\n|",
+ // Links.
+ "Link: /posts/p2/|",
+ // Code blocks
+ "CodeBlock: p1: bash|", "CodeBlock: Markdown: go|",
+ // Headings.
+ "Heading: Markdown: H1|", "Heading: p1: H2-p1|",
+ // Fragments.
+ "Fragments: [h1 h2-p1]|",
+ // Check that the special context markup is not rendered.
+ "! hugo_ctx",
+ )
+
+ b.AssertFileContent("public/markdown2/index.html", "! hugo_ctx", "Content:<p>|\n ![kitten](pixel1.png \"Pixel 1\")\n![kitten](pixel2.png \"Pixel 2\")\n|</p>\n|")
+
+ b.AssertFileContent("public/html/index.html", "! hugo_ctx")
+}
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 7cc2cba27..af4454a89 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -321,10 +321,16 @@ func prepareShortcode(
// Allow the caller to delay the rendering of the shortcode if needed.
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
+ if p.m.pageConfig.IsGoldmark && sc.doMarkup {
+ // Signal downwards that the content rendered will be
+ // parsed and rendered by Goldmark.
+ ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
+ }
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
+
b, hasVariants, err := r.renderShortcode(ctx)
if err != nil {
return nil, false, toParseErr(err)
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index b66cb8730..57980f138 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -135,10 +135,11 @@ func (b Bytes) Bytes() []byte {
// DocumentContext holds contextual information about the document to convert.
type DocumentContext struct {
- Document any // May be nil. Usually a page.Page
- DocumentID string
- DocumentName string
- Filename string
+ Document any // May be nil. Usually a page.Page
+ DocumentLookup func(uint64) any // May be nil.
+ DocumentID string
+ DocumentName string
+ Filename string
}
// RenderContext holds contextual information about the content to render.
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index bdc38f119..54babd320 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -32,8 +32,7 @@ type AttributesProvider interface {
// LinkContext is the context passed to a link render hook.
type LinkContext interface {
- // The Page being rendered.
- Page() any
+ PageProvider
// The link URL.
Destination() string
@@ -64,6 +63,7 @@ type ImageLinkContext interface {
type CodeblockContext interface {
AttributesProvider
text.Positioner
+ PageProvider
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
Options() map[string]any
@@ -76,9 +76,6 @@ type CodeblockContext interface {
// Zero-based ordinal for all code blocks in the current document.
Ordinal() int
-
- // The owning Page.
- Page() any
}
type AttributesOptionsSliceProvider interface {
@@ -101,8 +98,7 @@ type IsDefaultCodeBlockRendererProvider interface {
// HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading.
type HeadingContext interface {
- // Page is the page containing the heading.
- Page() any
+ PageProvider
// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
Level() int
// Anchor is the HTML id assigned to the heading.
@@ -116,6 +112,16 @@ type HeadingContext interface {
AttributesProvider
}
+type PageProvider interface {
+ // Page is the page being rendered.
+ Page() any
+
+ // PageInner may be different than Page when .RenderShortcodes is in play.
+ // The main use case for this is to include other pages' markdown into the current page
+ // but resolve resources and pages relative to the original.
+ PageInner() any
+}
+
// HeadingRenderer describes a uniquely identifiable rendering hook.
type HeadingRenderer interface {
// RenderHeading writes the rendered content to w using the data in w.
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index 67053640d..5dfa8262f 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -108,6 +108,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
}
cbctx := &codeBlockContext{
page: ctx.DocumentContext().Document,
+ pageInner: r.getPageInner(ctx),
lang: lang,
code: s,
ordinal: ordinal,
@@ -132,7 +133,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
w,
cbctx,
)
-
if err != nil {
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
}
@@ -140,11 +140,24 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
return ast.WalkContinue, nil
}
+func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
+ pid := rctx.PeekPid()
+ if pid > 0 {
+ if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+ if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+ return v
+ }
+ }
+ }
+ return rctx.DocumentContext().Document
+}
+
type codeBlockContext struct {
- page any
- lang string
- code string
- ordinal int
+ page any
+ pageInner any
+ lang string
+ code string
+ ordinal int
// This is only used in error situations and is expensive to create,
// to delay creation until needed.
@@ -159,6 +172,10 @@ func (c *codeBlockContext) Page() any {
return c.page
}
+func (c *codeBlockContext) PageInner() any {
+ return c.pageInner
+}
+
func (c *codeBlockContext) Type() string {
return c.lang
}
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index 643293c93..d7180140d 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -18,6 +18,7 @@ import (
"bytes"
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
+ "github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/yuin/goldmark/util"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
@@ -103,6 +104,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
renderer.WithNodeRenderers(util.Prioritized(emoji.NewHTMLRenderer(), 200)))
var (
extensions = []goldmark.Extender{
+ hugocontext.New(),
newLinks(cfg),
newTocExtension(tocRendererOptions),
}
diff --git a/markup/goldmark/hugocontext/hugocontext.go b/markup/goldmark/hugocontext/hugocontext.go
new file mode 100644
index 000000000..ed62bb8c6
--- /dev/null
+++ b/markup/goldmark/hugocontext/hugocontext.go
@@ -0,0 +1,165 @@
+// 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 hugocontext
+
+import (
+ "bytes"
+ "fmt"
+ "strconv"
+
+ "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+func New() goldmark.Extender {
+ return &hugoContextExtension{}
+}
+
+// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page
+// in .RenderShortcodes.
+func Wrap(b []byte, pid uint64) []byte {
+ buf := bufferpool.GetBuffer()
+ defer bufferpool.PutBuffer(buf)
+ buf.Write(prefix)
+ buf.WriteString(" pid=")
+ buf.WriteString(strconv.FormatUint(pid, 10))
+ buf.Write(endDelim)
+ buf.WriteByte('\n')
+ buf.Write(b)
+ buf.Write(prefix)
+ buf.Write(closingDelimAndNewline)
+ return buf.Bytes()
+}
+
+var kindHugoContext = ast.NewNodeKind("HugoContext")
+
+// HugoContext is a node that represents a Hugo context.
+type HugoContext struct {
+ ast.BaseInline
+
+ Closing bool
+
+ // Internal page ID. Not persisted.
+ Pid uint64
+}
+
+// Dump implements Node.Dump.
+func (n *HugoContext) Dump(source []byte, level int) {
+ m := map[string]string{}
+ m["Pid"] = fmt.Sprintf("%v", n.Pid)
+ ast.DumpHelper(n, source, level, m, nil)
+}
+
+func (n *HugoContext) parseAttrs(attrBytes []byte) {
+ keyPairs := bytes.Split(attrBytes, []byte(" "))
+ for _, keyPair := range keyPairs {
+ kv := bytes.Split(keyPair, []byte("="))
+ if len(kv) != 2 {
+ continue
+ }
+ key := string(kv[0])
+ val := string(kv[1])
+ switch key {
+ case "pid":
+ pid, _ := strconv.ParseUint(val, 10, 64)
+ n.Pid = pid
+ }
+ }
+}
+
+func (h *HugoContext) Kind() ast.NodeKind {
+ return kindHugoContext
+}
+
+var (
+ prefix = []byte("{{__hugo_ctx")
+ endDelim = []byte("}}")
+ closingDelimAndNewline = []byte("/}}\n")
+)
+
+var _ parser.InlineParser = (*hugoContextParser)(nil)
+
+type hugoContextParser struct{}
+
+func (s *hugoContextParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
+ line, _ := block.PeekLine()
+ if !bytes.HasPrefix(line, prefix) {
+ return nil
+ }
+ end := bytes.Index(line, endDelim)
+ if end == -1 {
+ return nil
+ }
+
+ block.Advance(end + len(endDelim) + 1) // +1 for the newline
+
+ if line[end-1] == '/' {
+ return &HugoContext{Closing: true}
+ }
+
+ attrBytes := line[len(prefix)+1 : end]
+ h := &HugoContext{}
+ h.parseAttrs(attrBytes)
+ return h
+}
+
+func (a *hugoContextParser) Trigger() []byte {
+ return []byte{'{'}
+}
+
+type hugoContextRenderer struct{}
+
+func (r *hugoContextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(kindHugoContext, r.handleHugoContext)
+}
+
+func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+
+ hctx := node.(*HugoContext)
+ ctx, ok := w.(*render.Context)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+ if hctx.Closing {
+ _ = ctx.PopPid()
+ } else {
+ ctx.PushPid(hctx.Pid)
+ }
+ return ast.WalkContinue, nil
+}
+
+type hugoContextExtension struct{}
+
+func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(
+ parser.WithInlineParsers(
+ util.Prioritized(&hugoContextParser{}, 50),
+ ),
+ )
+
+ m.Renderer().AddOptions(
+ renderer.WithNodeRenderers(
+ util.Prioritized(&hugoContextRenderer{}, 50),
+ ),
+ )
+}
diff --git a/markup/goldmark/hugocontext/hugocontext_test.go b/markup/goldmark/hugocontext/hugocontext_test.go
new file mode 100644
index 000000000..da96cc339
--- /dev/null
+++ b/markup/goldmark/hugocontext/hugocontext_test.go
@@ -0,0 +1,34 @@
+// 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 hugocontext
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestWrap(t *testing.T) {
+ c := qt.New(t)
+
+ b := []byte("test")
+
+ c.Assert(string(Wrap(b, 42)), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
+}
+
+func BenchmarkWrap(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Wrap([]byte("test"), 42)
+ }
+}
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index 578714339..306d26748 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -41,6 +41,7 @@ func (b *BufWriter) Flush() error {
type Context struct {
*BufWriter
positions []int
+ pids []uint64
ContextData
}
@@ -55,6 +56,30 @@ func (ctx *Context) PopPos() int {
return p
}
+// PushPid pushes a new page ID to the stack.
+func (ctx *Context) PushPid(pid uint64) {
+ ctx.pids = append(ctx.pids, pid)
+}
+
+// PeekPid returns the current page ID without removing it from the stack.
+func (ctx *Context) PeekPid() uint64 {
+ if len(ctx.pids) == 0 {
+ return 0
+ }
+ return ctx.pids[len(ctx.pids)-1]
+}
+
+// PopPid pops the last page ID from the stack.
+func (ctx *Context) PopPid() uint64 {
+ if len(ctx.pids) == 0 {
+ return 0
+ }
+ i := len(ctx.pids) - 1
+ p := ctx.pids[i]
+ ctx.pids = ctx.pids[:i]
+ return p
+}
+
type ContextData interface {
RenderContext() converter.RenderContext
DocumentContext() converter.DocumentContext
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index 8dcdc39c3..c127a2c0e 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -49,6 +49,7 @@ func newLinks(cfg goldmark_config.Config) goldmark.Extender {
type linkContext struct {
page any
+ pageInner any
destination string
title string
text hstring.RenderedString
@@ -64,6 +65,10 @@ func (ctx linkContext) Page() any {
return ctx.page
}
+func (ctx linkContext) PageInner() any {
+ return ctx.pageInner
+}
+
func (ctx linkContext) Text() hstring.RenderedString {
return ctx.text
}
@@ -92,6 +97,7 @@ func (ctx imageLinkContext) Ordinal() int {
type headingContext struct {
page any
+ pageInner any
level int
anchor string
text hstring.RenderedString
@@ -103,6 +109,10 @@ func (ctx headingContext) Page() any {
return ctx.page
}
+func (ctx headingContext) PageInner() any {
+ return ctx.pageInner
+}
+
func (ctx headingContext) Level() int {
return ctx.level
}
@@ -186,6 +196,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
imageLinkContext{
linkContext: linkContext{
page: ctx.DocumentContext().Document,
+ pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -200,6 +211,18 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, err
}
+func (r *hookedRenderer) getPageInner(rctx *render.Context) any {
+ pid := rctx.PeekPid()
+ if pid > 0 {
+ if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+ if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+ return v
+ }
+ }
+ }
+ return rctx.DocumentContext().Document
+}
+
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
n := 0
for _, x := range attrs {
@@ -274,6 +297,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
w,
linkContext{
page: ctx.DocumentContext().Document,
+ pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -339,6 +363,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
w,
linkContext{
page: ctx.DocumentContext().Document,
+ pageInner: r.getPageInner(ctx),
destination: url,
text: hstring.RenderedString(label),
plainText: label,
@@ -423,6 +448,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
w,
headingContext{
page: ctx.DocumentContext().Document,
+ pageInner: r.getPageInner(ctx),
level: n.Level,
anchor: string(anchor),
text: hstring.RenderedString(text),
diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
index 4e412c666..123dd4b70 100644
--- a/resources/page/pagemeta/page_frontmatter.go
+++ b/resources/page/pagemeta/page_frontmatter.go
@@ -88,6 +88,9 @@ type PageConfig struct {
// User defined params.
Params maps.Params
+
+ // Compiled values.
+ IsGoldmark bool `json:"-"`
}
// FrontMatterHandler maps front matter into Page fields and .Params.
diff --git a/tpl/template.go b/tpl/template.go
index e9725bd74..5ef0eecb8 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -165,10 +165,12 @@ var Context = struct {
SetDependencyManagerInCurrentScope func(context.Context, identity.Manager) context.Context
DependencyScope hcontext.ContextDispatcher[int]
Page hcontext.ContextDispatcher[page]
+ IsInGoldmark hcontext.ContextDispatcher[bool]
}{
DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")),
DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")),
Page: hcontext.NewContextDispatcher[page](contextKey("Page")),
+ IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKey("IsInGoldmark")),
}