turbo-editors/turbo-corepublic Fork 0
v1.0.1
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.

toml.go · 265 lines · 7.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package syntax
2
3import "strings"
4
5// highlightTOML returns the spans to colour for a TOML document, one slice per
6// line.
7//
8// TOML is scanned a line at a time rather than as one stream, because a span
9// may not straddle a line break and every TOML construct but one fits on a
10// line. The exception is the multi-line string, which is what carry says.
11func highlightTOML(src string) [][]Span {
12 return ScanLines(src, scanTOMLLine)
13}
14
15// multiline says which kind of multi-line string, if any, is still open at the
16// start of a line.
17type multiline uint8
18
19const (
20 noMultiline multiline = iota
21 basicMultiline // opened with """
22 literalMultiline // opened with '''
23)
24
25// delimiter returns the three characters that close an open multi-line string.
26func (m multiline) delimiter() string {
27 if m == basicMultiline {
28 return `"""`
29 }
30 return `'''`
31}
32
33// tomlScanner walks one line of TOML, emitting spans as it recognises things.
34type tomlScanner struct {
35 LineScanner
36 carry multiline
37}
38
39// scanTOMLLine returns the spans of one line, and what is left open at its end.
40func scanTOMLLine(line []rune, carry multiline) ([]Span, multiline) {
41 s := &tomlScanner{LineScanner: LineScanner{line: line}, carry: carry}
42
43 if carry != noMultiline {
44 s.finishMultiline(carry)
45 }
46 for s.pos < len(s.line) {
47 s.step()
48 }
49 return s.spans, s.carry
50}
51
52// step recognises whatever starts at the current position.
53func (s *tomlScanner) step() {
54 switch c := s.line[s.pos]; {
55 case c == ' ' || c == '\t':
56 s.pos++
57 case c == '#':
58 s.Emit(s.pos, len(s.line), ClassComment)
59 s.pos = len(s.line)
60 case isTOMLPunctuation(c):
61 s.Emit(s.pos, s.pos+1, ClassPunctuation)
62 s.pos++
63 case c == '=':
64 s.Emit(s.pos, s.pos+1, ClassOperator)
65 s.pos++
66 case c == '"' || c == '\'':
67 s.scanString(c)
68 case isTOMLBareRune(c):
69 s.scanWord()
70 default:
71 s.pos++
72 }
73}
74
75// scanString reads a quoted string, of any of TOML's four kinds.
76func (s *tomlScanner) scanString(quote rune) {
77 if kind, ok := s.openingMultiline(quote); ok {
78 start := s.pos
79 s.pos += 3 // past the opening delimiter, so it is not found as the closing one
80 s.finishMultilineFrom(start, kind)
81 return
82 }
83
84 start := s.pos
85 s.pos++ // the opening quote
86 for s.pos < len(s.line) {
87 // Only a basic string has escapes; in a literal string a backslash is
88 // a backslash, which is the point of literal strings.
89 if quote == '"' && s.line[s.pos] == '\\' && s.pos+1 < len(s.line) {
90 s.pos += 2
91 continue
92 }
93 if s.line[s.pos] == quote {
94 s.pos++
95 s.Emit(start, s.pos, ClassString)
96 return
97 }
98 s.pos++
99 }
100
101 // Unterminated: colour to the end of the line rather than giving up, so a
102 // string being typed stays coloured while it is still half-written.
103 s.Emit(start, len(s.line), ClassString)
104}
105
106// openingMultiline reports whether a triple quote starts here.
107func (s *tomlScanner) openingMultiline(quote rune) (multiline, bool) {
108 if s.pos+2 >= len(s.line) || s.line[s.pos+1] != quote || s.line[s.pos+2] != quote {
109 return noMultiline, false
110 }
111 if quote == '"' {
112 return basicMultiline, true
113 }
114 return literalMultiline, true
115}
116
117// finishMultiline colours the continuation of a multi-line string opened on an
118// earlier line, which starts at the very beginning of this one.
119func (s *tomlScanner) finishMultiline(kind multiline) {
120 s.finishMultilineFrom(s.pos, kind)
121}
122
123// finishMultilineFrom colours from start to the end of an open multi-line
124// string, or to the end of the line when it does not close here.
125//
126// The span starts at start but the search for the closing delimiter starts at
127// the current position, which is what keeps an opening """ from being found as
128// its own closing one. start is a parameter rather than something patched onto
129// the span afterwards because emit drops empty spans — an opening delimiter at
130// the very end of a line emits nothing, and patching would then have rewritten
131// whatever span came before it.
132func (s *tomlScanner) finishMultilineFrom(start int, kind multiline) {
133 closing := []rune(kind.delimiter())
134
135 for at := s.pos; at+len(closing) <= len(s.line); at++ {
136 if hasRunes(s.line, at, closing) {
137 s.pos = at + len(closing)
138 s.Emit(start, s.pos, ClassString)
139 s.carry = noMultiline
140 return
141 }
142 }
143
144 s.pos = len(s.line)
145 s.Emit(start, s.pos, ClassString)
146 s.carry = kind
147}
148
149// scanWord reads a bare word: a key, a table name, a boolean, a number or a
150// date, told apart by what follows it.
151func (s *tomlScanner) scanWord() {
152 start := s.pos
153 for s.pos < len(s.line) && isTOMLBareRune(s.line[s.pos]) {
154 s.pos++
155 }
156
157 word := string(s.line[start:s.pos])
158 s.Emit(start, s.pos, tomlWordClass(word, s.followedByEquals(), s.insideTableHeader(start)))
159}
160
161// followedByEquals reports whether the next thing on the line is an "=", which
162// is what makes a bare word a key rather than a value.
163func (s *tomlScanner) followedByEquals() bool {
164 for at := s.pos; at < len(s.line); at++ {
165 switch s.line[at] {
166 case ' ', '\t':
167 case '.':
168 return true // a dotted key: every part of it is still a key
169 case '=':
170 return true
171 default:
172 return false
173 }
174 }
175 return false
176}
177
178// insideTableHeader reports whether the word starting at an offset is part of
179// a [table] or [[array of tables]] header.
180//
181// It looks at what comes before rather than tracking a state, because a header
182// is always the first thing on its line.
183func (s *tomlScanner) insideTableHeader(start int) bool {
184 for at := range start {
185 switch s.line[at] {
186 case ' ', '\t', '[':
187 default:
188 return false
189 }
190 }
191 return start > 0 && s.line[start-1] == '['
192}
193
194// tomlWordClass decides what a bare word is.
195//
196// The classes are the ones the Go highlighter already uses, so a theme colours
197// TOML without naming a single new key: a table header reads as a type, a key
198// as an identifier, and true and false as the constants they are.
199func tomlWordClass(word string, key, header bool) Class {
200 if header {
201 return ClassType
202 }
203 if key {
204 return ClassIdentifier
205 }
206 return tomlValueClass(word)
207}
208
209// tomlValueClass decides what a bare word on the value side of an "=" is.
210func tomlValueClass(word string) Class {
211 switch {
212 case word == "true" || word == "false":
213 return ClassConstant
214 case word == "inf" || word == "nan", startsLikeANumber(word):
215 return ClassNumber
216 default:
217 return ClassIdentifier
218 }
219}
220
221// startsLikeANumber reports whether a word is a number, a date or a time.
222//
223// TOML's dates and times start with a digit and carry on through the same
224// characters a number may hold, so one test covers all of them.
225func startsLikeANumber(word string) bool {
226 if word == "" {
227 return false
228 }
229 first := rune(word[0])
230 if first == '+' || first == '-' {
231 return len(word) > 1 && IsDigit(rune(word[1]))
232 }
233 return IsDigit(first)
234}
235
236// tomlPunctuation are the characters that structure a document rather than
237// carry a value: table brackets, array brackets, inline-table braces, the
238// separator, and the dot of a dotted key.
239const tomlPunctuation = "[]{},."
240
241// isTOMLPunctuation reports whether a rune is one of them.
242func isTOMLPunctuation(r rune) bool { return strings.ContainsRune(tomlPunctuation, r) }
243
244// bareExtras are the characters that may appear in a bare word besides letters
245// and digits: key separators, number signs, and the colon of a time.
246const bareExtras = "_-+:"
247
248// isTOMLBareRune reports whether a rune may appear in a bare key, a number, a
249// date or a boolean — everything that is neither punctuation nor a string.
250func isTOMLBareRune(r rune) bool {
251 return IsLetter(r) || IsDigit(r) || strings.ContainsRune(bareExtras, r)
252}
253
254// hasRunes reports whether want appears in line at offset at.
255func hasRunes(line []rune, at int, want []rune) bool {
256 if at+len(want) > len(line) {
257 return false
258 }
259 for i, r := range want {
260 if line[at+i] != r {
261 return false
262 }
263 }
264 return true
265}