aboutsummaryrefslogtreecommitdiffhomepage
path: root/markup
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-08-05 11:00:47 +0200
committerBjørn Erik Pedersen <[email protected]>2024-08-07 18:28:23 +0200
commitc6227f1d8597f053986c13eac131ae5122a68444 (patch)
treecba7d8960a2b5b52adb1d0e361212e3beb7a5877 /markup
parent1781b18427134a7421ffba361e153c57b3ce2b2f (diff)
downloadhugo-c6227f1d8597f053986c13eac131ae5122a68444.tar.gz
hugo-c6227f1d8597f053986c13eac131ae5122a68444.zip
Add render hooks for inline and block passthrough snippets
Fixes #11927
Diffstat (limited to 'markup')
-rw-r--r--markup/converter/hooks/hooks.go18
-rw-r--r--markup/goldmark/codeblocks/render.go3
-rw-r--r--markup/goldmark/convert.go34
-rw-r--r--markup/goldmark/internal/render/context.go13
-rw-r--r--markup/goldmark/passthrough/passthrough.go219
-rw-r--r--markup/goldmark/passthrough/passthrough_integration_test.go62
6 files changed, 316 insertions, 33 deletions
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 54babd320..1e335fa46 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -78,6 +78,19 @@ type CodeblockContext interface {
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 {
AttributesSlice() []attributes.Attribute
OptionsSlice() []attributes.Attribute
@@ -91,6 +104,10 @@ type CodeBlockRenderer interface {
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 {
IsDefaultCodeBlockRenderer() bool
}
@@ -143,6 +160,7 @@ const (
ImageRendererType
HeadingRendererType
CodeBlockRendererType
+ PassthroughRendererType
)
type GetRendererFunc func(t RendererType, id any) any
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index 5dfa8262f..15f968f46 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -101,7 +101,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
attrtp = attributes.AttributesOwnerCodeBlockChroma
}
- // IsDefaultCodeBlockRendererProvider
attrs, attrStr, err := getAttributes(n.b, info)
if err != nil {
return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
@@ -160,7 +159,7 @@ type codeBlockContext struct {
ordinal int
// 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
posInit sync.Once
createPos func() htext.Position
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index 1c0d228ed..efb3100aa 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -18,15 +18,14 @@ import (
"bytes"
"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/goldmark_config"
+ "github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/goldmark/images"
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/gohugoio/hugo/markup/goldmark/passthrough"
+ "github.com/yuin/goldmark/util"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
@@ -177,32 +176,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
}
if cfg.Extensions.Passthrough.Enable {
- configuredInlines := cfg.Extensions.Passthrough.Delimiters.Inline
- 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,
- },
- ))
+ extensions = append(extensions, passthrough.New(cfg.Extensions.Passthrough))
}
if pcfg.Conf.EnableEmoji() {
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index 306d26748..712e1f053 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -18,6 +18,7 @@ import (
"math/bits"
"github.com/gohugoio/hugo/markup/converter"
+ "github.com/yuin/goldmark/ast"
)
type BufWriter struct {
@@ -40,9 +41,19 @@ func (b *BufWriter) Flush() error {
type Context struct {
*BufWriter
+ ContextData
positions []int
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) {
diff --git a/markup/goldmark/passthrough/passthrough.go b/markup/goldmark/passthrough/passthrough.go
new file mode 100644
index 000000000..20e42211e
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough.go
@@ -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
+}
diff --git a/markup/goldmark/passthrough/passthrough_integration_test.go b/markup/goldmark/passthrough/passthrough_integration_test.go
new file mode 100644
index 000000000..2d51c5961
--- /dev/null
+++ b/markup/goldmark/passthrough/passthrough_integration_test.go
@@ -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
+
+ `)
+}