Fix some recently introduced Position issues

Fixes #14710
This commit is contained in:
Bjørn Erik Pedersen
2026-04-03 18:24:00 +02:00
parent dc9b51d2e2
commit 4e91e14cb0
11 changed files with 109 additions and 40 deletions
+33 -10
View File
@@ -457,6 +457,17 @@ type contentTableOfContents struct {
contentPlaceholders map[string]shortcodeRenderer contentPlaceholders map[string]shortcodeRenderer
contentToRender []byte 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 { type contentSummary struct {
@@ -515,7 +526,7 @@ func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummar
if ct.astDoc != nil { if ct.astDoc != nil {
// The content is parsed, but not rendered. // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -638,12 +649,13 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
versionv := c.version(cp) versionv := c.version(cp)
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { 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 { if err != nil {
return nil, err return nil, err
} }
var ct contentTableOfContents
if err := cp.initRenderHooks(); err != nil { if err := cp.initRenderHooks(); err != nil {
return nil, err return nil, err
} }
@@ -678,7 +690,7 @@ func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfCont
ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
var hasVariants bool 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 { if err != nil {
return nil, err return nil, err
} }
@@ -926,9 +938,15 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
} }
if pageparser.HasShortcode(contentToRender) { 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. // String contains a shortcode.
parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{ parseInfo.itemsStep1, err = pageparser.ParseBytes(ct.source, pageparser.Config{
NoFrontMatter: true, NoFrontMatter: true,
NoSummaryDivider: 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) 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 return "", err
} }
@@ -946,14 +964,15 @@ func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (tem
return "", err 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 { if err != nil {
return "", err return "", err
} }
if hasVariants { if hasVariants {
pco.po.p.incrPageOutputTemplateVariation() 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 { if err != nil {
return "", pco.po.p.wrapError(err) 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) pco.po.p.m.content.hasShortcode.Store(&combined)
} else { } 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 { if err != nil {
return "", pco.po.p.wrapError(err) return "", pco.po.p.wrapError(err)
} }
+21 -16
View File
@@ -89,9 +89,6 @@ type pageContentOutput struct {
// Renders Markdown hooks. // Renders Markdown hooks.
renderHooks *renderHooks renderHooks *renderHooks
// Maps positions in the content sent to Goldmark back to the original source.
sourceMap []sourceMapEntry
} }
func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) { func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) {
@@ -246,22 +243,25 @@ func (pco *pageContentOutput) initRenderHooks() error {
renderCache := make(map[cacheKey]any) renderCache := make(map[cacheKey]any)
var renderCacheMu sync.Mutex var renderCacheMu sync.Mutex
resolvePosition := func(_ any, _ []byte, pos int) text.Position { resolvePosition := func(renderContext any, pos int) text.Position {
if pos == -1 { rc, ok := renderContext.(converter.RenderContext)
var si sourceInfo
if ok {
si, ok = rc.SourceInfo.(sourceInfo)
}
if pos == -1 || !ok {
return text.Position{ return text.Position{
Filename: pco.po.p.pathOrTitle(), Filename: pco.po.p.pathOrTitle(),
} }
} }
sourceOrig := pco.po.p.m.content.mustSource() offset := resolveSourceOffset(si.sourceMap, pos)
var offset int filename := si.filename
if filename == "" {
if sm := pco.sourceMap; len(sm) > 0 { filename = pco.po.p.pathOrTitle()
offset = resolveSourceOffset(sm, pos)
} else {
offset = pos + pco.po.p.m.content.pi.posMainContent
} }
return pco.po.p.posFromInput(sourceOrig, offset) return posFromInput(filename, si.source, offset)
} }
pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any { 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 { if err != nil {
return nil, err 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) { 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 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() c, err := pco.getContentConverter()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -445,6 +448,7 @@ func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte,
rctx := converter.RenderContext{ rctx := converter.RenderContext{
Ctx: ctx, Ctx: ctx,
Src: content, Src: content,
SourceInfo: sourceInfo,
RenderTOC: true, RenderTOC: true,
GetRenderer: pco.renderHooks.getRenderer, GetRenderer: pco.renderHooks.getRenderer,
} }
@@ -452,11 +456,12 @@ func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte,
return r, ok, err 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( r, err := c.Convert(
converter.RenderContext{ converter.RenderContext{
Ctx: ctx, Ctx: ctx,
Src: content, Src: content,
SourceInfo: sourceInfo,
RenderTOC: renderTOC, RenderTOC: renderTOC,
GetRenderer: pco.renderHooks.getRenderer, GetRenderer: pco.renderHooks.getRenderer,
}) })
+1 -1
View File
@@ -553,7 +553,7 @@ func (s *shortcodeParseInfo) prepareShortcodesForPage(po *pageOutput, isRenderSt
} }
func posFromInput(filename string, input []byte, offset int) text.Position { func posFromInput(filename string, input []byte, offset int) text.Position {
if offset < 0 { if offset < 0 || offset > len(input) {
return text.Position{ return text.Position{
Filename: filename, Filename: filename,
} }
+3 -3
View File
@@ -1682,7 +1682,7 @@ var infoOnMissingLayout = map[string]bool{
type hookRendererTemplate struct { type hookRendererTemplate struct {
templateHandler *tplimpl.TemplateStore templateHandler *tplimpl.TemplateStore
templ *tplimpl.TemplInfo 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 { 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) return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
} }
func (hr hookRendererTemplate) ResolvePosition(ctx any, srcRender []byte, pos int) text.Position { func (hr hookRendererTemplate) ResolvePosition(renderContext any, pos int) text.Position {
return hr.resolvePosition(ctx, srcRender, pos) return hr.resolvePosition(renderContext, pos)
} }
func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool { func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
+3
View File
@@ -150,6 +150,9 @@ type RenderContext struct {
// Src is the content to render. // Src is the content to render.
Src []byte Src []byte
// SourceInfo holds optional information about the source of the content to render.
SourceInfo any
// Whether to render TableOfContents. // Whether to render TableOfContents.
RenderTOC bool RenderTOC bool
+2 -5
View File
@@ -200,11 +200,8 @@ type HeadingRenderer interface {
// This may be both slow and approximate, so should only be // This may be both slow and approximate, so should only be
// used for error logging. // used for error logging.
type ElementPositionResolver interface { type ElementPositionResolver interface {
// ResolvePosition returns the position of the element in the original source document. // ResolvePosition returns the position of the element in the original source document, -1 if it's not defined.
// ctx is the context passed to the render hook, and srcPos is the zero-based byte offset of the element ResolvePosition(renderContext any, srcPos int) text.Position
// in srcRender (the rendered source buffer),
// -1 if it's not defined.
ResolvePosition(ctx any, srcRender []byte, srcPos int) text.Position
} }
type RendererType int type RendererType int
+1 -1
View File
@@ -184,7 +184,7 @@ func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, ordinal
b.createPos = func() htext.Position { b.createPos = func() htext.Position {
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok { if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
return resolver.ResolvePosition(b, src, n.Pos()) return resolver.ResolvePosition(rctx.RenderContext(), n.Pos())
} }
return htext.Position{ return htext.Position{
+1 -1
View File
@@ -114,7 +114,7 @@ type ContentRenderer interface {
// For internal use only. // For internal use only.
ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error)
// For internal use only. // 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. // FileProvider provides the source file.
+2 -2
View File
@@ -133,6 +133,6 @@ func (lcp *LazyContentProvider) ParseContent(ctx context.Context, content []byte
return lcp.init.Value(ctx).ParseContent(ctx, content) return lcp.init.Value(ctx).ParseContent(ctx, content)
} }
func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) { func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
return lcp.init.Value(ctx).RenderContent(ctx, content, doc) return lcp.init.Value(ctx).RenderContent(ctx, content, sourceInfo, doc)
} }
+1 -1
View File
@@ -522,7 +522,7 @@ func (r *nopContentRenderer) ParseContent(ctx context.Context, content []byte) (
return nil, false, nil 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 return nil, false, nil
} }
@@ -466,3 +466,44 @@ This is an \(inline\) passthrough element with opening and closing inline delimi
"passthrough.passthroughContext|p1.md|46:12|2|", "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|",
)
}