summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go612
1 files changed, 597 insertions, 15 deletions
diff --git a/main.go b/main.go
index 96c1115..150cdad 100644
--- a/main.go
+++ b/main.go
@@ -1,12 +1,14 @@
package main
import (
+ "bufio"
"bytes"
"encoding/json"
"encoding/xml"
"errors"
"flag"
"fmt"
+ stdhtml "html"
"html/template"
"io"
"io/fs"
@@ -20,11 +22,18 @@ import (
"syscall"
"time"
+ chromahtml "github.com/alecthomas/chroma/v3/formatters/html"
+ "github.com/alecthomas/chroma/v3/lexers"
+ "github.com/alecthomas/chroma/v3/styles"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
+ extast "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ gmhtml "github.com/yuin/goldmark/renderer/html"
gmtext "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
"golang.org/x/net/html"
"gopkg.in/yaml.v3"
)
@@ -45,6 +54,9 @@ type Page struct {
Meta map[string]any
ModTime time.Time
Sections []template.HTML
+
+ markdownSource []byte
+ markdownAST ast.Node
}
type templateData struct {
@@ -67,6 +79,7 @@ type site struct {
templates *template.Template
fallback string
warnings []string
+ markdown goldmark.Markdown
}
type manifest struct {
@@ -109,13 +122,19 @@ func buildWithFallback(root, canonicalRoot, fallback string, manageGitignore boo
return fmt.Errorf("site root is not a directory: %s", abs)
}
- s := &site{root: abs, canonicalRoot: canonicalRoot, fallback: fallback, pages: map[string][]*Page{}}
+ s := &site{
+ root: abs, canonicalRoot: canonicalRoot, fallback: fallback,
+ pages: map[string][]*Page{}, markdown: newMarkdown(),
+ }
if err := s.discover(); err != nil {
return err
}
if err := s.loadTemplates(); err != nil {
return err
}
+ if err := s.prepareMarkdown(); err != nil {
+ return err
+ }
outputs, err := s.render()
if err != nil {
return err
@@ -252,8 +271,10 @@ func (s *site) parsePage(rel, out string, info fs.FileInfo) (*Page, []string, er
if err != nil {
return nil, nil, err
}
- md := goldmark.New(goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions(parser.WithAutoHeadingID()))
- doc := md.Parser().Parse(gmtext.NewReader(body))
+ if s.markdown == nil {
+ s.markdown = newMarkdown()
+ }
+ doc := s.markdown.Parser().Parse(gmtext.NewReader(body))
rewriteLinks(doc)
title := extractTitle(doc, body)
var warnings []string
@@ -266,10 +287,6 @@ func (s *site) parsePage(rel, out string, info fs.FileInfo) (*Page, []string, er
warnings = append(warnings, fmt.Sprintf("%s has no H1; using filename-derived title %q", rel, title))
}
}
- sections, err := renderSections(md, doc, body)
- if err != nil {
- return nil, nil, err
- }
urlPath := "/" + out
if path.Base(out) == "index.html" {
urlPath = "/" + strings.TrimSuffix(out, "index.html")
@@ -277,10 +294,17 @@ func (s *site) parsePage(rel, out string, info fs.FileInfo) (*Page, []string, er
return &Page{
SourcePath: rel, OutputPath: out, URL: urlPath,
CanonicalURL: s.canonicalRoot + urlPath, Title: title, Meta: meta,
- ModTime: info.ModTime(), Sections: sections,
+ ModTime: info.ModTime(), markdownSource: body, markdownAST: doc,
}, warnings, nil
}
+func newMarkdown() goldmark.Markdown {
+ return goldmark.New(
+ goldmark.WithExtensions(extension.GFM),
+ goldmark.WithParserOptions(parser.WithAutoHeadingID()),
+ )
+}
+
func frontmatter(raw []byte) (map[string]any, []byte, error) {
meta := map[string]any{}
if !bytes.HasPrefix(raw, []byte("---\n")) && !bytes.HasPrefix(raw, []byte("---\r\n")) {
@@ -341,17 +365,12 @@ func rewriteDestination(destination []byte) []byte {
func renderSections(md goldmark.Markdown, doc ast.Node, source []byte) ([]template.HTML, error) {
var sections []template.HTML
- for node := doc.FirstChild(); node != nil; {
- next := node.NextSibling()
- doc.RemoveChild(doc, node)
- fragment := ast.NewDocument()
- fragment.AppendChild(fragment, node)
+ for node := doc.FirstChild(); node != nil; node = node.NextSibling() {
var out bytes.Buffer
- if err := md.Renderer().Render(&out, source, fragment); err != nil {
+ if err := md.Renderer().Render(&out, source, node); err != nil {
return nil, err
}
sections = append(sections, template.HTML(out.String())) // Goldmark raw HTML is disabled.
- node = next
}
return sections, nil
}
@@ -387,10 +406,573 @@ func (s *site) loadTemplates() error {
return fmt.Errorf("parse template %s: %w", src.path, err)
}
}
+ for _, tmpl := range t.Templates() {
+ if strings.HasPrefix(tmpl.Name(), "markdown/") && !supportedMarkdownTemplates[tmpl.Name()] {
+ return fmt.Errorf("unsupported Markdown template %q", tmpl.Name())
+ }
+ }
s.templates = t
return nil
}
+func (s *site) prepareMarkdown() error {
+ r := &markdownRenderer{templates: s.templates}
+ r.renderer = s.markdown.Renderer()
+ s.markdown.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(r, 0)))
+ for _, src := range s.sources {
+ if src.kind != "markdown" {
+ continue
+ }
+ r.sourcePath = src.path
+ sections, err := renderSections(s.markdown, src.page.markdownAST, src.page.markdownSource)
+ if err != nil {
+ return err
+ }
+ src.page.Sections = sections
+ }
+ return nil
+}
+
+var supportedMarkdownTemplates = map[string]bool{
+ "markdown/heading1": true, "markdown/heading2": true,
+ "markdown/heading3": true, "markdown/heading4": true,
+ "markdown/heading5": true, "markdown/heading6": true,
+ "markdown/paragraph": true, "markdown/blockquote": true,
+ "markdown/code_block": true, "markdown/fenced_code_block": true,
+ "markdown/unordered_list": true, "markdown/ordered_list": true,
+ "markdown/list_item": true, "markdown/thematic_break": true,
+ "markdown/emphasis": true, "markdown/strong": true,
+ "markdown/code_span": true, "markdown/link": true,
+ "markdown/autolink": true, "markdown/email_autolink": true,
+ "markdown/image": true, "markdown/hard_break": true,
+ "markdown/strikethrough": true, "markdown/task_checkbox": true,
+ "markdown/table": true, "markdown/table_header": true,
+ "markdown/table_body": true, "markdown/table_row": true,
+ "markdown/table_header_cell": true, "markdown/table_cell": true,
+}
+
+var tableMarkdownTemplates = []string{
+ "markdown/table", "markdown/table_header", "markdown/table_body",
+ "markdown/table_row", "markdown/table_header_cell", "markdown/table_cell",
+}
+
+type markdownContent struct{ Content template.HTML }
+type markdownHeading struct {
+ Content template.HTML
+ ID string
+}
+type markdownFencedCode struct {
+ Content template.HTML
+ HighlightedContent template.HTML
+ Language string
+}
+type markdownList struct {
+ Content template.HTML
+ Tight bool
+}
+type markdownOrderedList struct {
+ Content template.HTML
+ Start int
+ Tight bool
+}
+type markdownLink struct {
+ Content template.HTML
+ Destination string
+ Title string
+}
+type markdownAutoLink struct {
+ Content template.HTML
+ Destination string
+}
+type markdownImage struct {
+ Alt string
+ Destination string
+ Title string
+}
+type markdownTask struct{ Checked bool }
+type markdownCell struct {
+ Content template.HTML
+ Alignment string
+}
+
+type markdownRenderer struct {
+ templates *template.Template
+ renderer renderer.Renderer
+ sourcePath string
+}
+
+func (r *markdownRenderer) has(name string) bool { return r.templates.Lookup(name) != nil }
+
+func (r *markdownRenderer) hasAny(names ...string) bool {
+ for _, name := range names {
+ if r.has(name) {
+ return true
+ }
+ }
+ return false
+}
+
+func (r *markdownRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ if r.hasAny("markdown/heading1", "markdown/heading2", "markdown/heading3", "markdown/heading4", "markdown/heading5", "markdown/heading6") {
+ reg.Register(ast.KindHeading, r.renderHeading)
+ }
+ if r.has("markdown/paragraph") {
+ reg.Register(ast.KindParagraph, r.renderContainer("markdown/paragraph"))
+ }
+ if r.has("markdown/blockquote") {
+ reg.Register(ast.KindBlockquote, r.renderContainer("markdown/blockquote"))
+ }
+ if r.has("markdown/code_block") {
+ reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
+ }
+ if r.has("markdown/fenced_code_block") {
+ reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
+ }
+ if r.hasAny("markdown/unordered_list", "markdown/ordered_list") {
+ reg.Register(ast.KindList, r.renderList)
+ }
+ if r.has("markdown/list_item") {
+ reg.Register(ast.KindListItem, r.renderContainer("markdown/list_item"))
+ }
+ if r.has("markdown/thematic_break") {
+ reg.Register(ast.KindThematicBreak, r.renderLeaf("markdown/thematic_break", struct{}{}))
+ }
+ if r.hasAny("markdown/emphasis", "markdown/strong") {
+ reg.Register(ast.KindEmphasis, r.renderEmphasis)
+ }
+ if r.has("markdown/code_span") {
+ reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
+ }
+ if r.has("markdown/link") {
+ reg.Register(ast.KindLink, r.renderLink)
+ }
+ if r.hasAny("markdown/autolink", "markdown/email_autolink") {
+ reg.Register(ast.KindAutoLink, r.renderAutoLink)
+ }
+ if r.has("markdown/image") {
+ reg.Register(ast.KindImage, r.renderImage)
+ }
+ if r.has("markdown/hard_break") {
+ reg.Register(ast.KindText, r.renderText)
+ }
+ if r.has("markdown/strikethrough") {
+ reg.Register(extast.KindStrikethrough, r.renderContainer("markdown/strikethrough"))
+ }
+ if r.has("markdown/task_checkbox") {
+ reg.Register(extast.KindTaskCheckBox, r.renderTaskCheckbox)
+ }
+ if r.hasAny(tableMarkdownTemplates...) {
+ reg.Register(extast.KindTable, r.renderTable)
+ }
+}
+
+func (r *markdownRenderer) children(source []byte, node ast.Node) (template.HTML, error) {
+ var out bytes.Buffer
+ for child := node.FirstChild(); child != nil; child = child.NextSibling() {
+ if err := r.renderer.Render(&out, source, child); err != nil {
+ return "", err
+ }
+ }
+ return template.HTML(out.String()), nil
+}
+
+func (r *markdownRenderer) execute(name string, data any) ([]byte, error) {
+ var out bytes.Buffer
+ if err := r.templates.ExecuteTemplate(&out, name, data); err != nil {
+ return nil, fmt.Errorf("render %s %s: %w", r.sourcePath, name, err)
+ }
+ return out.Bytes(), nil
+}
+
+func (r *markdownRenderer) writeTemplate(w util.BufWriter, name string, data any) (ast.WalkStatus, error) {
+ out, err := r.execute(name, data)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ _, _ = w.Write(out)
+ return ast.WalkSkipChildren, nil
+}
+
+func (r *markdownRenderer) renderContainer(name string) renderer.NodeRendererFunc {
+ return func(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ content, err := r.children(source, node)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ return r.writeTemplate(w, name, markdownContent{Content: content})
+ }
+}
+
+func (r *markdownRenderer) renderLeaf(name string, data any) renderer.NodeRendererFunc {
+ return func(w util.BufWriter, _ []byte, _ ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ return r.writeTemplate(w, name, data)
+ }
+}
+
+func (r *markdownRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ heading := node.(*ast.Heading)
+ content, err := r.children(source, node)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ var id string
+ if value, ok := heading.AttributeString("id"); ok {
+ switch value := value.(type) {
+ case []byte:
+ id = string(value)
+ case string:
+ id = value
+ }
+ }
+ name := fmt.Sprintf("markdown/heading%d", heading.Level)
+ if r.has(name) {
+ return r.writeTemplate(w, name, markdownHeading{Content: content, ID: id})
+ }
+ _, _ = fmt.Fprintf(w, "<h%d", heading.Level)
+ if id != "" {
+ _, _ = w.WriteString(` id="`)
+ _, _ = w.Write(util.EscapeHTML([]byte(id)))
+ _ = w.WriteByte('"')
+ }
+ _, _ = fmt.Fprintf(w, ">%s</h%d>\n", content, heading.Level)
+ return ast.WalkSkipChildren, nil
+}
+
+func rawLines(source []byte, node ast.Node) []byte {
+ var out bytes.Buffer
+ for i := 0; i < node.Lines().Len(); i++ {
+ line := node.Lines().At(i)
+ _, _ = out.Write(line.Value(source))
+ }
+ return out.Bytes()
+}
+
+func escapedMarkdown(source []byte) template.HTML {
+ return template.HTML(util.EscapeHTML(source))
+}
+
+func (r *markdownRenderer) renderCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ return r.writeTemplate(w, "markdown/code_block", markdownContent{Content: escapedMarkdown(rawLines(source, node))})
+}
+
+func highlightedCode(language string, source []byte) template.HTML {
+ if language == "" {
+ return ""
+ }
+ known := false
+ for _, name := range lexers.Names(true) {
+ if strings.EqualFold(name, language) {
+ known = true
+ break
+ }
+ }
+ if !known {
+ return ""
+ }
+ lexer := lexers.Get(language)
+ if lexer == nil {
+ return ""
+ }
+ tokens, err := lexer.Tokenise(nil, string(source))
+ if err != nil {
+ return ""
+ }
+ formatter := chromahtml.New(
+ chromahtml.WithClasses(true),
+ chromahtml.ClassPrefix("chroma-"),
+ chromahtml.PreventSurroundingPre(true),
+ )
+ var out bytes.Buffer
+ if err := formatter.Format(&out, styles.Fallback, tokens); err != nil {
+ return ""
+ }
+ return template.HTML(out.String())
+}
+
+func (r *markdownRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ block := node.(*ast.FencedCodeBlock)
+ code := rawLines(source, node)
+ language := string(block.Language(source))
+ return r.writeTemplate(w, "markdown/fenced_code_block", markdownFencedCode{
+ Content: escapedMarkdown(code), HighlightedContent: highlightedCode(language, code), Language: language,
+ })
+}
+
+func (r *markdownRenderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ list := node.(*ast.List)
+ content, err := r.children(source, node)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ name := "markdown/unordered_list"
+ if list.IsOrdered() {
+ name = "markdown/ordered_list"
+ }
+ if r.has(name) {
+ if list.IsOrdered() {
+ return r.writeTemplate(w, name, markdownOrderedList{Content: content, Start: list.Start, Tight: list.IsTight})
+ }
+ return r.writeTemplate(w, name, markdownList{Content: content, Tight: list.IsTight})
+ }
+ tag := "ul"
+ if list.IsOrdered() {
+ tag = "ol"
+ }
+ _, _ = fmt.Fprintf(w, "<%s", tag)
+ if list.IsOrdered() && list.Start != 1 {
+ _, _ = fmt.Fprintf(w, ` start="%d"`, list.Start)
+ }
+ _, _ = fmt.Fprintf(w, ">\n%s</%s>\n", content, tag)
+ return ast.WalkSkipChildren, nil
+}
+
+func (r *markdownRenderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ emphasis := node.(*ast.Emphasis)
+ content, err := r.children(source, node)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ name, tag := "markdown/emphasis", "em"
+ if emphasis.Level == 2 {
+ name, tag = "markdown/strong", "strong"
+ }
+ if r.has(name) {
+ return r.writeTemplate(w, name, markdownContent{Content: content})
+ }
+ _, _ = fmt.Fprintf(w, "<%s>%s</%s>", tag, content, tag)
+ return ast.WalkSkipChildren, nil
+}
+
+func (r *markdownRenderer) renderCodeSpan(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ var code bytes.Buffer
+ for child := node.FirstChild(); child != nil; child = child.NextSibling() {
+ value := child.(*ast.Text).Segment.Value(source)
+ if bytes.HasSuffix(value, []byte("\n")) {
+ _, _ = code.Write(value[:len(value)-1])
+ _ = code.WriteByte(' ')
+ } else {
+ _, _ = code.Write(value)
+ }
+ }
+ return r.writeTemplate(w, "markdown/code_span", markdownContent{Content: escapedMarkdown(code.Bytes())})
+}
+
+func (r *markdownRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ link := node.(*ast.Link)
+ content, err := r.children(source, node)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ return r.writeTemplate(w, "markdown/link", markdownLink{
+ Content: content, Destination: string(link.Destination), Title: string(link.Title),
+ })
+}
+
+func (r *markdownRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ link := node.(*ast.AutoLink)
+ destination := string(link.URL(source))
+ name := "markdown/autolink"
+ if link.AutoLinkType == ast.AutoLinkEmail {
+ name = "markdown/email_autolink"
+ if !strings.HasPrefix(strings.ToLower(destination), "mailto:") {
+ destination = "mailto:" + destination
+ }
+ }
+ content := escapedMarkdown(link.Label(source))
+ if r.has(name) {
+ return r.writeTemplate(w, name, markdownAutoLink{Content: content, Destination: destination})
+ }
+ _, _ = w.WriteString(`<a href="`)
+ _, _ = w.Write(util.EscapeHTML(util.URLEscape([]byte(destination), false)))
+ _, _ = fmt.Fprintf(w, `">%s</a>`, content)
+ return ast.WalkSkipChildren, nil
+}
+
+func imageAlt(source []byte, node ast.Node) string {
+ var out bytes.Buffer
+ writer := bufio.NewWriter(&out)
+ var walk func(ast.Node)
+ walk = func(parent ast.Node) {
+ for child := parent.FirstChild(); child != nil; child = child.NextSibling() {
+ switch child := child.(type) {
+ case *ast.Text:
+ gmhtml.DefaultWriter.Write(writer, child.Value(source))
+ if child.SoftLineBreak() || child.HardLineBreak() {
+ _ = writer.WriteByte('\n')
+ }
+ case *ast.String:
+ if child.IsCode() {
+ _, _ = writer.Write(child.Value)
+ } else {
+ gmhtml.DefaultWriter.Write(writer, child.Value)
+ }
+ default:
+ walk(child)
+ }
+ }
+ }
+ walk(node)
+ _ = writer.Flush()
+ return stdhtml.UnescapeString(out.String())
+}
+
+func (r *markdownRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ image := node.(*ast.Image)
+ return r.writeTemplate(w, "markdown/image", markdownImage{
+ Alt: imageAlt(source, node), Destination: string(image.Destination), Title: string(image.Title),
+ })
+}
+
+func (r *markdownRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ text := node.(*ast.Text)
+ if text.IsRaw() {
+ gmhtml.DefaultWriter.RawWrite(w, text.Segment.Value(source))
+ } else {
+ gmhtml.DefaultWriter.Write(w, text.Segment.Value(source))
+ }
+ if text.HardLineBreak() {
+ return r.writeTemplate(w, "markdown/hard_break", struct{}{})
+ }
+ if text.SoftLineBreak() {
+ _ = w.WriteByte('\n')
+ }
+ return ast.WalkContinue, nil
+}
+
+func (r *markdownRenderer) renderTaskCheckbox(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ checkbox := node.(*extast.TaskCheckBox)
+ return r.writeTemplate(w, "markdown/task_checkbox", markdownTask{Checked: checkbox.IsChecked})
+}
+
+func (r *markdownRenderer) tablePart(name string, fallback []byte, data any) ([]byte, error) {
+ if !r.has(name) {
+ return fallback, nil
+ }
+ return r.execute(name, data)
+}
+
+func (r *markdownRenderer) tableCell(source []byte, node *extast.TableCell, header bool) ([]byte, error) {
+ content, err := r.children(source, node)
+ if err != nil {
+ return nil, err
+ }
+ name, tag := "markdown/table_cell", "td"
+ if header {
+ name, tag = "markdown/table_header_cell", "th"
+ }
+ if r.has(name) {
+ return r.execute(name, markdownCell{Content: content, Alignment: node.Alignment.String()})
+ }
+ var out bytes.Buffer
+ _, _ = fmt.Fprintf(&out, "<%s", tag)
+ if node.Alignment != extast.AlignNone {
+ _, _ = fmt.Fprintf(&out, ` style="text-align:%s"`, node.Alignment.String())
+ }
+ _, _ = fmt.Fprintf(&out, ">%s</%s>\n", content, tag)
+ return out.Bytes(), nil
+}
+
+func (r *markdownRenderer) tableHeader(source []byte, header *extast.TableHeader) ([]byte, error) {
+ var content bytes.Buffer
+ for node := header.FirstChild(); node != nil; node = node.NextSibling() {
+ cell, err := r.tableCell(source, node.(*extast.TableCell), true)
+ if err != nil {
+ return nil, err
+ }
+ _, _ = content.Write(cell)
+ }
+ fallback := []byte("<thead>\n<tr>\n" + content.String() + "</tr>\n</thead>\n")
+ return r.tablePart("markdown/table_header", fallback, markdownContent{Content: template.HTML(content.String())})
+}
+
+func (r *markdownRenderer) tableRow(source []byte, row *extast.TableRow) ([]byte, error) {
+ var content bytes.Buffer
+ for node := row.FirstChild(); node != nil; node = node.NextSibling() {
+ cell, err := r.tableCell(source, node.(*extast.TableCell), false)
+ if err != nil {
+ return nil, err
+ }
+ _, _ = content.Write(cell)
+ }
+ fallback := []byte("<tr>\n" + content.String() + "</tr>\n")
+ return r.tablePart("markdown/table_row", fallback, markdownContent{Content: template.HTML(content.String())})
+}
+
+func (r *markdownRenderer) renderTable(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ var content bytes.Buffer
+ child := node.FirstChild()
+ if header, ok := child.(*extast.TableHeader); ok {
+ rendered, err := r.tableHeader(source, header)
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ _, _ = content.Write(rendered)
+ child = child.NextSibling()
+ }
+ if child != nil {
+ var rows bytes.Buffer
+ for ; child != nil; child = child.NextSibling() {
+ rendered, err := r.tableRow(source, child.(*extast.TableRow))
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ _, _ = rows.Write(rendered)
+ }
+ body, err := r.tablePart("markdown/table_body", []byte("<tbody>\n"+rows.String()+"</tbody>\n"), markdownContent{Content: template.HTML(rows.String())})
+ if err != nil {
+ return ast.WalkStop, err
+ }
+ _, _ = content.Write(body)
+ }
+ if r.has("markdown/table") {
+ return r.writeTemplate(w, "markdown/table", markdownContent{Content: template.HTML(content.String())})
+ }
+ _, _ = fmt.Fprintf(w, "<table>\n%s</table>\n", content.String())
+ return ast.WalkSkipChildren, nil
+}
+
func templateFuncs() template.FuncMap {
return template.FuncMap{
"rel": relativePath,