mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-14 20:37:55 -05:00
Add render hooks for inline and block passthrough snippets
Fixes #11927
This commit is contained in:
parent
1781b18427
commit
c6227f1d85
10 changed files with 328 additions and 34 deletions
2
go.mod
2
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
219
markup/goldmark/passthrough/passthrough.go
Normal file
219
markup/goldmark/passthrough/passthrough.go
Normal 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
|
||||
}
|
62
markup/goldmark/passthrough/passthrough_integration_test.go
Normal file
62
markup/goldmark/passthrough/passthrough_integration_test.go
Normal 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
|
||||
|
||||
`)
|
||||
}
|
Loading…
Reference in a new issue