| 📦 Turbo JS 91999d1 k33g 12h ago | 1 | package jslang |
| 2 | |
| 3 | import "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. |
| 12 | type 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. |
| 30 | type 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. |
| 55 | func Highlight(src string) [][]syntax.Span { |
| 56 | return syntax.ScanLines(src, scanLine) |
| 57 | } |
| 58 | |
| 59 | // scanLine colours one line and returns what it leaves open. |
| 60 | func 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. |
| 87 | func 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. |
| 107 | func 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. |
| 172 | func (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() {}. |
| 179 | func (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. |
| 191 | func (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. |
| 197 | func (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. |
| 204 | func (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. |
| 227 | func 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. |
| 256 | func 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. |
| 273 | func 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 | } |