]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Pass .RenderShortcodes' Page to render hooks as .PageInner
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 11 Apr 2024 15:46:18 +0000 (17:46 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 15 Apr 2024 07:49:57 +0000 (09:49 +0200)
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

18 files changed:
hugolib/content_map_page.go
hugolib/page__content.go
hugolib/page__meta.go
hugolib/page__per_output.go
hugolib/rendershortcodes_test.go
hugolib/shortcode.go
markup/converter/converter.go
markup/converter/hooks/hooks.go
markup/goldmark/codeblocks/render.go
markup/goldmark/convert.go
markup/goldmark/hugocontext/hugocontext.go [new file with mode: 0644]
markup/goldmark/hugocontext/hugocontext_test.go [new file with mode: 0644]
markup/goldmark/internal/render/context.go
markup/goldmark/render_hooks.go
resources/page/pagemeta/page_frontmatter.go
tpl/template.go
tpl/tplimpl/embedded/templates/_default/_markup/render-image.html
tpl/tplimpl/embedded/templates/_default/_markup/render-link.html

index 336fe197c36ecdad27f436fb5db9468e9ba16386..aa32b5320719e295705f50ad5d45ed10bdacdbfc 100644 (file)
@@ -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
                        }
 
index 799fc89b67598626623a743aa54b819ab0e8677b..f10c25d7b8b368aa42fc1be947e698a5b86cef49 100644 (file)
@@ -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
index ebf57f3b3026a896c3bcc827f8c1992b84bcdace..d8203fe751ff3f82833b5e8c79ce035ae343a080 100644 (file)
@@ -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 {
index fac719ea95965ed7bad36f97eded252be7ef3094..4ff67c074bf84f8d84be883973e4ba638a265331 100644 (file)
@@ -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
 }
 
index 696ed8f416c3388fc34a88ced15965b5a94ec8db..67b1a85ef6376a58c3f7b6bd24d1766ad96b2b82 100644 (file)
@@ -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")
+}
index 7cc2cba278bc7d1aabeb1121ac76c784edee07ec..af4454a89c2a1e6580f97e9f429d4ac53332b8a8 100644 (file)
@@ -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)
index b66cb8730042f3e617b3c0bed63549630c621e57..57980f1384fa9f25474025ff0ecdfa18e96ab68b 100644 (file)
@@ -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.
index bdc38f119e11c84d19584675446a28ad42f69f08..54babd320867abeb9caf5114053c634baaae81d0 100644 (file)
@@ -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.
index 67053640dfac73562baf145e3d648b98c39adbe4..5dfa8262fed469596a5c7f71712b6d2febe4485d 100644 (file)
@@ -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
 }
index 643293c9348987f2e7ca49112405e1ed1ee190c9..d7180140d91c879da1baa97420d7b7119a00a496 100644 (file)
@@ -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 (file)
index 0000000..ed62bb8
--- /dev/null
@@ -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 (file)
index 0000000..da96cc3
--- /dev/null
@@ -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)
+       }
+}
index 5787143396d13121977d2b465b1006aaf5ddea12..306d26748b174519c60995d43abc521c01fb39d1 100644 (file)
@@ -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
index 8dcdc39c3a4646927f321737905c07078812b123..c127a2c0e8682fe5043829bfd9f249bd638abfa0 100644 (file)
@@ -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),
index 4e412c666d44993212aa4374f58afc8b15225702..123dd4b704dcbb659a900fad8a2391ea6e3a7c56 100644 (file)
@@ -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.
index e9725bd7412fef94f4cc937aa754dafe3250bd34..5ef0eecb8406a105b3e68f6e3cd9b13deabecb71 100644 (file)
@@ -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")),
 }
 
 func init() {
index fa78cd00c15a8c58a21f5f180bab535de9161fc0..013e31235a4d19c048a8b49a459a10161517e609 100644 (file)
@@ -1,7 +1,7 @@
 {{- $u := urls.Parse .Destination -}}
 {{- $src := $u.String -}}
 {{- if not $u.IsAbs -}}
-  {{- with or (.Page.Resources.Get $u.Path) (resources.Get $u.Path) -}}
+  {{- with or (.PageInner.Resources.Get $u.Path) (resources.Get $u.Path) -}}
     {{- $src = .RelPermalink -}}
   {{- end -}}
 {{- end -}}
index cfc95ab3a2d86e8a8e89b332a40877e94eabd5ba..8903d3dfbbb5f353ccdc0637cc9981b6a44c80c9 100644 (file)
@@ -1,11 +1,11 @@
 {{- $u := urls.Parse .Destination -}}
 {{- $href := $u.String -}}
 {{- if strings.HasPrefix $u.String "#" }}
-  {{- $href = printf "%s#%s" .Page.RelPermalink $u.Fragment }}
+  {{- $href = printf "%s#%s" .PageInner.RelPermalink $u.Fragment }}
 {{- else if not $u.IsAbs -}}
   {{- with or
-    ($.Page.GetPage $u.Path)
-    ($.Page.Resources.Get $u.Path)
+    ($.PageInner.GetPage $u.Path)
+    ($.PageInner.Resources.Get $u.Path)
     (resources.Get $u.Path)
   -}}
     {{- $href = .RelPermalink -}}