Skip to content

Commit

Permalink
Pass .RenderShortcodes' Page to render hooks as .PageInner
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bep committed Apr 15, 2024
1 parent a18e2bc commit df11327
Show file tree
Hide file tree
Showing 18 changed files with 443 additions and 28 deletions.
2 changes: 1 addition & 1 deletion hugolib/content_map_page.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 4 additions & 1 deletion hugolib/page__content.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions hugolib/page__meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 15 additions & 2 deletions hugolib/page__per_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
Expand All @@ -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.

Expand Down Expand Up @@ -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
}

Expand Down
96 changes: 96 additions & 0 deletions hugolib/rendershortcodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
6 changes: 6 additions & 0 deletions hugolib/shortcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions markup/converter/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 13 additions & 7 deletions markup/converter/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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.
Expand All @@ -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.
Expand Down
27 changes: 22 additions & 5 deletions markup/goldmark/codeblocks/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -132,19 +133,31 @@ 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())
}

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.
Expand All @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions markup/goldmark/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
}
Expand Down
Loading

0 comments on commit df11327

Please sign in to comment.