From c6227f1d8597f053986c13eac131ae5122a68444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 5 Aug 2024 11:00:47 +0200 Subject: [PATCH] Add render hooks for inline and block passthrough snippets Fixes #11927 --- go.mod | 2 +- go.sum | 2 + hugolib/page__per_output.go | 5 + hugolib/site.go | 4 + markup/converter/hooks/hooks.go | 18 ++ markup/goldmark/codeblocks/render.go | 3 +- markup/goldmark/convert.go | 34 +-- markup/goldmark/internal/render/context.go | 13 +- markup/goldmark/passthrough/passthrough.go | 219 ++++++++++++++++++ .../passthrough_integration_test.go | 62 +++++ 10 files changed, 328 insertions(+), 34 deletions(-) create mode 100644 markup/goldmark/passthrough/passthrough.go create mode 100644 markup/goldmark/passthrough/passthrough_integration_test.go diff --git a/go.mod b/go.mod index a7dbcb6e3..adfe48d2a 100644 --- 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 8b41cba44..fcd25692e 100644 --- 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= diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 6dffd18a5..59ba722a8 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -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 { diff --git a/hugolib/site.go b/hugolib/site.go index 2113c4f20..0b089767a 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -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) } 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 + + `) +}