Skip to content

Commit b42d294

Browse files
committed
Fix markdown preview $$ support
1 parent d655ff1 commit b42d294

File tree

6 files changed

+220
-1
lines changed

6 files changed

+220
-1
lines changed

modules/markup/markdown/markdown_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,14 @@ func TestMathBlock(t *testing.T) {
555555
"$a$ ($b$) [$c$] {$d$}",
556556
`<p><code class="language-math is-loading">a</code> (<code class="language-math is-loading">b</code>) [$c$] {$d$}</p>` + nl,
557557
},
558+
{
559+
"$$a$$ test",
560+
`<p></p><pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre> test<p></p>` + nl,
561+
},
562+
{
563+
"test $$a$$",
564+
`<p>test </p><pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre><p></p>` + nl,
565+
},
558566
}
559567

560568
for _, test := range testcases {

modules/markup/markdown/math/block_parser.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
5353
}
5454
idx := bytes.Index(line[pos+2:], endBytes)
5555
if idx >= 0 {
56+
// for case $$ ... $$ any other text
57+
for i := pos + idx + 4; i < len(line); i++ {
58+
if line[i] != ' ' && line[i] != '\n' {
59+
return nil, parser.NoChildren
60+
}
61+
}
5662
segment.Stop = segment.Start + idx + 2
5763
reader.Advance(segment.Len() - 1)
5864
segment.Start += 2
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package math
5+
6+
import (
7+
"github.com/yuin/goldmark/ast"
8+
"github.com/yuin/goldmark/util"
9+
)
10+
11+
// InlineBlock represents inline math e.g. $$...$$
12+
type InlineBlock struct {
13+
ast.BaseInline
14+
}
15+
16+
// InlineBlock implements InlineBlock.
17+
func (n *InlineBlock) InlineBlock() {}
18+
19+
// IsBlank returns if this inline block node is empty
20+
func (n *InlineBlock) IsBlank(source []byte) bool {
21+
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
22+
text := c.(*ast.Text).Segment
23+
if !util.IsBlank(text.Value(source)) {
24+
return false
25+
}
26+
}
27+
return true
28+
}
29+
30+
// Dump renders this inline block math as debug
31+
func (n *InlineBlock) Dump(source []byte, level int) {
32+
ast.DumpHelper(n, source, level, nil, nil)
33+
}
34+
35+
// KindInlineBlock is the kind for math inline block
36+
var KindInlineBlock = ast.NewNodeKind("MathInlineBlock")
37+
38+
// Kind returns KindInlineBlock
39+
func (n *InlineBlock) Kind() ast.NodeKind {
40+
return KindInlineBlock
41+
}
42+
43+
// NewInlineBlock creates a new ast math inline block node
44+
func NewInlineBlock() *InlineBlock {
45+
return &InlineBlock{
46+
BaseInline: ast.BaseInline{},
47+
}
48+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package math
5+
6+
import (
7+
"bytes"
8+
9+
"github.com/yuin/goldmark/ast"
10+
"github.com/yuin/goldmark/parser"
11+
"github.com/yuin/goldmark/text"
12+
)
13+
14+
type inlineBlockParser struct {
15+
start []byte
16+
end []byte
17+
}
18+
19+
var defaultInlineDualDollarParser = &inlineBlockParser{
20+
start: []byte{'$', '$'},
21+
end: []byte{'$', '$'},
22+
}
23+
24+
// NewInlineDualDollarParser returns a new inline block parser
25+
func NewInlineDualDollarParser() parser.InlineParser {
26+
return defaultInlineDualDollarParser
27+
}
28+
29+
// Trigger triggers this parser on $$
30+
func (parser *inlineBlockParser) Trigger() []byte {
31+
return parser.start[0:2]
32+
}
33+
34+
// Parse parses the current line and returns a result of parsing.
35+
func (parser *inlineBlockParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
36+
line, _ := block.PeekLine()
37+
38+
if !bytes.HasPrefix(line, parser.start) {
39+
// We'll catch this one on the next time round
40+
return nil
41+
}
42+
43+
precedingCharacter := block.PrecendingCharacter()
44+
if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
45+
// need to exclude things like `a$$` from being considered a start
46+
return nil
47+
}
48+
49+
// move the opener marker point at the start of the text
50+
opener := len(parser.start)
51+
52+
// Now look for an ending line
53+
ender := opener
54+
for {
55+
pos := bytes.Index(line[ender:], parser.end)
56+
if pos < 0 {
57+
return nil
58+
}
59+
60+
ender += pos
61+
62+
// Now we want to check the character at the end of our parser section
63+
// that is ender + len(parser.end) and check if char before ender is '\'
64+
pos = ender + len(parser.end)
65+
if len(line) <= pos {
66+
break
67+
}
68+
suceedingCharacter := line[pos]
69+
if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') && !isBracket(suceedingCharacter) {
70+
return nil
71+
}
72+
if line[ender-1] != '\\' {
73+
break
74+
}
75+
76+
// move the pointer onwards
77+
ender += len(parser.end)
78+
}
79+
80+
block.Advance(opener)
81+
_, pos := block.Position()
82+
node := NewInlineBlock()
83+
segment := pos.WithStop(pos.Start + ender - opener)
84+
node.AppendChild(node, ast.NewRawTextSegment(segment))
85+
block.Advance(ender - opener + len(parser.end))
86+
87+
trimInlineBlock(node, block)
88+
return node
89+
}
90+
91+
func trimInlineBlock(node *InlineBlock, block text.Reader) {
92+
if node.IsBlank(block.Source()) {
93+
return
94+
}
95+
96+
// trim first space and last space
97+
first := node.FirstChild().(*ast.Text)
98+
if !(!first.Segment.IsEmpty() && block.Source()[first.Segment.Start] == ' ') {
99+
return
100+
}
101+
102+
last := node.LastChild().(*ast.Text)
103+
if !(!last.Segment.IsEmpty() && block.Source()[last.Segment.Stop-1] == ' ') {
104+
return
105+
}
106+
107+
first.Segment = first.Segment.WithStart(first.Segment.Start + 1)
108+
last.Segment = last.Segment.WithStop(last.Segment.Stop - 1)
109+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package math
5+
6+
import (
7+
"bytes"
8+
9+
"github.com/yuin/goldmark/ast"
10+
"github.com/yuin/goldmark/renderer"
11+
"github.com/yuin/goldmark/util"
12+
)
13+
14+
// InlineBlockRenderer is an inline renderer
15+
type InlineBlockRenderer struct{}
16+
17+
// NewInlineBlockRenderer returns a new renderer for inline math
18+
func NewInlineBlockRenderer() renderer.NodeRenderer {
19+
return &InlineBlockRenderer{}
20+
}
21+
22+
func (r *InlineBlockRenderer) renderInlineBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
23+
if entering {
24+
_, _ = w.WriteString(`<pre class="code-block is-loading"><code class="chroma language-math display">`)
25+
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
26+
segment := c.(*ast.Text).Segment
27+
value := util.EscapeHTML(segment.Value(source))
28+
if bytes.HasSuffix(value, []byte("\n")) {
29+
_, _ = w.Write(value[:len(value)-1])
30+
if c != n.LastChild() {
31+
_, _ = w.Write([]byte(" "))
32+
}
33+
} else {
34+
_, _ = w.Write(value)
35+
}
36+
}
37+
return ast.WalkSkipChildren, nil
38+
}
39+
_, _ = w.WriteString(`</code></pre>`)
40+
return ast.WalkContinue, nil
41+
}
42+
43+
// RegisterFuncs registers the renderer for inline math nodes
44+
func (r *InlineBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
45+
reg.Register(KindInlineBlock, r.renderInlineBlock)
46+
}

modules/markup/markdown/math/math.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,14 @@ func (e *Extension) Extend(m goldmark.Markdown) {
9696
util.Prioritized(NewInlineBracketParser(), 501),
9797
}
9898
if e.parseDollarInline {
99-
inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 501))
99+
inlines = append(inlines, util.Prioritized(NewInlineDualDollarParser(), 501),
100+
util.Prioritized(NewInlineDollarParser(), 502))
100101
}
101102
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
102103

103104
m.Renderer().AddOptions(renderer.WithNodeRenderers(
104105
util.Prioritized(NewBlockRenderer(), 501),
105106
util.Prioritized(NewInlineRenderer(), 502),
107+
util.Prioritized(NewInlineBlockRenderer(), 503),
106108
))
107109
}

0 commit comments

Comments
 (0)