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>
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
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
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=
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=
"fmt"
"html/template"
"io"
+ "sort"
"strings"
"sync/atomic"
"unicode/utf8"
}
}
+// 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]
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)...)
}
}
- return c, hasVariants, nil
+ return c, sm, hasVariants, nil
}
func (c *cachedContent) IsZero() bool {
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
}
return "", err
}
- contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
+ contentToRender, _, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
if err != nil {
return "", err
}
package hugolib
import (
- "bytes"
"context"
"errors"
"fmt"
// 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) {
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 {
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 {
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 {
AlertSign() string
}
-type PositionerSourceTargetProvider interface {
- // For internal use.
- PositionerSourceTarget() []byte
-}
-
// PassThroughContext is the context passed to a passthrough render hook.
type PassthroughContext interface {
BaseContext
// 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
}
}
- 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),
*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
}
"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.
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",
)
}
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")
+}
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),
*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
}
"strings"
"sync"
- "github.com/gohugoio/hugo-goldmark-extensions/passthrough"
bp "github.com/gohugoio/hugo/bufferpool"
east "github.com/yuin/goldmark-emoji/ast"
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
}
// 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{
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 {
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.
// 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),
*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
}
}
type linkContext struct {
- page any
- pageInner any
+ hooks.BaseContext
destination string
title string
text hstring.HTML
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
}
type imageLinkContext struct {
linkContext
- ordinal int
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
*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
}
// 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,
},
)
}
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),
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,
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),
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,
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
}
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|",
+ )
+}
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) {