-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscan_javascript.go
More file actions
151 lines (145 loc) · 4.81 KB
/
Copy pathscan_javascript.go
File metadata and controls
151 lines (145 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package highlight
// jsKeywords is the reserved words, plus the literals that read as
// keywords. The contextual ones -- get, set, from, static -- are left
// out: they are ordinary method names often enough that coloring them
// would be wrong more than right.
var jsKeywords = words(
"async", "await", "break", "case", "catch", "class", "const",
"continue", "debugger", "default", "delete", "do", "else",
"export", "extends", "false", "finally", "for", "function",
"if", "import", "in", "instanceof", "let", "new", "null", "of",
"return", "super", "switch", "this", "throw", "true", "try",
"typeof", "undefined", "var", "void", "while", "with", "yield",
)
// jsNames is the globals a page reaches for. They are the closest
// thing JavaScript has to Go's predeclared identifiers; everything else
// that gets the name color is a call, recognized by the paren after it.
var jsNames = words(
"AbortController", "Array", "Boolean", "DOMParser", "Date",
"Error", "EventSource", "FormData", "JSON", "Map", "Math",
"Number", "Object", "Promise", "RegExp", "Set", "String", "URL",
"URLSearchParams", "clearInterval", "clearTimeout", "console",
"document", "fetch", "history", "localStorage", "location",
"navigator", "queueMicrotask", "requestAnimationFrame",
"sessionStorage", "setInterval", "setTimeout",
"structuredClone", "window",
)
// scanJS tokenizes one line of JavaScript, carrying the two states that
// outlive a line: a template literal and a block comment. A quoted
// string is not one of them, since a newline inside one is an error.
//
// A slash is the language's ambiguity: it opens a regex where a value
// belongs and divides where an operand just ended. What precedes it
// decides, so each branch says whether what it read was a value. That is
// the same rule a parser uses, minus the parser.
func scanJS(st state, line string) ([]token, state) {
var ts tokens
// Whether the last token ended an operand, for the slash above.
// False at the start of a line, which reads as "a value belongs
// here": a line beginning with a regex is a line beginning with a
// value.
//
// This was the byte the previous token began with, which a literal
// could never satisfy -- see closesOperand.
var operand bool
for i := 0; i < len(line); {
switch st {
case stateRawString:
n, closed := ts.drain("s", line[i:], "`")
i += n
if !closed {
return ts.done(), st
}
st = stateCode
// A template literal is a value, so a slash after it
// divides. This said the same thing by setting prev to a
// backtick, which closesOperand answers no to, so it
// read the slash in `` `a` / b / c `` as a regex.
operand = true
continue
case stateBlockComment:
n, closed := ts.drain("c", line[i:], "*/")
i += n
if !closed {
return ts.done(), st
}
st = stateCode
continue
}
c := line[i]
switch {
case c == ' ' || c == '\t':
ts.add("", line[i:i+1])
i++
continue
case c == '/' && i+1 < len(line) && line[i+1] == '/':
ts.add("c", line[i:])
return ts.done(), st
case c == '/' && i+1 < len(line) && line[i+1] == '*':
ts.add("c", "/*")
i += 2
st = stateBlockComment
operand = false
case c == '/' && !operand:
// A regex, if it closes on this line. One that does not is
// a division whose right side is still being typed.
if n := scanRegex(line[i:]); n > 0 {
ts.add("s", line[i:i+n])
i += n
operand = true
break
}
ts.add("", line[i:i+1])
i++
operand = false
case c == '`':
ts.add("s", "`")
i++
st = stateRawString
// Not a value yet. The branch above sets it when the
// literal closes, which may be lines from here.
operand = false
case c == '"' || c == '\'':
n := scanQuoted(line[i:], c)
ts.add("s", line[i:i+n])
i += n
operand = true
case isDigit(c) || (c == '.' && i+1 < len(line) && isDigit(line[i+1])):
n := scanNumber(line[i:])
ts.add("m", line[i:i+n])
i += n
operand = true
case isJSIdentStart(c):
j := i + 1
for j < len(line) && isJSIdent(line[j]) {
j++
}
word := line[i:j]
switch {
case jsKeywords[word]:
ts.add("k", word)
case jsNames[word]:
ts.add("n", word)
case j < len(line) && line[j] == '(':
ts.add("n", word)
default:
ts.add("", word)
}
i = j
// Every word, keyword included. `return /re/` is a regex
// after a keyword that expects a value, which this reads
// as a division -- the same answer the byte gave, and a
// keyword list of its own to fix.
operand = true
default:
ts.add("", line[i:i+1])
i++
operand = closesOperand(c)
}
}
return ts.done(), st
}
// isJSIdentStart reports whether c can begin a name. The dollar sign
// can, and is a name of its own in plenty of code.
func isJSIdentStart(c byte) bool { return c == '$' || isIdentStart(c) }
func isJSIdent(c byte) bool { return isJSIdentStart(c) || isDigit(c) }