]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Improve and extend .Position handling in Goldmark render hooks
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 25 Mar 2026 17:28:19 +0000 (18:28 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 2 Apr 2026 17:30:28 +0000 (19:30 +0200)
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>
16 files changed:
go.mod
go.sum
hugolib/page__content.go
hugolib/page__per_output.go
hugolib/site.go
markup/converter/hooks/hooks.go
markup/goldmark/blockquotes/blockquotes.go
markup/goldmark/blockquotes/blockquotes_integration_test.go
markup/goldmark/codeblocks/codeblocks_integration_test.go
markup/goldmark/codeblocks/render.go
markup/goldmark/internal/render/context.go
markup/goldmark/passthrough/passthrough.go
markup/goldmark/render_hooks.go
markup/goldmark/tables/tables.go
tpl/tplimpl/render_hook_integration_test.go
tpl/transform/transform_integration_test.go

diff --git a/go.mod b/go.mod
index 482388e3a496dab1429952dfd04dc71b55a78834..7fcd915cf4d89897591fa849611a8c33cc2c89c1 100644 (file)
--- 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 f9a0d2fd805c1b57f9a635b789d28b7ea1b60f6f..5cf47629692afc4f42f56854b28b08b1b23d52f9 100644 (file)
--- 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=
index 0cca67bdb385a639697ffc1f1aae406fd733adc0..63c9730508342c3f51ab5a1f9da494d0bad2fc36 100644 (file)
@@ -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
                }
index 3d0cb5ff78c79252893bc749ab24fdf25a6376e1..0b889eeb96f9774b4f58d6e3e25de3f5e7573f1b 100644 (file)
@@ -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 {
index 8e0b2888feeb7c327717a4c962dbcf71f39cf143..9c7c51fd1cae7c15c9bb0187b6120a7164191f23 100644 (file)
@@ -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 {
index 1fc513acb23d1eeb0f05f1fdb7a722469fda24e0..411d16f0bf4b6228ef437d33f4977aa485305005 100644 (file)
@@ -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
index c82b9f8f4b599bec7552b687cbf44914cf6e2a98..e80c05a519af7fdff79079ab676d619ce344062e 100644 (file)
@@ -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
 }
 
index 046ccaac4e5c168a4e2fdff5dbe12e908afa30c9..93e3bc84434c774b667da7eeed5f357bad1ef193 100644 (file)
@@ -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.
index d2f2e0ddaca62395143ccd6a1cd6768679e1c3cb..1eb88d34d2f886ee7eb06aab3f5df36a4f595993 100644 (file)
@@ -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")
+}
index c29632b909b968355cbe54553768f438da68fab8..c218410e961e41ef72dc5dc2e59d92166000886b 100644 (file)
@@ -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
 }
 
index fd8e788ede42229c9edfb44676049bc8f42d2a6c..e95d44bbaa6c63fbc4caf47406a1d18fc836def3 100644 (file)
@@ -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.
index c56842f3d755f4712e15feb7a95c8e74ee2f9789..c38f3eac09ac0cf5cc280db2c3a42cea26c29d48 100644 (file)
@@ -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
 }
index 4183580544d92c630c592387eb38f079eff2b6e8..376c7dae46ca463a310885613db9eac393ac93e1 100644 (file)
@@ -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),
index a7dfb8ee6ed598dc90047c8171afd18e84692b4d..df75a0b9b68264768552f185407a044881da9e01 100644 (file)
@@ -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
 }
index c27dbaccee9c3b97fdec818038b26ef212456a55..0c7d85591a49a2ffb3d42cbb9423f868880594c9 100644 (file)
@@ -339,3 +339,130 @@ TR-IMAGE
        b.AssertFileContent("public/p1/index.html", "<img src=\"img.jpg\" alt=\"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|",
+       )
+}
index 3cc96f20ab219ec7368e8492eac3f3548dfd3063..6347452cf203b70818e14767f8e128fc6c7b61dc 100644 (file)
@@ -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) {