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:
Bjørn Erik Pedersen
2026-03-25 18:28:19 +01:00
parent beb57a6839
commit 303e443ea7
16 changed files with 259 additions and 168 deletions
+3 -3
View File
@@ -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
+8
View File
@@ -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=
+32 -5
View 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
}
+15 -15
View 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()
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
switch v := ctx.(type) {
case hooks.PositionerSourceTargetProvider:
offset = bytes.Index(source, v.PositionerSourceTarget())
if sm := pco.sourceMap; len(sm) > 0 {
offset = resolveSourceOffset(sm, pos)
} else {
offset = pos + pco.po.p.m.content.pi.posMainContent
}
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
}
return pos
return pco.po.p.posFromInput(sourceOrig, offset)
}
pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
+3 -3
View 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 {
+5 -6
View 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
+7 -7
View 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
}
@@ -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")
}
+4 -4
View 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
}
+6 -69
View 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.
+4 -4
View 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
}
+9 -41
View 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),
+4 -4
View 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
}
+127
View 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|",
)
}
+2 -2
View 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) {