]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add render hooks for inline and block passthrough snippets
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 5 Aug 2024 09:00:47 +0000 (11:00 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 7 Aug 2024 16:28:23 +0000 (18:28 +0200)
Fixes #11927

go.mod
go.sum
hugolib/page__per_output.go
hugolib/site.go
markup/converter/hooks/hooks.go
markup/goldmark/codeblocks/render.go
markup/goldmark/convert.go
markup/goldmark/internal/render/context.go
markup/goldmark/passthrough/passthrough.go [new file with mode: 0644]
markup/goldmark/passthrough/passthrough_integration_test.go [new file with mode: 0644]

diff --git a/go.mod b/go.mod
index a7dbcb6e36dbdde3e4acc2800fa5647f612e6ff2..adfe48d2a67ec0b13f997fd493ff91064dfdeff7 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -40,7 +40,7 @@ require (
        github.com/gohugoio/hashstructure v0.1.0
        github.com/gohugoio/httpcache v0.7.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/localescompressed v1.0.1
        github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95
diff --git a/go.sum b/go.sum
index 8b41cba44cecf0a16bdf11ae17471bceacf0dbad..fcd25692e98d1228d0dd57a8fb79ffce16941e1d 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -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/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.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/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
 github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
index 6dffd18a579eeade4f65f7a2304526d288751920..59ba722a88b3b5e21b5ae32d14719e5f69a5aafc 100644 (file)
@@ -476,6 +476,11 @@ func (pco *pageContentOutput) initRenderHooks() error {
                                layoutDescriptor.Kind = "render-image"
                        case hooks.HeadingRendererType:
                                layoutDescriptor.Kind = "render-heading"
+                       case hooks.PassthroughRendererType:
+                               layoutDescriptor.Kind = "render-passthrough"
+                               if id != nil {
+                                       layoutDescriptor.KindVariants = id.(string)
+                               }
                        case hooks.CodeBlockRendererType:
                                layoutDescriptor.Kind = "render-codeblock"
                                if id != nil {
index 2113c4f2086e35b1acaa91e84ac56fd28cec8fe3..0b089767aecd49ea9c59ef67cac1304262a588e4 100644 (file)
@@ -915,6 +915,10 @@ func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.Fle
        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 {
        return hr.resolvePosition(ctx)
 }
index 54babd320867abeb9caf5114053c634baaae81d0..1e335fa466f30ba6d939a9f4915be536b1c84ad0 100644 (file)
@@ -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
index 5dfa8262fed469596a5c7f71712b6d2febe4485d..15f968f4643ce7e2193247d15bcd1c2828362d39 100644 (file)
@@ -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
index 1c0d228edfbc3ea5a0dde719e373af378e53a422..efb3100aa6dab5f5bb14ac65e901f948ddbd1b8e 100644 (file)
@@ -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() {
index 306d26748b174519c60995d43abc521c01fb39d1..712e1f0531bf1aa6351b427f1c4c674dcc008ff7 100644 (file)
@@ -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 (file)
index 0000000..20e4221
--- /dev/null
@@ -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 (file)
index 0000000..2d51c59
--- /dev/null
@@ -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
+
+       `)
+}