mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
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
This commit is contained in:
parent
a18e2bcb9a
commit
df11327ba9
18 changed files with 443 additions and 28 deletions
|
@ -1603,7 +1603,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
|
||||||
targetPaths := ps.targetPaths()
|
targetPaths := ps.targetPaths()
|
||||||
baseTarget := targetPaths.SubResourceBaseTarget
|
baseTarget := targetPaths.SubResourceBaseTarget
|
||||||
duplicateResourceFiles := true
|
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
|
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -522,6 +522,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
|
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
|
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) {
|
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
|
||||||
|
cp.otherOutputs[cp2.po.p.pid] = cp2
|
||||||
|
|
||||||
// Merge content placeholders
|
// Merge content placeholders
|
||||||
for k, v := range ct2.contentPlaceholders {
|
for k, v := range ct2.contentPlaceholders {
|
||||||
ct.contentPlaceholders[k] = v
|
ct.contentPlaceholders[k] = v
|
||||||
|
|
|
@ -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 {
|
if p.pageConfig.Title == "" && p.f == nil {
|
||||||
switch p.Kind() {
|
switch p.Kind() {
|
||||||
case kinds.KindHome:
|
case kinds.KindHome:
|
||||||
|
@ -794,9 +796,23 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
|
||||||
path = p.Path()
|
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(
|
cpp, err := cp.New(
|
||||||
converter.DocumentContext{
|
converter.DocumentContext{
|
||||||
Document: newPageForRenderHook(ps),
|
Document: doc,
|
||||||
|
DocumentLookup: documentLookup,
|
||||||
DocumentID: id,
|
DocumentID: id,
|
||||||
DocumentName: path,
|
DocumentName: path,
|
||||||
Filename: filename,
|
Filename: filename,
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
"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/highlight/chromalexers"
|
||||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||||
|
|
||||||
|
@ -70,6 +71,7 @@ func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
|
||||||
cp := &pageContentOutput{
|
cp := &pageContentOutput{
|
||||||
po: po,
|
po: po,
|
||||||
renderHooks: &renderHooks{},
|
renderHooks: &renderHooks{},
|
||||||
|
otherOutputs: make(map[uint64]*pageContentOutput),
|
||||||
}
|
}
|
||||||
return cp, nil
|
return cp, nil
|
||||||
}
|
}
|
||||||
|
@ -83,6 +85,10 @@ type renderHooks struct {
|
||||||
type pageContentOutput struct {
|
type pageContentOutput struct {
|
||||||
po *pageOutput
|
po *pageOutput
|
||||||
|
|
||||||
|
// Other pages involved in rendering of this page,
|
||||||
|
// typically included with .RenderShortcodes.
|
||||||
|
otherOutputs map[uint64]*pageContentOutput
|
||||||
|
|
||||||
contentRenderedVersion int // Incremented on reset.
|
contentRenderedVersion int // Incremented on reset.
|
||||||
contentRendered bool // Set on content render.
|
contentRendered bool // Set on content render.
|
||||||
|
|
||||||
|
@ -165,6 +171,13 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
|
||||||
cb(pco, ct)
|
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
|
return helpers.BytesToHTML(c), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -200,3 +200,99 @@ Myshort Original.
|
||||||
b.Build()
|
b.Build()
|
||||||
b.AssertFileContent("public/p1/index.html", "Edited")
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -321,10 +321,16 @@ func prepareShortcode(
|
||||||
|
|
||||||
// Allow the caller to delay the rendering of the shortcode if needed.
|
// Allow the caller to delay the rendering of the shortcode if needed.
|
||||||
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
|
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)
|
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, toParseErr(err)
|
return nil, false, toParseErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, hasVariants, err := r.renderShortcode(ctx)
|
b, hasVariants, err := r.renderShortcode(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, toParseErr(err)
|
return nil, false, toParseErr(err)
|
||||||
|
|
|
@ -136,6 +136,7 @@ func (b Bytes) Bytes() []byte {
|
||||||
// DocumentContext holds contextual information about the document to convert.
|
// DocumentContext holds contextual information about the document to convert.
|
||||||
type DocumentContext struct {
|
type DocumentContext struct {
|
||||||
Document any // May be nil. Usually a page.Page
|
Document any // May be nil. Usually a page.Page
|
||||||
|
DocumentLookup func(uint64) any // May be nil.
|
||||||
DocumentID string
|
DocumentID string
|
||||||
DocumentName string
|
DocumentName string
|
||||||
Filename string
|
Filename string
|
||||||
|
|
|
@ -32,8 +32,7 @@ type AttributesProvider interface {
|
||||||
|
|
||||||
// LinkContext is the context passed to a link render hook.
|
// LinkContext is the context passed to a link render hook.
|
||||||
type LinkContext interface {
|
type LinkContext interface {
|
||||||
// The Page being rendered.
|
PageProvider
|
||||||
Page() any
|
|
||||||
|
|
||||||
// The link URL.
|
// The link URL.
|
||||||
Destination() string
|
Destination() string
|
||||||
|
@ -64,6 +63,7 @@ type ImageLinkContext interface {
|
||||||
type CodeblockContext interface {
|
type CodeblockContext interface {
|
||||||
AttributesProvider
|
AttributesProvider
|
||||||
text.Positioner
|
text.Positioner
|
||||||
|
PageProvider
|
||||||
|
|
||||||
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
|
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
|
||||||
Options() map[string]any
|
Options() map[string]any
|
||||||
|
@ -76,9 +76,6 @@ type CodeblockContext interface {
|
||||||
|
|
||||||
// Zero-based ordinal for all code blocks in the current document.
|
// Zero-based ordinal for all code blocks in the current document.
|
||||||
Ordinal() int
|
Ordinal() int
|
||||||
|
|
||||||
// The owning Page.
|
|
||||||
Page() any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttributesOptionsSliceProvider interface {
|
type AttributesOptionsSliceProvider interface {
|
||||||
|
@ -101,8 +98,7 @@ type IsDefaultCodeBlockRendererProvider interface {
|
||||||
// HeadingContext contains accessors to all attributes that a HeadingRenderer
|
// HeadingContext contains accessors to all attributes that a HeadingRenderer
|
||||||
// can use to render a heading.
|
// can use to render a heading.
|
||||||
type HeadingContext interface {
|
type HeadingContext interface {
|
||||||
// Page is the page containing the heading.
|
PageProvider
|
||||||
Page() any
|
|
||||||
// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
|
// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
|
||||||
Level() int
|
Level() int
|
||||||
// Anchor is the HTML id assigned to the heading.
|
// Anchor is the HTML id assigned to the heading.
|
||||||
|
@ -116,6 +112,16 @@ type HeadingContext interface {
|
||||||
AttributesProvider
|
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.
|
// HeadingRenderer describes a uniquely identifiable rendering hook.
|
||||||
type HeadingRenderer interface {
|
type HeadingRenderer interface {
|
||||||
// RenderHeading writes the rendered content to w using the data in w.
|
// RenderHeading writes the rendered content to w using the data in w.
|
||||||
|
|
|
@ -108,6 +108,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||||
}
|
}
|
||||||
cbctx := &codeBlockContext{
|
cbctx := &codeBlockContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: ctx.DocumentContext().Document,
|
||||||
|
pageInner: r.getPageInner(ctx),
|
||||||
lang: lang,
|
lang: lang,
|
||||||
code: s,
|
code: s,
|
||||||
ordinal: ordinal,
|
ordinal: ordinal,
|
||||||
|
@ -132,7 +133,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||||
w,
|
w,
|
||||||
cbctx,
|
cbctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
|
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
|
||||||
}
|
}
|
||||||
|
@ -140,8 +140,21 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||||
return ast.WalkContinue, nil
|
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 {
|
type codeBlockContext struct {
|
||||||
page any
|
page any
|
||||||
|
pageInner any
|
||||||
lang string
|
lang string
|
||||||
code string
|
code string
|
||||||
ordinal int
|
ordinal int
|
||||||
|
@ -159,6 +172,10 @@ func (c *codeBlockContext) Page() any {
|
||||||
return c.page
|
return c.page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *codeBlockContext) PageInner() any {
|
||||||
|
return c.pageInner
|
||||||
|
}
|
||||||
|
|
||||||
func (c *codeBlockContext) Type() string {
|
func (c *codeBlockContext) Type() string {
|
||||||
return c.lang
|
return c.lang
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
||||||
|
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
|
"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)))
|
renderer.WithNodeRenderers(util.Prioritized(emoji.NewHTMLRenderer(), 200)))
|
||||||
var (
|
var (
|
||||||
extensions = []goldmark.Extender{
|
extensions = []goldmark.Extender{
|
||||||
|
hugocontext.New(),
|
||||||
newLinks(cfg),
|
newLinks(cfg),
|
||||||
newTocExtension(tocRendererOptions),
|
newTocExtension(tocRendererOptions),
|
||||||
}
|
}
|
||||||
|
|
165
markup/goldmark/hugocontext/hugocontext.go
Normal file
165
markup/goldmark/hugocontext/hugocontext.go
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
34
markup/goldmark/hugocontext/hugocontext_test.go
Normal file
34
markup/goldmark/hugocontext/hugocontext_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ func (b *BufWriter) Flush() error {
|
||||||
type Context struct {
|
type Context struct {
|
||||||
*BufWriter
|
*BufWriter
|
||||||
positions []int
|
positions []int
|
||||||
|
pids []uint64
|
||||||
ContextData
|
ContextData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,30 @@ func (ctx *Context) PopPos() int {
|
||||||
return p
|
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 {
|
type ContextData interface {
|
||||||
RenderContext() converter.RenderContext
|
RenderContext() converter.RenderContext
|
||||||
DocumentContext() converter.DocumentContext
|
DocumentContext() converter.DocumentContext
|
||||||
|
|
|
@ -49,6 +49,7 @@ func newLinks(cfg goldmark_config.Config) goldmark.Extender {
|
||||||
|
|
||||||
type linkContext struct {
|
type linkContext struct {
|
||||||
page any
|
page any
|
||||||
|
pageInner any
|
||||||
destination string
|
destination string
|
||||||
title string
|
title string
|
||||||
text hstring.RenderedString
|
text hstring.RenderedString
|
||||||
|
@ -64,6 +65,10 @@ func (ctx linkContext) Page() any {
|
||||||
return ctx.page
|
return ctx.page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx linkContext) PageInner() any {
|
||||||
|
return ctx.pageInner
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx linkContext) Text() hstring.RenderedString {
|
func (ctx linkContext) Text() hstring.RenderedString {
|
||||||
return ctx.text
|
return ctx.text
|
||||||
}
|
}
|
||||||
|
@ -92,6 +97,7 @@ func (ctx imageLinkContext) Ordinal() int {
|
||||||
|
|
||||||
type headingContext struct {
|
type headingContext struct {
|
||||||
page any
|
page any
|
||||||
|
pageInner any
|
||||||
level int
|
level int
|
||||||
anchor string
|
anchor string
|
||||||
text hstring.RenderedString
|
text hstring.RenderedString
|
||||||
|
@ -103,6 +109,10 @@ func (ctx headingContext) Page() any {
|
||||||
return ctx.page
|
return ctx.page
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctx headingContext) PageInner() any {
|
||||||
|
return ctx.pageInner
|
||||||
|
}
|
||||||
|
|
||||||
func (ctx headingContext) Level() int {
|
func (ctx headingContext) Level() int {
|
||||||
return ctx.level
|
return ctx.level
|
||||||
}
|
}
|
||||||
|
@ -186,6 +196,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
imageLinkContext{
|
imageLinkContext{
|
||||||
linkContext: linkContext{
|
linkContext: linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: ctx.DocumentContext().Document,
|
||||||
|
pageInner: r.getPageInner(ctx),
|
||||||
destination: string(n.Destination),
|
destination: string(n.Destination),
|
||||||
title: string(n.Title),
|
title: string(n.Title),
|
||||||
text: hstring.RenderedString(text),
|
text: hstring.RenderedString(text),
|
||||||
|
@ -200,6 +211,18 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
return ast.WalkContinue, err
|
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 {
|
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
|
||||||
n := 0
|
n := 0
|
||||||
for _, x := range attrs {
|
for _, x := range attrs {
|
||||||
|
@ -274,6 +297,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: ctx.DocumentContext().Document,
|
||||||
|
pageInner: r.getPageInner(ctx),
|
||||||
destination: string(n.Destination),
|
destination: string(n.Destination),
|
||||||
title: string(n.Title),
|
title: string(n.Title),
|
||||||
text: hstring.RenderedString(text),
|
text: hstring.RenderedString(text),
|
||||||
|
@ -339,6 +363,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: ctx.DocumentContext().Document,
|
||||||
|
pageInner: r.getPageInner(ctx),
|
||||||
destination: url,
|
destination: url,
|
||||||
text: hstring.RenderedString(label),
|
text: hstring.RenderedString(label),
|
||||||
plainText: label,
|
plainText: label,
|
||||||
|
@ -423,6 +448,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||||
w,
|
w,
|
||||||
headingContext{
|
headingContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: ctx.DocumentContext().Document,
|
||||||
|
pageInner: r.getPageInner(ctx),
|
||||||
level: n.Level,
|
level: n.Level,
|
||||||
anchor: string(anchor),
|
anchor: string(anchor),
|
||||||
text: hstring.RenderedString(text),
|
text: hstring.RenderedString(text),
|
||||||
|
|
|
@ -88,6 +88,9 @@ type PageConfig struct {
|
||||||
|
|
||||||
// User defined params.
|
// User defined params.
|
||||||
Params maps.Params
|
Params maps.Params
|
||||||
|
|
||||||
|
// Compiled values.
|
||||||
|
IsGoldmark bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FrontMatterHandler maps front matter into Page fields and .Params.
|
// FrontMatterHandler maps front matter into Page fields and .Params.
|
||||||
|
|
|
@ -165,10 +165,12 @@ var Context = struct {
|
||||||
SetDependencyManagerInCurrentScope func(context.Context, identity.Manager) context.Context
|
SetDependencyManagerInCurrentScope func(context.Context, identity.Manager) context.Context
|
||||||
DependencyScope hcontext.ContextDispatcher[int]
|
DependencyScope hcontext.ContextDispatcher[int]
|
||||||
Page hcontext.ContextDispatcher[page]
|
Page hcontext.ContextDispatcher[page]
|
||||||
|
IsInGoldmark hcontext.ContextDispatcher[bool]
|
||||||
}{
|
}{
|
||||||
DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")),
|
DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")),
|
||||||
DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")),
|
DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")),
|
||||||
Page: hcontext.NewContextDispatcher[page](contextKey("Page")),
|
Page: hcontext.NewContextDispatcher[page](contextKey("Page")),
|
||||||
|
IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKey("IsInGoldmark")),
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{- $u := urls.Parse .Destination -}}
|
{{- $u := urls.Parse .Destination -}}
|
||||||
{{- $src := $u.String -}}
|
{{- $src := $u.String -}}
|
||||||
{{- if not $u.IsAbs -}}
|
{{- 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 -}}
|
{{- $src = .RelPermalink -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{{- $u := urls.Parse .Destination -}}
|
{{- $u := urls.Parse .Destination -}}
|
||||||
{{- $href := $u.String -}}
|
{{- $href := $u.String -}}
|
||||||
{{- if strings.HasPrefix $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 -}}
|
{{- else if not $u.IsAbs -}}
|
||||||
{{- with or
|
{{- with or
|
||||||
($.Page.GetPage $u.Path)
|
($.PageInner.GetPage $u.Path)
|
||||||
($.Page.Resources.Get $u.Path)
|
($.PageInner.Resources.Get $u.Path)
|
||||||
(resources.Get $u.Path)
|
(resources.Get $u.Path)
|
||||||
-}}
|
-}}
|
||||||
{{- $href = .RelPermalink -}}
|
{{- $href = .RelPermalink -}}
|
||||||
|
|
Loading…
Reference in a new issue