mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ title: "p1"
|
||||
"Blockquote Alert: |<p>This is a caution with some whitespace before the alert type.</p>|alert|",
|
||||
"Blockquote: |<p>A regular blockquote.</p>\n|regular|",
|
||||
"Blockquote Alert Attributes: |<p>This is a tip with attributes.</p>|map[class:foo bar id:baz]|",
|
||||
filepath.FromSlash("/content/p1.md:19:3"),
|
||||
filepath.FromSlash("/content/p1.md:20:1"),
|
||||
"Blockquote Alert Page: |<p>This is a tip with attributes.</p>|p1|p1|",
|
||||
|
||||
// Issue 12767.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user