mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05: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 {
|
if id != nil {
|
||||||
layoutDescriptor.KindVariants = id.(string)
|
layoutDescriptor.KindVariants = id.(string)
|
||||||
}
|
}
|
||||||
|
case hooks.TableRendererType:
|
||||||
|
layoutDescriptor.Kind = "render-table"
|
||||||
case hooks.CodeBlockRendererType:
|
case hooks.CodeBlockRendererType:
|
||||||
layoutDescriptor.Kind = "render-codeblock"
|
layoutDescriptor.Kind = "render-codeblock"
|
||||||
if id != nil {
|
if id != nil {
|
||||||
|
@ -334,13 +336,23 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
||||||
|
|
||||||
templ, found1 := getHookTemplate(pco.po.f)
|
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.
|
// 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 {
|
if f.Name == pco.po.f.Name {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
templ2, found2 := getHookTemplate(f)
|
templ2, found2 := getHookTemplate(f)
|
||||||
|
|
||||||
if found2 {
|
if found2 {
|
||||||
if !found1 {
|
if !found1 {
|
||||||
templ = templ2
|
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)
|
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 {
|
func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
|
||||||
return hr.resolvePosition(ctx)
|
return hr.resolvePosition(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,9 +61,8 @@ type ImageLinkContext interface {
|
||||||
|
|
||||||
// CodeblockContext is the context passed to a code block render hook.
|
// CodeblockContext is the context passed to a code block render hook.
|
||||||
type CodeblockContext interface {
|
type CodeblockContext interface {
|
||||||
|
BaseContext
|
||||||
AttributesProvider
|
AttributesProvider
|
||||||
text.Positioner
|
|
||||||
PageProvider
|
|
||||||
|
|
||||||
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
|
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
|
||||||
Options() map[string]any
|
Options() map[string]any
|
||||||
|
@ -73,19 +72,31 @@ type CodeblockContext interface {
|
||||||
|
|
||||||
// The text between the code fences.
|
// The text between the code fences.
|
||||||
Inner() string
|
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
|
Ordinal() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockquoteContext is the context passed to a blockquote render hook.
|
// BlockquoteContext is the context passed to a blockquote render hook.
|
||||||
type BlockquoteContext interface {
|
type BlockquoteContext interface {
|
||||||
AttributesProvider
|
BaseContext
|
||||||
text.Positioner
|
|
||||||
PageProvider
|
|
||||||
|
|
||||||
// Zero-based ordinal for all block quotes in the current document.
|
AttributesProvider
|
||||||
Ordinal() int
|
|
||||||
|
|
||||||
// The blockquote text.
|
// The blockquote text.
|
||||||
// If type is "alert", this will be the alert 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.
|
// PassThroughContext is the context passed to a passthrough render hook.
|
||||||
type PassthroughContext interface {
|
type PassthroughContext interface {
|
||||||
|
BaseContext
|
||||||
AttributesProvider
|
AttributesProvider
|
||||||
text.Positioner
|
|
||||||
PageProvider
|
|
||||||
|
|
||||||
// Currently one of "inline" or "block".
|
// Currently one of "inline" or "block".
|
||||||
Type() string
|
Type() string
|
||||||
|
|
||||||
// The inner content of the passthrough element, excluding the delimiters.
|
// The inner content of the passthrough element, excluding the delimiters.
|
||||||
Inner() string
|
Inner() string
|
||||||
|
|
||||||
// Zero-based ordinal for all passthrough elements in the document.
|
|
||||||
Ordinal() int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttributesOptionsSliceProvider interface {
|
type AttributesOptionsSliceProvider interface {
|
||||||
|
@ -138,6 +145,10 @@ type BlockquoteRenderer interface {
|
||||||
RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx BlockquoteContext) error
|
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 {
|
type PassthroughRenderer interface {
|
||||||
RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
|
RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
|
||||||
}
|
}
|
||||||
|
@ -196,6 +207,19 @@ const (
|
||||||
CodeBlockRendererType
|
CodeBlockRendererType
|
||||||
PassthroughRendererType
|
PassthroughRendererType
|
||||||
BlockquoteRendererType
|
BlockquoteRendererType
|
||||||
|
TableRendererType
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetRendererFunc func(t RendererType, id any) any
|
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 (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/herrors"
|
"github.com/gohugoio/hugo/common/herrors"
|
||||||
htext "github.com/gohugoio/hugo/common/text"
|
|
||||||
"github.com/gohugoio/hugo/common/types/hstring"
|
"github.com/gohugoio/hugo/common/types/hstring"
|
||||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
"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
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := ctx.PopPos()
|
text := ctx.PopRenderedString()
|
||||||
text := ctx.Buffer.Bytes()[pos:]
|
|
||||||
ctx.Buffer.Truncate(pos)
|
|
||||||
|
|
||||||
ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)
|
ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)
|
||||||
|
|
||||||
texts := string(text)
|
|
||||||
typ := typeRegular
|
typ := typeRegular
|
||||||
alertType := resolveGitHubAlert(texts)
|
alertType := resolveGitHubAlert(string(text))
|
||||||
if alertType != "" {
|
if alertType != "" {
|
||||||
typ = typeAlert
|
typ = typeAlert
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer := ctx.RenderContext().GetRenderer(hooks.BlockquoteRendererType, typ)
|
renderer := ctx.RenderContext().GetRenderer(hooks.BlockquoteRendererType, typ)
|
||||||
if renderer == nil {
|
if renderer == nil {
|
||||||
return r.renderBlockquoteDefault(w, n, texts)
|
return r.renderBlockquoteDefault(w, n, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if typ == typeAlert {
|
if typ == typeAlert {
|
||||||
// Trim preamble: <p>[!NOTE]<br>\n but preserve leading paragraph.
|
// Trim preamble: <p>[!NOTE]<br>\n but preserve leading paragraph.
|
||||||
// We could possibly complicate this by moving this to the parser, but
|
// We could possibly complicate this by moving this to the parser, but
|
||||||
// keep it simple for now.
|
// keep it simple for now.
|
||||||
texts = "<p>" + texts[strings.Index(texts, "\n")+1:]
|
text = "<p>" + text[strings.Index(text, "\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]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bqctx := &blockquoteContext{
|
bqctx := &blockquoteContext{
|
||||||
page: ctx.DocumentContext().Document,
|
BaseContext: render.NewBaseContext(ctx, renderer, n, src, nil, ordinal),
|
||||||
pageInner: r.getPageInner(ctx),
|
|
||||||
typ: typ,
|
typ: typ,
|
||||||
alertType: alertType,
|
alertType: alertType,
|
||||||
text: hstring.RenderedString(texts),
|
text: hstring.RenderedString(text),
|
||||||
sourceRef: sourceRef,
|
|
||||||
ordinal: ordinal,
|
|
||||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
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)
|
cr := renderer.(hooks.BlockquoteRenderer)
|
||||||
|
|
||||||
err := cr.RenderBlockquote(
|
err := cr.RenderBlockquote(
|
||||||
|
@ -143,24 +107,12 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
|
||||||
bqctx,
|
bqctx,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.createPos())
|
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.Position())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ast.WalkContinue, nil
|
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.
|
// Code borrowed from goldmark's html renderer.
|
||||||
func (r *htmlRenderer) renderBlockquoteDefault(
|
func (r *htmlRenderer) renderBlockquoteDefault(
|
||||||
w util.BufWriter, n ast.Node, text string,
|
w util.BufWriter, n ast.Node, text string,
|
||||||
|
@ -180,19 +132,11 @@ func (r *htmlRenderer) renderBlockquoteDefault(
|
||||||
}
|
}
|
||||||
|
|
||||||
type blockquoteContext struct {
|
type blockquoteContext struct {
|
||||||
page any
|
hooks.BaseContext
|
||||||
pageInner any
|
|
||||||
text hstring.RenderedString
|
|
||||||
typ string
|
|
||||||
sourceRef []byte
|
|
||||||
alertType string
|
|
||||||
ordinal int
|
|
||||||
|
|
||||||
// This is only used in error situations and is expensive to create,
|
text hstring.RenderedString
|
||||||
// so delay creation until needed.
|
alertType string
|
||||||
pos htext.Position
|
typ string
|
||||||
posInit sync.Once
|
|
||||||
createPos func() htext.Position
|
|
||||||
|
|
||||||
*attributes.AttributesHolder
|
*attributes.AttributesHolder
|
||||||
}
|
}
|
||||||
|
@ -205,35 +149,10 @@ func (c *blockquoteContext) AlertType() string {
|
||||||
return c.alertType
|
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 {
|
func (c *blockquoteContext) Text() hstring.RenderedString {
|
||||||
return c.text
|
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
|
// 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:
|
// Five types:
|
||||||
// [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
|
// [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/herrors"
|
"github.com/gohugoio/hugo/common/herrors"
|
||||||
htext "github.com/gohugoio/hugo/common/text"
|
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 {
|
if err != nil {
|
||||||
return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
|
return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
|
||||||
}
|
}
|
||||||
|
|
||||||
cbctx := &codeBlockContext{
|
cbctx := &codeBlockContext{
|
||||||
page: ctx.DocumentContext().Document,
|
BaseContext: render.NewBaseContext(ctx, renderer, node, src, func() []byte { return []byte(s) }, ordinal),
|
||||||
pageInner: r.getPageInner(ctx),
|
|
||||||
lang: lang,
|
lang: lang,
|
||||||
code: s,
|
code: s,
|
||||||
ordinal: ordinal,
|
|
||||||
AttributesHolder: attributes.New(attrs, attrtp),
|
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)
|
cr := renderer.(hooks.CodeBlockRenderer)
|
||||||
|
|
||||||
err = cr.RenderCodeblock(
|
err = cr.RenderCodeblock(
|
||||||
|
@ -129,50 +116,20 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||||
cbctx,
|
cbctx,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
|
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.Position())
|
||||||
}
|
}
|
||||||
|
|
||||||
return ast.WalkContinue, nil
|
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 {
|
type codeBlockContext struct {
|
||||||
page any
|
hooks.BaseContext
|
||||||
pageInner any
|
lang string
|
||||||
lang string
|
code 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
|
|
||||||
|
|
||||||
*attributes.AttributesHolder
|
*attributes.AttributesHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *codeBlockContext) Page() any {
|
|
||||||
return c.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *codeBlockContext) PageInner() any {
|
|
||||||
return c.pageInner
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *codeBlockContext) Type() string {
|
func (c *codeBlockContext) Type() string {
|
||||||
return c.lang
|
return c.lang
|
||||||
}
|
}
|
||||||
|
@ -181,22 +138,6 @@ func (c *codeBlockContext) Inner() string {
|
||||||
return c.code
|
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 {
|
func getLang(node *ast.FencedCodeBlock, src []byte) string {
|
||||||
langWithAttributes := string(node.Language(src))
|
langWithAttributes := string(node.Language(src))
|
||||||
lang, _, _ := strings.Cut(langWithAttributes, "{")
|
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/extensions/attributes"
|
||||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
||||||
"github.com/gohugoio/hugo/markup/goldmark/passthrough"
|
"github.com/gohugoio/hugo/markup/goldmark/passthrough"
|
||||||
|
"github.com/gohugoio/hugo/markup/goldmark/tables"
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
|
@ -131,6 +132,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
|
||||||
|
|
||||||
if cfg.Extensions.Table {
|
if cfg.Extensions.Table {
|
||||||
extensions = append(extensions, extension.Table)
|
extensions = append(extensions, extension.Table)
|
||||||
|
extensions = append(extensions, tables.New())
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Extensions.Strikethrough {
|
if cfg.Extensions.Strikethrough {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package goldmark_test
|
package goldmark_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -30,6 +31,7 @@ import (
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/markup_config"
|
"github.com/gohugoio/hugo/markup/markup_config"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/common/hugio"
|
||||||
"github.com/gohugoio/hugo/common/loggers"
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"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)
|
h := highlight.New(mconf.Highlight)
|
||||||
|
|
||||||
getRenderer := func(t hooks.RendererType, id any) any {
|
getRenderer := func(t hooks.RendererType, id any) any {
|
||||||
if t == hooks.CodeBlockRendererType {
|
switch t {
|
||||||
|
case hooks.CodeBlockRendererType:
|
||||||
return h
|
return h
|
||||||
|
case hooks.TableRendererType:
|
||||||
|
return tableRenderer(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,8 +174,6 @@ unsafe = true
|
||||||
b := convert(c, testconfig.GetTestConfig(nil, cfg), content)
|
b := convert(c, testconfig.GetTestConfig(nil, cfg), content)
|
||||||
got := string(b.Bytes())
|
got := string(b.Bytes())
|
||||||
|
|
||||||
fmt.Println(got)
|
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
|
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>`)
|
c.Assert(got, qt.Contains, `<a href="https://foo.bar/">https://foo.bar/</a>`)
|
||||||
|
@ -191,7 +195,7 @@ unsafe = true
|
||||||
// Extensions
|
// Extensions
|
||||||
c.Assert(got, qt.Contains, `Autolink: <a href="https://gohugo.io/">https://gohugo.io/</a>`)
|
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, `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, `<li><input disabled="" type="checkbox"> Push my commits to GitHub</li>`)
|
||||||
|
|
||||||
c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`)
|
c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`)
|
||||||
|
@ -378,7 +382,7 @@ func TestConvertAttributes(t *testing.T) {
|
||||||
| ------------- |:-------------:| -----:|
|
| ------------- |:-------------:| -----:|
|
||||||
| AV | BV |
|
| AV | BV |
|
||||||
{.myclass }`,
|
{.myclass }`,
|
||||||
"<table class=\"myclass\">\n<thead>",
|
"Table",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Title and Blockquote",
|
"Title and Blockquote",
|
||||||
|
@ -741,3 +745,11 @@ escapedSpace=true
|
||||||
|
|
||||||
c.Assert(got, qt.Contains, "<p>私は太郎です。\nプログラミングが好きです。運動が苦手です。</p>\n")
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
htext "github.com/gohugoio/hugo/common/text"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,6 +49,7 @@ type Context struct {
|
||||||
positions []int
|
positions []int
|
||||||
pids []uint64
|
pids []uint64
|
||||||
ordinals map[ast.NodeKind]int
|
ordinals map[ast.NodeKind]int
|
||||||
|
values map[ast.NodeKind][]any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
|
func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
|
||||||
|
@ -67,6 +72,13 @@ func (ctx *Context) PopPos() int {
|
||||||
return p
|
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.
|
// PushPid pushes a new page ID to the stack.
|
||||||
func (ctx *Context) PushPid(pid uint64) {
|
func (ctx *Context) PushPid(pid uint64) {
|
||||||
ctx.pids = append(ctx.pids, pid)
|
ctx.pids = append(ctx.pids, pid)
|
||||||
|
@ -91,6 +103,38 @@ func (ctx *Context) PopPid() uint64 {
|
||||||
return p
|
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 {
|
type ContextData interface {
|
||||||
RenderContext() converter.RenderContext
|
RenderContext() converter.RenderContext
|
||||||
DocumentContext() converter.DocumentContext
|
DocumentContext() converter.DocumentContext
|
||||||
|
@ -108,3 +152,109 @@ func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
|
||||||
func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
|
func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
|
||||||
return ctx.Dctx
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"sync"
|
|
||||||
|
|
||||||
htext "github.com/gohugoio/hugo/common/text"
|
|
||||||
|
|
||||||
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
||||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
"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)]
|
s = s[len(delims.Open) : len(s)-len(delims.Close)]
|
||||||
|
|
||||||
pctx := &passthroughContext{
|
pctx := &passthroughContext{
|
||||||
ordinal: ordinal,
|
BaseContext: render.NewBaseContext(ctx, renderer, node, src, nil, ordinal),
|
||||||
page: ctx.DocumentContext().Document,
|
|
||||||
pageInner: r.getPageInner(ctx),
|
|
||||||
inner: s,
|
inner: s,
|
||||||
typ: typ,
|
typ: typ,
|
||||||
AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral),
|
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)
|
pr := renderer.(hooks.PassthroughRenderer)
|
||||||
|
|
||||||
if err := pr.RenderPassthrough(ctx.RenderContext().Ctx, w, pctx); err != nil {
|
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
|
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 {
|
type passthroughContext struct {
|
||||||
page any
|
hooks.BaseContext
|
||||||
pageInner any
|
|
||||||
typ string // inner or block
|
typ string // inner or block
|
||||||
inner string
|
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
|
*attributes.AttributesHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *passthroughContext) Page() any {
|
|
||||||
return p.page
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *passthroughContext) PageInner() any {
|
|
||||||
return p.pageInner
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *passthroughContext) Type() string {
|
func (p *passthroughContext) Type() string {
|
||||||
return p.typ
|
return p.typ
|
||||||
}
|
}
|
||||||
|
@ -206,21 +164,3 @@ func (p *passthroughContext) Type() string {
|
||||||
func (p *passthroughContext) Inner() string {
|
func (p *passthroughContext) Inner() string {
|
||||||
return p.inner
|
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
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := ctx.PopPos()
|
text := ctx.PopRenderedString()
|
||||||
text := ctx.Buffer.Bytes()[pos:]
|
|
||||||
ctx.Buffer.Truncate(pos)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
isBlock bool
|
isBlock bool
|
||||||
|
@ -190,13 +188,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
// internal attributes before rendering.
|
// internal attributes before rendering.
|
||||||
attrs := r.filterInternalAttributes(n.Attributes())
|
attrs := r.filterInternalAttributes(n.Attributes())
|
||||||
|
|
||||||
|
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||||
|
|
||||||
err := lr.RenderLink(
|
err := lr.RenderLink(
|
||||||
ctx.RenderContext().Ctx,
|
ctx.RenderContext().Ctx,
|
||||||
w,
|
w,
|
||||||
imageLinkContext{
|
imageLinkContext{
|
||||||
linkContext: linkContext{
|
linkContext: linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: page,
|
||||||
pageInner: r.getPageInner(ctx),
|
pageInner: pageInner,
|
||||||
destination: string(n.Destination),
|
destination: string(n.Destination),
|
||||||
title: string(n.Title),
|
title: string(n.Title),
|
||||||
text: hstring.RenderedString(text),
|
text: hstring.RenderedString(text),
|
||||||
|
@ -211,18 +211,6 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||||
return ast.WalkContinue, err
|
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 {
|
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
|
||||||
n := 0
|
n := 0
|
||||||
for _, x := range attrs {
|
for _, x := range attrs {
|
||||||
|
@ -288,16 +276,16 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := ctx.PopPos()
|
text := ctx.PopRenderedString()
|
||||||
text := ctx.Buffer.Bytes()[pos:]
|
|
||||||
ctx.Buffer.Truncate(pos)
|
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||||
|
|
||||||
err := lr.RenderLink(
|
err := lr.RenderLink(
|
||||||
ctx.RenderContext().Ctx,
|
ctx.RenderContext().Ctx,
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: page,
|
||||||
pageInner: r.getPageInner(ctx),
|
pageInner: pageInner,
|
||||||
destination: string(n.Destination),
|
destination: string(n.Destination),
|
||||||
title: string(n.Title),
|
title: string(n.Title),
|
||||||
text: hstring.RenderedString(text),
|
text: hstring.RenderedString(text),
|
||||||
|
@ -358,12 +346,14 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||||
url = "mailto:" + url
|
url = "mailto:" + url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||||
|
|
||||||
err := lr.RenderLink(
|
err := lr.RenderLink(
|
||||||
ctx.RenderContext().Ctx,
|
ctx.RenderContext().Ctx,
|
||||||
w,
|
w,
|
||||||
linkContext{
|
linkContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: page,
|
||||||
pageInner: r.getPageInner(ctx),
|
pageInner: pageInner,
|
||||||
destination: url,
|
destination: url,
|
||||||
text: hstring.RenderedString(label),
|
text: hstring.RenderedString(label),
|
||||||
plainText: label,
|
plainText: label,
|
||||||
|
@ -435,20 +425,21 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := ctx.PopPos()
|
text := ctx.PopRenderedString()
|
||||||
text := ctx.Buffer.Bytes()[pos:]
|
|
||||||
ctx.Buffer.Truncate(pos)
|
|
||||||
// All ast.Heading nodes are guaranteed to have an attribute called "id"
|
// All ast.Heading nodes are guaranteed to have an attribute called "id"
|
||||||
// that is an array of bytes that encode a valid string.
|
// that is an array of bytes that encode a valid string.
|
||||||
anchori, _ := n.AttributeString("id")
|
anchori, _ := n.AttributeString("id")
|
||||||
anchor := anchori.([]byte)
|
anchor := anchori.([]byte)
|
||||||
|
|
||||||
|
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||||
|
|
||||||
err := hr.RenderHeading(
|
err := hr.RenderHeading(
|
||||||
ctx.RenderContext().Ctx,
|
ctx.RenderContext().Ctx,
|
||||||
w,
|
w,
|
||||||
headingContext{
|
headingContext{
|
||||||
page: ctx.DocumentContext().Document,
|
page: page,
|
||||||
pageInner: r.getPageInner(ctx),
|
pageInner: pageInner,
|
||||||
level: n.Level,
|
level: n.Level,
|
||||||
anchor: string(anchor),
|
anchor: string(anchor),
|
||||||
text: hstring.RenderedString(text),
|
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