159 lines
5.5 KiB
Go
159 lines
5.5 KiB
Go
package parser
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
|
|
"github.com/alecthomas/chroma/v2"
|
|
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
|
"github.com/alecthomas/chroma/v2/lexers"
|
|
"github.com/alecthomas/chroma/v2/styles"
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/extension"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/renderer/html"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// mermaidLang is the fenced-code-block language tag that triggers Mermaid
|
|
// rendering.
|
|
var mermaidLang = []byte("mermaid")
|
|
|
|
// chromaFormatter renders tokenized code as HTML using CSS classes (rather
|
|
// than inline styles), so the actual colors live in web/assets/chroma.css
|
|
// (see web/gen_chroma_css.go) and can vary between light and dark mode.
|
|
var chromaFormatter = chromahtml.New(chromahtml.WithClasses(true))
|
|
|
|
// chromaBaseStyle is passed to chromaFormatter.Format as required by its
|
|
// signature. In WithClasses mode its colors are unused (they live in the
|
|
// generated stylesheet instead); only its background-vs-foreground
|
|
// classification matters, which is the same across chroma styles.
|
|
var chromaBaseStyle = styles.Get("github")
|
|
|
|
// md is the shared goldmark instance used to convert slide Markdown bodies
|
|
// to HTML. It enables GFM (tables, strikethrough, autolinks, task lists),
|
|
// footnotes, raw inline/block HTML passthrough, and Mermaid code-block
|
|
// rendering.
|
|
var md = goldmark.New(
|
|
goldmark.WithExtensions(extension.GFM, extension.Footnote, mermaidExtension),
|
|
goldmark.WithRendererOptions(html.WithUnsafe()),
|
|
)
|
|
|
|
// renderMarkdown converts a Markdown slide body to HTML.
|
|
func renderMarkdown(body []byte) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := md.Convert(body, &buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// mermaidExtender registers a NodeRenderer that intercepts ```mermaid fenced
|
|
// code blocks so they render as <pre class="mermaid">...</pre> instead of
|
|
// the default <pre><code class="language-mermaid">...</code></pre>, since
|
|
// mermaid.js expects the diagram source as the direct text content of a
|
|
// <pre class="mermaid"> element.
|
|
type mermaidExtender struct{}
|
|
|
|
var mermaidExtension goldmark.Extender = mermaidExtender{}
|
|
|
|
func (mermaidExtender) Extend(m goldmark.Markdown) {
|
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
|
util.Prioritized(&mermaidRenderer{}, 100),
|
|
))
|
|
}
|
|
|
|
// mermaidRenderer renders fenced code blocks, special-casing the "mermaid"
|
|
// language and falling back to goldmark's standard rendering otherwise.
|
|
type mermaidRenderer struct{}
|
|
|
|
func (r *mermaidRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
|
|
}
|
|
|
|
func (r *mermaidRenderer) renderFencedCodeBlock(
|
|
w util.BufWriter, source []byte, node ast.Node, entering bool,
|
|
) (ast.WalkStatus, error) {
|
|
n := node.(*ast.FencedCodeBlock)
|
|
language := n.Language(source)
|
|
|
|
if bytes.Equal(language, mermaidLang) {
|
|
if entering {
|
|
_, _ = w.WriteString(`<pre class="mermaid">`)
|
|
writeLinesEscaped(w, source, n)
|
|
} else {
|
|
_, _ = w.WriteString("</pre>\n")
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
if lexer := lexers.Get(string(language)); lexer != nil {
|
|
if entering {
|
|
if err := renderHighlighted(w, source, n, lexer); err != nil {
|
|
return ast.WalkStop, err
|
|
}
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
return renderDefaultFencedCodeBlock(w, source, n, entering)
|
|
}
|
|
|
|
// renderHighlighted writes a fenced code block as syntax-highlighted HTML
|
|
// using chroma, given a lexer already matched to the block's language tag.
|
|
// It writes the complete "<pre>...</pre>" in one call (on the "entering"
|
|
// walk step); the corresponding "!entering" step is a no-op, mirroring how
|
|
// renderDefaultFencedCodeBlock already writes the opening tag and content
|
|
// together and only the closing tag separately.
|
|
func renderHighlighted(w util.BufWriter, source []byte, n *ast.FencedCodeBlock, lexer chroma.Lexer) error {
|
|
var code bytes.Buffer
|
|
lines := n.Lines()
|
|
for i := 0; i < lines.Len(); i++ {
|
|
line := lines.At(i)
|
|
code.Write(line.Value(source))
|
|
}
|
|
|
|
iterator, err := lexer.Tokenise(nil, code.String())
|
|
if err != nil {
|
|
return fmt.Errorf("tokenizing code block for syntax highlighting: %w", err)
|
|
}
|
|
if err := chromaFormatter.Format(w, chromaBaseStyle, iterator); err != nil {
|
|
return fmt.Errorf("formatting syntax-highlighted code block: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// renderDefaultFencedCodeBlock reproduces goldmark's standard
|
|
// <pre><code class="language-...">...</code></pre> rendering for
|
|
// non-Mermaid fenced code blocks.
|
|
func renderDefaultFencedCodeBlock(
|
|
w util.BufWriter, source []byte, n *ast.FencedCodeBlock, entering bool,
|
|
) (ast.WalkStatus, error) {
|
|
if entering {
|
|
_, _ = w.WriteString("<pre><code")
|
|
if language := n.Language(source); language != nil {
|
|
_, _ = w.WriteString(` class="language-`)
|
|
_, _ = w.Write(util.EscapeHTML(language))
|
|
_, _ = w.WriteString(`"`)
|
|
}
|
|
_ = w.WriteByte('>')
|
|
writeLinesEscaped(w, source, n)
|
|
} else {
|
|
_, _ = w.WriteString("</code></pre>\n")
|
|
}
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
// writeLinesEscaped writes a node's source lines HTML-escaped, matching
|
|
// goldmark's own code-block output. Escaping is safe here (and required, to
|
|
// prevent literal HTML injection from slide content) because a browser
|
|
// decodes entities back to their original characters before Mermaid reads
|
|
// an element's text content.
|
|
func writeLinesEscaped(w util.BufWriter, source []byte, n ast.Node) {
|
|
lines := n.Lines()
|
|
for i := 0; i < lines.Len(); i++ {
|
|
line := lines.At(i)
|
|
_, _ = w.Write(util.EscapeHTML(line.Value(source)))
|
|
}
|
|
}
|