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

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

scan.go · 281 lines · 9.8 KBGo Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 12h ago1package jslang
2
3import "rickub.com/turbo-editors/turbo-core/syntax"
4
5// carry is what a line of JavaScript leaves open for the next one.
6//
7// Two constructs in JavaScript can cross a line break: a block comment, which
8// does not nest, and a template literal, which is the one string in the
9// language that may hold a real newline. Each is a flag rather than a depth
10// for that reason. Everything else — a regular expression, an ordinary string,
11// a number — ends on the line it started, by the language's own rules.
12type carry struct {
13 // inComment says a /* ran past the end of a line.
14 inComment bool
15 // inTemplate says a `…` template literal ran past the end of a line.
16 inTemplate bool
17 // past says at least one line has been scanned, so that a #! on this one
18 // is two operators rather than the hashbang only the first line may carry.
19 past bool
20}
21
22// lineState is what the scanner knows about the line in front of it that no
23// single token says: whether a slash here would begin a regular expression,
24// and what the previous significant token was.
25//
26// It is line-local. A statement that breaks a line between an operator and a
27// regular expression is not carried, so a `/` at the start of a line is read
28// as a regular expression when one closes on that line, and as division when
29// none does.
30type lineState struct {
31 // regexAllowed says a / here opens a regular expression rather than
32 // dividing: true after an operator, an opening bracket, a keyword and at
33 // the start of a line; false after anything that can end an expression.
34 regexAllowed bool
35 // afterDot says the previous token was a member-access dot, so the word
36 // here is a property name whatever it is spelt like: obj.default,
37 // map.get(k), promise.catch(…).
38 afterDot bool
39 // afterKeyword is the previous word when it was function or class, so the
40 // name that follows is coloured by its position.
41 afterKeyword string
42}
43
44// Highlight colours JavaScript source.
45//
46// It is written against syntax.LineScanner, a line at a time, with the two
47// multi-line constructs above threaded through carry. It replaces the scanner
48// turbo-core ships for JavaScript: what it adds is regular-expression
49// literals, the hashbang line, Node's globals, the name after function and
50// class, and a leading capital read as a class name.
51//
52// It is deliberately tolerant of broken input: source under the cursor is
53// invalid most of the time it is being typed, and a highlighter that gives up
54// is a highlighter that flickers off.
55func Highlight(src string) [][]syntax.Span {
56 return syntax.ScanLines(src, scanLine)
57}
58
59// scanLine colours one line and returns what it leaves open.
60func scanLine(line []rune, open carry) ([]syntax.Span, carry) {
61 s := syntax.NewLineScanner(line)
62 state := lineState{regexAllowed: true}
63
64 // Whatever ran past the end of the previous line is finished first: until
65 // it closes, nothing on this line is code.
66 if !finishCarried(s, &open, &state) {
67 open.past = true
68 return s.Spans(), open
69 }
70
71 // A hashbang is the first line of a script run as a command. Node strips
72 // it before parsing, and so does this scanner — on the first line only,
73 // because anywhere else #! is exactly what it looks like.
74 if !open.past && s.HasPrefix(0, "#!") {
75 s.TakeRest(syntax.ClassComment)
76 }
77
78 for !s.AtEnd() {
79 scanToken(s, &open, &state)
80 }
81 open.past = true
82 return s.Spans(), open
83}
84
85// finishCarried closes whatever the previous line left open, and reports
86// whether the rest of this line is code.
87func finishCarried(s *syntax.LineScanner, open *carry, state *lineState) bool {
88 switch {
89 case open.inComment:
90 if !syntax.FinishBlockComment(s, "*/", syntax.ClassComment) {
91 return false
92 }
93 open.inComment = false
94 case open.inTemplate:
95 if !finishTemplate(s) {
96 return false
97 }
98 open.inTemplate = false
99 // A template that has just closed is a value, so a slash after it
100 // divides.
101 state.operand()
102 }
103 return true
104}
105
106// scanToken colours whatever starts at the scanner's position.
107func scanToken(s *syntax.LineScanner, open *carry, state *lineState) {
108 r := s.Peek(0)
109
110 switch {
111 case r == ' ' || r == '\t':
112 s.SkipSpaces()
113 case s.HasPrefix(0, "//"):
114 s.TakeRest(syntax.ClassComment)
115 case s.HasPrefix(0, "/*"):
116 if !syntax.OpenBlockComment(s, "/*", "*/", syntax.ClassComment) {
117 open.inComment = true
118 }
119 case r == '`':
120 if !openTemplate(s) {
121 open.inTemplate = true
122 }
123 state.operand()
124 case r == '"' || r == '\'':
125 syntax.TakeQuoted(s, r, syntax.ClassString)
126 state.operand()
127 case r == '/' && state.regexAllowed && takeRegex(s):
128 // takeRegex consumes nothing when no regular expression closes on
129 // this line, so the slash falls through to the operator case below.
130 state.operand()
131 case syntax.IsDigit(r) || (r == '.' && syntax.IsDigit(s.Peek(1))):
132 takeNumber(s)
133 state.operand()
134 case isWordStart(r):
135 takeWord(s, state)
136 case r == '#' && isWordStart(s.Peek(1)):
137 takePrivateName(s, state)
138 case r == '@' && isWordStart(s.Peek(1)):
139 takeDecorator(s, state)
140 case s.HasPrefix(0, "..."):
141 // A spread is one thing, not three dots: colouring it as three
142 // member accesses would also leave afterDot set for the name it
143 // spreads.
144 s.Take(3, syntax.ClassPunctuation)
145 state.punctuation('.')
146 case s.HasPrefix(0, "?."):
147 // Optional chaining is a member access with a question in front of
148 // it, and the name after it is a property like any other.
149 s.Take(2, syntax.ClassOperator)
150 state.dot()
151 case r == '.':
152 s.Take(1, syntax.ClassPunctuation)
153 state.dot()
154 case syntax.IsOperatorRune(r):
155 start := s.Pos()
156 s.TakeWhile(syntax.ClassOperator, syntax.IsOperatorRune)
157 state.operator(wordAt(s, start))
158 case syntax.IsPunctuationRune(r):
159 s.Take(1, syntax.ClassPunctuation)
160 state.punctuation(r)
161 default:
162 // A rune nothing here claims — a stray backslash, an emoji outside a
163 // string — is stepped over uncoloured rather than guessed at.
164 s.Advance(1)
165 }
166}
167
168// --- what the previous token allows ------------------------------------------
169
170// operand records that the previous token could end an expression, so a slash
171// after it divides.
172func (st *lineState) operand() {
173 *st = lineState{}
174}
175
176// operator records an operator, after which a slash opens a regular
177// expression. The one keyword that survives an operator is function, whose
178// generator form puts a * between it and the name: function* gen() {}.
179func (st *lineState) operator(text string) {
180 keyword := ""
181 if st.afterKeyword == "function" && text == "*" {
182 keyword = "function"
183 }
184 *st = lineState{regexAllowed: true, afterKeyword: keyword}
185}
186
187// punctuation records a bracket, a comma or a semicolon. A closing parenthesis
188// or bracket ends an expression, so a slash after one divides; anything else
189// — including a closing brace, which ends a block far more often than an
190// object literal — lets a regular expression begin.
191func (st *lineState) punctuation(r rune) {
192 *st = lineState{regexAllowed: r != ')' && r != ']'}
193}
194
195// dot records a member-access dot, after which the next word is a property
196// name whatever it is spelt like.
197func (st *lineState) dot() {
198 *st = lineState{afterDot: true}
199}
200
201// keyword records a keyword. A slash after return, typeof, case and the rest
202// opens a regular expression; function and class are remembered so the name
203// after them is coloured by its position.
204func (st *lineState) keyword(word string) {
205 *st = lineState{regexAllowed: true}
206 if word == "function" || word == "class" {
207 st.afterKeyword = word
208 }
209}
210
211// --- regular expressions -----------------------------------------------------
212
213// takeRegex colours a regular-expression literal starting at the slash, and
214// reports whether there was one.
215//
216// A literal must close on the line it opened, so the closing slash is looked
217// for first — outside a […] class, past any escape — and the slash is left
218// alone when none is found: `a /` at the end of a line divides, whatever the
219// previous token was. That bound is what makes the previous-token rule safe
220// enough to use here where the library declined to: a wrong guess costs at
221// most one line, never the rest of the file.
222//
223// The literal is coloured as a **character literal**. JavaScript has no
224// character literal, so the class is free, and a regular expression is the
225// other kind of quoted thing in the language — worth telling from a string by
226// colour, which is what every theme in the family does with the two classes.
227func takeRegex(s *syntax.LineScanner) bool {
228 at, inClass := 1, false
229 for s.Pos()+at < s.Len() {
230 r := s.Peek(at)
231 switch {
232 case r == '\\':
233 at++ // whatever follows a backslash is part of the literal
234 case r == '[':
235 inClass = true
236 case r == ']':
237 inClass = false
238 case r == '/' && !inClass:
239 at++
240 for isWordRune(s.Peek(at)) { // the flags: /x/gi
241 at++
242 }
243 s.Take(at, syntax.ClassChar)
244 return true
245 }
246 at++
247 }
248 return false
249}
250
251// --- comments and the words that are not words -------------------------------
252
253// takePrivateName colours a #private class member as the identifier it is,
254// the # included: #count is one name and colouring the # alone would make it
255// read as a comment marker from another language.
256func takePrivateName(s *syntax.LineScanner, state *lineState) {
257 start := s.Pos()
258 s.Advance(1)
259 for !s.AtEnd() && isWordRune(s.Peek(0)) {
260 s.Advance(1)
261 }
262 class := syntax.ClassIdentifier
263 if isCallSite(s) {
264 class = syntax.ClassFunction
265 }
266 s.Emit(start, s.Pos(), class)
267 state.operand()
268}
269
270// takeDecorator colours a @decorator, dotted path included, as an attribute:
271// it is the same kind of thing a Rust #[attribute] is, said about the
272// declaration below it, and it is the class the family already uses for that.
273func takeDecorator(s *syntax.LineScanner, state *lineState) {
274 start := s.Pos()
275 s.Advance(1)
276 for !s.AtEnd() && (isWordRune(s.Peek(0)) || s.Peek(0) == '.') {
277 s.Advance(1)
278 }
279 s.Emit(start, s.Pos(), syntax.ClassAttribute)
280 *state = lineState{regexAllowed: true}
281}