turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

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

🛟 Updated. 28d5985 · on v1.0.0 · k33g · 16h ago
javascript.go · 209 lines · 7.2 KBGo Blame HistoryRaw
  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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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 == '.'
	})
}