markup/goldmark: Add Position to CodeblockContext

But note that this is not particulary fast and the recommendad usage is error logging only.

Updates #9574
This commit is contained in:
Bjørn Erik Pedersen 2022-02-26 12:52:06 +01:00
parent 2e54c00933
commit 928a896962
7 changed files with 161 additions and 19 deletions

View file

@ -23,6 +23,7 @@ import (
"sync" "sync"
"unicode/utf8" "unicode/utf8"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -430,6 +431,25 @@ func (p *pageContentOutput) initRenderHooks() error {
renderCache := make(map[cacheKey]interface{}) renderCache := make(map[cacheKey]interface{})
var renderCacheMu sync.Mutex var renderCacheMu sync.Mutex
resolvePosition := func(ctx interface{}) text.Position {
var offset int
switch v := ctx.(type) {
case hooks.CodeblockContext:
offset = bytes.Index(p.p.source.parsed.Input(), []byte(v.Code()))
}
pos := p.p.posFromInput(p.p.source.parsed.Input(), 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
}
p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} { p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} {
renderCacheMu.Lock() renderCacheMu.Lock()
defer renderCacheMu.Unlock() defer renderCacheMu.Unlock()
@ -510,6 +530,7 @@ func (p *pageContentOutput) initRenderHooks() error {
templateHandler: p.p.s.Tmpl(), templateHandler: p.p.s.Tmpl(),
SearchProvider: templ.(identity.SearchProvider), SearchProvider: templ.(identity.SearchProvider),
templ: templ, templ: templ,
resolvePosition: resolvePosition,
} }
renderCache[key] = r renderCache[key] = r
return r return r

View file

@ -1778,7 +1778,8 @@ var infoOnMissingLayout = map[string]bool{
type hookRendererTemplate struct { type hookRendererTemplate struct {
templateHandler tpl.TemplateHandler templateHandler tpl.TemplateHandler
identity.SearchProvider identity.SearchProvider
templ tpl.Template templ tpl.Template
resolvePosition func(ctx interface{}) text.Position
} }
func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error { func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
@ -1793,6 +1794,10 @@ func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.Co
return hr.templateHandler.Execute(hr.templ, w, ctx) return hr.templateHandler.Execute(hr.templ, w, ctx)
} }
func (hr hookRendererTemplate) ResolvePosition(ctx interface{}) text.Position {
return hr.resolvePosition(ctx)
}
func (s *Site) renderForTemplate(name, outputFormat string, d interface{}, w io.Writer, templ tpl.Template) (err error) { func (s *Site) renderForTemplate(name, outputFormat string, d interface{}, w io.Writer, templ tpl.Template) (err error) {
if templ == nil { if templ == nil {
s.logMissingLayout(name, "", "", outputFormat) s.logMissingLayout(name, "", "", outputFormat)

View file

@ -128,9 +128,13 @@ type DocumentContext struct {
// RenderContext holds contextual information about the content to render. // RenderContext holds contextual information about the content to render.
type RenderContext struct { type RenderContext struct {
Src []byte // Src is the content to render.
Src []byte
// Whether to render TableOfContents.
RenderTOC bool RenderTOC bool
// GerRenderer provides hook renderers on demand.
GetRenderer hooks.GetRendererFunc GetRenderer hooks.GetRendererFunc
} }

View file

@ -17,6 +17,7 @@ import (
"io" "io"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal/attributes" "github.com/gohugoio/hugo/markup/internal/attributes"
) )
@ -37,6 +38,7 @@ type LinkContext interface {
type CodeblockContext interface { type CodeblockContext interface {
AttributesProvider AttributesProvider
text.Positioner
Options() map[string]interface{} Options() map[string]interface{}
Lang() string Lang() string
Code() string Code() string
@ -59,6 +61,10 @@ type CodeBlockRenderer interface {
identity.Provider identity.Provider
} }
type IsDefaultCodeBlockRendererProvider interface {
IsDefaultCodeBlockRenderer() bool
}
// HeadingContext contains accessors to all attributes that a HeadingRenderer // HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading. // can use to render a heading.
type HeadingContext interface { type HeadingContext interface {
@ -84,6 +90,14 @@ type HeadingRenderer interface {
identity.Provider identity.Provider
} }
// ElementPositionRevolver provides a way to resolve the start Position
// of a markdown element in the original source document.
// This may be both slow and aproximate, so should only be
// used for error logging.
type ElementPositionRevolver interface {
ResolvePosition(ctx interface{}) text.Position
}
type RendererType int type RendererType int
const ( const (

View file

@ -141,3 +141,70 @@ echo "p1";
b.AssertFileContent("public/p1/index.html", "|echo \"p1\";|") b.AssertFileContent("public/p1/index.html", "|echo \"p1\";|")
} }
func TestCodePosition(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- content/p1.md --
---
title: "p1"
---
## Code
§§§
echo "p1";
§§§
-- layouts/_default/single.html --
{{ .Content }}
-- layouts/_default/_markup/render-codeblock.html --
Position: {{ .Position | safeHTML }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()
b.AssertFileContent("public/p1/index.html", "Position: \"content/p1.md:7:1\"")
}
// Issue 9571
func TestOptionsNonChroma(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
-- content/p1.md --
---
title: "p1"
---
## Code
§§§bash {style=monokai}
echo "p1";
§§§
-- layouts/_default/single.html --
{{ .Content }}
-- layouts/_default/_markup/render-codeblock.html --
Style: {{ .Attributes }}|
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()
b.AssertFileContent("public/p1/index.html", "asdfadf")
}

View file

@ -16,7 +16,9 @@ package codeblocks
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"sync"
"github.com/gohugoio/hugo/common/herrors"
htext "github.com/gohugoio/hugo/common/text" htext "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/internal/render" "github.com/gohugoio/hugo/markup/goldmark/internal/render"
@ -59,6 +61,8 @@ func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
} }
func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
defer herrors.Recover()
ctx := w.(*render.Context) ctx := w.(*render.Context)
if entering { if entering {
@ -67,6 +71,11 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
n := node.(*codeBlock) n := node.(*codeBlock)
lang := string(n.b.Language(src)) lang := string(n.b.Language(src))
renderer := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
if renderer == nil {
return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
}
ordinal := n.ordinal ordinal := n.ordinal
var buff bytes.Buffer var buff bytes.Buffer
@ -77,30 +86,37 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
buff.Write(line.Value(src)) buff.Write(line.Value(src))
} }
text := htext.Chomp(buff.String()) s := htext.Chomp(buff.String())
var info []byte var info []byte
if n.b.Info != nil { if n.b.Info != nil {
info = n.b.Info.Segment.Value(src) info = n.b.Info.Segment.Value(src)
} }
attrs := getAttributes(n.b, info) attrs := getAttributes(n.b, info)
cbctx := &codeBlockContext{
v := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang) page: ctx.DocumentContext().Document,
if v == nil { lang: lang,
return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang) code: s,
ordinal: ordinal,
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock),
} }
cr := v.(hooks.CodeBlockRenderer) cbctx.createPos = func() htext.Position {
if resolver, ok := renderer.(hooks.ElementPositionRevolver); ok {
return resolver.ResolvePosition(cbctx)
}
return htext.Position{
Filename: ctx.DocumentContext().Filename,
LineNumber: 0,
ColumnNumber: 0,
}
}
cr := renderer.(hooks.CodeBlockRenderer)
err := cr.RenderCodeblock( err := cr.RenderCodeblock(
w, w,
codeBlockContext{ cbctx,
page: ctx.DocumentContext().Document,
lang: lang,
code: text,
ordinal: ordinal,
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock),
},
) )
ctx.AddIdentity(cr) ctx.AddIdentity(cr)
@ -113,25 +129,39 @@ type codeBlockContext struct {
lang string lang string
code string code string
ordinal int ordinal int
// This is only used in error situations and is expensive to create,
// to deleay creation until needed.
pos htext.Position
posInit sync.Once
createPos func() htext.Position
*attributes.AttributesHolder *attributes.AttributesHolder
} }
func (c codeBlockContext) Page() interface{} { func (c *codeBlockContext) Page() interface{} {
return c.page return c.page
} }
func (c codeBlockContext) Lang() string { func (c *codeBlockContext) Lang() string {
return c.lang return c.lang
} }
func (c codeBlockContext) Code() string { func (c *codeBlockContext) Code() string {
return c.code return c.code
} }
func (c codeBlockContext) Ordinal() int { func (c *codeBlockContext) Ordinal() int {
return c.ordinal return c.ordinal
} }
func (c *codeBlockContext) Position() htext.Position {
c.posInit.Do(func() {
c.pos = c.createPos()
})
return c.pos
}
func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute { func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute {
if node.Attributes() != nil { if node.Attributes() != nil {
return node.Attributes() return node.Attributes()

View file

@ -40,6 +40,7 @@ func (*Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser
} }
codeBlocks = append(codeBlocks, cb) codeBlocks = append(codeBlocks, cb)
return ast.WalkContinue, nil return ast.WalkContinue, nil
}) })