diff options
Diffstat (limited to 'markup/goldmark/passthrough/passthrough.go')
-rw-r--r-- | markup/goldmark/passthrough/passthrough.go | 219 |
1 files changed, 219 insertions, 0 deletions
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 +} |