package syntax // javaScriptKeywords are the words that structure the language. var javaScriptKeywords = map[string]Class{ "await": ClassKeyword, "break": ClassKeyword, "case": ClassKeyword, "catch": ClassKeyword, "class": ClassKeyword, "const": ClassKeyword, "continue": ClassKeyword, "debugger": ClassKeyword, "default": ClassKeyword, "delete": ClassKeyword, "do": ClassKeyword, "else": ClassKeyword, "export": ClassKeyword, "extends": ClassKeyword, "finally": ClassKeyword, "for": ClassKeyword, "from": ClassKeyword, "function": ClassKeyword, "get": ClassKeyword, "if": ClassKeyword, "import": ClassKeyword, "in": ClassKeyword, "instanceof": ClassKeyword, "let": ClassKeyword, "new": ClassKeyword, "of": ClassKeyword, "return": ClassKeyword, "set": ClassKeyword, "static": ClassKeyword, "super": ClassKeyword, "switch": ClassKeyword, "throw": ClassKeyword, "try": ClassKeyword, "typeof": ClassKeyword, "var": ClassKeyword, "void": ClassKeyword, "while": ClassKeyword, "with": ClassKeyword, "yield": ClassKeyword, "async": ClassKeyword, "true": ClassConstant, "false": ClassConstant, "null": ClassConstant, "undefined": ClassConstant, "NaN": ClassConstant, "Infinity": ClassConstant, "this": ClassConstant, // The globals worth telling apart from a variable of your own. Recognised // by name, as Go's predeclared identifiers are, because a file may shadow // them and colouring the shadowed one anyway is what every editor does. "console": ClassBuiltin, "document": ClassBuiltin, "window": ClassBuiltin, "Array": ClassBuiltin, "Object": ClassBuiltin, "String": ClassBuiltin, "Number": ClassBuiltin, "Boolean": ClassBuiltin, "Promise": ClassBuiltin, "Math": ClassBuiltin, "JSON": ClassBuiltin, "Date": ClassBuiltin, "RegExp": ClassBuiltin, "Error": ClassBuiltin, "Map": ClassBuiltin, "Set": ClassBuiltin, "Symbol": ClassBuiltin, "globalThis": ClassBuiltin, } // javaScriptCarry is what a line can leave open: a block comment, or a // template literal, which is the only string in the language that may span // lines. type javaScriptCarry uint8 const ( javaScriptGround javaScriptCarry = iota javaScriptInBlockComment javaScriptInTemplate ) // highlightJavaScript colours JavaScript, one slice of spans per line. // // Regular-expression literals are deliberately not recognised. Telling `/x/g` // from a division needs to know whether the previous token could end an // expression, and getting it wrong colours the rest of a line as a string — // a worse failure than leaving a regex the colour of an operator. func highlightJavaScript(src string) [][]Span { return ScanLines(src, scanJavaScriptLine) } // scanJavaScriptLine returns the spans of one line, and what is left open. func scanJavaScriptLine(line []rune, carry javaScriptCarry) ([]Span, javaScriptCarry) { s := &LineScanner{line: line} switch carry { case javaScriptInBlockComment: if !FinishBlockComment(s, "*/", ClassComment) { return s.spans, javaScriptInBlockComment } case javaScriptInTemplate: if !finishTemplate(s) { return s.spans, javaScriptInTemplate } } for !s.AtEnd() { if next, stop := stepJavaScript(s); stop { return s.spans, next } } return s.spans, javaScriptGround } // stepJavaScript recognises whatever starts at the current position, and says // whether it left something open at the end of the line. func stepJavaScript(s *LineScanner) (javaScriptCarry, bool) { switch { case s.line[s.pos] == ' ' || s.line[s.pos] == '\t': s.pos++ case s.HasPrefix(0, "//"): s.TakeRest(ClassComment) case s.HasPrefix(0, "/*"): if !OpenBlockComment(s, "/*", "*/", ClassComment) { return javaScriptInBlockComment, true } case s.line[s.pos] == '`': if !openTemplate(s) { return javaScriptInTemplate, true } case s.line[s.pos] == '"' || s.line[s.pos] == '\'': TakeQuoted(s, s.line[s.pos], ClassString) case IsDigit(s.line[s.pos]): takeJavaScriptNumber(s) case isJavaScriptWordStart(s.line[s.pos]): takeJavaScriptWord(s) case IsPunctuationRune(s.line[s.pos]): s.Take(1, ClassPunctuation) case IsOperatorRune(s.line[s.pos]): s.TakeWhile(ClassOperator, IsOperatorRune) default: s.pos++ } return javaScriptGround, false } // isJavaScriptWordStart reports whether a rune can begin an identifier. func isJavaScriptWordStart(r rune) bool { return IsLetter(r) || r == '_' || r == '$' } // isJavaScriptWordRune reports whether a rune can continue one. func isJavaScriptWordRune(r rune) bool { return IsWordRune(r) || r == '$' } // takeJavaScriptWord colours an identifier, keyword, constant or global. // // A name followed by "(" is a call, which reads as a function — the same rule // the Go highlighter uses, and the same reason: it is what the eye is looking // for when scanning unfamiliar code. func takeJavaScriptWord(s *LineScanner) { start := s.pos for !s.AtEnd() && isJavaScriptWordRune(s.line[s.pos]) { s.pos++ } word := string(s.line[start:s.pos]) if class, ok := javaScriptKeywords[word]; ok { s.Emit(start, s.pos, class) return } if isCallSite(s) { s.Emit(start, s.pos, ClassFunction) return } s.Emit(start, s.pos, ClassIdentifier) } // isCallSite reports whether the next thing on the line, past any spaces, is // an opening parenthesis. func isCallSite(s *LineScanner) bool { for at := s.pos; at < len(s.line); at++ { switch s.line[at] { case ' ', '\t': case '(': return true default: return false } } return false } // openTemplate colours a template literal that starts here, and reports // whether it also ended on this line. // // The whole literal is a string, interpolations included. Colouring the code // inside ${…} would mean the scanner reaching back into itself with a nesting // depth to track, for a construct that is usually one short expression. func openTemplate(s *LineScanner) bool { start := s.pos s.pos++ // past the opening backtick, so it is not found as the closing one return takeTemplateFrom(s, start) } // finishTemplate colours the continuation of a template opened on an earlier // line, and reports whether it ended on this one. func finishTemplate(s *LineScanner) bool { return takeTemplateFrom(s, s.pos) } // takeTemplateFrom colours from start to the end of the template, or to the // end of the line when it does not close here. // // The start is passed in rather than read back from the scanner afterwards: // runToBacktick has already moved the position, and emit drops empty spans, so // anything that tried to colour "what is left" would colour nothing at all. func takeTemplateFrom(s *LineScanner, start int) bool { closed := runToBacktick(s) if !closed { s.pos = len(s.line) } s.Emit(start, s.pos, ClassString) return closed } // runToBacktick advances to just past the next unescaped backtick, and reports // whether it found one. func runToBacktick(s *LineScanner) bool { for !s.AtEnd() { if s.line[s.pos] == '\\' && s.pos+1 < len(s.line) { s.pos += 2 continue } if s.line[s.pos] == '`' { s.pos++ return true } s.pos++ } return false } // takeJavaScriptNumber colours a numeric literal, including the 0x, 0o and 0b // forms, underscore separators and the trailing n of a BigInt. func takeJavaScriptNumber(s *LineScanner) { s.TakeWhile(ClassNumber, func(r rune) bool { return IsDigit(r) || IsLetter(r) || r == '_' || r == '.' }) }