Skip to content

Commit

Permalink
Add Markdown render hooks for tables
Browse files Browse the repository at this point in the history
  • Loading branch information
bep committed Aug 30, 2024
1 parent a3684c8 commit 14ddc0b
Show file tree
Hide file tree
Showing 13 changed files with 515 additions and 244 deletions.
4 changes: 3 additions & 1 deletion common/types/hstring/stringtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

package hstring

type RenderedString string
import "html/template"

type RenderedString template.HTML

func (s RenderedString) String() string {
return string(s)
Expand Down
2 changes: 2 additions & 0 deletions hugolib/page__per_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions hugolib/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
50 changes: 37 additions & 13 deletions markup/converter/hooks/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -73,19 +72,31 @@ type CodeblockContext interface {

// The text between the code fences.
Inner() string

// Zero-based ordinal for all code blocks in the current document.
Ordinal() int
}

// BlockquoteContext is the context passed to a blockquote render hook.
type BlockquoteContext interface {
// TableContext is the context passed to a table render hook.
type TableContext interface {
BaseContext
AttributesProvider

THead() []TableRow
TBody() []TableRow
}

// BaseContext is the base context used in most render hooks.
type BaseContext interface {
text.Positioner
PageProvider

// Zero-based ordinal for all block quotes in the current document.
// Zero-based ordinal for all elements of this kind in the current document.
Ordinal() int
}

// BlockquoteContext is the context passed to a blockquote render hook.
type BlockquoteContext interface {
BaseContext

AttributesProvider

// The blockquote text.
// If type is "alert", this will be the alert text.
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
89 changes: 9 additions & 80 deletions markup/goldmark/blockquotes/blockquotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,69 +72,37 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
}

pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:]
text := string(ctx.Buffer.Bytes()[pos:])
ctx.Buffer.Truncate(pos)

ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)

texts := string(text)
typ := typeRegular
alertType := resolveGitHubAlert(texts)
alertType := resolveGitHubAlert(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, 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(
Expand All @@ -149,18 +117,6 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
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,
Expand All @@ -180,13 +136,11 @@ func (r *htmlRenderer) renderBlockquoteDefault(
}

type blockquoteContext struct {
page any
pageInner any
hooks.BaseContext

text hstring.RenderedString
typ string
sourceRef []byte
alertType string
ordinal int
typ string

// This is only used in error situations and is expensive to create,
// so delay creation until needed.
Expand All @@ -205,35 +159,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]
Expand Down
71 changes: 6 additions & 65 deletions markup/goldmark/codeblocks/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"strings"
"sync"

"github.com/gohugoio/hugo/common/herrors"
htext "github.com/gohugoio/hugo/common/text"
Expand Down Expand Up @@ -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, 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(
Expand All @@ -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
}
Expand All @@ -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, "{")
Expand Down
Loading

0 comments on commit 14ddc0b

Please sign in to comment.