This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / pages / markup / extension / math.go
3.7 kB 109 lines
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' }