contentPlaceholders map[string]shortcodeRenderer
contentToRender []byte
+
+ sourceInfo
+}
+
+type sourceInfo struct {
+ // Optional override for the filename used in position reporting.
+ filename string
+ // The original source bytes.
+ source []byte
+ // Maps positions in the content sent to Goldmark back to the original source.
+ sourceMap []sourceMapEntry
}
type contentSummary struct {
if ct.astDoc != nil {
// The content is parsed, but not rendered.
- r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
+ r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.sourceInfo, ct.astDoc)
if err != nil {
return nil, err
}
versionv := c.version(cp)
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
- source, err := c.pi.contentSource(c)
+ var err error
+ var ct contentTableOfContents
+ ct.source, err = c.pi.contentSource(c)
if err != nil {
return nil, err
}
- var ct contentTableOfContents
if err := cp.initRenderHooks(); err != nil {
return nil, err
}
ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback)
var hasVariants bool
- ct.contentToRender, cp.sourceMap, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders)
+ ct.contentToRender, ct.sourceMap, hasVariants, err = c.pi.contentToRender(ctx, ct.source, ct.contentPlaceholders)
if err != nil {
return nil, err
}
}
if pageparser.HasShortcode(contentToRender) {
- contentToRenderb := []byte(contentToRender)
+ ct := contentTableOfContents{
+ sourceInfo: sourceInfo{
+ filename: pco.po.p.pathOrTitle() + " (rendered from string)",
+ source: []byte(contentToRender),
+ },
+ }
+ ct.contentToRender = ct.source
// String contains a shortcode.
- parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
+ parseInfo.itemsStep1, err = pageparser.ParseBytes(ct.source, pageparser.Config{
NoFrontMatter: true,
NoSummaryDivider: true,
})
}
parseInfo.shortcodeParseInfo = newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s.h.Deps)
- if err := parseInfo.parseSource(contentToRenderb, true); err != nil {
+ if err := parseInfo.parseSource(ct.source, true); err != nil {
return "", err
}
return "", err
}
- contentToRender, _, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
+ var hasVariants bool
+ ct.contentToRender, ct.sourceMap, hasVariants, err = parseInfo.contentToRender(ctx, ct.source, placeholders)
if err != nil {
return "", err
}
if hasVariants {
pco.po.p.incrPageOutputTemplateVariation()
}
- b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
+ b, err := pco.renderContentWithConverter(ctx, conv, ct.contentToRender, ct.sourceInfo, false)
if err != nil {
return "", pco.po.p.wrapError(err)
}
pco.po.p.m.content.hasShortcode.Store(&combined)
} else {
- c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
+ si := sourceInfo{
+ filename: pco.po.p.pathOrTitle() + " (rendered from string)",
+ source: []byte(contentToRender),
+ }
+ c, err := pco.renderContentWithConverter(ctx, conv, si.source, si, false)
if err != nil {
return "", pco.po.p.wrapError(err)
}
// 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(_ any, _ []byte, pos int) text.Position {
- if pos == -1 {
+ resolvePosition := func(renderContext any, pos int) text.Position {
+ rc, ok := renderContext.(converter.RenderContext)
+ var si sourceInfo
+ if ok {
+ si, ok = rc.SourceInfo.(sourceInfo)
+ }
+
+ if pos == -1 || !ok {
return text.Position{
Filename: pco.po.p.pathOrTitle(),
}
}
- sourceOrig := pco.po.p.m.content.mustSource()
- var offset int
-
- if sm := pco.sourceMap; len(sm) > 0 {
- offset = resolveSourceOffset(sm, pos)
- } else {
- offset = pos + pco.po.p.m.content.pi.posMainContent
+ offset := resolveSourceOffset(si.sourceMap, pos)
+ filename := si.filename
+ if filename == "" {
+ filename = pco.po.p.pathOrTitle()
}
- return pco.po.p.posFromInput(sourceOrig, offset)
+ return posFromInput(filename, si.source, offset)
}
pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any {
if err != nil {
return nil, err
}
- return cp.renderContentWithConverter(ctx, c, content, renderTOC)
+ si := sourceInfo{
+ source: content,
+ }
+ return cp.renderContentWithConverter(ctx, c, content, si, renderTOC)
}
func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) {
return r, ok, err
}
-func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
+func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
c, err := pco.getContentConverter()
if err != nil {
return nil, false, err
rctx := converter.RenderContext{
Ctx: ctx,
Src: content,
+ SourceInfo: sourceInfo,
RenderTOC: true,
GetRenderer: pco.renderHooks.getRenderer,
}
return r, ok, err
}
-func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) {
+func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, sourceInfo any, renderTOC bool) (converter.ResultRender, error) {
r, err := c.Convert(
converter.RenderContext{
Ctx: ctx,
Src: content,
+ SourceInfo: sourceInfo,
RenderTOC: renderTOC,
GetRenderer: pco.renderHooks.getRenderer,
})
}
func posFromInput(filename string, input []byte, offset int) text.Position {
- if offset < 0 {
+ if offset < 0 || offset > len(input) {
return text.Position{
Filename: filename,
}
type hookRendererTemplate struct {
templateHandler *tplimpl.TemplateStore
templ *tplimpl.TemplInfo
- resolvePosition func(ctx any, srcRender []byte, pos int) text.Position
+ resolvePosition func(renderContext any, 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, srcRender []byte, pos int) text.Position {
- return hr.resolvePosition(ctx, srcRender, pos)
+func (hr hookRendererTemplate) ResolvePosition(renderContext any, pos int) text.Position {
+ return hr.resolvePosition(renderContext, pos)
}
func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool {
// Src is the content to render.
Src []byte
+ // SourceInfo holds optional information about the source of the content to render.
+ SourceInfo any
+
// Whether to render TableOfContents.
RenderTOC bool
// This may be both slow and approximate, so should only be
// used for error logging.
type ElementPositionResolver interface {
- // 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
+ // ResolvePosition returns the position of the element in the original source document, -1 if it's not defined.
+ ResolvePosition(renderContext any, srcPos int) text.Position
}
type RendererType int
b.createPos = func() htext.Position {
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
- return resolver.ResolvePosition(b, src, n.Pos())
+ return resolver.ResolvePosition(rctx.RenderContext(), n.Pos())
}
return htext.Position{
// For internal use only.
ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error)
// For internal use only.
- RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error)
+ RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error)
}
// FileProvider provides the source file.
return lcp.init.Value(ctx).ParseContent(ctx, content)
}
-func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
- return lcp.init.Value(ctx).RenderContent(ctx, content, doc)
+func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
+ return lcp.init.Value(ctx).RenderContent(ctx, content, sourceInfo, doc)
}
return nil, false, nil
}
-func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
+func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, sourceInfo, doc any) (converter.ResultRender, bool, error) {
return nil, false, nil
}
"passthrough.passthroughContext|p1.md|46:12|2|",
)
}
+
+func TestRenderHooksPositionRenderString(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- assets/a.txt --
+
+## Heading
+
+[link](b.txt)
+
+-- assets/b.txt --
+{{% myshortcode %}}
+{{< myshortcode >}}
+
+
+
+ [link](a.txt)
+-- layouts/shortcodes/myshortcode.html --
+My Shortcode.
+# This is a heading in the shortcode.
+Some text.
+-- layouts/_markup/render-link.html --
+{{ $pos := .Position }}
+{{ printf "%T" . }}|{{ path.Join $pos.Filename }}|{{ printf "%d:%d" $pos.LineNumber $pos.ColumnNumber }}|{{ $.Ordinal }}|
+-- layouts/all.html --
+{{ $a := resources.Get "a.txt" }}
+a: {{ .RenderString $a.Content }}
+b: {{ .RenderString (resources.Get "b.txt").Content }}
+-- content/p1.md --
+
+
+`
+
+ b := hugolib.Test(t, files)
+ b.AssertFileContent("public/p1/index.html",
+ "/content/p1.md (rendered from string)|4:1|0|",
+ "/content/p1.md (rendered from string)|6:3|0|",
+ )
+}