| 🛟 Updated. 28d5985 k33g 19h ago | 1 | package syntax |
| 2 | |
| 3 | // javaScriptKeywords are the words that structure the language. |
| 4 | var javaScriptKeywords = map[string]Class{ |
| 5 | "await": ClassKeyword, "break": ClassKeyword, "case": ClassKeyword, |
| 6 | "catch": ClassKeyword, "class": ClassKeyword, "const": ClassKeyword, |
| 7 | "continue": ClassKeyword, "debugger": ClassKeyword, "default": ClassKeyword, |
| 8 | "delete": ClassKeyword, "do": ClassKeyword, "else": ClassKeyword, |
| 9 | "export": ClassKeyword, "extends": ClassKeyword, "finally": ClassKeyword, |
| 10 | "for": ClassKeyword, "from": ClassKeyword, "function": ClassKeyword, |
| 11 | "get": ClassKeyword, "if": ClassKeyword, "import": ClassKeyword, |
| 12 | "in": ClassKeyword, "instanceof": ClassKeyword, "let": ClassKeyword, |
| 13 | "new": ClassKeyword, "of": ClassKeyword, "return": ClassKeyword, |
| 14 | "set": ClassKeyword, "static": ClassKeyword, "super": ClassKeyword, |
| 15 | "switch": ClassKeyword, "throw": ClassKeyword, "try": ClassKeyword, |
| 16 | "typeof": ClassKeyword, "var": ClassKeyword, "void": ClassKeyword, |
| 17 | "while": ClassKeyword, "with": ClassKeyword, "yield": ClassKeyword, |
| 18 | "async": ClassKeyword, |
| 19 | |
| 20 | "true": ClassConstant, "false": ClassConstant, "null": ClassConstant, |
| 21 | "undefined": ClassConstant, "NaN": ClassConstant, "Infinity": ClassConstant, |
| 22 | "this": ClassConstant, |
| 23 | |
| 24 | // The globals worth telling apart from a variable of your own. Recognised |
| 25 | // by name, as Go's predeclared identifiers are, because a file may shadow |
| 26 | // them and colouring the shadowed one anyway is what every editor does. |
| 27 | "console": ClassBuiltin, "document": ClassBuiltin, "window": ClassBuiltin, |
| 28 | "Array": ClassBuiltin, "Object": ClassBuiltin, "String": ClassBuiltin, |
| 29 | "Number": ClassBuiltin, "Boolean": ClassBuiltin, "Promise": ClassBuiltin, |
| 30 | "Math": ClassBuiltin, "JSON": ClassBuiltin, "Date": ClassBuiltin, |
| 31 | "RegExp": ClassBuiltin, "Error": ClassBuiltin, "Map": ClassBuiltin, |
| 32 | "Set": ClassBuiltin, "Symbol": ClassBuiltin, "globalThis": ClassBuiltin, |
| 33 | } |
| 34 | |
| 35 | // javaScriptCarry is what a line can leave open: a block comment, or a |
| 36 | // template literal, which is the only string in the language that may span |
| 37 | // lines. |
| 38 | type javaScriptCarry uint8 |
| 39 | |
| 40 | const ( |
| 41 | javaScriptGround javaScriptCarry = iota |
| 42 | javaScriptInBlockComment |
| 43 | javaScriptInTemplate |
| 44 | ) |
| 45 | |
| 46 | // highlightJavaScript colours JavaScript, one slice of spans per line. |
| 47 | // |
| 48 | // Regular-expression literals are deliberately not recognised. Telling `/x/g` |
| 49 | // from a division needs to know whether the previous token could end an |
| 50 | // expression, and getting it wrong colours the rest of a line as a string — |
| 51 | // a worse failure than leaving a regex the colour of an operator. |
| 52 | func highlightJavaScript(src string) [][]Span { |
| 53 | return ScanLines(src, scanJavaScriptLine) |
| 54 | } |
| 55 | |
| 56 | // scanJavaScriptLine returns the spans of one line, and what is left open. |
| 57 | func scanJavaScriptLine(line []rune, carry javaScriptCarry) ([]Span, javaScriptCarry) { |
| 58 | s := &LineScanner{line: line} |
| 59 | |
| 60 | switch carry { |
| 61 | case javaScriptInBlockComment: |
| 62 | if !FinishBlockComment(s, "*/", ClassComment) { |
| 63 | return s.spans, javaScriptInBlockComment |
| 64 | } |
| 65 | case javaScriptInTemplate: |
| 66 | if !finishTemplate(s) { |
| 67 | return s.spans, javaScriptInTemplate |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | for !s.AtEnd() { |
| 72 | if next, stop := stepJavaScript(s); stop { |
| 73 | return s.spans, next |
| 74 | } |
| 75 | } |
| 76 | return s.spans, javaScriptGround |
| 77 | } |
| 78 | |
| 79 | // stepJavaScript recognises whatever starts at the current position, and says |
| 80 | // whether it left something open at the end of the line. |
| 81 | func stepJavaScript(s *LineScanner) (javaScriptCarry, bool) { |
| 82 | switch { |
| 83 | case s.line[s.pos] == ' ' || s.line[s.pos] == '\t': |
| 84 | s.pos++ |
| 85 | case s.HasPrefix(0, "//"): |
| 86 | s.TakeRest(ClassComment) |
| 87 | case s.HasPrefix(0, "/*"): |
| 88 | if !OpenBlockComment(s, "/*", "*/", ClassComment) { |
| 89 | return javaScriptInBlockComment, true |
| 90 | } |
| 91 | case s.line[s.pos] == '`': |
| 92 | if !openTemplate(s) { |
| 93 | return javaScriptInTemplate, true |
| 94 | } |
| 95 | case s.line[s.pos] == '"' || s.line[s.pos] == '\'': |
| 96 | TakeQuoted(s, s.line[s.pos], ClassString) |
| 97 | case IsDigit(s.line[s.pos]): |
| 98 | takeJavaScriptNumber(s) |
| 99 | case isJavaScriptWordStart(s.line[s.pos]): |
| 100 | takeJavaScriptWord(s) |
| 101 | case IsPunctuationRune(s.line[s.pos]): |
| 102 | s.Take(1, ClassPunctuation) |
| 103 | case IsOperatorRune(s.line[s.pos]): |
| 104 | s.TakeWhile(ClassOperator, IsOperatorRune) |
| 105 | default: |
| 106 | s.pos++ |
| 107 | } |
| 108 | return javaScriptGround, false |
| 109 | } |
| 110 | |
| 111 | // isJavaScriptWordStart reports whether a rune can begin an identifier. |
| 112 | func isJavaScriptWordStart(r rune) bool { return IsLetter(r) || r == '_' || r == '$' } |
| 113 | |
| 114 | // isJavaScriptWordRune reports whether a rune can continue one. |
| 115 | func isJavaScriptWordRune(r rune) bool { return IsWordRune(r) || r == '$' } |
| 116 | |
| 117 | // takeJavaScriptWord colours an identifier, keyword, constant or global. |
| 118 | // |
| 119 | // A name followed by "(" is a call, which reads as a function — the same rule |
| 120 | // the Go highlighter uses, and the same reason: it is what the eye is looking |
| 121 | // for when scanning unfamiliar code. |
| 122 | func takeJavaScriptWord(s *LineScanner) { |
| 123 | start := s.pos |
| 124 | for !s.AtEnd() && isJavaScriptWordRune(s.line[s.pos]) { |
| 125 | s.pos++ |
| 126 | } |
| 127 | |
| 128 | word := string(s.line[start:s.pos]) |
| 129 | if class, ok := javaScriptKeywords[word]; ok { |
| 130 | s.Emit(start, s.pos, class) |
| 131 | return |
| 132 | } |
| 133 | if isCallSite(s) { |
| 134 | s.Emit(start, s.pos, ClassFunction) |
| 135 | return |
| 136 | } |
| 137 | s.Emit(start, s.pos, ClassIdentifier) |
| 138 | } |
| 139 | |
| 140 | // isCallSite reports whether the next thing on the line, past any spaces, is |
| 141 | // an opening parenthesis. |
| 142 | func isCallSite(s *LineScanner) bool { |
| 143 | for at := s.pos; at < len(s.line); at++ { |
| 144 | switch s.line[at] { |
| 145 | case ' ', '\t': |
| 146 | case '(': |
| 147 | return true |
| 148 | default: |
| 149 | return false |
| 150 | } |
| 151 | } |
| 152 | return false |
| 153 | } |
| 154 | |
| 155 | // openTemplate colours a template literal that starts here, and reports |
| 156 | // whether it also ended on this line. |
| 157 | // |
| 158 | // The whole literal is a string, interpolations included. Colouring the code |
| 159 | // inside ${…} would mean the scanner reaching back into itself with a nesting |
| 160 | // depth to track, for a construct that is usually one short expression. |
| 161 | func openTemplate(s *LineScanner) bool { |
| 162 | start := s.pos |
| 163 | s.pos++ // past the opening backtick, so it is not found as the closing one |
| 164 | return takeTemplateFrom(s, start) |
| 165 | } |
| 166 | |
| 167 | // finishTemplate colours the continuation of a template opened on an earlier |
| 168 | // line, and reports whether it ended on this one. |
| 169 | func finishTemplate(s *LineScanner) bool { return takeTemplateFrom(s, s.pos) } |
| 170 | |
| 171 | // takeTemplateFrom colours from start to the end of the template, or to the |
| 172 | // end of the line when it does not close here. |
| 173 | // |
| 174 | // The start is passed in rather than read back from the scanner afterwards: |
| 175 | // runToBacktick has already moved the position, and emit drops empty spans, so |
| 176 | // anything that tried to colour "what is left" would colour nothing at all. |
| 177 | func takeTemplateFrom(s *LineScanner, start int) bool { |
| 178 | closed := runToBacktick(s) |
| 179 | if !closed { |
| 180 | s.pos = len(s.line) |
| 181 | } |
| 182 | s.Emit(start, s.pos, ClassString) |
| 183 | return closed |
| 184 | } |
| 185 | |
| 186 | // runToBacktick advances to just past the next unescaped backtick, and reports |
| 187 | // whether it found one. |
| 188 | func runToBacktick(s *LineScanner) bool { |
| 189 | for !s.AtEnd() { |
| 190 | if s.line[s.pos] == '\\' && s.pos+1 < len(s.line) { |
| 191 | s.pos += 2 |
| 192 | continue |
| 193 | } |
| 194 | if s.line[s.pos] == '`' { |
| 195 | s.pos++ |
| 196 | return true |
| 197 | } |
| 198 | s.pos++ |
| 199 | } |
| 200 | return false |
| 201 | } |
| 202 | |
| 203 | // takeJavaScriptNumber colours a numeric literal, including the 0x, 0o and 0b |
| 204 | // forms, underscore separators and the trailing n of a BigInt. |
| 205 | func takeJavaScriptNumber(s *LineScanner) { |
| 206 | s.TakeWhile(ClassNumber, func(r rune) bool { |
| 207 | return IsDigit(r) || IsLetter(r) || r == '_' || r == '.' |
| 208 | }) |
| 209 | } |