From: Bjørn Erik Pedersen Date: Wed, 25 Mar 2026 17:28:19 +0000 (+0100) Subject: Improve and extend .Position handling in Goldmark render hooks X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=303e443ea7ba5c22dc5d2b5df5d7c5392b0dcc3a;p=brevno-suite%2Fhugo Improve and extend .Position handling in Goldmark render hooks Goldmark v1.8 reports source positions in render hooks. Use this to build a source map that translates Goldmark positions back to the original content source. Now all render hooks' context object implenent the `BaseContext` interface: ```go type BaseContext interface { Position() Position Page() any PageInner() any Ordinal() int } ``` Closes #14663 Co-Authored-By: Claude Opus 4.6 (1M context) --- diff --git a/go.mod b/go.mod index 482388e3a..7fcd915cf 100644 --- a/go.mod +++ b/go.mod @@ -41,8 +41,8 @@ require ( github.com/gohugoio/go-radix v1.2.0 github.com/gohugoio/hashstructure v0.6.0 github.com/gohugoio/httpcache v0.8.0 - github.com/gohugoio/hugo-goldmark-extensions/extras v0.6.0 - github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.4.0 + github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 + github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 github.com/google/go-cmp v0.7.0 github.com/gorilla/websocket v1.5.3 github.com/hairyhenderson/go-codeowners v0.7.0 @@ -69,7 +69,7 @@ require ( github.com/tdewolff/minify/v2 v2.24.11 github.com/tdewolff/parse/v2 v2.8.11 github.com/tetratelabs/wazero v1.11.0 - github.com/yuin/goldmark v1.7.17 + github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-emoji v1.0.6 go.uber.org/automaxprocs v1.5.3 gocloud.dev v0.45.0 diff --git a/go.sum b/go.sum index f9a0d2fd8..5cf476296 100644 --- a/go.sum +++ b/go.sum @@ -276,8 +276,12 @@ github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/Sfico github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= github.com/gohugoio/hugo-goldmark-extensions/extras v0.6.0 h1:c16engMi6zyOGeCrP73RWC9fom94wXGpVzncu3GXBjI= github.com/gohugoio/hugo-goldmark-extensions/extras v0.6.0/go.mod h1:e3+TRCT4Uz6NkZOAVMOMgPeJ+7KEtQMX8hdB+WG4qRs= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.4.0 h1:awFlqaCQ0N/RS9ndIBpDYNms101I1sGbDRG1bksa5Js= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.4.0/go.mod h1:lK1CjqrueCd3OBnsLLQJGrQ+uodWfT9M9Cq2zfDWJCE= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -515,6 +519,10 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.7.17 h1:p36OVWwRb246iHxA/U4p8OPEpOTESm4n+g+8t0EE5uA= github.com/yuin/goldmark v1.7.17/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.1 h1:id2TeYXe5FpqwLco0Pso4cNM5Z6Okt4g7kDw9QBMhTA= +github.com/yuin/goldmark v1.8.1/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 0cca67bdb..63c973050 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -19,6 +19,7 @@ import ( "fmt" "html/template" "io" + "sort" "strings" "sync/atomic" "unicode/utf8" @@ -197,18 +198,44 @@ func (pi *contentParseInfo) AddShortcode(s *shortcode) { } } +// sourceMapEntry maps a range in rendered content back to a position in the original source. +type sourceMapEntry struct { + renderOffset int // start offset in content sent to Goldmark + sourceOffset int // corresponding offset in original source file + isShortcode bool // if true, any pos in this segment maps to sourceOffset +} + +// resolveSourceOffset maps a position in rendered content to an offset in the original source. +func resolveSourceOffset(sm []sourceMapEntry, pos int) int { + i := sort.Search(len(sm), func(i int) bool { + return sm[i].renderOffset > pos + }) - 1 + if i < 0 { + return pos + } + e := sm[i] + if e.isShortcode { + return e.sourceOffset + } + return e.sourceOffset + (pos - e.renderOffset) +} + // contentToRenderForItems returns the content to be processed by Goldmark or similar. -func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) { +func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, []sourceMapEntry, bool, error) { var hasVariants bool c := make([]byte, 0, len(source)+(len(source)/10)) + var sm []sourceMapEntry for _, it := range pi.itemsStep2 { switch v := it.(type) { case pageparser.Item: + sm = append(sm, sourceMapEntry{renderOffset: len(c), sourceOffset: v.Pos()}) c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) case pageContentReplacement: + sm = append(sm, sourceMapEntry{renderOffset: len(c), sourceOffset: v.source.Pos()}) c = append(c, v.val...) case *shortcode: + sm = append(sm, sourceMapEntry{renderOffset: len(c), sourceOffset: v.pos, isShortcode: true}) if !v.insertPlaceholder() { // Insert the rendered shortcode. renderedShortcode, found := renderedShortcodes[v.placeholder] @@ -219,7 +246,7 @@ func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, b, more, err := renderedShortcode.renderShortcode(ctx) if err != nil { - return nil, false, fmt.Errorf("failed to render shortcode: %w", err) + return nil, nil, false, fmt.Errorf("failed to render shortcode: %w", err) } hasVariants = hasVariants || more c = append(c, []byte(b)...) @@ -234,7 +261,7 @@ func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, } } - return c, hasVariants, nil + return c, sm, hasVariants, nil } func (c *cachedContent) IsZero() bool { @@ -651,7 +678,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) var hasVariants bool - ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders) + ct.contentToRender, cp.sourceMap, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders) if err != nil { return nil, err } @@ -919,7 +946,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem return "", err } - contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) + contentToRender, _, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) if err != nil { return "", err } diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 3d0cb5ff7..0b889eeb9 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -14,7 +14,6 @@ package hugolib import ( - "bytes" "context" "errors" "fmt" @@ -90,6 +89,9 @@ type pageContentOutput struct { // Renders Markdown hooks. renderHooks *renderHooks + + // Maps positions in the content sent to Goldmark back to the original source. + sourceMap []sourceMapEntry } func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) { @@ -244,24 +246,22 @@ func (pco *pageContentOutput) initRenderHooks() error { renderCache := make(map[cacheKey]any) var renderCacheMu sync.Mutex - resolvePosition := func(ctx any) text.Position { - source := pco.po.p.m.content.mustSource() - var offset int - - switch v := ctx.(type) { - case hooks.PositionerSourceTargetProvider: - offset = bytes.Index(source, v.PositionerSourceTarget()) + resolvePosition := func(_ any, _ []byte, pos int) text.Position { + if pos == -1 { + return text.Position{ + Filename: pco.po.p.pathOrTitle(), + } } + sourceOrig := pco.po.p.m.content.mustSource() + var offset int - pos := pco.po.p.posFromInput(source, offset) - - if pos.LineNumber > 0 { - // Move up to the code fence delimiter. - // This is in line with how we report on shortcodes. - pos.LineNumber = pos.LineNumber - 1 + if sm := pco.sourceMap; len(sm) > 0 { + offset = resolveSourceOffset(sm, pos) + } else { + offset = pos + pco.po.p.m.content.pi.posMainContent } - return pos + return pco.po.p.posFromInput(sourceOrig, offset) } pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any { diff --git a/hugolib/site.go b/hugolib/site.go index 8e0b2888f..9c7c51fd1 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1682,7 +1682,7 @@ var infoOnMissingLayout = map[string]bool{ type hookRendererTemplate struct { templateHandler *tplimpl.TemplateStore templ *tplimpl.TemplInfo - resolvePosition func(ctx any) text.Position + resolvePosition func(ctx any, srcRender []byte, pos int) text.Position } func (hr hookRendererTemplate) RenderLink(cctx context.Context, w io.Writer, ctx hooks.LinkContext) error { @@ -1709,8 +1709,8 @@ func (hr hookRendererTemplate) RenderTable(cctx context.Context, w hugio.FlexiWr return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) } -func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position { - return hr.resolvePosition(ctx) +func (hr hookRendererTemplate) ResolvePosition(ctx any, srcRender []byte, pos int) text.Position { + return hr.resolvePosition(ctx, srcRender, pos) } func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool { diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go index 1fc513acb..411d16f0b 100644 --- a/markup/converter/hooks/hooks.go +++ b/markup/converter/hooks/hooks.go @@ -121,11 +121,6 @@ type BlockquoteContext interface { AlertSign() string } -type PositionerSourceTargetProvider interface { - // For internal use. - PositionerSourceTarget() []byte -} - // PassThroughContext is the context passed to a passthrough render hook. type PassthroughContext interface { BaseContext @@ -205,7 +200,11 @@ type HeadingRenderer interface { // This may be both slow and approximate, so should only be // used for error logging. type ElementPositionResolver interface { - ResolvePosition(ctx any) text.Position + // ResolvePosition returns the position of the element in the original source document. + // ctx is the context passed to the render hook, and srcPos is the zero-based byte offset of the element + // in srcRender (the rendered source buffer), + // -1 if it's not defined. + ResolvePosition(ctx any, srcRender []byte, srcPos int) text.Position } type RendererType int diff --git a/markup/goldmark/blockquotes/blockquotes.go b/markup/goldmark/blockquotes/blockquotes.go index c82b9f8f4..e80c05a51 100644 --- a/markup/goldmark/blockquotes/blockquotes.go +++ b/markup/goldmark/blockquotes/blockquotes.go @@ -102,8 +102,8 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N } } - bqctx := &blockquoteContext{ - BaseContext: render.NewBaseContext(ctx, renderer, n, src, nil, ordinal), + bqctx := blockquoteContext{ + BaseContext: render.NewBaseContext(ctx, renderer, n, src, ordinal), typ: typ, alert: alert, text: hstring.HTML(text), @@ -150,23 +150,23 @@ type blockquoteContext struct { *attributes.AttributesHolder } -func (c *blockquoteContext) Type() string { +func (c blockquoteContext) Type() string { return c.typ } -func (c *blockquoteContext) AlertType() string { +func (c blockquoteContext) AlertType() string { return c.alert.typ } -func (c *blockquoteContext) AlertTitle() hstring.HTML { +func (c blockquoteContext) AlertTitle() hstring.HTML { return hstring.HTML(c.alert.title) } -func (c *blockquoteContext) AlertSign() string { +func (c blockquoteContext) AlertSign() string { return c.alert.sign } -func (c *blockquoteContext) Text() hstring.HTML { +func (c blockquoteContext) Text() hstring.HTML { return c.text } diff --git a/markup/goldmark/blockquotes/blockquotes_integration_test.go b/markup/goldmark/blockquotes/blockquotes_integration_test.go index 046ccaac4..93e3bc844 100644 --- a/markup/goldmark/blockquotes/blockquotes_integration_test.go +++ b/markup/goldmark/blockquotes/blockquotes_integration_test.go @@ -79,7 +79,7 @@ title: "p1" "Blockquote Alert: |

This is a caution with some whitespace before the alert type.

|alert|", "Blockquote: |

A regular blockquote.

\n|regular|", "Blockquote Alert Attributes: |

This is a tip with attributes.

|map[class:foo bar id:baz]|", - filepath.FromSlash("/content/p1.md:19:3"), + filepath.FromSlash("/content/p1.md:20:1"), "Blockquote Alert Page: |

This is a tip with attributes.

|p1|p1|", // Issue 12767. diff --git a/markup/goldmark/codeblocks/codeblocks_integration_test.go b/markup/goldmark/codeblocks/codeblocks_integration_test.go index d2f2e0dda..1eb88d34d 100644 --- a/markup/goldmark/codeblocks/codeblocks_integration_test.go +++ b/markup/goldmark/codeblocks/codeblocks_integration_test.go @@ -181,10 +181,8 @@ title: "p1" b := hugolib.Test(t, files) - b.AssertFileContent("public/p1/index.html", ` -# Issue 9627: For the Position in code blocks we try to match the .Inner with the original source. This isn't always possible. -p1.md:0:0 - `, + b.AssertFileContent("public/p1/index.html", + "p1.md:7:1", ) } @@ -388,3 +386,30 @@ Attributes: {{ .Attributes }}|Type: {{ .Type }}| b.Assert(err, qt.Not(qt.IsNil)) b.Assert(err.Error(), qt.Contains, "p1.md:7:9\": failed to parse Markdown attributes; you may need to quote the values") } + +func TestCodeblockPosition(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/_markup/render-codeblock.html -- +{{ .Position | safeHTML }} +-- layouts/single.html -- +{{ .Content }} +-- content/p1.md -- +--- +title: "p1" +--- + +## Simple + +§§§text +Some code. +§§§ + +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", "p1.md:7:1") +} diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go index c29632b90..c218410e9 100644 --- a/markup/goldmark/codeblocks/render.go +++ b/markup/goldmark/codeblocks/render.go @@ -101,8 +101,8 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr} } - cbctx := &codeBlockContext{ - BaseContext: render.NewBaseContext(ctx, renderer, node, src, func() []byte { return []byte(s) }, ordinal), + cbctx := codeBlockContext{ + BaseContext: render.NewBaseContext(ctx, renderer, node, src, ordinal), lang: lang, code: s, AttributesHolder: attributes.New(attrs, attrtp), @@ -130,11 +130,11 @@ type codeBlockContext struct { *attributes.AttributesHolder } -func (c *codeBlockContext) Type() string { +func (c codeBlockContext) Type() string { return c.lang } -func (c *codeBlockContext) Inner() string { +func (c codeBlockContext) Inner() string { return c.code } diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go index fd8e788ed..e95d44bba 100644 --- a/markup/goldmark/internal/render/context.go +++ b/markup/goldmark/internal/render/context.go @@ -19,7 +19,6 @@ import ( "strings" "sync" - "github.com/gohugoio/hugo-goldmark-extensions/passthrough" bp "github.com/gohugoio/hugo/bufferpool" east "github.com/yuin/goldmark-emoji/ast" @@ -160,51 +159,6 @@ func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext return ctx.Dctx } -// extractSourceSample returns a sample of the source for the given node. -// Note that this is not a copy of the source, but a slice of it, -// so it assumes that the source is not mutated. -func extractSourceSample(n ast.Node, src []byte) []byte { - if n.Type() == ast.TypeInline { - switch n := n.(type) { - case *passthrough.PassthroughInline: - return n.Segment.Value(src) - } - - return nil - } - - var sample []byte - - getStartStop := func(n ast.Node) (int, int) { - if n == nil { - return 0, 0 - } - - var start, stop int - for i := 0; i < n.Lines().Len() && i < 2; i++ { - line := n.Lines().At(i) - if i == 0 { - start = line.Start - } - stop = line.Stop - } - return start, stop - } - - start, stop := getStartStop(n) - if stop == 0 { - // Try first child. - start, stop = getStartStop(n.FirstChild()) - } - - if stop > 0 { - // We do not mutate the source, so this is safe. - sample = src[start:stop] - } - - return sample -} - // GetPageAndPageInner returns the current page and the inner page for the given context. func GetPageAndPageInner(rctx *Context) (any, any) { p := rctx.DocumentContext().Document @@ -220,24 +174,17 @@ func GetPageAndPageInner(rctx *Context) (any, any) { } // NewBaseContext creates a new BaseContext. -func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, getSourceSample func() []byte, ordinal int) hooks.BaseContext { - if getSourceSample == nil { - getSourceSample = func() []byte { - return extractSourceSample(n, src) - } - } +func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, ordinal int) hooks.BaseContext { page, pageInner := GetPageAndPageInner(rctx) b := &hookBase{ page: page, pageInner: pageInner, - - getSourceSample: getSourceSample, - ordinal: ordinal, + ordinal: ordinal, } b.createPos = func() htext.Position { if resolver, ok := renderer.(hooks.ElementPositionResolver); ok { - return resolver.ResolvePosition(b) + return resolver.ResolvePosition(b, src, n.Pos()) } return htext.Position{ @@ -250,19 +197,14 @@ func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, getSour return b } -var _ hooks.PositionerSourceTargetProvider = (*hookBase)(nil) - type hookBase struct { page any pageInner any ordinal int - // This is only used in error situations and is expensive to create, - // so delay creation until needed. - pos htext.Position - posInit sync.Once - createPos func() htext.Position - getSourceSample func() []byte + pos htext.Position + posInit sync.Once + createPos func() htext.Position } func (c *hookBase) Page() any { @@ -284,11 +226,6 @@ func (c *hookBase) Position() htext.Position { return c.pos } -// For internal use. -func (c *hookBase) PositionerSourceTarget() []byte { - return c.getSourceSample() -} - // TextPlain returns a plain text representation of the given node. // This will resolve any leftover HTML entities. This will typically be // entities inserted by e.g. the typographer extension. diff --git a/markup/goldmark/passthrough/passthrough.go b/markup/goldmark/passthrough/passthrough.go index c56842f3d..c38f3eac0 100644 --- a/markup/goldmark/passthrough/passthrough.go +++ b/markup/goldmark/passthrough/passthrough.go @@ -132,8 +132,8 @@ func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node // Trim the delimiters. s = s[len(delims.Open) : len(s)-len(delims.Close)] - pctx := &passthroughContext{ - BaseContext: render.NewBaseContext(ctx, renderer, node, src, nil, ordinal), + pctx := passthroughContext{ + BaseContext: render.NewBaseContext(ctx, renderer, node, src, ordinal), inner: s, typ: typ, AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral), @@ -157,10 +157,10 @@ type passthroughContext struct { *attributes.AttributesHolder } -func (p *passthroughContext) Type() string { +func (p passthroughContext) Type() string { return p.typ } -func (p *passthroughContext) Inner() string { +func (p passthroughContext) Inner() string { return p.inner } diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go index 418358054..376c7dae4 100644 --- a/markup/goldmark/render_hooks.go +++ b/markup/goldmark/render_hooks.go @@ -48,8 +48,7 @@ func newLinks(cfg goldmark_config.Config) goldmark.Extender { } type linkContext struct { - page any - pageInner any + hooks.BaseContext destination string title string text hstring.HTML @@ -61,14 +60,6 @@ func (ctx linkContext) Destination() string { return ctx.destination } -func (ctx linkContext) Page() any { - return ctx.page -} - -func (ctx linkContext) PageInner() any { - return ctx.pageInner -} - func (ctx linkContext) Text() hstring.HTML { return ctx.text } @@ -83,7 +74,6 @@ func (ctx linkContext) Title() string { type imageLinkContext struct { linkContext - ordinal int isBlock bool } @@ -91,13 +81,8 @@ func (ctx imageLinkContext) IsBlock() bool { return ctx.isBlock } -func (ctx imageLinkContext) Ordinal() int { - return ctx.ordinal -} - type headingContext struct { - page any - pageInner any + hooks.BaseContext level int anchor string text hstring.HTML @@ -105,14 +90,6 @@ type headingContext struct { *attributes.AttributesHolder } -func (ctx headingContext) Page() any { - return ctx.page -} - -func (ctx headingContext) PageInner() any { - return ctx.pageInner -} - func (ctx headingContext) Level() int { return ctx.level } @@ -188,22 +165,18 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N // internal attributes before rendering. attrs := r.filterInternalAttributes(n.Attributes()) - page, pageInner := render.GetPageAndPageInner(ctx) - err := lr.RenderLink( ctx.RenderContext().Ctx, w, imageLinkContext{ linkContext: linkContext{ - page: page, - pageInner: pageInner, + BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal), destination: string(n.Destination), title: string(n.Title), text: hstring.HTML(text), plainText: render.TextPlain(n, source), AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral), }, - ordinal: ordinal, isBlock: isBlock, }, ) @@ -277,15 +250,13 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No } text := ctx.PopRenderedString() - - page, pageInner := render.GetPageAndPageInner(ctx) + ordinal := ctx.GetAndIncrementOrdinal(node.Kind()) err := lr.RenderLink( ctx.RenderContext().Ctx, w, linkContext{ - page: page, - pageInner: pageInner, + BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal), destination: string(n.Destination), title: string(n.Title), text: hstring.HTML(text), @@ -420,14 +391,13 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as url = "mailto:" + url } - page, pageInner := render.GetPageAndPageInner(ctx) + ordinal := ctx.GetAndIncrementOrdinal(n.Kind()) err := lr.RenderLink( ctx.RenderContext().Ctx, w, linkContext{ - page: page, - pageInner: pageInner, + BaseContext: render.NewBaseContext(ctx, lr, node, source, ordinal), destination: url, text: hstring.HTML(label), plainText: label, @@ -508,15 +478,13 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast if anchori, ok := n.AttributeString("id"); ok { anchor, _ = anchori.([]byte) } - - page, pageInner := render.GetPageAndPageInner(ctx) + ordinal := ctx.GetAndIncrementOrdinal(n.Kind()) err := hr.RenderHeading( ctx.RenderContext().Ctx, w, headingContext{ - page: page, - pageInner: pageInner, + BaseContext: render.NewBaseContext(ctx, hr, node, source, ordinal), level: n.Level, anchor: string(anchor), text: hstring.HTML(text), diff --git a/markup/goldmark/tables/tables.go b/markup/goldmark/tables/tables.go index a7dfb8ee6..df75a0b9b 100644 --- a/markup/goldmark/tables/tables.go +++ b/markup/goldmark/tables/tables.go @@ -76,8 +76,8 @@ func (r *htmlRenderer) renderTable(w util.BufWriter, source []byte, n ast.Node, ordinal := ctx.GetAndIncrementOrdinal(gast.KindTable) - tctx := &tableContext{ - BaseContext: render.NewBaseContext(ctx, renderer, n, source, nil, ordinal), + tctx := tableContext{ + BaseContext: render.NewBaseContext(ctx, renderer, n, source, ordinal), AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral), tHead: table.THead, tBody: table.TBody, @@ -166,10 +166,10 @@ type tableContext struct { tBody []hooks.TableRow } -func (c *tableContext) THead() []hooks.TableRow { +func (c tableContext) THead() []hooks.TableRow { return c.tHead } -func (c *tableContext) TBody() []hooks.TableRow { +func (c tableContext) TBody() []hooks.TableRow { return c.tBody } diff --git a/tpl/tplimpl/render_hook_integration_test.go b/tpl/tplimpl/render_hook_integration_test.go index c27dbacce..0c7d85591 100644 --- a/tpl/tplimpl/render_hook_integration_test.go +++ b/tpl/tplimpl/render_hook_integration_test.go @@ -339,3 +339,130 @@ TR-IMAGE b.AssertFileContent("public/p1/index.html", "\"alt\"") b.AssertFileContent("public/tr/p1/index.html", "TR-IMAGE") } + +// Hooks: +// table +// passthrough +// link +// image +// heading +// codeblock +// blockquote +func TestRenderHooksPosition(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +[markup] +[markup.goldmark] +[markup.goldmark.extensions] +[markup.goldmark.extensions.passthrough] +enable = true +[markup.goldmark.extensions.passthrough.delimiters] +block = [['\[', '\]'], ['$$', '$$']] +inline = [['\(', '\)']] +-- layouts/_markup/render-table.html -- +HOOK_CONTENT +-- layouts/_markup/render-passthrough.html -- +HOOK_CONTENT +-- layouts/_markup/render-link.html -- +HOOK_CONTENT +-- layouts/_markup/render-image.html -- +HOOK_CONTENT +-- layouts/_markup/render-heading.html -- +HOOK_CONTENT +-- layouts/_markup/render-codeblock.html -- +HOOK_CONTENT +-- layouts/_markup/render-blockquote.html -- +HOOK_CONTENT +-- layouts/shortcodes/myheading.html -- +{{ $s := .Get 0 -}} + + + {{ printf "## %s" $s }} +-- layouts/shortcodes/mylink.html -- +{{ $s := .Get 0 -}} + +A + +B + + {{ printf "[%s](%s)" $s $s }} + + +C +-- layouts/single.html -- +{{ .Content }} +-- content/p1.md -- +--- +title: "p1" +--- + +[p1](/p1) + [p2](/p2) +7 +8 +{{% mylink "p3" %}} +10 +11 +12 +## My Heading 2 +14 +### My Heading 3 +16 +{{% myheading "h4" %}} +18 +## Table 1 +20 +| Month | Savings | +| -------- | ------- | +| January | $250 | +| February | $80 | +| March | $420 | + +> blockquote 1 + +Foo. + +> blockquote 2 + + +### Code block + +§§§go +fmt.Println("hello") +§§§ + +## PassThrough + +\[block1\] + + \[block2\] + +This is an \(inline\) passthrough element with opening and closing inline delimiters. + + +` + + files = strings.ReplaceAll(files, "HOOK_CONTENT", ` +{{ $pos := .Position }} +{{ printf "%T" . }}|{{ path.Base $pos.Filename }}|{{ printf "%d:%d" $pos.LineNumber $pos.ColumnNumber }}|{{ $.Ordinal }}| +`) + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", + "goldmark.linkContext|p1.md|5:1|0|", + "goldmark.linkContext|p1.md|6:2|1|", + "goldmark.linkContext|p1.md|9:1|2|", + "goldmark.headingContext|p1.md|13:1|0|", + "goldmark.headingContext|p1.md|15:1|1|", + "goldmark.headingContext|p1.md|17:1|2|", + "tables.tableContext|p1.md|20:1|0|", + "blockquotes.blockquoteContext|p1.md|27:1|0|", + "blockquotes.blockquoteContext|p1.md|31:1|1|", + "codeblocks.codeBlockContext|p1.md|36:1|0|", + "passthrough.passthroughContext|p1.md|42:1|0|", + "passthrough.passthroughContext|p1.md|44:2|1|", + "passthrough.passthroughContext|p1.md|46:12|2|", + ) +} diff --git a/tpl/transform/transform_integration_test.go b/tpl/transform/transform_integration_test.go index 3cc96f20a..6347452cf 100644 --- a/tpl/transform/transform_integration_test.go +++ b/tpl/transform/transform_integration_test.go @@ -304,13 +304,13 @@ Content: {{ .Content }}| files := strings.Replace(filesTemplate, "$$1+2$$", "$$\\foo1+2$$", 1) b, err := hugolib.TestE(t, files) b.Assert(err, qt.IsNotNil) - b.AssertLogContains("p1.md:6:1") + b.AssertLogContains("p1.md:7:1") // Inline math. files = strings.Replace(filesTemplate, "$1+3$", "$\\foo1+3$", 1) b, err = hugolib.TestE(t, files) b.Assert(err, qt.IsNotNil) - b.AssertLogContains("p1.md:8:13") + b.AssertLogContains("p1.md:9:13") } func TestToMathMacros(t *testing.T) {