diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 29beb672e55..07ab65390b0 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -23,6 +23,7 @@ import ( "sync" "unicode/utf8" + "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/identity" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -430,6 +431,25 @@ func (p *pageContentOutput) initRenderHooks() error { renderCache := make(map[cacheKey]interface{}) 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{} { renderCacheMu.Lock() defer renderCacheMu.Unlock() @@ -510,6 +530,7 @@ func (p *pageContentOutput) initRenderHooks() error { templateHandler: p.p.s.Tmpl(), SearchProvider: templ.(identity.SearchProvider), templ: templ, + resolvePosition: resolvePosition, } renderCache[key] = r return r @@ -551,6 +572,7 @@ func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, c r, err := c.Convert( converter.RenderContext{ Src: content, + SrcOffset: cp.p.source.posMainContent, RenderTOC: renderTOC, GetRenderer: cp.renderHooks.getRenderer, }) diff --git a/hugolib/site.go b/hugolib/site.go index d78a4e10c9c..ebda29f463b 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1778,7 +1778,8 @@ var infoOnMissingLayout = map[string]bool{ type hookRendererTemplate struct { templateHandler tpl.TemplateHandler 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 { @@ -1793,6 +1794,10 @@ func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.Co 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) { if templ == nil { s.logMissingLayout(name, "", "", outputFormat) diff --git a/markup/converter/converter.go b/markup/converter/converter.go index 30addfec657..72de2783687 100644 --- a/markup/converter/converter.go +++ b/markup/converter/converter.go @@ -128,9 +128,17 @@ type DocumentContext struct { // RenderContext holds contextual information about the content to render. type RenderContext struct { - Src []byte + // Src is the content to render. + Src []byte + + // SrcOffset is the offset in bytes for the source content in the original document. + // This is only used in error messages to get accurate line numbers. + SrcOffset int + + // Whether to render TableOfContents. RenderTOC bool + // GerRenderer provides hook renderers on demand. GetRenderer hooks.GetRendererFunc } diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go index 987cb1dc36b..a570113ff34 100644 --- a/markup/converter/hooks/hooks.go +++ b/markup/converter/hooks/hooks.go @@ -17,6 +17,7 @@ import ( "io" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal/attributes" ) @@ -37,6 +38,7 @@ type LinkContext interface { type CodeblockContext interface { AttributesProvider + text.Positioner Options() map[string]interface{} Lang() string Code() string @@ -59,6 +61,10 @@ type CodeBlockRenderer interface { identity.Provider } +type IsDefaultCodeBlockRendererProvider interface { + IsDefaultCodeBlockRenderer() bool +} + // HeadingContext contains accessors to all attributes that a HeadingRenderer // can use to render a heading. type HeadingContext interface { @@ -84,6 +90,14 @@ type HeadingRenderer interface { 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 const ( diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go index 01510fc8103..68f6b809e2d 100644 --- a/markup/goldmark/codeblocks/integration_test.go +++ b/markup/goldmark/codeblocks/integration_test.go @@ -141,3 +141,70 @@ 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") +} diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go index 6cc43128be9..bbf15bef30a 100644 --- a/markup/goldmark/codeblocks/render.go +++ b/markup/goldmark/codeblocks/render.go @@ -16,7 +16,9 @@ package codeblocks import ( "bytes" "fmt" + "sync" + "github.com/gohugoio/hugo/common/herrors" htext "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/markup/converter/hooks" "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) { + defer herrors.Recover() + ctx := w.(*render.Context) if entering { @@ -67,6 +71,11 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No n := node.(*codeBlock) 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 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)) } - text := htext.Chomp(buff.String()) + s := htext.Chomp(buff.String()) var info []byte if n.b.Info != nil { info = n.b.Info.Segment.Value(src) } attrs := getAttributes(n.b, info) + cbctx := &codeBlockContext{ + page: ctx.DocumentContext().Document, + lang: lang, + code: s, + ordinal: ordinal, + AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock), + } - v := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang) - if v == nil { - return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang) + 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 := v.(hooks.CodeBlockRenderer) + cr := renderer.(hooks.CodeBlockRenderer) err := cr.RenderCodeblock( w, - codeBlockContext{ - page: ctx.DocumentContext().Document, - lang: lang, - code: text, - ordinal: ordinal, - AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock), - }, + cbctx, ) ctx.AddIdentity(cr) @@ -113,25 +129,39 @@ type codeBlockContext struct { lang string code string 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 } -func (c codeBlockContext) Page() interface{} { +func (c *codeBlockContext) Page() interface{} { return c.page } -func (c codeBlockContext) Lang() string { +func (c *codeBlockContext) Lang() string { return c.lang } -func (c codeBlockContext) Code() string { +func (c *codeBlockContext) Code() string { return c.code } -func (c codeBlockContext) Ordinal() int { +func (c *codeBlockContext) Ordinal() int { 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 { if node.Attributes() != nil { return node.Attributes() diff --git a/markup/goldmark/codeblocks/transform.go b/markup/goldmark/codeblocks/transform.go index 791e99a5c3c..be5334b5f90 100644 --- a/markup/goldmark/codeblocks/transform.go +++ b/markup/goldmark/codeblocks/transform.go @@ -40,6 +40,7 @@ func (*Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser } codeBlocks = append(codeBlocks, cb) + return ast.WalkContinue, nil })