mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-23 00:23:06 +00:00
parent
b63f24adc7
commit
f738669a4d
13 changed files with 651 additions and 271 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
// 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 elements of this kind in the current document.
|
||||
Ordinal() int
|
||||
}
|
||||
|
||||
// BlockquoteContext is the context passed to a blockquote render hook.
|
||||
type BlockquoteContext interface {
|
||||
AttributesProvider
|
||||
text.Positioner
|
||||
PageProvider
|
||||
BaseContext
|
||||
|
||||
// Zero-based ordinal for all block quotes in the current document.
|
||||
Ordinal() int
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
text hstring.RenderedString
|
||||
typ string
|
||||
sourceRef []byte
|
||||
alertType string
|
||||
ordinal int
|
||||
hooks.BaseContext
|
||||
|
||||
// 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
|
||||
text hstring.RenderedString
|
||||
alertType string
|
||||
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]
|
||||
|
|
|
@ -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, "{")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 “quotes” and single ‘quotes’`)
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,41 +148,15 @@ 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
|
||||
hooks.BaseContext
|
||||
|
||||
typ string // inner or block
|
||||
inner string
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
175
markup/goldmark/tables/tables.go
Normal file
175
markup/goldmark/tables/tables.go
Normal file
|
@ -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
|
||||
}
|
181
markup/goldmark/tables/tables_integration_test.go
Normal file
181
markup/goldmark/tables/tables_integration_test.go
Normal file
|
@ -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>")
|
||||
}
|
|
@ -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>
|
Loading…
Reference in a new issue