]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix some recently introduced Position issues
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 3 Apr 2026 16:24:00 +0000 (18:24 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 4 Apr 2026 12:53:20 +0000 (14:53 +0200)
Fixes #14710

hugolib/page__content.go
hugolib/page__per_output.go
hugolib/shortcode.go
hugolib/site.go
markup/converter/converter.go
markup/converter/hooks/hooks.go
markup/goldmark/internal/render/context.go
resources/page/page.go
resources/page/page_lazy_contentprovider.go
resources/page/page_nop.go
tpl/tplimpl/render_hook_integration_test.go

index 63c9730508342c3f51ab5a1f9da494d0bad2fc36..7377936f30935c4f8ec4aaae52adbf6ae23053fb 100644 (file)
@@ -457,6 +457,17 @@ type contentTableOfContents struct {
        contentPlaceholders map[string]shortcodeRenderer
 
        contentToRender []byte
+
+       sourceInfo
+}
+
+type sourceInfo struct {
+       // Optional override for the filename used in position reporting.
+       filename string
+       // The original source bytes.
+       source []byte
+       // Maps positions in the content sent to Goldmark back to the original source.
+       sourceMap []sourceMapEntry
 }
 
 type contentSummary struct {
@@ -515,7 +526,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
 
                        if ct.astDoc != nil {
                                // The content is parsed, but not rendered.
-                               r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
+                               r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.sourceInfo, ct.astDoc)
                                if err != nil {
                                        return nil, err
                                }
@@ -638,12 +649,13 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
        versionv := c.version(cp)
 
        v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
-               source, err := c.pi.contentSource(c)
+               var err error
+               var ct contentTableOfContents
+               ct.source, err = c.pi.contentSource(c)
                if err != nil {
                        return nil, err
                }
 
-               var ct contentTableOfContents
                if err := cp.initRenderHooks(); err != nil {
                        return nil, err
                }
@@ -678,7 +690,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
                ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
 
                var hasVariants bool
-               ct.contentToRender, cp.sourceMap, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
+               ct.contentToRender, ct.sourceMap, hasVariants, err = c.pi.contentToRender(ctx, ct.source, ct.contentPlaceholders)
                if err != nil {
                        return nil, err
                }
@@ -926,9 +938,15 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
        }
 
        if pageparser.HasShortcode(contentToRender) {
-               contentToRenderb := []byte(contentToRender)
+               ct := contentTableOfContents{
+                       sourceInfo: sourceInfo{
+                               filename: pco.po.p.pathOrTitle() + " (rendered from string)",
+                               source:   []byte(contentToRender),
+                       },
+               }
+               ct.contentToRender = ct.source
                // String contains a shortcode.
-               parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
+               parseInfo.itemsStep1, err = pageparser.ParseBytes(ct.source, pageparser.Config{
                        NoFrontMatter:    true,
                        NoSummaryDivider: true,
                })
@@ -937,7 +955,7 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
                }
 
                parseInfo.shortcodeParseInfo = newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s.h.Deps)
-               if err := parseInfo.parseSource(contentToRenderb, true); err != nil {
+               if err := parseInfo.parseSource(ct.source, true); err != nil {
                        return "", err
                }
 
@@ -946,14 +964,15 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
                        return "", err
                }
 
-               contentToRender, _, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
+               var hasVariants bool
+               ct.contentToRender, ct.sourceMap, hasVariants, err = parseInfo.contentToRender(ctx, ct.source, placeholders)
                if err != nil {
                        return "", err
                }
                if hasVariants {
                        pco.po.p.incrPageOutputTemplateVariation()
                }
-               b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
+               b, err := pco.renderContentWithConverter(ctx, conv, ct.contentToRender, ct.sourceInfo, false)
                if err != nil {
                        return "", pco.po.p.wrapError(err)
                }
@@ -998,7 +1017,11 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
                pco.po.p.m.content.hasShortcode.Store(&combined)
 
        } else {
-               c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
+               si := sourceInfo{
+                       filename: pco.po.p.pathOrTitle() + " (rendered from string)",
+                       source:   []byte(contentToRender),
+               }
+               c, err := pco.renderContentWithConverter(ctx, conv, si.source, si, false)
                if err != nil {
                        return "", pco.po.p.wrapError(err)
                }
index 0b889eeb96f9774b4f58d6e3e25de3f5e7573f1b..9009165ff2ecf94c19bc092c981d7f4e94393f28 100644 (file)
@@ -89,9 +89,6 @@ 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) {
@@ -246,22 +243,25 @@ func (pco *pageContentOutput) initRenderHooks() error {
                renderCache := make(map[cacheKey]any)
                var renderCacheMu sync.Mutex
 
-               resolvePosition := func(_ any, _ []byte, pos int) text.Position {
-                       if pos == -1 {
+               resolvePosition := func(renderContext any, pos int) text.Position {
+                       rc, ok := renderContext.(converter.RenderContext)
+                       var si sourceInfo
+                       if ok {
+                               si, ok = rc.SourceInfo.(sourceInfo)
+                       }
+
+                       if pos == -1 || !ok {
                                return text.Position{
                                        Filename: pco.po.p.pathOrTitle(),
                                }
                        }
-                       sourceOrig := pco.po.p.m.content.mustSource()
-                       var offset int
-
-                       if sm := pco.sourceMap; len(sm) > 0 {
-                               offset = resolveSourceOffset(sm, pos)
-                       } else {
-                               offset = pos + pco.po.p.m.content.pi.posMainContent
+                       offset := resolveSourceOffset(si.sourceMap, pos)
+                       filename := si.filename
+                       if filename == "" {
+                               filename = pco.po.p.pathOrTitle()
                        }
 
-                       return pco.po.p.posFromInput(sourceOrig, offset)
+                       return posFromInput(filename, si.source, offset)
                }
 
                pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
@@ -411,7 +411,10 @@ func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content
        if err != nil {
                return nil, err
        }
-       return cp.renderContentWithConverter(ctx, c, content, renderTOC)
+       si := sourceInfo{
+               source: content,
+       }
+       return cp.renderContentWithConverter(ctx, c, content, si, renderTOC)
 }
 
 func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) {
@@ -433,7 +436,7 @@ func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte)
        return r, ok, err
 }
 
-func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
+func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
        c, err := pco.getContentConverter()
        if err != nil {
                return nil, false, err
@@ -445,6 +448,7 @@ func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte,
        rctx := converter.RenderContext{
                Ctx:         ctx,
                Src:         content,
+               SourceInfo:  sourceInfo,
                RenderTOC:   true,
                GetRenderer: pco.renderHooks.getRenderer,
        }
@@ -452,11 +456,12 @@ func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte,
        return r, ok, err
 }
 
-func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
+func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, sourceInfo any, renderTOC bool) (converter.ResultRender, error) {
        r, err := c.Convert(
                converter.RenderContext{
                        Ctx:         ctx,
                        Src:         content,
+                       SourceInfo:  sourceInfo,
                        RenderTOC:   renderTOC,
                        GetRenderer: pco.renderHooks.getRenderer,
                })
index c680620cfbe2b5ddcf43cb87db50402b2b844248..aae0330511766bcc9350011e6ac4166013a74fe4 100644 (file)
@@ -553,7 +553,7 @@ func (s *shortcodeParseInfo) prepareShortcodesForPage(po *pageOutput, isRenderSt
 }
 
 func posFromInput(filename string, input []byte, offset int) text.Position {
-       if offset < 0 {
+       if offset < 0 || offset > len(input) {
                return text.Position{
                        Filename: filename,
                }
index 9c7c51fd1cae7c15c9bb0187b6120a7164191f23..678fea7cddd817d6f4fa3c0ea825c9e46d804e37 100644 (file)
@@ -1682,7 +1682,7 @@ var infoOnMissingLayout = map[string]bool{
 type hookRendererTemplate struct {
        templateHandler *tplimpl.TemplateStore
        templ           *tplimpl.TemplInfo
-       resolvePosition func(ctx any, srcRender []byte, pos int) text.Position
+       resolvePosition func(renderContext any, 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, srcRender []byte, pos int) text.Position {
-       return hr.resolvePosition(ctx, srcRender, pos)
+func (hr hookRendererTemplate) ResolvePosition(renderContext any, pos int) text.Position {
+       return hr.resolvePosition(renderContext, pos)
 }
 
 func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
index 0d5da5e69fc17f829de52556cbf79437d9b072ab..b4d820bbb7737f05574fdd94f905774b8ed5949a 100644 (file)
@@ -150,6 +150,9 @@ type RenderContext struct {
        // Src is the content to render.
        Src []byte
 
+       // SourceInfo holds optional information about the source of the content to render.
+       SourceInfo any
+
        // Whether to render TableOfContents.
        RenderTOC bool
 
index 411d16f0bf4b6228ef437d33f4977aa485305005..d64998e8500efc87357e674e36697d5582c03344 100644 (file)
@@ -200,11 +200,8 @@ type HeadingRenderer interface {
 // This may be both slow and approximate, so should only be
 // used for error logging.
 type ElementPositionResolver interface {
-       // 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
+       // ResolvePosition returns the position of the element in the original source document, -1 if it's not defined.
+       ResolvePosition(renderContext any, srcPos int) text.Position
 }
 
 type RendererType int
index e95d44bbaa6c63fbc4caf47406a1d18fc836def3..7daf1c0716118256f1c09419c2c33040cccd7265 100644 (file)
@@ -184,7 +184,7 @@ func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, ordinal
 
        b.createPos = func() htext.Position {
                if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
-                       return resolver.ResolvePosition(b, src, n.Pos())
+                       return resolver.ResolvePosition(rctx.RenderContext(), n.Pos())
                }
 
                return htext.Position{
index 0f5af33e3e3c2c710cc7d25a9747b78a0d658c47..cc7b96448ef8e06397040a51f27d4692da8e34fe 100644 (file)
@@ -114,7 +114,7 @@ type ContentRenderer interface {
        // For internal use only.
        ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error)
        // For internal use only.
-       RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error)
+       RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error)
 }
 
 // FileProvider provides the source file.
index 78ca09bbed1efd3b614a7f43f81d157a50dd7828..84fb246826fe472316ab93abd685885b02fe9aa6 100644 (file)
@@ -133,6 +133,6 @@ func (lcp *LazyContentProvider) ParseContent(ctx context.Context, content []byte
        return lcp.init.Value(ctx).ParseContent(ctx, content)
 }
 
-func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
-       return lcp.init.Value(ctx).RenderContent(ctx, content, doc)
+func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
+       return lcp.init.Value(ctx).RenderContent(ctx, content, sourceInfo, doc)
 }
index 80fe5025829c8391bdb8f7ed80da0da1aaff15f7..900857902dacd94695076e3f95c3723c43f21987 100644 (file)
@@ -522,7 +522,7 @@ func (r *nopContentRenderer) ParseContent(ctx context.Context, content []byte) (
        return nil, false, nil
 }
 
-func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
+func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
        return nil, false, nil
 }
 
index 0c7d85591a49a2ffb3d42cbb9423f868880594c9..ea1c33239a784b1aedfde21fa821a1172d5744f6 100644 (file)
@@ -466,3 +466,44 @@ This is an \(inline\) passthrough element with opening and closing inline delimi
                "passthrough.passthroughContext|p1.md|46:12|2|",
        )
 }
+
+func TestRenderHooksPositionRenderString(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+-- assets/a.txt --
+
+## Heading
+
+[link](b.txt)
+
+-- assets/b.txt --
+{{% myshortcode %}}
+{{< myshortcode >}}
+
+
+
+  [link](a.txt)
+-- layouts/shortcodes/myshortcode.html --
+My Shortcode.
+# This is a heading in the shortcode.
+Some text.
+-- layouts/_markup/render-link.html --
+{{ $pos := .Position }}
+{{ printf "%T" . }}|{{ path.Join $pos.Filename }}|{{ printf "%d:%d" $pos.LineNumber $pos.ColumnNumber }}|{{ $.Ordinal }}|
+-- layouts/all.html --
+{{ $a := resources.Get "a.txt" }}
+a: {{ .RenderString $a.Content }}
+b: {{ .RenderString (resources.Get "b.txt").Content }}
+-- content/p1.md --
+
+
+`
+
+       b := hugolib.Test(t, files)
+       b.AssertFileContent("public/p1/index.html",
+               "/content/p1.md (rendered from string)|4:1|0|",
+               "/content/p1.md (rendered from string)|6:3|0|",
+       )
+}