This repository has no description
1package extension
2
3import (
4 "bytes"
5
6 "github.com/gohugoio/hugo-goldmark-extensions/passthrough"
7 "github.com/yuin/goldmark"
8 "github.com/yuin/goldmark/ast"
9 "github.com/yuin/goldmark/renderer"
10 "github.com/yuin/goldmark/util"
11)
12
13// MathExt renders LaTeX math for client-side typesetting.
14//
15// Detection is delegated to the Hugo passthrough extension, which correctly
16// handles single-line "$$...$$" display blocks and protects markdown inside
17// math (e.g. "$a_1$" is not emphasis).
18// We override passthrough's verbatim renderers so each span is wrapped in
19// <span class="math inline|display"> carrying the raw LaTeX with MathJax
20// \( \) / \[ \] delimiters. MathJax (loaded on demand in layouts/base.html)
21// typesets only these spans, so surrounding prose is never scanned — which is
22// what keeps a stray "$" in body text from being interpreted as math.
23var MathExt = &mathExt{}
24
25type mathExt struct{}
26
27func (e *mathExt) Extend(m goldmark.Markdown) {
28 // Installs the passthrough parsers (and its own verbatim renderers, which
29 // we override below).
30 passthrough.New(passthrough.Config{
31 InlineDelimiters: []passthrough.Delimiters{
32 {Open: "$", Close: "$"},
33 {Open: `\(`, Close: `\)`},
34 },
35 BlockDelimiters: []passthrough.Delimiters{
36 {Open: "$$", Close: "$$"},
37 {Open: `\[`, Close: `\]`},
38 },
39 }).Extend(m)
40
41 // Higher priority than passthrough's default renderers (priority 100), so
42 // ours win for the passthrough node kinds.
43 m.Renderer().AddOptions(renderer.WithNodeRenderers(
44 util.Prioritized(&mathRenderer{}, 1),
45 ))
46}
47
48type mathRenderer struct{}
49
50func (r *mathRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
51 reg.Register(passthrough.KindPassthroughInline, r.renderInline)
52 reg.Register(passthrough.KindPassthroughBlock, r.renderBlock)
53}
54
55func (r *mathRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
56 if !entering {
57 return ast.WalkSkipChildren, nil
58 }
59 node := n.(*passthrough.PassthroughInline)
60 open, closing := node.Delimiters.Open, node.Delimiters.Close
61 raw := node.Segment.Value(source)
62 inner := raw[len(open) : len(raw)-len(closing)]
63
64 // Currency guard for "$"-delimited inline math (Pandoc's rules): a "$...$"
65 // run is not math if the inner text is empty or space-padded, or if the
66 // closing "$" is immediately followed by a digit (e.g. "$5 and $10"). In
67 // those cases emit the run verbatim so MathJax never sees it.
68 if open == "$" {
69 var after byte
70 if node.Segment.Stop < len(source) {
71 after = source[node.Segment.Stop]
72 }
73 if len(inner) == 0 || inner[0] == ' ' || inner[len(inner)-1] == ' ' || isASCIIDigit(after) {
74 w.Write(raw)
75 return ast.WalkSkipChildren, nil
76 }
77 }
78
79 w.WriteString(`<span class="math inline">\(`)
80 w.Write(util.EscapeHTML(inner))
81 w.WriteString(`\)</span>`)
82 return ast.WalkSkipChildren, nil
83}
84
85func (r *mathRenderer) renderBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
86 if !entering {
87 return ast.WalkSkipChildren, nil
88 }
89 node := n.(*passthrough.PassthroughBlock)
90 open, closing := node.Delimiters.Open, node.Delimiters.Close
91
92 var buf bytes.Buffer
93 for i := 0; i < node.Lines().Len(); i++ {
94 seg := node.Lines().At(i)
95 buf.Write(seg.Value(source))
96 }
97
98 inner := bytes.TrimSpace(buf.Bytes())
99 inner = bytes.TrimPrefix(inner, []byte(open))
100 inner = bytes.TrimSuffix(inner, []byte(closing))
101 inner = bytes.TrimSpace(inner)
102
103 w.WriteString(`<p><span class="math display">\[`)
104 w.Write(util.EscapeHTML(inner))
105 w.WriteString(`\]</span></p>`)
106 return ast.WalkSkipChildren, nil
107}
108
109func isASCIIDigit(b byte) bool { return b >= '0' && b <= '9' }