loading up the forgejo repo on tangled to test page performance
1// Copyright 2022 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package math
5
6import (
7 "bytes"
8
9 "github.com/yuin/goldmark/ast"
10 "github.com/yuin/goldmark/parser"
11 "github.com/yuin/goldmark/text"
12)
13
14type inlineParser struct {
15 start []byte
16 end []byte
17}
18
19var defaultInlineDollarParser = &inlineParser{
20 start: []byte{'$'},
21 end: []byte{'$'},
22}
23
24var defaultDualDollarParser = &inlineParser{
25 start: []byte{'$', '$'},
26 end: []byte{'$', '$'},
27}
28
29// NewInlineDollarParser returns a new inline parser
30func NewInlineDollarParser() parser.InlineParser {
31 return defaultInlineDollarParser
32}
33
34func NewInlineDualDollarParser() parser.InlineParser {
35 return defaultDualDollarParser
36}
37
38var defaultInlineBracketParser = &inlineParser{
39 start: []byte{'\\', '('},
40 end: []byte{'\\', ')'},
41}
42
43// NewInlineDollarParser returns a new inline parser
44func NewInlineBracketParser() parser.InlineParser {
45 return defaultInlineBracketParser
46}
47
48// Trigger triggers this parser on $ or \
49func (parser *inlineParser) Trigger() []byte {
50 return parser.start
51}
52
53func isPunctuation(b byte) bool {
54 return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
55}
56
57func isBracket(b byte) bool {
58 return b == ')'
59}
60
61func isAlphanumeric(b byte) bool {
62 return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
63}
64
65// Parse parses the current line and returns a result of parsing.
66func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
67 line, _ := block.PeekLine()
68
69 if !bytes.HasPrefix(line, parser.start) {
70 // We'll catch this one on the next time round
71 return nil
72 }
73
74 precedingCharacter := block.PrecendingCharacter()
75 if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) {
76 // need to exclude things like `a$` from being considered a start
77 return nil
78 }
79
80 // move the opener marker point at the start of the text
81 opener := len(parser.start)
82
83 // Now look for an ending line
84 ender := opener
85 for {
86 pos := bytes.Index(line[ender:], parser.end)
87 if pos < 0 {
88 return nil
89 }
90
91 ender += pos
92
93 // Now we want to check the character at the end of our parser section
94 // that is ender + len(parser.end) and check if char before ender is '\'
95 pos = ender + len(parser.end)
96 if len(line) <= pos {
97 break
98 }
99 suceedingCharacter := line[pos]
100 // check valid ending character
101 if !isPunctuation(suceedingCharacter) &&
102 (suceedingCharacter != ' ') &&
103 (suceedingCharacter != '\n') &&
104 !isBracket(suceedingCharacter) {
105 return nil
106 }
107 if line[ender-1] != '\\' {
108 break
109 }
110
111 // move the pointer onwards
112 ender += len(parser.end)
113 }
114
115 block.Advance(opener)
116 _, pos := block.Position()
117 var node ast.Node
118 if parser == defaultDualDollarParser {
119 node = NewInlineBlock()
120 } else {
121 node = NewInline()
122 }
123 segment := pos.WithStop(pos.Start + ender - opener)
124 node.AppendChild(node, ast.NewRawTextSegment(segment))
125 block.Advance(ender - opener + len(parser.end))
126
127 if parser == defaultDualDollarParser {
128 trimBlock(&(node.(*InlineBlock)).Inline, block)
129 } else {
130 trimBlock(node.(*Inline), block)
131 }
132 return node
133}
134
135func trimBlock(node *Inline, block text.Reader) {
136 if node.IsBlank(block.Source()) {
137 return
138 }
139
140 // trim first space and last space
141 first := node.FirstChild().(*ast.Text)
142 if first.Segment.IsEmpty() || block.Source()[first.Segment.Start] != ' ' {
143 return
144 }
145
146 last := node.LastChild().(*ast.Text)
147 if last.Segment.IsEmpty() || block.Source()[last.Segment.Stop-1] != ' ' {
148 return
149 }
150
151 first.Segment = first.Segment.WithStart(first.Segment.Start + 1)
152 last.Segment = last.Segment.WithStop(last.Segment.Stop - 1)
153}