Skip to content

Commit

Permalink
Add render hooks for inline and block passthrough snippets
Browse files Browse the repository at this point in the history
Fixes #11927
  • Loading branch information
bep committed Aug 7, 2024
1 parent 1781b18 commit c6227f1
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 34 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
5 changes: 5 additions & 0 deletions hugolib/page__per_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 18 additions & 0 deletions markup/converter/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -143,6 +160,7 @@ const (
ImageRendererType
HeadingRendererType
CodeBlockRendererType
PassthroughRendererType
)

type GetRendererFunc func(t RendererType, id any) any
3 changes: 1 addition & 2 deletions markup/goldmark/codeblocks/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
34 changes: 4 additions & 30 deletions markup/goldmark/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down
13 changes: 12 additions & 1 deletion markup/goldmark/internal/render/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"math/bits"

"github.com/gohugoio/hugo/markup/converter"
"github.com/yuin/goldmark/ast"
)

type BufWriter struct {
Expand All @@ -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) {
Expand Down
219 changes: 219 additions & 0 deletions markup/goldmark/passthrough/passthrough.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit c6227f1

Please sign in to comment.