turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

markdown.go · 159 lines · 4.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 6h ago1package syntax
2
3// highlightMarkdown colours a Markdown document, one slice of spans per line.
4//
5// Markdown is scanned a line at a time because almost all of it is decided by
6// a line's own shape: what starts it says whether it is a heading, a list item
7// or a quotation. The exception is the fenced code block, which is what the
8// carried state is for.
9func highlightMarkdown(src string) [][]Span {
10 return ScanLines(src, scanMarkdownLine)
11}
12
13// fence is the state carried between lines: the run of characters that will
14// close an open code block, or "" when none is open.
15type fence string
16
17// fenceMinimum is how many backticks or tildes open a code block.
18const fenceMinimum = 3
19
20// scanMarkdownLine returns the spans of one line, and what is left open.
21func scanMarkdownLine(line []rune, open fence) ([]Span, fence) {
22 s := &LineScanner{line: line}
23
24 if open != "" {
25 return closeOrContinueFence(s, open)
26 }
27 if marker, ok := fenceOpener(line); ok {
28 // The whole opening line, language tag and all, reads as the block.
29 s.TakeRest(ClassString)
30 return s.spans, marker
31 }
32
33 scanMarkdownBlock(s)
34 return s.spans, ""
35}
36
37// closeOrContinueFence colours a line inside a code block, and says whether
38// the block ended on it.
39//
40// A fenced block is one colour whatever language it announces. Colouring the
41// contents with that language's scanner would be better and is a feature of
42// its own: it means every scanner has to be reachable from every other, and
43// the columns of the inner result have to be mapped back out.
44func closeOrContinueFence(s *LineScanner, open fence) ([]Span, fence) {
45 s.TakeRest(ClassString)
46 if closesFence(s.line, open) {
47 return s.spans, ""
48 }
49 return s.spans, open
50}
51
52// fenceOpener reports whether a line opens a code block, and with what.
53func fenceOpener(line []rune) (fence, bool) {
54 trimmed := skipLeadingSpaces(line)
55 if len(trimmed) < fenceMinimum {
56 return "", false
57 }
58
59 marker := trimmed[0]
60 if marker != '`' && marker != '~' {
61 return "", false
62 }
63 for i := range fenceMinimum {
64 if trimmed[i] != marker {
65 return "", false
66 }
67 }
68 return fence(string(marker)), true
69}
70
71// closesFence reports whether a line is the closing run of an open block.
72//
73// A closing fence is a run of the same character and nothing else, which is
74// what keeps a line of prose containing backticks from ending the block.
75func closesFence(line []rune, open fence) bool {
76 trimmed := skipLeadingSpaces(line)
77 marker := []rune(string(open))[0]
78
79 count := 0
80 for count < len(trimmed) && trimmed[count] == marker {
81 count++
82 }
83 if count < fenceMinimum {
84 return false
85 }
86 for _, r := range trimmed[count:] {
87 if r != ' ' && r != '\t' {
88 return false
89 }
90 }
91 return true
92}
93
94// skipLeadingSpaces returns a line without its indentation.
95func skipLeadingSpaces(line []rune) []rune {
96 for i, r := range line {
97 if r != ' ' && r != '\t' {
98 return line[i:]
99 }
100 }
101 return nil
102}
103
104// scanMarkdownBlock colours one line that is not inside a code block.
105func scanMarkdownBlock(s *LineScanner) {
106 s.SkipSpaces()
107
108 switch {
109 case s.Peek(0) == '#':
110 s.TakeRest(ClassHeading) // a heading is coloured whole, marker included
111 case isHorizontalRule(s.line):
112 s.TakeRest(ClassPunctuation)
113 case s.Peek(0) == '>':
114 s.Take(1, ClassPunctuation)
115 scanMarkdownInline(s)
116 default:
117 takeListMarker(s)
118 scanMarkdownInline(s)
119 }
120}
121
122// isHorizontalRule reports whether a line is three or more of -, * or _ with
123// nothing else on it.
124//
125// It is checked before the list marker, because "---" is a rule and "- item"
126// is a list, and they start the same way.
127func isHorizontalRule(line []rune) bool {
128 trimmed := skipLeadingSpaces(line)
129 if len(trimmed) < fenceMinimum {
130 return false
131 }
132
133 marker := trimmed[0]
134 if marker != '-' && marker != '*' && marker != '_' {
135 return false
136 }
137 for _, r := range trimmed {
138 if r != marker && r != ' ' && r != '\t' {
139 return false
140 }
141 }
142 return true
143}
144
145// takeListMarker colours the bullet or the number that starts a list item.
146func takeListMarker(s *LineScanner) {
147 if (s.Peek(0) == '-' || s.Peek(0) == '*' || s.Peek(0) == '+') && s.Peek(1) == ' ' {
148 s.Take(1, ClassPunctuation)
149 return
150 }
151
152 digits := 0
153 for IsDigit(s.Peek(digits)) {
154 digits++
155 }
156 if digits > 0 && (s.Peek(digits) == '.' || s.Peek(digits) == ')') && s.Peek(digits+1) == ' ' {
157 s.Take(digits+1, ClassPunctuation)
158 }
159}