Skip to content

Commit

Permalink
Pass .RenderShortcodes' Page to render hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Apr 13, 2024
1 parent 2a060b3 commit dc6a479
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 16 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 := ps.pageOutput.pco.otherOutputs[id]; v != nil {
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
73 changes: 73 additions & 0 deletions hugolib/rendershortcodes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,76 @@ 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 .Page.Resources.Get .Destination -}}Image: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/_markup/render-link.html --
{{- with .Page.GetPage .Destination -}}Link: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/list.html --
Content:{{ .Content }}|
-- layouts/_default/single.html --
Content:{{ .Content }}|
-- layouts/shortcodes/include.html --
{{ with site.GetPage (.Get 0) }}
{{ .RenderShortcodes }}
{{ end }}
-- content/markdown/_index.md --
---
title: "Markdown"
---
|{{% include "/posts/p1" %}}|
![kitten](pixel3.png "Pixel 3")
-- 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"
---
![kitten](pixel1.png "Pixel 1")
![kitten](pixel2.png "Pixel 2")
[p2](p2)
-- 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",
// Check images.
"Content:<p>|\nImage: /posts/p1/pixel1.png|\nImage: /posts/p1/pixel2.png|\n|\nImage: /markdown/pixel3.png|</p>\n|",
// Check links.
"Link: /posts/p2/|",
// 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
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
159 changes: 159 additions & 0 deletions markup/goldmark/hugocontext/hugocontext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// 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/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 {
pre := fmt.Sprintf("%s pid=%d%s", prefix, pid, endDelim)
b = append([]byte(pre), b...)
b = append(b, prefix...)
b = append(b, closingDelim...)

return b
}

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("}}")
closingDelim = []byte("/}}")
)

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))

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),
),
)
}
Loading

0 comments on commit dc6a479

Please sign in to comment.