Add render hooks for inline and block passthrough snippets

Fixes #11927
This commit is contained in:
Bjørn Erik Pedersen 2024-08-05 11:00:47 +02:00
parent 1781b18427
commit c6227f1d85
10 changed files with 328 additions and 34 deletions

2
go.mod
View file

@ -40,7 +40,7 @@ require (
github.com/gohugoio/hashstructure v0.1.0 github.com/gohugoio/hashstructure v0.1.0
github.com/gohugoio/httpcache v0.7.0 github.com/gohugoio/httpcache v0.7.0
github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0
github.com/gohugoio/locales v0.14.0 github.com/gohugoio/locales v0.14.0
github.com/gohugoio/localescompressed v1.0.1 github.com/gohugoio/localescompressed v1.0.1
github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95 github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95

2
go.sum
View file

@ -235,6 +235,8 @@ github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAf
github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo= github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM= github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=

View file

@ -476,6 +476,11 @@ func (pco *pageContentOutput) initRenderHooks() error {
layoutDescriptor.Kind = "render-image" layoutDescriptor.Kind = "render-image"
case hooks.HeadingRendererType: case hooks.HeadingRendererType:
layoutDescriptor.Kind = "render-heading" layoutDescriptor.Kind = "render-heading"
case hooks.PassthroughRendererType:
layoutDescriptor.Kind = "render-passthrough"
if id != nil {
layoutDescriptor.KindVariants = id.(string)
}
case hooks.CodeBlockRendererType: case hooks.CodeBlockRendererType:
layoutDescriptor.Kind = "render-codeblock" layoutDescriptor.Kind = "render-codeblock"
if id != nil { if id != nil {

View file

@ -915,6 +915,10 @@ func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.Fle
return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
} }
func (hr hookRendererTemplate) RenderPassthrough(cctx context.Context, w io.Writer, ctx hooks.PassthroughContext) error {
return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
}
func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position { func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
return hr.resolvePosition(ctx) return hr.resolvePosition(ctx)
} }

View file

@ -78,6 +78,19 @@ type CodeblockContext interface {
Ordinal() int Ordinal() int
} }
// PassThroughContext is the context passed to a passthrough render hook.
type PassthroughContext interface {
AttributesProvider
text.Positioner
PageProvider
// Currently one of "inline" or "block".
Type() string
// Zero-based ordinal for all passthrough elements in the document.
Ordinal() int
}
type AttributesOptionsSliceProvider interface { type AttributesOptionsSliceProvider interface {
AttributesSlice() []attributes.Attribute AttributesSlice() []attributes.Attribute
OptionsSlice() []attributes.Attribute OptionsSlice() []attributes.Attribute
@ -91,6 +104,10 @@ type CodeBlockRenderer interface {
RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx CodeblockContext) error RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx CodeblockContext) error
} }
type PassthroughRenderer interface {
RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
}
type IsDefaultCodeBlockRendererProvider interface { type IsDefaultCodeBlockRendererProvider interface {
IsDefaultCodeBlockRenderer() bool IsDefaultCodeBlockRenderer() bool
} }
@ -143,6 +160,7 @@ const (
ImageRendererType ImageRendererType
HeadingRendererType HeadingRendererType
CodeBlockRendererType CodeBlockRendererType
PassthroughRendererType
) )
type GetRendererFunc func(t RendererType, id any) any type GetRendererFunc func(t RendererType, id any) any

View file

@ -101,7 +101,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
attrtp = attributes.AttributesOwnerCodeBlockChroma attrtp = attributes.AttributesOwnerCodeBlockChroma
} }
// IsDefaultCodeBlockRendererProvider
attrs, attrStr, err := getAttributes(n.b, info) attrs, attrStr, err := getAttributes(n.b, info)
if err != nil { if err != nil {
return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr} return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
@ -160,7 +159,7 @@ type codeBlockContext struct {
ordinal int ordinal int
// This is only used in error situations and is expensive to create, // This is only used in error situations and is expensive to create,
// to delay creation until needed. // so delay creation until needed.
pos htext.Position pos htext.Position
posInit sync.Once posInit sync.Once
createPos func() htext.Position createPos func() htext.Position

View file

@ -18,15 +18,14 @@ import (
"bytes" "bytes"
"github.com/gohugoio/hugo-goldmark-extensions/extras" "github.com/gohugoio/hugo-goldmark-extensions/extras"
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/yuin/goldmark/util"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks" "github.com/gohugoio/hugo/markup/goldmark/codeblocks"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/goldmark/images" "github.com/gohugoio/hugo/markup/goldmark/images"
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes" "github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
"github.com/gohugoio/hugo/markup/goldmark/internal/render" "github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/gohugoio/hugo/markup/goldmark/passthrough"
"github.com/yuin/goldmark/util"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji" emoji "github.com/yuin/goldmark-emoji"
@ -177,32 +176,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
} }
if cfg.Extensions.Passthrough.Enable { if cfg.Extensions.Passthrough.Enable {
configuredInlines := cfg.Extensions.Passthrough.Delimiters.Inline extensions = append(extensions, passthrough.New(cfg.Extensions.Passthrough))
configuredBlocks := cfg.Extensions.Passthrough.Delimiters.Block
inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
for i, d := range configuredInlines {
inlineDelimiters[i] = passthrough.Delimiters{
Open: d[0],
Close: d[1],
}
}
for i, d := range configuredBlocks {
blockDelimiters[i] = passthrough.Delimiters{
Open: d[0],
Close: d[1],
}
}
extensions = append(extensions, passthrough.New(
passthrough.Config{
InlineDelimiters: inlineDelimiters,
BlockDelimiters: blockDelimiters,
},
))
} }
if pcfg.Conf.EnableEmoji() { if pcfg.Conf.EnableEmoji() {

View file

@ -18,6 +18,7 @@ import (
"math/bits" "math/bits"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/yuin/goldmark/ast"
) )
type BufWriter struct { type BufWriter struct {
@ -40,9 +41,19 @@ func (b *BufWriter) Flush() error {
type Context struct { type Context struct {
*BufWriter *BufWriter
ContextData
positions []int positions []int
pids []uint64 pids []uint64
ContextData ordinals map[ast.NodeKind]int
}
func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
if ctx.ordinals == nil {
ctx.ordinals = make(map[ast.NodeKind]int)
}
i := ctx.ordinals[kind]
ctx.ordinals[kind]++
return i
} }
func (ctx *Context) PushPos(n int) { func (ctx *Context) PushPos(n int) {

View file

@ -0,0 +1,219 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package passthrough
import (
"bytes"
"sync"
htext "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/gohugoio/hugo/markup/internal/attributes"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
func New(cfg goldmark_config.Passthrough) goldmark.Extender {
if !cfg.Enable {
return nil
}
return &passthroughExtension{cfg: cfg}
}
type (
passthroughExtension struct {
cfg goldmark_config.Passthrough
}
htmlRenderer struct{}
)
func (e *passthroughExtension) Extend(m goldmark.Markdown) {
configuredInlines := e.cfg.Delimiters.Inline
configuredBlocks := e.cfg.Delimiters.Block
inlineDelimiters := make([]passthrough.Delimiters, len(configuredInlines))
blockDelimiters := make([]passthrough.Delimiters, len(configuredBlocks))
for i, d := range configuredInlines {
inlineDelimiters[i] = passthrough.Delimiters{
Open: d[0],
Close: d[1],
}
}
for i, d := range configuredBlocks {
blockDelimiters[i] = passthrough.Delimiters{
Open: d[0],
Close: d[1],
}
}
pse := passthrough.New(
passthrough.Config{
InlineDelimiters: inlineDelimiters,
BlockDelimiters: blockDelimiters,
},
)
pse.Extend(m)
// Set up render hooks if configured.
// Upstream passthrough inline = 101
// Upstream passthrough block = 99
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(newHTMLRenderer(), 90),
))
}
func newHTMLRenderer() renderer.NodeRenderer {
r := &htmlRenderer{}
return r
}
func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(passthrough.KindPassthroughBlock, r.renderPassthroughBlock)
reg.Register(passthrough.KindPassthroughInline, r.renderPassthroughBlock)
}
func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
ctx := w.(*render.Context)
if entering {
return ast.WalkContinue, nil
}
var (
s string
typ string
delims *passthrough.Delimiters
)
switch nn := node.(type) {
case *passthrough.PassthroughInline:
s = string(nn.Text(src))
typ = "inline"
delims = nn.Delimiters
case (*passthrough.PassthroughBlock):
l := nn.Lines().Len()
var buff bytes.Buffer
for i := 0; i < l; i++ {
line := nn.Lines().At(i)
buff.Write(line.Value(src))
}
s = buff.String()
typ = "block"
delims = nn.Delimiters
}
renderer := ctx.RenderContext().GetRenderer(hooks.PassthroughRendererType, typ)
if renderer == nil {
// Write the raw content if no renderer is found.
ctx.WriteString(s)
return ast.WalkContinue, nil
}
// Inline and block passthroughs share the same ordinal counter.
ordinal := ctx.GetAndIncrementOrdinal(passthrough.KindPassthroughBlock)
// Trim the delimiters.
s = s[len(delims.Open) : len(s)-len(delims.Close)]
pctx := &passthroughContext{
ordinal: ordinal,
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
inner: s,
typ: typ,
AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral),
}
pctx.createPos = func() htext.Position {
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
return resolver.ResolvePosition(pctx)
}
return htext.Position{
Filename: ctx.DocumentContext().Filename,
LineNumber: 1,
ColumnNumber: 1,
}
}
pr := renderer.(hooks.PassthroughRenderer)
if err := pr.RenderPassthrough(ctx.RenderContext().Ctx, w, pctx); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
pid := rctx.PeekPid()
if pid > 0 {
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
return v
}
}
}
return rctx.DocumentContext().Document
}
type passthroughContext struct {
page any
pageInner any
typ string // inner or block
inner string
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
*attributes.AttributesHolder
}
func (p *passthroughContext) Page() any {
return p.page
}
func (p *passthroughContext) PageInner() any {
return p.pageInner
}
func (p *passthroughContext) Type() string {
return p.typ
}
func (p *passthroughContext) Inner() string {
return p.inner
}
func (p *passthroughContext) Ordinal() int {
return p.ordinal
}
func (p *passthroughContext) Position() htext.Position {
p.posInit.Do(func() {
p.pos = p.createPos()
})
return p.pos
}

View file

@ -0,0 +1,62 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package passthrough_test
import (
"testing"
"github.com/gohugoio/hugo/hugolib"
)
func TestPassthroughRenderHook(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
[markup.goldmark.extensions.passthrough]
enable = true
[markup.goldmark.extensions.passthrough.delimiters]
block = [['$$', '$$']]
inline = [['$', '$']]
-- content/p1.md --
---
title: "p1"
---
## LaTeX test
Some inline LaTeX 1: $a^*=x-b^*$.
Block equation that would be mangled by default parser:
$$a^*=x-b^*$$
Some inline LaTeX 2: $a^*=x-b^*$.
-- layouts/_default/single.html --
{{ .Content }}
-- layouts/_default/_markup/render-passthrough-block.html --
Passthrough block: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
-- layouts/_default/_markup/render-passthrough-inline.html --
Passthrough inline: {{ .Inner | safeHTML }}|{{ .Type }}|{{ .Ordinal }}:END
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", `
Some inline LaTeX 1: Passthrough inline: a^*=x-b^*|inline|0:END
Passthrough block: a^*=x-b^*|block|1:END
Some inline LaTeX 2: Passthrough inline: a^*=x-b^*|inline|2:END
`)
}