]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add Markdown render hooks for tables
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 30 Aug 2024 08:58:43 +0000 (10:58 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 31 Aug 2024 15:21:31 +0000 (17:21 +0200)
Fixes #9316
Fixes #12811

13 files changed:
hugolib/page__per_output.go
hugolib/site.go
markup/converter/hooks/hooks.go
markup/goldmark/blockquotes/blockquotes.go
markup/goldmark/codeblocks/render.go
markup/goldmark/convert.go
markup/goldmark/convert_test.go
markup/goldmark/internal/render/context.go
markup/goldmark/passthrough/passthrough.go
markup/goldmark/render_hooks.go
markup/goldmark/tables/tables.go [new file with mode: 0644]
markup/goldmark/tables/tables_integration_test.go [new file with mode: 0644]
tpl/tplimpl/embedded/templates/_default/_markup/render-table.html [new file with mode: 0644]

index f074e8db716a74a98105520b99689bf982d44780..6ebddbe442ec32104f8d8fec68d406ab0caeb668 100644 (file)
@@ -296,6 +296,8 @@ func (pco *pageContentOutput) initRenderHooks() error {
                                if id != nil {
                                        layoutDescriptor.KindVariants = id.(string)
                                }
+                       case hooks.TableRendererType:
+                               layoutDescriptor.Kind = "render-table"
                        case hooks.CodeBlockRendererType:
                                layoutDescriptor.Kind = "render-codeblock"
                                if id != nil {
@@ -334,13 +336,23 @@ func (pco *pageContentOutput) initRenderHooks() error {
 
                        templ, found1 := getHookTemplate(pco.po.f)
 
-                       if pco.po.p.reusePageOutputContent() {
+                       if !found1 || pco.po.p.reusePageOutputContent() {
+                               // Some hooks may only be available in HTML, and if
+                               // this site is configured to not have HTML output, we need to
+                               // make sure we have a fallback. This should be very rare.
+                               candidates := pco.po.p.s.renderFormats
+                               if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
+                                       if _, found := candidates.GetBySuffix("html"); !found {
+                                               candidates = append(candidates, output.HTMLFormat)
+                                       }
+                               }
                                // Check if some of the other output formats would give a different template.
-                               for _, f := range pco.po.p.s.renderFormats {
+                               for _, f := range candidates {
                                        if f.Name == pco.po.f.Name {
                                                continue
                                        }
                                        templ2, found2 := getHookTemplate(f)
+
                                        if found2 {
                                                if !found1 {
                                                        templ = templ2
index a93bbdbe63b8cae2fac0e3c3cc8302714e02ffad..d0546e9107f247d101b4b2fd91b19236261d44c5 100644 (file)
@@ -930,6 +930,10 @@ func (hr hookRendererTemplate) RenderBlockquote(cctx context.Context, w hugio.Fl
        return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
 }
 
+func (hr hookRendererTemplate) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
+       return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
+}
+
 func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
        return hr.resolvePosition(ctx)
 }
index 29e848d80cc59e3511c394a242cba3519a71289a..9487fd0a71002e4fd19b8454a14aa6a9687078c9 100644 (file)
@@ -61,9 +61,8 @@ type ImageLinkContext interface {
 
 // CodeblockContext is the context passed to a code block render hook.
 type CodeblockContext interface {
+       BaseContext
        AttributesProvider
-       text.Positioner
-       PageProvider
 
        // Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
        Options() map[string]any
@@ -73,19 +72,31 @@ type CodeblockContext interface {
 
        // The text between the code fences.
        Inner() string
-
-       // Zero-based ordinal for all code blocks in the current document.
-       Ordinal() int
 }
 
-// BlockquoteContext is the context passed to a blockquote render hook.
-type BlockquoteContext interface {
+// TableContext is the context passed to a table render hook.
+type TableContext interface {
+       BaseContext
        AttributesProvider
+
+       THead() []TableRow
+       TBody() []TableRow
+}
+
+// BaseContext is the base context used in most render hooks.
+type BaseContext interface {
        text.Positioner
        PageProvider
 
-       // Zero-based ordinal for all block quotes in the current document.
+       // Zero-based ordinal for all elements of this kind in the current document.
        Ordinal() int
+}
+
+// BlockquoteContext is the context passed to a blockquote render hook.
+type BlockquoteContext interface {
+       BaseContext
+
+       AttributesProvider
 
        // The blockquote text.
        // If type is "alert", this will be the alert text.
@@ -107,18 +118,14 @@ type PositionerSourceTargetProvider interface {
 
 // PassThroughContext is the context passed to a passthrough render hook.
 type PassthroughContext interface {
+       BaseContext
        AttributesProvider
-       text.Positioner
-       PageProvider
 
        // Currently one of "inline" or "block".
        Type() string
 
        // The inner content of the passthrough element, excluding the delimiters.
        Inner() string
-
-       // Zero-based ordinal for all passthrough elements in the document.
-       Ordinal() int
 }
 
 type AttributesOptionsSliceProvider interface {
@@ -138,6 +145,10 @@ type BlockquoteRenderer interface {
        RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx BlockquoteContext) error
 }
 
+type TableRenderer interface {
+       RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx TableContext) error
+}
+
 type PassthroughRenderer interface {
        RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
 }
@@ -196,6 +207,19 @@ const (
        CodeBlockRendererType
        PassthroughRendererType
        BlockquoteRendererType
+       TableRendererType
 )
 
 type GetRendererFunc func(t RendererType, id any) any
+
+type TableCell struct {
+       Text      hstring.RenderedString
+       Alignment string // left, center, or right
+}
+
+type TableRow []TableCell
+
+type Table struct {
+       THead []TableRow
+       TBody []TableRow
+}
index d26c92669fdbc24baf52df7c698b055f2ead920f..a261ec4fe3980bb183d18675d06758801326fcbb 100644 (file)
@@ -16,10 +16,8 @@ package blockquotes
 import (
        "regexp"
        "strings"
-       "sync"
 
        "github.com/gohugoio/hugo/common/herrors"
-       htext "github.com/gohugoio/hugo/common/text"
        "github.com/gohugoio/hugo/common/types/hstring"
        "github.com/gohugoio/hugo/markup/converter/hooks"
        "github.com/gohugoio/hugo/markup/goldmark/internal/render"
@@ -71,70 +69,36 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
                return ast.WalkContinue, nil
        }
 
-       pos := ctx.PopPos()
-       text := ctx.Buffer.Bytes()[pos:]
-       ctx.Buffer.Truncate(pos)
+       text := ctx.PopRenderedString()
 
        ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)
 
-       texts := string(text)
        typ := typeRegular
-       alertType := resolveGitHubAlert(texts)
+       alertType := resolveGitHubAlert(string(text))
        if alertType != "" {
                typ = typeAlert
        }
 
        renderer := ctx.RenderContext().GetRenderer(hooks.BlockquoteRendererType, typ)
        if renderer == nil {
-               return r.renderBlockquoteDefault(w, n, texts)
+               return r.renderBlockquoteDefault(w, n, text)
        }
 
        if typ == typeAlert {
                // Trim preamble: <p>[!NOTE]<br>\n but preserve leading paragraph.
                // We could possibly complicate this by moving this to the parser, but
                // keep it simple for now.
-               texts = "<p>" + texts[strings.Index(texts, "\n")+1:]
-       }
-
-       var sourceRef []byte
-
-       // Extract a source sample to use for position information.
-       if nn := n.FirstChild(); nn != nil {
-               var start, stop int
-               for i := 0; i < nn.Lines().Len() && i < 2; i++ {
-                       line := nn.Lines().At(i)
-                       if i == 0 {
-                               start = line.Start
-                       }
-                       stop = line.Stop
-               }
-               // We do not mutate the source, so this is safe.
-               sourceRef = src[start:stop]
+               text = "<p>" + text[strings.Index(text, "\n")+1:]
        }
 
        bqctx := &blockquoteContext{
-               page:             ctx.DocumentContext().Document,
-               pageInner:        r.getPageInner(ctx),
+               BaseContext:      render.NewBaseContext(ctx, renderer, n, src, nil, ordinal),
                typ:              typ,
                alertType:        alertType,
-               text:             hstring.RenderedString(texts),
-               sourceRef:        sourceRef,
-               ordinal:          ordinal,
+               text:             hstring.RenderedString(text),
                AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
        }
 
-       bqctx.createPos = func() htext.Position {
-               if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
-                       return resolver.ResolvePosition(bqctx)
-               }
-
-               return htext.Position{
-                       Filename:     ctx.DocumentContext().Filename,
-                       LineNumber:   1,
-                       ColumnNumber: 1,
-               }
-       }
-
        cr := renderer.(hooks.BlockquoteRenderer)
 
        err := cr.RenderBlockquote(
@@ -143,24 +107,12 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
                bqctx,
        )
        if err != nil {
-               return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.createPos())
+               return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.Position())
        }
 
        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
-}
-
 // Code borrowed from goldmark's html renderer.
 func (r *htmlRenderer) renderBlockquoteDefault(
        w util.BufWriter, n ast.Node, text string,
@@ -180,19 +132,11 @@ func (r *htmlRenderer) renderBlockquoteDefault(
 }
 
 type blockquoteContext struct {
-       page      any
-       pageInner any
+       hooks.BaseContext
+
        text      hstring.RenderedString
-       typ       string
-       sourceRef []byte
        alertType 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
+       typ       string
 
        *attributes.AttributesHolder
 }
@@ -205,35 +149,10 @@ func (c *blockquoteContext) AlertType() string {
        return c.alertType
 }
 
-func (c *blockquoteContext) Page() any {
-       return c.page
-}
-
-func (c *blockquoteContext) PageInner() any {
-       return c.pageInner
-}
-
 func (c *blockquoteContext) Text() hstring.RenderedString {
        return c.text
 }
 
-func (c *blockquoteContext) Ordinal() int {
-       return c.ordinal
-}
-
-func (c *blockquoteContext) Position() htext.Position {
-       c.posInit.Do(func() {
-               c.pos = c.createPos()
-       })
-       return c.pos
-}
-
-func (c *blockquoteContext) PositionerSourceTarget() []byte {
-       return c.sourceRef
-}
-
-var _ hooks.PositionerSourceTargetProvider = (*blockquoteContext)(nil)
-
 // https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
 // Five types:
 // [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
index fad3ac458091f2595ec0ea5c7775295ba595834b..4164f0e0a4a85f786f9b3b70606cf316e7ef70bb 100644 (file)
@@ -18,7 +18,6 @@ import (
        "errors"
        "fmt"
        "strings"
-       "sync"
 
        "github.com/gohugoio/hugo/common/herrors"
        htext "github.com/gohugoio/hugo/common/text"
@@ -101,26 +100,14 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
        if err != nil {
                return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
        }
+
        cbctx := &codeBlockContext{
-               page:             ctx.DocumentContext().Document,
-               pageInner:        r.getPageInner(ctx),
+               BaseContext:      render.NewBaseContext(ctx, renderer, node, src, func() []byte { return []byte(s) }, ordinal),
                lang:             lang,
                code:             s,
-               ordinal:          ordinal,
                AttributesHolder: attributes.New(attrs, attrtp),
        }
 
-       cbctx.createPos = func() htext.Position {
-               if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
-                       return resolver.ResolvePosition(cbctx)
-               }
-               return htext.Position{
-                       Filename:     ctx.DocumentContext().Filename,
-                       LineNumber:   1,
-                       ColumnNumber: 1,
-               }
-       }
-
        cr := renderer.(hooks.CodeBlockRenderer)
 
        err = cr.RenderCodeblock(
@@ -129,50 +116,20 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
                cbctx,
        )
        if err != nil {
-               return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
+               return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.Position())
        }
 
        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
-}
-
-var _ hooks.PositionerSourceTargetProvider = (*codeBlockContext)(nil)
-
 type codeBlockContext struct {
-       page      any
-       pageInner any
-       lang      string
-       code      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
+       hooks.BaseContext
+       lang string
+       code string
 
        *attributes.AttributesHolder
 }
 
-func (c *codeBlockContext) Page() any {
-       return c.page
-}
-
-func (c *codeBlockContext) PageInner() any {
-       return c.pageInner
-}
-
 func (c *codeBlockContext) Type() string {
        return c.lang
 }
@@ -181,22 +138,6 @@ func (c *codeBlockContext) Inner() string {
        return c.code
 }
 
-func (c *codeBlockContext) Ordinal() int {
-       return c.ordinal
-}
-
-func (c *codeBlockContext) Position() htext.Position {
-       c.posInit.Do(func() {
-               c.pos = c.createPos()
-       })
-       return c.pos
-}
-
-// For internal use.
-func (c *codeBlockContext) PositionerSourceTarget() []byte {
-       return []byte(c.code)
-}
-
 func getLang(node *ast.FencedCodeBlock, src []byte) string {
        langWithAttributes := string(node.Language(src))
        lang, _, _ := strings.Cut(langWithAttributes, "{")
index 357be732860b5da0b43d69df86e81c7a85db444d..5c31eee40d9a08e655fc277b7a04cdde28cb2d3c 100644 (file)
@@ -26,6 +26,7 @@ import (
        "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/gohugoio/hugo/markup/goldmark/tables"
        "github.com/yuin/goldmark/util"
 
        "github.com/yuin/goldmark"
@@ -131,6 +132,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
 
        if cfg.Extensions.Table {
                extensions = append(extensions, extension.Table)
+               extensions = append(extensions, tables.New())
        }
 
        if cfg.Extensions.Strikethrough {
index 4c5039e74187a7483d4db69b206ac5238875fb26..6048bce397ac3888b25849aa9f41f7837650a34c 100644 (file)
@@ -14,6 +14,7 @@
 package goldmark_test
 
 import (
+       "context"
        "fmt"
        "strings"
        "testing"
@@ -30,6 +31,7 @@ import (
 
        "github.com/gohugoio/hugo/markup/markup_config"
 
+       "github.com/gohugoio/hugo/common/hugio"
        "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/common/maps"
 
@@ -60,9 +62,13 @@ func convert(c *qt.C, conf config.AllProvider, content string) converter.ResultR
        h := highlight.New(mconf.Highlight)
 
        getRenderer := func(t hooks.RendererType, id any) any {
-               if t == hooks.CodeBlockRendererType {
+               switch t {
+               case hooks.CodeBlockRendererType:
                        return h
+               case hooks.TableRendererType:
+                       return tableRenderer(0)
                }
+
                return nil
        }
 
@@ -168,8 +174,6 @@ unsafe = true
        b := convert(c, testconfig.GetTestConfig(nil, cfg), content)
        got := string(b.Bytes())
 
-       fmt.Println(got)
-
        // Links
        c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
        c.Assert(got, qt.Contains, `<a href="https://foo.bar/">https://foo.bar/</a>`)
@@ -191,7 +195,7 @@ unsafe = true
        // Extensions
        c.Assert(got, qt.Contains, `Autolink: <a href="https://gohugo.io/">https://gohugo.io/</a>`)
        c.Assert(got, qt.Contains, `Strikethrough:<del>Hi</del> Hello, world`)
-       c.Assert(got, qt.Contains, `<th>foo</th>`)
+       c.Assert(got, qt.Contains, `Table`)
        c.Assert(got, qt.Contains, `<li><input disabled="" type="checkbox"> Push my commits to GitHub</li>`)
 
        c.Assert(got, qt.Contains, `Straight double &ldquo;quotes&rdquo; and single &lsquo;quotes&rsquo;`)
@@ -378,7 +382,7 @@ func TestConvertAttributes(t *testing.T) {
 | ------------- |:-------------:| -----:|
 | AV      | BV |
 {.myclass }`,
-                       "<table class=\"myclass\">\n<thead>",
+                       "Table",
                },
                {
                        "Title and Blockquote",
@@ -741,3 +745,11 @@ escapedSpace=true
 
        c.Assert(got, qt.Contains, "<p>私は太郎です。\nプログラミングが好きです。運動が苦手です。</p>\n")
 }
+
+type tableRenderer int
+
+func (hr tableRenderer) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
+       // This is set up with a render hook in the hugolib package, make it simple here.
+       fmt.Fprintln(w, "Table")
+       return nil
+}
index 712e1f0531bf1aa6351b427f1c4c674dcc008ff7..b8cf9ba54efc6f0efa14c6ae0ade12505fcfd921 100644 (file)
@@ -16,8 +16,12 @@ package render
 import (
        "bytes"
        "math/bits"
+       "sync"
+
+       htext "github.com/gohugoio/hugo/common/text"
 
        "github.com/gohugoio/hugo/markup/converter"
+       "github.com/gohugoio/hugo/markup/converter/hooks"
        "github.com/yuin/goldmark/ast"
 )
 
@@ -45,6 +49,7 @@ type Context struct {
        positions []int
        pids      []uint64
        ordinals  map[ast.NodeKind]int
+       values    map[ast.NodeKind][]any
 }
 
 func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
@@ -67,6 +72,13 @@ func (ctx *Context) PopPos() int {
        return p
 }
 
+func (ctx *Context) PopRenderedString() string {
+       pos := ctx.PopPos()
+       text := string(ctx.Bytes()[pos:])
+       ctx.Truncate(pos)
+       return text
+}
+
 // PushPid pushes a new page ID to the stack.
 func (ctx *Context) PushPid(pid uint64) {
        ctx.pids = append(ctx.pids, pid)
@@ -91,6 +103,38 @@ func (ctx *Context) PopPid() uint64 {
        return p
 }
 
+func (ctx *Context) PushValue(k ast.NodeKind, v any) {
+       if ctx.values == nil {
+               ctx.values = make(map[ast.NodeKind][]any)
+       }
+       ctx.values[k] = append(ctx.values[k], v)
+}
+
+func (ctx *Context) PopValue(k ast.NodeKind) any {
+       if ctx.values == nil {
+               return nil
+       }
+       v := ctx.values[k]
+       if len(v) == 0 {
+               return nil
+       }
+       i := len(v) - 1
+       r := v[i]
+       ctx.values[k] = v[:i]
+       return r
+}
+
+func (ctx *Context) PeekValue(k ast.NodeKind) any {
+       if ctx.values == nil {
+               return nil
+       }
+       v := ctx.values[k]
+       if len(v) == 0 {
+               return nil
+       }
+       return v[len(v)-1]
+}
+
 type ContextData interface {
        RenderContext() converter.RenderContext
        DocumentContext() converter.DocumentContext
@@ -108,3 +152,109 @@ func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
 func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
        return ctx.Dctx
 }
+
+// extractSourceSample returns a sample of the source for the given node.
+// Note that this is not a copy of the source, but a slice of it,
+// so it assumes that the source is not mutated.
+func extractSourceSample(n ast.Node, src []byte) []byte {
+       var sample []byte
+
+       // Extract a source sample to use for position information.
+       if nn := n.FirstChild(); nn != nil {
+               var start, stop int
+               for i := 0; i < nn.Lines().Len() && i < 2; i++ {
+                       line := nn.Lines().At(i)
+                       if i == 0 {
+                               start = line.Start
+                       }
+                       stop = line.Stop
+               }
+               // We do not mutate the source, so this is safe.
+               sample = src[start:stop]
+       }
+       return sample
+}
+
+// GetPageAndPageInner returns the current page and the inner page for the given context.
+func GetPageAndPageInner(rctx *Context) (any, any) {
+       p := rctx.DocumentContext().Document
+       pid := rctx.PeekPid()
+       if pid > 0 {
+               if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
+                       if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
+                               return p, v
+                       }
+               }
+       }
+       return p, p
+}
+
+// NewBaseContext creates a new BaseContext.
+func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, getSourceSample func() []byte, ordinal int) hooks.BaseContext {
+       if getSourceSample == nil {
+               getSourceSample = func() []byte {
+                       return extractSourceSample(n, src)
+               }
+       }
+       page, pageInner := GetPageAndPageInner(rctx)
+       b := &hookBase{
+               page:      page,
+               pageInner: pageInner,
+
+               getSourceSample: getSourceSample,
+               ordinal:         ordinal,
+       }
+
+       b.createPos = func() htext.Position {
+               if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
+                       return resolver.ResolvePosition(b)
+               }
+
+               return htext.Position{
+                       Filename:     rctx.DocumentContext().Filename,
+                       LineNumber:   1,
+                       ColumnNumber: 1,
+               }
+       }
+
+       return b
+}
+
+var _ hooks.PositionerSourceTargetProvider = (*hookBase)(nil)
+
+type hookBase struct {
+       page      any
+       pageInner any
+       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
+       getSourceSample func() []byte
+}
+
+func (c *hookBase) Page() any {
+       return c.page
+}
+
+func (c *hookBase) PageInner() any {
+       return c.pageInner
+}
+
+func (c *hookBase) Ordinal() int {
+       return c.ordinal
+}
+
+func (c *hookBase) Position() htext.Position {
+       c.posInit.Do(func() {
+               c.pos = c.createPos()
+       })
+       return c.pos
+}
+
+// For internal use.
+func (c *hookBase) PositionerSourceTarget() []byte {
+       return c.getSourceSample()
+}
index aafb1544b9423fa03911d08f463ae169874f38a1..4d72e7c80f2c65b7aadf7bd7ab3556499a6cb7a2 100644 (file)
@@ -15,9 +15,6 @@ 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"
@@ -136,25 +133,12 @@ func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node
        s = s[len(delims.Open) : len(s)-len(delims.Close)]
 
        pctx := &passthroughContext{
-               ordinal:          ordinal,
-               page:             ctx.DocumentContext().Document,
-               pageInner:        r.getPageInner(ctx),
+               BaseContext:      render.NewBaseContext(ctx, renderer, node, src, nil, ordinal),
                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 {
@@ -164,39 +148,13 @@ func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node
        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
-}
+       hooks.BaseContext
 
-func (p *passthroughContext) Page() any {
-       return p.page
-}
+       typ   string // inner or block
+       inner string
 
-func (p *passthroughContext) PageInner() any {
-       return p.pageInner
+       *attributes.AttributesHolder
 }
 
 func (p *passthroughContext) Type() string {
@@ -206,21 +164,3 @@ func (p *passthroughContext) Type() string {
 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
-}
-
-// For internal use.
-func (p *passthroughContext) PositionerSourceTarget() []byte {
-       return []byte(p.inner)
-}
-
-var _ hooks.PositionerSourceTargetProvider = (*passthroughContext)(nil)
index c127a2c0e8682fe5043829bfd9f249bd638abfa0..790b2f4caa71af8e6c4d89cb2cdf9eff25a4939c 100644 (file)
@@ -169,9 +169,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
                return ast.WalkContinue, nil
        }
 
-       pos := ctx.PopPos()
-       text := ctx.Buffer.Bytes()[pos:]
-       ctx.Buffer.Truncate(pos)
+       text := ctx.PopRenderedString()
 
        var (
                isBlock bool
@@ -190,13 +188,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
        // internal attributes before rendering.
        attrs := r.filterInternalAttributes(n.Attributes())
 
+       page, pageInner := render.GetPageAndPageInner(ctx)
+
        err := lr.RenderLink(
                ctx.RenderContext().Ctx,
                w,
                imageLinkContext{
                        linkContext: linkContext{
-                               page:             ctx.DocumentContext().Document,
-                               pageInner:        r.getPageInner(ctx),
+                               page:             page,
+                               pageInner:        pageInner,
                                destination:      string(n.Destination),
                                title:            string(n.Title),
                                text:             hstring.RenderedString(text),
@@ -211,18 +211,6 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
        return ast.WalkContinue, err
 }
 
-func (r *hookedRenderer) 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
-}
-
 func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
        n := 0
        for _, x := range attrs {
@@ -288,16 +276,16 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
                return ast.WalkContinue, nil
        }
 
-       pos := ctx.PopPos()
-       text := ctx.Buffer.Bytes()[pos:]
-       ctx.Buffer.Truncate(pos)
+       text := ctx.PopRenderedString()
+
+       page, pageInner := render.GetPageAndPageInner(ctx)
 
        err := lr.RenderLink(
                ctx.RenderContext().Ctx,
                w,
                linkContext{
-                       page:             ctx.DocumentContext().Document,
-                       pageInner:        r.getPageInner(ctx),
+                       page:             page,
+                       pageInner:        pageInner,
                        destination:      string(n.Destination),
                        title:            string(n.Title),
                        text:             hstring.RenderedString(text),
@@ -358,12 +346,14 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
                url = "mailto:" + url
        }
 
+       page, pageInner := render.GetPageAndPageInner(ctx)
+
        err := lr.RenderLink(
                ctx.RenderContext().Ctx,
                w,
                linkContext{
-                       page:             ctx.DocumentContext().Document,
-                       pageInner:        r.getPageInner(ctx),
+                       page:             page,
+                       pageInner:        pageInner,
                        destination:      url,
                        text:             hstring.RenderedString(label),
                        plainText:        label,
@@ -435,20 +425,21 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
                return ast.WalkContinue, nil
        }
 
-       pos := ctx.PopPos()
-       text := ctx.Buffer.Bytes()[pos:]
-       ctx.Buffer.Truncate(pos)
+       text := ctx.PopRenderedString()
+
        // All ast.Heading nodes are guaranteed to have an attribute called "id"
        // that is an array of bytes that encode a valid string.
        anchori, _ := n.AttributeString("id")
        anchor := anchori.([]byte)
 
+       page, pageInner := render.GetPageAndPageInner(ctx)
+
        err := hr.RenderHeading(
                ctx.RenderContext().Ctx,
                w,
                headingContext{
-                       page:             ctx.DocumentContext().Document,
-                       pageInner:        r.getPageInner(ctx),
+                       page:             page,
+                       pageInner:        pageInner,
                        level:            n.Level,
                        anchor:           string(anchor),
                        text:             hstring.RenderedString(text),
diff --git a/markup/goldmark/tables/tables.go b/markup/goldmark/tables/tables.go
new file mode 100644 (file)
index 0000000..61c9b89
--- /dev/null
@@ -0,0 +1,175 @@
+// 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 tables
+
+import (
+       "github.com/gohugoio/hugo/common/herrors"
+       "github.com/gohugoio/hugo/common/types/hstring"
+       "github.com/gohugoio/hugo/markup/converter/hooks"
+       "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+       "github.com/gohugoio/hugo/markup/internal/attributes"
+       "github.com/yuin/goldmark"
+       "github.com/yuin/goldmark/ast"
+       gast "github.com/yuin/goldmark/extension/ast"
+       "github.com/yuin/goldmark/renderer"
+       "github.com/yuin/goldmark/util"
+)
+
+type (
+       ext          struct{}
+       htmlRenderer struct{}
+)
+
+func New() goldmark.Extender {
+       return &ext{}
+}
+
+func (e *ext) Extend(m goldmark.Markdown) {
+       m.Renderer().AddOptions(renderer.WithNodeRenderers(
+               util.Prioritized(newHTMLRenderer(), 100),
+       ))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+       r := &htmlRenderer{}
+       return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+       reg.Register(gast.KindTable, r.renderTable)
+       reg.Register(gast.KindTableHeader, r.renderHeaderOrRow)
+       reg.Register(gast.KindTableRow, r.renderHeaderOrRow)
+       reg.Register(gast.KindTableCell, r.renderCell)
+}
+
+func (r *htmlRenderer) renderTable(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+       ctx := w.(*render.Context)
+       if entering {
+               // This will be modified below.
+               table := &hooks.Table{}
+               ctx.PushValue(gast.KindTable, table)
+               return ast.WalkContinue, nil
+       }
+
+       v := ctx.PopValue(gast.KindTable)
+       if v == nil {
+               panic("table not found")
+       }
+
+       table := v.(*hooks.Table)
+
+       renderer := ctx.RenderContext().GetRenderer(hooks.TableRendererType, nil)
+       if renderer == nil {
+               panic("table hook renderer not found")
+       }
+
+       ordinal := ctx.GetAndIncrementOrdinal(gast.KindTable)
+
+       tctx := &tableContext{
+               BaseContext:      render.NewBaseContext(ctx, renderer, n, source, nil, ordinal),
+               AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
+               tHead:            table.THead,
+               tBody:            table.TBody,
+       }
+
+       cr := renderer.(hooks.TableRenderer)
+
+       err := cr.RenderTable(
+               ctx.RenderContext().Ctx,
+               w,
+               tctx,
+       )
+       if err != nil {
+               return ast.WalkContinue, herrors.NewFileErrorFromPos(err, tctx.Position())
+       }
+
+       return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) peekTable(ctx *render.Context) *hooks.Table {
+       v := ctx.PeekValue(gast.KindTable)
+       if v == nil {
+               panic("table not found")
+       }
+       return v.(*hooks.Table)
+}
+
+func (r *htmlRenderer) renderCell(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+       ctx := w.(*render.Context)
+
+       if entering {
+               // Store the current pos so we can capture the rendered text.
+               ctx.PushPos(ctx.Buffer.Len())
+               return ast.WalkContinue, nil
+       }
+
+       n := node.(*gast.TableCell)
+
+       text := ctx.PopRenderedString()
+
+       table := r.peekTable(ctx)
+
+       var alignment string
+       switch n.Alignment {
+       case gast.AlignLeft:
+               alignment = "left"
+       case gast.AlignRight:
+               alignment = "right"
+       case gast.AlignCenter:
+               alignment = "center"
+       default:
+               alignment = "left"
+       }
+
+       cell := hooks.TableCell{Text: hstring.RenderedString(text), Alignment: alignment}
+
+       if node.Parent().Kind() == gast.KindTableHeader {
+               table.THead[len(table.THead)-1] = append(table.THead[len(table.THead)-1], cell)
+       } else {
+               table.TBody[len(table.TBody)-1] = append(table.TBody[len(table.TBody)-1], cell)
+       }
+
+       return ast.WalkContinue, nil
+}
+
+func (r *htmlRenderer) renderHeaderOrRow(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+       ctx := w.(*render.Context)
+       table := r.peekTable(ctx)
+       if entering {
+               if n.Kind() == gast.KindTableHeader {
+                       table.THead = append(table.THead, hooks.TableRow{})
+               } else {
+                       table.TBody = append(table.TBody, hooks.TableRow{})
+               }
+               return ast.WalkContinue, nil
+       }
+
+       return ast.WalkContinue, nil
+}
+
+type tableContext struct {
+       hooks.BaseContext
+       *attributes.AttributesHolder
+
+       tHead []hooks.TableRow
+       tBody []hooks.TableRow
+}
+
+func (c *tableContext) THead() []hooks.TableRow {
+       return c.tHead
+}
+
+func (c *tableContext) TBody() []hooks.TableRow {
+       return c.tBody
+}
diff --git a/markup/goldmark/tables/tables_integration_test.go b/markup/goldmark/tables/tables_integration_test.go
new file mode 100644 (file)
index 0000000..8055265
--- /dev/null
@@ -0,0 +1,181 @@
+// 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 tables_test
+
+import (
+       "testing"
+
+       "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestTableHook(t *testing.T) {
+       t.Parallel()
+       files := `
+-- hugo.toml --
+[markup.goldmark.parser.attribute]
+block = true
+title = true
+-- content/p1.md --
+## Table 1
+
+| Item              | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat        |   True   | 23.99 |
+| SQL Hat           |   True   | 23.99 |
+| Codecademy Tee    |  False   | 19.99 |
+| Codecademy Hoodie |  False   | 42.99 |
+{.foo foo="bar"}
+
+## Table 2
+
+| Month | Savings |
+| -------- | ------- |
+| January | $250 |
+| February | $80 |
+| March | $420 |
+
+-- layouts/_default/single.html --
+{{ .Content }}
+-- layouts/_default/_markup/render-table.html --
+Attributes: {{ .Attributes }}|
+{{ template "print" (dict "what" (printf "table-%d-thead" $.Ordinal) "rows" .THead) }}
+{{ template "print" (dict "what" (printf "table-%d-tbody" $.Ordinal)  "rows" .TBody) }}
+{{ define "print" }}
+ {{ .what }}:{{ range $i, $a := .rows }} {{ $i }}:{{ range $j, $b := . }} {{ $j }}: {{ .Alignment }}: {{ .Text }}|{{ end }}{{ end }}$
+{{ end }}
+
+`
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/p1/index.html",
+               "Attributes: map[class:foo foo:bar]|",
+               "table-0-thead: 0: 0: left: Item| 1: center: In Stock| 2: right: Price|$",
+               "table-0-tbody: 0: 0: left: Python Hat| 1: center: True| 2: right: 23.99| 1: 0: left: SQL Hat| 1: center: True| 2: right: 23.99| 2: 0: left: Codecademy Tee| 1: center: False| 2: right: 19.99| 3: 0: left: Codecademy Hoodie| 1: center: False| 2: right: 42.99|$",
+       )
+
+       b.AssertFileContent("public/p1/index.html",
+               "table-1-thead: 0: 0: left: Month| 1: left: Savings|$",
+               "table-1-tbody: 0: 0: left: January| 1: left: $250| 1: 0: left: February| 1: left: $80| 2: 0: left: March| 1: left: $420|$",
+       )
+}
+
+func TestTableDefault(t *testing.T) {
+       t.Parallel()
+       files := `
+-- hugo.toml --
+[markup.goldmark.parser.attribute]
+block = true
+title = true
+-- content/p1.md --
+
+## Table 1
+
+| Item              | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat        |   True   | 23.99 |
+| SQL Hat           |   True   | 23.99 |
+| Codecademy Tee    |  False   | 19.99 |
+| Codecademy Hoodie |  False   | 42.99 |
+{.foo}
+
+
+-- layouts/_default/single.html --
+Summary: {{ .Summary }}
+Content: {{ .Content }}
+
+`
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/p1/index.html", "<table class=\"foo\">")
+}
+
+// Issue 12811.
+func TestTableDefaultRSSAndHTML(t *testing.T) {
+       t.Parallel()
+       files := `
+-- hugo.toml --
+[outputFormats]
+  [outputFormats.rss]
+    weight = 30
+ [outputFormats.html]
+    weight = 20
+-- content/_index.md --
+---
+title: "Home"
+output: ["rss", "html"]
+---
+
+| Item              | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat        |   True   | 23.99 |
+| SQL Hat           |   True   | 23.99 |
+| Codecademy Tee    |  False   | 19.99 |
+| Codecademy Hoodie |  False   | 42.99 |
+
+{{< foo >}}
+
+-- layouts/index.html --
+Content: {{ .Content }}
+-- layouts/index.xml --
+Content: {{ .Content }}
+-- layouts/shortcodes/foo.xml --
+foo xml
+-- layouts/shortcodes/foo.html --
+foo html
+
+`
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/index.xml", "<table>")
+       b.AssertFileContent("public/index.html", "<table>")
+}
+
+func TestTableDefaultRSSOnly(t *testing.T) {
+       t.Parallel()
+       files := `
+-- hugo.toml --
+[outputs]
+  home = ['rss']
+  section = ['rss']
+  taxonomy = ['rss']
+  term = ['rss']
+  page = ['rss']
+disableKinds = ["taxonomy", "term", "page", "section"]
+-- content/_index.md --
+---
+title: "Home"
+---
+
+## Table 1
+
+| Item              | In Stock | Price |
+| :---------------- | :------: | ----: |
+| Python Hat        |   True   | 23.99 |
+| SQL Hat           |   True   | 23.99 |
+| Codecademy Tee    |  False   | 19.99 |
+| Codecademy Hoodie |  False   | 42.99 |
+
+
+
+
+
+-- layouts/index.xml --
+Content: {{ .Content }}
+
+
+`
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/index.xml", "<table>")
+}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-table.html b/tpl/tplimpl/embedded/templates/_default/_markup/render-table.html
new file mode 100644 (file)
index 0000000..307f0a5
--- /dev/null
@@ -0,0 +1,29 @@
+<table
+  {{- range $k, $v := .Attributes }}
+    {{- if $v }}
+      {{- printf " %s=%q" $k $v | safeHTMLAttr }}
+    {{- end }}
+  {{- end }}>
+  <thead>
+    {{- range .THead }}
+      <tr>
+        {{- range . }}
+          <th {{ printf "style=%q" (printf "text-align: %s" .Alignment) | safeHTMLAttr }}>
+            {{- .Text | safeHTML -}}
+          </th>
+        {{- end }}
+      </tr>
+    {{- end }}
+  </thead>
+  <tbody>
+    {{- range .TBody }}
+      <tr>
+        {{- range . }}
+          <td {{ printf "style=%q" (printf "text-align: %s" .Alignment) | safeHTMLAttr }}>
+            {{- .Text | safeHTML -}}
+          </td>
+        {{- end }}
+      </tr>
+    {{- end }}
+  </tbody>
+</table>