aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-08-30 10:58:43 +0200
committerBjørn Erik Pedersen <[email protected]>2024-08-31 17:21:31 +0200
commitf738669a4d09ca04619f4d0f89d90c9b414e9f2e (patch)
tree5e05f8a849d921dc4380ac8552fd90891331b04a
parentb63f24adc78e09c1044b448daa7c6cb375cebee6 (diff)
downloadhugo-f738669a4d09ca04619f4d0f89d90c9b414e9f2e.tar.gz
hugo-f738669a4d09ca04619f4d0f89d90c9b414e9f2e.zip
Add Markdown render hooks for tables
Fixes #9316 Fixes #12811
-rw-r--r--hugolib/page__per_output.go16
-rw-r--r--hugolib/site.go4
-rw-r--r--markup/converter/hooks/hooks.go50
-rw-r--r--markup/goldmark/blockquotes/blockquotes.go101
-rw-r--r--markup/goldmark/codeblocks/render.go71
-rw-r--r--markup/goldmark/convert.go2
-rw-r--r--markup/goldmark/convert_test.go22
-rw-r--r--markup/goldmark/internal/render/context.go150
-rw-r--r--markup/goldmark/passthrough/passthrough.go70
-rw-r--r--markup/goldmark/render_hooks.go49
-rw-r--r--markup/goldmark/tables/tables.go175
-rw-r--r--markup/goldmark/tables/tables_integration_test.go181
-rw-r--r--tpl/tplimpl/embedded/templates/_default/_markup/render-table.html29
13 files changed, 650 insertions, 270 deletions
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index f074e8db7..6ebddbe44 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -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
diff --git a/hugolib/site.go b/hugolib/site.go
index a93bbdbe6..d0546e910 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -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)
}
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index 29e848d80..9487fd0a7 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -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
+}
diff --git a/markup/goldmark/blockquotes/blockquotes.go b/markup/goldmark/blockquotes/blockquotes.go
index d26c92669..a261ec4fe 100644
--- a/markup/goldmark/blockquotes/blockquotes.go
+++ b/markup/goldmark/blockquotes/blockquotes.go
@@ -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]
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
index fad3ac458..4164f0e0a 100644
--- a/markup/goldmark/codeblocks/render.go
+++ b/markup/goldmark/codeblocks/render.go
@@ -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, "{")
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index 357be7328..5c31eee40 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -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 {
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
index 4c5039e74..6048bce39 100644
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -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
+}
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
index 712e1f053..b8cf9ba54 100644
--- a/markup/goldmark/internal/render/context.go
+++ b/markup/goldmark/internal/render/context.go
@@ -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()
+}
diff --git a/markup/goldmark/passthrough/passthrough.go b/markup/goldmark/passthrough/passthrough.go
index aafb1544b..4d72e7c80 100644
--- a/markup/goldmark/passthrough/passthrough.go
+++ b/markup/goldmark/passthrough/passthrough.go
@@ -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)
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index c127a2c0e..790b2f4ca 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -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
index 000000000..61c9b893f
--- /dev/null
+++ b/markup/goldmark/tables/tables.go
@@ -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
index 000000000..8055265c8
--- /dev/null
+++ b/markup/goldmark/tables/tables_integration_test.go
@@ -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
index 000000000..307f0a5a3
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/_markup/render-table.html
@@ -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>