dsh-live-trace 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/README.zh.md +790 -0
- package/assets/rain.ogg +0 -0
- package/bin/dsh-glyph-probe.js +51 -0
- package/bin/dsh-live-trace.js +29 -0
- package/bin/dsh-live-working.js +14 -0
- package/cordis.patch.yml +31 -0
- package/icon.svg +12 -0
- package/index.js +328 -0
- package/lib/client.js +178 -0
- package/lib/instance.js +68 -0
- package/lib/normalize.js +850 -0
- package/lib/paths.js +66 -0
- package/lib/protocol.js +115 -0
- package/lib/registry.js +232 -0
- package/lib/tools.js +257 -0
- package/lib/tracker.js +648 -0
- package/lib/transport.js +231 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -0
- package/picture/call1.png +0 -0
- package/picture/call2.png +0 -0
- package/picture/sleep1.png +0 -0
- package/picture/sleep2.png +0 -0
- package/picture/tui1.png +0 -0
- package/picture/tui2.png +0 -0
- package/picture/type1.png +0 -0
- package/picture/type2.png +0 -0
- package/scripts/bench-render.mjs +69 -0
- package/scripts/demo-working.mjs +130 -0
- package/scripts/demo.mjs +284 -0
- package/scripts/install-profile.mjs +174 -0
- package/scripts/mock-provider.mjs +211 -0
- package/src/cli/cellsize.js +120 -0
- package/src/cli/format.js +73 -0
- package/src/cli/highlight.js +932 -0
- package/src/cli/i18n.js +457 -0
- package/src/cli/main.js +630 -0
- package/src/cli/markdown.js +753 -0
- package/src/cli/renderer.js +1044 -0
- package/src/cli/screen.js +270 -0
- package/src/cli/theme.js +221 -0
- package/src/cli/view-state.js +396 -0
- package/src/cli/views.js +406 -0
- package/src/cli/width.js +337 -0
- package/src/cli/working/art.js +413 -0
- package/src/cli/working/main.js +569 -0
- package/src/cli/working/packing.js +159 -0
- package/src/cli/working/picker.js +75 -0
- package/src/cli/working/props.js +385 -0
- package/src/cli/working/scene.js +837 -0
- package/src/cli/working/sky.js +641 -0
- package/src/cli/working/sound.js +400 -0
- package/src/cli/working/state.js +528 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency syntax highlighting for the dashboard's code panes.
|
|
3
|
+
*
|
|
4
|
+
* The highlighter never emits escape sequences: it returns {@link Segment}
|
|
5
|
+
* lists whose `sgr` fields are raw SGR parameter strings handed to
|
|
6
|
+
* `renderRow()` in `width.js`. Every palette value may be the empty string
|
|
7
|
+
* (color disabled), which this module normalizes to "no style".
|
|
8
|
+
*
|
|
9
|
+
* The one hard invariant is *character preservation*: for any input line `l`,
|
|
10
|
+
*
|
|
11
|
+
* highlightLine(l, lang, syn).map((s) => s.text).join('') === sanitize(l)
|
|
12
|
+
*
|
|
13
|
+
* The scanner achieves this structurally — it walks the sanitized line and
|
|
14
|
+
* emits contiguous slices, so a malformed string, an unterminated comment or a
|
|
15
|
+
* stray control character can never drop or duplicate text. Unknown languages
|
|
16
|
+
* degrade to plain text rather than throwing.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-live-trace/highlight
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { sanitize } from './width.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{ text: string, sgr?: string }} Segment
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Shared empty set so every config has a Set to query. */
|
|
28
|
+
const EMPTY_SET = new Set()
|
|
29
|
+
|
|
30
|
+
/** Sticky word regex for C-like identifiers. */
|
|
31
|
+
const RE_WORD = /[A-Za-z_$][A-Za-z0-9_$]*/y
|
|
32
|
+
/** CSS identifiers may start with one or two dashes and contain dashes. */
|
|
33
|
+
const RE_WORD_CSS = /(?:--|-)?[A-Za-z_][A-Za-z0-9_-]*/y
|
|
34
|
+
/** Numbers: hex / binary / octal / decimal / fraction / exponent, optional BigInt `n`. */
|
|
35
|
+
const RE_NUMBER =
|
|
36
|
+
/(?:0[xX][0-9a-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+|(?:\d[\d_]*(?:\.[\d_]*)?|\.[\d_]+)(?:[eE][+-]?\d[\d_]*)?)(?:[nN])?/y
|
|
37
|
+
/** CSS numbers may carry a unit (`10px`) or a percentage (`50%`). */
|
|
38
|
+
const RE_NUMBER_UNITS =
|
|
39
|
+
/(?:\d[\d_]*(?:\.[\d_]*)?|\.[\d_]+)(?:[eE][+-]?\d+)?(?:%|[A-Za-z]+)?/y
|
|
40
|
+
/** Bash `-x` / `--long-flag`. */
|
|
41
|
+
const RE_DASH_FLAG = /--?[A-Za-z][A-Za-z0-9-]*/y
|
|
42
|
+
/** Multi-character operators first, then the single-character operator class. */
|
|
43
|
+
const RE_OPERATOR =
|
|
44
|
+
/(?:=>|->|::|\.\.\.|===|!==|==|!=|<=|>=|&&|\|\||\?\?|\?\.|\*\*|<<=|>>=|\+=|-=|\*=|\/=|%=|&=|\|=|\^=|<<|>>|\+\+|--|[-+*/%=<>!&|^~])/y
|
|
45
|
+
/** Structural punctuation. */
|
|
46
|
+
const RE_PUNCT = /[()[\]{},;.:?]/y
|
|
47
|
+
/** YAML/JSON-ish key colon: `key:` followed by whitespace or a flow bracket. */
|
|
48
|
+
const RE_STRICT_COLON_TAIL = /^[\s[{,}\]]/
|
|
49
|
+
/** CSS hex colour. */
|
|
50
|
+
const RE_HEX_COLOR = /^#[0-9a-fA-F]{3,8}/
|
|
51
|
+
/** Diff hunk header. */
|
|
52
|
+
const RE_HUNK = /^@@/
|
|
53
|
+
|
|
54
|
+
function set(...items) {
|
|
55
|
+
return new Set(items)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isDigit(ch) {
|
|
59
|
+
return ch >= '0' && ch <= '9'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isWordStart(ch) {
|
|
63
|
+
return typeof ch === 'string' && /[A-Za-z_$]/.test(ch)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a palette entry to a style, collapsing `''`/missing/`undefined` to
|
|
68
|
+
* "unstyled" so disabled colors merge cleanly.
|
|
69
|
+
* @param {Record<string, string> | undefined | null} palette
|
|
70
|
+
* @param {string | undefined} key
|
|
71
|
+
* @returns {string | undefined}
|
|
72
|
+
*/
|
|
73
|
+
function styleOf(palette, key) {
|
|
74
|
+
if (key === undefined || palette === null || palette === undefined) return undefined
|
|
75
|
+
const value = palette[key]
|
|
76
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Append text to a segment list, merging with the previous segment when the
|
|
81
|
+
* style is identical. Unstyled runs are stored without an `sgr` key.
|
|
82
|
+
* @param {Segment[]} out
|
|
83
|
+
* @param {string} text
|
|
84
|
+
* @param {string | undefined} sgr
|
|
85
|
+
*/
|
|
86
|
+
function pushSegment(out, text, sgr) {
|
|
87
|
+
if (text.length === 0) return
|
|
88
|
+
const style = sgr === '' ? undefined : sgr
|
|
89
|
+
const last = out[out.length - 1]
|
|
90
|
+
if (last !== undefined && last.sgr === style) {
|
|
91
|
+
last.text += text
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if (style === undefined) out.push({ text })
|
|
95
|
+
else out.push({ text, sgr: style })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Count a run of identical characters starting at `index`. */
|
|
99
|
+
function countRun(line, index, ch) {
|
|
100
|
+
let end = index
|
|
101
|
+
while (end < line.length && line[end] === ch) end += 1
|
|
102
|
+
return end - index
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Match a config word at `index`, or `null`. */
|
|
106
|
+
function readWord(line, index, cfg) {
|
|
107
|
+
const re = cfg.wordPattern ?? RE_WORD
|
|
108
|
+
re.lastIndex = index
|
|
109
|
+
const match = re.exec(line)
|
|
110
|
+
return match === null ? null : match[0]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Strict key colon: whitespace or a flow bracket must follow. */
|
|
114
|
+
function isStrictKeyColon(line, index) {
|
|
115
|
+
if (line[index] !== ':') return false
|
|
116
|
+
const next = line[index + 1]
|
|
117
|
+
if (next === undefined) return true
|
|
118
|
+
return RE_STRICT_COLON_TAIL.test(next)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Fill in the shared scanner defaults. */
|
|
122
|
+
function makeConfig(partial) {
|
|
123
|
+
return {
|
|
124
|
+
lineComments: [],
|
|
125
|
+
blockComments: [],
|
|
126
|
+
tripleStrings: [],
|
|
127
|
+
strings: [],
|
|
128
|
+
templates: [],
|
|
129
|
+
keywords: EMPTY_SET,
|
|
130
|
+
types: EMPTY_SET,
|
|
131
|
+
literals: EMPTY_SET,
|
|
132
|
+
builtins: EMPTY_SET,
|
|
133
|
+
selfWords: EMPTY_SET,
|
|
134
|
+
globals: EMPTY_SET,
|
|
135
|
+
...partial
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ------------------------------------------------------------------ *
|
|
140
|
+
* Per-language token sets
|
|
141
|
+
* ------------------------------------------------------------------ */
|
|
142
|
+
|
|
143
|
+
const JS_KEYWORDS = set(
|
|
144
|
+
'as', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
|
|
145
|
+
'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
|
|
146
|
+
'for', 'from', 'function', 'get', 'if', 'import', 'in', 'instanceof', 'let',
|
|
147
|
+
'new', 'of', 'return', 'set', 'static', 'switch', 'throw', 'try', 'typeof',
|
|
148
|
+
'var', 'void', 'while', 'with', 'yield'
|
|
149
|
+
)
|
|
150
|
+
const JS_TYPES = set(
|
|
151
|
+
'Array', 'ArrayBuffer', 'BigInt', 'Boolean', 'DataView', 'Date', 'Error',
|
|
152
|
+
'EvalError', 'Float32Array', 'Float64Array', 'Function', 'Int8Array',
|
|
153
|
+
'Int16Array', 'Int32Array', 'JSON', 'Map', 'Math', 'Number', 'Object',
|
|
154
|
+
'Promise', 'Proxy', 'RangeError', 'ReferenceError', 'Reflect', 'RegExp', 'Set',
|
|
155
|
+
'String', 'Symbol', 'SyntaxError', 'TypeError', 'Uint8Array',
|
|
156
|
+
'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'URIError', 'WeakMap',
|
|
157
|
+
'WeakSet'
|
|
158
|
+
)
|
|
159
|
+
const JS_LITERALS = set('true', 'false', 'null', 'undefined', 'NaN', 'Infinity')
|
|
160
|
+
const JS_BUILTINS = set(
|
|
161
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'decodeURI',
|
|
162
|
+
'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'require',
|
|
163
|
+
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'queueMicrotask',
|
|
164
|
+
'structuredClone', 'fetch'
|
|
165
|
+
)
|
|
166
|
+
const JS_GLOBALS = set(
|
|
167
|
+
'console', 'process', 'globalThis', 'window', 'document', 'global', 'module',
|
|
168
|
+
'exports', '__dirname', '__filename', 'Buffer'
|
|
169
|
+
)
|
|
170
|
+
const JS_CONFIG = makeConfig({
|
|
171
|
+
lineComments: ['//'],
|
|
172
|
+
blockComments: [['/*', '*/']],
|
|
173
|
+
strings: ["'", '"'],
|
|
174
|
+
templates: ['`'],
|
|
175
|
+
keywords: JS_KEYWORDS,
|
|
176
|
+
types: JS_TYPES,
|
|
177
|
+
literals: JS_LITERALS,
|
|
178
|
+
builtins: JS_BUILTINS,
|
|
179
|
+
globals: JS_GLOBALS,
|
|
180
|
+
decorators: true
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const TS_KEYWORDS = new Set([
|
|
184
|
+
...JS_KEYWORDS,
|
|
185
|
+
'abstract', 'declare', 'enum', 'implements', 'infer', 'interface', 'is',
|
|
186
|
+
'keyof', 'module', 'namespace', 'override', 'private', 'protected', 'public',
|
|
187
|
+
'readonly', 'satisfies', 'type', 'unique', 'using', 'accessor'
|
|
188
|
+
])
|
|
189
|
+
const TS_TYPES = new Set([
|
|
190
|
+
...JS_TYPES,
|
|
191
|
+
'any', 'bigint', 'boolean', 'never', 'number', 'object', 'string', 'symbol',
|
|
192
|
+
'unknown'
|
|
193
|
+
])
|
|
194
|
+
const TS_CONFIG = makeConfig({ ...JS_CONFIG, keywords: TS_KEYWORDS, types: TS_TYPES })
|
|
195
|
+
|
|
196
|
+
const JSON_CONFIG = makeConfig({
|
|
197
|
+
strings: ['"'],
|
|
198
|
+
literals: set('true', 'false', 'null'),
|
|
199
|
+
jsonKeys: true
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const BASH_KEYWORDS = set(
|
|
203
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done',
|
|
204
|
+
'case', 'esac', 'in', 'function', 'select', 'time', 'coproc', 'return',
|
|
205
|
+
'local', 'declare', 'typeset', 'export', 'readonly', 'unset', 'shift', 'exit',
|
|
206
|
+
'break', 'continue', 'eval', 'exec', 'trap', 'set', 'source'
|
|
207
|
+
)
|
|
208
|
+
const BASH_BUILTINS = set(
|
|
209
|
+
'echo', 'printf', 'read', 'cd', 'pwd', 'ls', 'cat', 'grep', 'sed', 'awk',
|
|
210
|
+
'cut', 'sort', 'uniq', 'head', 'tail', 'wc', 'tr', 'find', 'xargs', 'mkdir',
|
|
211
|
+
'rmdir', 'rm', 'cp', 'mv', 'touch', 'chmod', 'chown', 'ln', 'tar', 'gzip',
|
|
212
|
+
'curl', 'wget', 'git', 'npm', 'pnpm', 'yarn', 'node', 'python', 'python3',
|
|
213
|
+
'pip', 'make', 'docker', 'sudo', 'env', 'which', 'type', 'test', 'true',
|
|
214
|
+
'false', 'kill', 'jobs', 'wait', 'sleep', 'date', 'basename', 'dirname',
|
|
215
|
+
'realpath', 'tee', 'diff', 'patch'
|
|
216
|
+
)
|
|
217
|
+
const BASH_CONFIG = makeConfig({
|
|
218
|
+
lineComments: ['#'],
|
|
219
|
+
strings: ["'", '"'],
|
|
220
|
+
keywords: BASH_KEYWORDS,
|
|
221
|
+
builtins: BASH_BUILTINS,
|
|
222
|
+
dollarVariables: true,
|
|
223
|
+
dashFlags: true
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const PYTHON_KEYWORDS = set(
|
|
227
|
+
'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def',
|
|
228
|
+
'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if',
|
|
229
|
+
'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise',
|
|
230
|
+
'return', 'try', 'while', 'with', 'yield', 'match', 'case'
|
|
231
|
+
)
|
|
232
|
+
const PYTHON_TYPES = set(
|
|
233
|
+
'int', 'float', 'complex', 'str', 'bytes', 'bytearray', 'bool', 'list',
|
|
234
|
+
'tuple', 'dict', 'set', 'frozenset', 'range', 'object', 'type', 'memoryview'
|
|
235
|
+
)
|
|
236
|
+
const PYTHON_BUILTINS = set(
|
|
237
|
+
'print', 'len', 'enumerate', 'zip', 'map', 'filter', 'sum', 'min', 'max',
|
|
238
|
+
'abs', 'sorted', 'reversed', 'open', 'input', 'isinstance', 'issubclass',
|
|
239
|
+
'getattr', 'setattr', 'hasattr', 'delattr', 'super', 'next', 'iter', 'any',
|
|
240
|
+
'all', 'repr', 'format', 'id', 'round', 'divmod', 'pow', 'hash', 'callable',
|
|
241
|
+
'vars', 'dir', 'globals', 'locals', 'exec', 'compile', 'staticmethod',
|
|
242
|
+
'classmethod', 'property', 'ord', 'chr', 'hex', 'oct', 'bin'
|
|
243
|
+
)
|
|
244
|
+
const PYTHON_CONFIG = makeConfig({
|
|
245
|
+
lineComments: ['#'],
|
|
246
|
+
tripleStrings: ['"""', "'''"],
|
|
247
|
+
strings: ["'", '"'],
|
|
248
|
+
keywords: PYTHON_KEYWORDS,
|
|
249
|
+
types: PYTHON_TYPES,
|
|
250
|
+
literals: set('True', 'False', 'None', 'NotImplemented', 'Ellipsis'),
|
|
251
|
+
builtins: PYTHON_BUILTINS,
|
|
252
|
+
selfWords: set('self', 'cls'),
|
|
253
|
+
decorators: true
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
const GO_KEYWORDS = set(
|
|
257
|
+
'break', 'case', 'chan', 'const', 'continue', 'default', 'defer', 'else',
|
|
258
|
+
'fallthrough', 'for', 'func', 'go', 'goto', 'if', 'import', 'interface',
|
|
259
|
+
'map', 'package', 'range', 'return', 'select', 'struct', 'switch', 'type',
|
|
260
|
+
'var'
|
|
261
|
+
)
|
|
262
|
+
const GO_TYPES = set(
|
|
263
|
+
'bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64',
|
|
264
|
+
'int', 'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint', 'uint8',
|
|
265
|
+
'uint16', 'uint32', 'uint64', 'uintptr', 'any', 'comparable'
|
|
266
|
+
)
|
|
267
|
+
const GO_BUILTINS = set(
|
|
268
|
+
'make', 'len', 'cap', 'new', 'append', 'copy', 'delete', 'panic', 'recover',
|
|
269
|
+
'print', 'println', 'close', 'complex', 'real', 'imag', 'min', 'max', 'clear'
|
|
270
|
+
)
|
|
271
|
+
const GO_CONFIG = makeConfig({
|
|
272
|
+
lineComments: ['//'],
|
|
273
|
+
blockComments: [['/*', '*/']],
|
|
274
|
+
strings: ['"', "'"],
|
|
275
|
+
templates: ['`'],
|
|
276
|
+
keywords: GO_KEYWORDS,
|
|
277
|
+
types: GO_TYPES,
|
|
278
|
+
literals: set('true', 'false', 'nil', 'iota'),
|
|
279
|
+
builtins: GO_BUILTINS
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const RUST_KEYWORDS = set(
|
|
283
|
+
'as', 'async', 'await', 'break', 'const', 'continue', 'crate', 'dyn', 'else',
|
|
284
|
+
'enum', 'extern', 'fn', 'for', 'if', 'impl', 'in', 'let', 'loop', 'match',
|
|
285
|
+
'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static',
|
|
286
|
+
'struct', 'super', 'trait', 'type', 'unsafe', 'use', 'where', 'while', 'union',
|
|
287
|
+
'macro', 'yield', 'try'
|
|
288
|
+
)
|
|
289
|
+
const RUST_TYPES = set(
|
|
290
|
+
'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'u8', 'u16', 'u32', 'u64', 'u128',
|
|
291
|
+
'usize', 'f32', 'f64', 'bool', 'char', 'str', 'String', 'Vec', 'Option',
|
|
292
|
+
'Result', 'Box', 'Rc', 'Arc', 'Cell', 'RefCell', 'Mutex', 'RwLock',
|
|
293
|
+
'HashMap', 'HashSet', 'BTreeMap', 'BTreeSet', 'Cow', 'Pin', 'PhantomData',
|
|
294
|
+
'Ok', 'Err', 'Some', 'None'
|
|
295
|
+
)
|
|
296
|
+
const RUST_BUILTINS = set(
|
|
297
|
+
'println', 'print', 'eprintln', 'eprint', 'format', 'vec', 'panic', 'assert',
|
|
298
|
+
'assert_eq', 'assert_ne', 'debug_assert', 'write', 'writeln', 'matches',
|
|
299
|
+
'todo', 'unimplemented', 'unreachable', 'include_str', 'include_bytes', 'env',
|
|
300
|
+
'cfg', 'stringify', 'concat', 'dbg'
|
|
301
|
+
)
|
|
302
|
+
const RUST_CONFIG = makeConfig({
|
|
303
|
+
lineComments: ['//'],
|
|
304
|
+
blockComments: [['/*', '*/']],
|
|
305
|
+
strings: ['"', "'"],
|
|
306
|
+
keywords: RUST_KEYWORDS,
|
|
307
|
+
types: RUST_TYPES,
|
|
308
|
+
literals: set('true', 'false'),
|
|
309
|
+
builtins: RUST_BUILTINS,
|
|
310
|
+
macros: true,
|
|
311
|
+
lifetime: true
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const SQL_KEYWORDS = set(
|
|
315
|
+
'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'UPDATE', 'DELETE', 'CREATE',
|
|
316
|
+
'TABLE', 'ALTER', 'DROP', 'INDEX', 'VIEW', 'JOIN', 'LEFT', 'RIGHT', 'INNER',
|
|
317
|
+
'OUTER', 'FULL', 'CROSS', 'ON', 'AS', 'AND', 'OR', 'NOT', 'NULL', 'IS', 'IN',
|
|
318
|
+
'LIKE', 'BETWEEN', 'EXISTS', 'DISTINCT', 'UNION', 'ALL', 'GROUP', 'BY',
|
|
319
|
+
'ORDER', 'HAVING', 'LIMIT', 'OFFSET', 'VALUES', 'SET', 'PRIMARY', 'KEY',
|
|
320
|
+
'FOREIGN', 'REFERENCES', 'DEFAULT', 'CASCADE', 'CONSTRAINT', 'UNIQUE', 'CHECK',
|
|
321
|
+
'WITH', 'RETURNING', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'ASC', 'DESC',
|
|
322
|
+
'USING', 'NATURAL', 'TRUNCATE', 'EXPLAIN', 'ANALYZE', 'VACUUM', 'BEGIN',
|
|
323
|
+
'COMMIT', 'ROLLBACK', 'TRANSACTION', 'GRANT', 'REVOKE', 'IF', 'REPLACE'
|
|
324
|
+
)
|
|
325
|
+
const SQL_TYPES = set(
|
|
326
|
+
'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT', 'SERIAL', 'BIGSERIAL',
|
|
327
|
+
'TEXT', 'VARCHAR', 'CHAR', 'BOOLEAN', 'BOOL', 'DATE', 'TIME', 'TIMESTAMP',
|
|
328
|
+
'TIMESTAMPTZ', 'INTERVAL', 'DECIMAL', 'NUMERIC', 'REAL', 'DOUBLE', 'FLOAT',
|
|
329
|
+
'JSON', 'JSONB', 'UUID', 'BLOB', 'BYTEA', 'ARRAY'
|
|
330
|
+
)
|
|
331
|
+
const SQL_BUILTINS = set(
|
|
332
|
+
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COALESCE', 'NULLIF', 'CAST', 'NOW',
|
|
333
|
+
'CURRENT_DATE', 'CURRENT_TIMESTAMP', 'LOWER', 'UPPER', 'LENGTH', 'TRIM',
|
|
334
|
+
'SUBSTRING', 'ROUND', 'ABS', 'CONCAT', 'STRING_AGG', 'ARRAY_AGG',
|
|
335
|
+
'ROW_NUMBER', 'RANK', 'DENSE_RANK'
|
|
336
|
+
)
|
|
337
|
+
const SQL_CONFIG = makeConfig({
|
|
338
|
+
lineComments: ['--'],
|
|
339
|
+
blockComments: [['/*', '*/']],
|
|
340
|
+
strings: ["'", '"'],
|
|
341
|
+
keywords: SQL_KEYWORDS,
|
|
342
|
+
types: SQL_TYPES,
|
|
343
|
+
literals: set('TRUE', 'FALSE', 'NULL'),
|
|
344
|
+
builtins: SQL_BUILTINS,
|
|
345
|
+
caseInsensitive: true
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const CSS_CONFIG = makeConfig({
|
|
349
|
+
blockComments: [['/*', '*/']],
|
|
350
|
+
strings: ['"', "'"],
|
|
351
|
+
wordPattern: RE_WORD_CSS,
|
|
352
|
+
keywords: set(
|
|
353
|
+
'important', 'inherit', 'initial', 'unset', 'revert', 'none', 'auto',
|
|
354
|
+
'block', 'inline', 'flex', 'grid', 'absolute', 'relative', 'fixed',
|
|
355
|
+
'sticky', 'transparent', 'currentColor'
|
|
356
|
+
),
|
|
357
|
+
dashWord: true,
|
|
358
|
+
keyColonLoose: true,
|
|
359
|
+
hashColor: true,
|
|
360
|
+
numberUnits: true,
|
|
361
|
+
decorators: true
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const YAML_CONFIG = makeConfig({
|
|
365
|
+
lineComments: ['#'],
|
|
366
|
+
strings: ["'", '"'],
|
|
367
|
+
literals: set(
|
|
368
|
+
'true', 'false', 'null', 'yes', 'no', 'on', 'off', 'True', 'False', 'Null',
|
|
369
|
+
'NULL', 'TRUE', 'FALSE', 'Yes', 'No', 'On', 'Off', '~'
|
|
370
|
+
),
|
|
371
|
+
keyColon: true,
|
|
372
|
+
anchorChars: '&*',
|
|
373
|
+
metaAtStart: true,
|
|
374
|
+
tildeLiteral: true
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
/* ------------------------------------------------------------------ *
|
|
378
|
+
* Custom line handlers
|
|
379
|
+
* ------------------------------------------------------------------ */
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* HTML/XML: comments, doctype, tag names, attributes and quoted values.
|
|
383
|
+
* Text outside tags is left unstyled. Never loses a character.
|
|
384
|
+
* @returns {Segment[]}
|
|
385
|
+
*/
|
|
386
|
+
function htmlLine(line, syn, state) {
|
|
387
|
+
const out = []
|
|
388
|
+
const n = line.length
|
|
389
|
+
let i = 0
|
|
390
|
+
while (i < n) {
|
|
391
|
+
if (state.block) {
|
|
392
|
+
const idx = line.indexOf(state.block, i)
|
|
393
|
+
if (idx === -1) {
|
|
394
|
+
pushSegment(out, line.slice(i), styleOf(syn, 'comment'))
|
|
395
|
+
return out
|
|
396
|
+
}
|
|
397
|
+
pushSegment(out, line.slice(i, idx + state.block.length), styleOf(syn, 'comment'))
|
|
398
|
+
i = idx + state.block.length
|
|
399
|
+
state.block = null
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
if (line.startsWith('<!--', i)) {
|
|
403
|
+
const idx = line.indexOf('-->', i + 4)
|
|
404
|
+
if (idx === -1) {
|
|
405
|
+
state.block = '-->'
|
|
406
|
+
pushSegment(out, line.slice(i), styleOf(syn, 'comment'))
|
|
407
|
+
return out
|
|
408
|
+
}
|
|
409
|
+
pushSegment(out, line.slice(i, idx + 3), styleOf(syn, 'comment'))
|
|
410
|
+
i = idx + 3
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
if (line[i] === '<') {
|
|
414
|
+
if (/^<!doctype/i.test(line.slice(i, i + 9)) || line.startsWith('<!', i) || line.startsWith('<?', i)) {
|
|
415
|
+
const idx = line.indexOf('>', i)
|
|
416
|
+
const end = idx === -1 ? n : idx + 1
|
|
417
|
+
pushSegment(out, line.slice(i, end), styleOf(syn, 'meta'))
|
|
418
|
+
i = end
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
const idx = line.indexOf('>', i)
|
|
422
|
+
const end = idx === -1 ? n : idx + 1
|
|
423
|
+
highlightTag(line.slice(i, end), out, syn)
|
|
424
|
+
i = end
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
const next = line.indexOf('<', i)
|
|
428
|
+
const end = next === -1 ? n : next
|
|
429
|
+
pushSegment(out, line.slice(i, end))
|
|
430
|
+
i = end
|
|
431
|
+
}
|
|
432
|
+
return out
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Tokenize the inside of a single `<...>` tag. */
|
|
436
|
+
function highlightTag(text, out, syn) {
|
|
437
|
+
const n = text.length
|
|
438
|
+
let i = 0
|
|
439
|
+
if (text.startsWith('</')) {
|
|
440
|
+
pushSegment(out, '</', styleOf(syn, 'tag'))
|
|
441
|
+
i = 2
|
|
442
|
+
} else if (text.startsWith('<')) {
|
|
443
|
+
pushSegment(out, '<', styleOf(syn, 'tag'))
|
|
444
|
+
i = 1
|
|
445
|
+
}
|
|
446
|
+
let j = i
|
|
447
|
+
while (j < n && /[A-Za-z0-9:_.-]/.test(text[j])) j += 1
|
|
448
|
+
if (j > i) {
|
|
449
|
+
pushSegment(out, text.slice(i, j), styleOf(syn, 'tag'))
|
|
450
|
+
i = j
|
|
451
|
+
}
|
|
452
|
+
while (i < n) {
|
|
453
|
+
const ch = text[i]
|
|
454
|
+
if (ch === '>' || ch === '/') {
|
|
455
|
+
pushSegment(out, ch, styleOf(syn, 'tag'))
|
|
456
|
+
i += 1
|
|
457
|
+
continue
|
|
458
|
+
}
|
|
459
|
+
if (ch === '=') {
|
|
460
|
+
pushSegment(out, '=', styleOf(syn, 'operator'))
|
|
461
|
+
i += 1
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
if (ch === '"' || ch === "'") {
|
|
465
|
+
let end = i + 1
|
|
466
|
+
while (end < n && text[end] !== ch) end += 1
|
|
467
|
+
if (end < n) end += 1
|
|
468
|
+
pushSegment(out, text.slice(i, end), styleOf(syn, 'string'))
|
|
469
|
+
i = end
|
|
470
|
+
continue
|
|
471
|
+
}
|
|
472
|
+
if (/\s/.test(ch)) {
|
|
473
|
+
let end = i
|
|
474
|
+
while (end < n && /\s/.test(text[end])) end += 1
|
|
475
|
+
pushSegment(out, text.slice(i, end))
|
|
476
|
+
i = end
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
let end = i
|
|
480
|
+
while (end < n && /[A-Za-z0-9_:.@[\]-]/.test(text[end])) end += 1
|
|
481
|
+
if (end === i) end = i + 1
|
|
482
|
+
pushSegment(out, text.slice(i, end), styleOf(syn, 'attribute'))
|
|
483
|
+
i = end
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Markdown source highlighting: fences, headings, quotes, inline code, emphasis
|
|
489
|
+
* and links. Purely cosmetic — the block renderer lives in `markdown.js`.
|
|
490
|
+
* @returns {Segment[]}
|
|
491
|
+
*/
|
|
492
|
+
function markdownLine(line, syn) {
|
|
493
|
+
const out = []
|
|
494
|
+
const fence = /^ {0,3}(`{3,}|~{3,})/.exec(line)
|
|
495
|
+
if (fence) {
|
|
496
|
+
pushSegment(out, line, styleOf(syn, 'meta'))
|
|
497
|
+
return out
|
|
498
|
+
}
|
|
499
|
+
const heading = /^(#{1,6})(\s.*)?$/.exec(line)
|
|
500
|
+
if (heading) {
|
|
501
|
+
pushSegment(out, heading[1], styleOf(syn, 'keyword'))
|
|
502
|
+
if (heading[2] !== undefined) pushSegment(out, heading[2], styleOf(syn, 'type'))
|
|
503
|
+
return out
|
|
504
|
+
}
|
|
505
|
+
const n = line.length
|
|
506
|
+
let i = 0
|
|
507
|
+
while (i < n) {
|
|
508
|
+
const ch = line[i]
|
|
509
|
+
if (ch === '`') {
|
|
510
|
+
const run = countRun(line, i, '`')
|
|
511
|
+
const close = line.indexOf('`'.repeat(run), i + run)
|
|
512
|
+
if (close === -1) {
|
|
513
|
+
pushSegment(out, line.slice(i), styleOf(syn, 'string'))
|
|
514
|
+
i = n
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
pushSegment(out, line.slice(i, i + run), styleOf(syn, 'punctuation'))
|
|
518
|
+
pushSegment(out, line.slice(i + run, close), styleOf(syn, 'string'))
|
|
519
|
+
pushSegment(out, line.slice(close, close + run), styleOf(syn, 'punctuation'))
|
|
520
|
+
i = close + run
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
if (line.startsWith('**', i) || line.startsWith('__', i) || line.startsWith('~~', i)) {
|
|
524
|
+
pushSegment(out, line.slice(i, i + 2), styleOf(syn, 'keyword'))
|
|
525
|
+
i += 2
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
if (ch === '*' || ch === '_' || ch === '>') {
|
|
529
|
+
pushSegment(out, ch, styleOf(syn, 'keyword'))
|
|
530
|
+
i += 1
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
if (ch === '[') {
|
|
534
|
+
const close = line.indexOf(']', i + 1)
|
|
535
|
+
const open = close === -1 ? -1 : line.indexOf('(', close + 1)
|
|
536
|
+
if (close !== -1 && open === close + 1) {
|
|
537
|
+
const end = line.indexOf(')', open + 1)
|
|
538
|
+
if (end !== -1) {
|
|
539
|
+
pushSegment(out, line.slice(i, close + 1), styleOf(syn, 'function'))
|
|
540
|
+
pushSegment(out, line.slice(close + 1, end + 1), styleOf(syn, 'string'))
|
|
541
|
+
i = end + 1
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
let end = i
|
|
547
|
+
while (end < n && !'`*_~[]>#-'.includes(line[end])) end += 1
|
|
548
|
+
if (end === i) end = i + 1
|
|
549
|
+
pushSegment(out, line.slice(i, end))
|
|
550
|
+
i = end
|
|
551
|
+
}
|
|
552
|
+
return out
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Diff: whole-line colouring so additions and removals read at a glance.
|
|
557
|
+
* @returns {Segment[]}
|
|
558
|
+
*/
|
|
559
|
+
function diffLine(line, syn) {
|
|
560
|
+
const out = []
|
|
561
|
+
if (RE_HUNK.test(line)) pushSegment(out, line, styleOf(syn, 'meta'))
|
|
562
|
+
else if (line.startsWith('+++')) pushSegment(out, line, styleOf(syn, 'added'))
|
|
563
|
+
else if (line.startsWith('---')) pushSegment(out, line, styleOf(syn, 'removed'))
|
|
564
|
+
else if (line.startsWith('+')) pushSegment(out, line, styleOf(syn, 'added'))
|
|
565
|
+
else if (line.startsWith('-')) pushSegment(out, line, styleOf(syn, 'removed'))
|
|
566
|
+
else if (/^(?:diff |index |new file|deleted file|similarity |rename |old mode|new mode)/.test(line)) {
|
|
567
|
+
pushSegment(out, line, styleOf(syn, 'meta'))
|
|
568
|
+
} else pushSegment(out, line)
|
|
569
|
+
return out
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* ------------------------------------------------------------------ *
|
|
573
|
+
* Scanner
|
|
574
|
+
* ------------------------------------------------------------------ */
|
|
575
|
+
|
|
576
|
+
const CONFIGS = {
|
|
577
|
+
js: JS_CONFIG,
|
|
578
|
+
ts: TS_CONFIG,
|
|
579
|
+
json: JSON_CONFIG,
|
|
580
|
+
bash: BASH_CONFIG,
|
|
581
|
+
python: PYTHON_CONFIG,
|
|
582
|
+
diff: makeConfig({ handler: diffLine }),
|
|
583
|
+
css: CSS_CONFIG,
|
|
584
|
+
html: makeConfig({ handler: htmlLine }),
|
|
585
|
+
sql: SQL_CONFIG,
|
|
586
|
+
go: GO_CONFIG,
|
|
587
|
+
rust: RUST_CONFIG,
|
|
588
|
+
yaml: YAML_CONFIG,
|
|
589
|
+
markdown: makeConfig({ handler: markdownLine }),
|
|
590
|
+
text: makeConfig({ plain: true })
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Read one token starting at `index`.
|
|
595
|
+
* @returns {{ end: number, key?: string }}
|
|
596
|
+
*/
|
|
597
|
+
function readToken(line, index, cfg, syn, state) {
|
|
598
|
+
const n = line.length
|
|
599
|
+
|
|
600
|
+
// Resume a construct that ran off the end of the previous line.
|
|
601
|
+
if (state.block !== undefined && state.block !== null) {
|
|
602
|
+
const close = state.block
|
|
603
|
+
const idx = line.indexOf(close, index)
|
|
604
|
+
if (idx === -1) return { end: n, key: 'comment' }
|
|
605
|
+
state.block = null
|
|
606
|
+
return { end: idx + close.length, key: 'comment' }
|
|
607
|
+
}
|
|
608
|
+
if (state.triple !== undefined && state.triple !== null) {
|
|
609
|
+
const close = state.triple
|
|
610
|
+
const idx = line.indexOf(close, index)
|
|
611
|
+
if (idx === -1) return { end: n, key: 'string' }
|
|
612
|
+
state.triple = null
|
|
613
|
+
return { end: idx + close.length, key: 'string' }
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (cfg.metaAtStart === true && index === 0) {
|
|
617
|
+
if (/^(?:---|\.\.\.)(?:\s|$)/.test(line)) return { end: 3, key: 'meta' }
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
for (const marker of cfg.lineComments) {
|
|
621
|
+
if (line.startsWith(marker, index)) return { end: n, key: 'comment' }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for (const [open, close] of cfg.blockComments) {
|
|
625
|
+
if (line.startsWith(open, index)) {
|
|
626
|
+
const idx = line.indexOf(close, index + open.length)
|
|
627
|
+
if (idx === -1) {
|
|
628
|
+
state.block = close
|
|
629
|
+
return { end: n, key: 'comment' }
|
|
630
|
+
}
|
|
631
|
+
return { end: idx + close.length, key: 'comment' }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for (const quote of cfg.tripleStrings) {
|
|
636
|
+
if (line.startsWith(quote, index)) {
|
|
637
|
+
const idx = line.indexOf(quote, index + quote.length)
|
|
638
|
+
if (idx === -1) {
|
|
639
|
+
state.triple = quote
|
|
640
|
+
return { end: n, key: 'string' }
|
|
641
|
+
}
|
|
642
|
+
return { end: idx + quote.length, key: 'string' }
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const ch = line[index]
|
|
647
|
+
|
|
648
|
+
if (cfg.dashFlags === true && ch === '-' && /[-A-Za-z]/.test(line[index + 1] ?? '')) {
|
|
649
|
+
RE_DASH_FLAG.lastIndex = index
|
|
650
|
+
const match = RE_DASH_FLAG.exec(line)
|
|
651
|
+
if (match !== null) return { end: index + match[0].length, key: 'attribute' }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (cfg.dollarVariables === true && ch === '$') {
|
|
655
|
+
if (line[index + 1] === '{') {
|
|
656
|
+
const idx = line.indexOf('}', index + 2)
|
|
657
|
+
return { end: idx === -1 ? n : idx + 1, key: 'variable' }
|
|
658
|
+
}
|
|
659
|
+
const word = readWord(line, index + 1, cfg)
|
|
660
|
+
if (word !== null) return { end: index + 1 + word.length, key: 'variable' }
|
|
661
|
+
if ('0123456789?@*#$!'.includes(line[index + 1] ?? '')) {
|
|
662
|
+
return { end: index + 2, key: 'variable' }
|
|
663
|
+
}
|
|
664
|
+
return { end: index + 1, key: 'operator' }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (
|
|
668
|
+
cfg.anchorChars !== undefined &&
|
|
669
|
+
cfg.anchorChars.includes(ch) &&
|
|
670
|
+
/[A-Za-z0-9_]/.test(line[index + 1] ?? '')
|
|
671
|
+
) {
|
|
672
|
+
const word = readWord(line, index + 1, cfg)
|
|
673
|
+
return { end: word === null ? index + 1 : index + 1 + word.length, key: 'variable' }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (cfg.decorators === true && ch === '@') {
|
|
677
|
+
const word = readWord(line, index + 1, cfg)
|
|
678
|
+
if (word !== null) return { end: index + 1 + word.length, key: 'meta' }
|
|
679
|
+
return { end: index + 1, key: 'punctuation' }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (cfg.hashColor === true && ch === '#') {
|
|
683
|
+
const match = RE_HEX_COLOR.exec(line.slice(index))
|
|
684
|
+
if (match !== null) return { end: index + match[0].length, key: 'number' }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (cfg.tildeLiteral === true && ch === '~') return { end: index + 1, key: 'keyword' }
|
|
688
|
+
|
|
689
|
+
// Rust: a lifetime looks like a char literal but has no closing quote.
|
|
690
|
+
if (cfg.lifetime === true && ch === "'") {
|
|
691
|
+
const word = readWord(line, index + 1, cfg)
|
|
692
|
+
if (word !== null && line[index + 1 + word.length] !== "'") {
|
|
693
|
+
return { end: index + 1 + word.length, key: 'type' }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
for (const quote of cfg.templates) {
|
|
698
|
+
if (ch === quote) {
|
|
699
|
+
let end = index + 1
|
|
700
|
+
while (end < n) {
|
|
701
|
+
if (line[end] === '\\') {
|
|
702
|
+
end += 2
|
|
703
|
+
continue
|
|
704
|
+
}
|
|
705
|
+
if (line[end] === quote) {
|
|
706
|
+
end += 1
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
end += 1
|
|
710
|
+
}
|
|
711
|
+
return { end: Math.min(end, n), key: 'string' }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const quote of cfg.strings) {
|
|
716
|
+
if (ch === quote) {
|
|
717
|
+
let end = index + 1
|
|
718
|
+
while (end < n) {
|
|
719
|
+
if (line[end] === '\\') {
|
|
720
|
+
end += 2
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
if (line[end] === quote) {
|
|
724
|
+
end += 1
|
|
725
|
+
break
|
|
726
|
+
}
|
|
727
|
+
end += 1
|
|
728
|
+
}
|
|
729
|
+
const stop = Math.min(end, n)
|
|
730
|
+
if (cfg.jsonKeys === true && /^\s*:/.test(line.slice(stop))) {
|
|
731
|
+
return { end: stop, key: 'property' }
|
|
732
|
+
}
|
|
733
|
+
return { end: stop, key: 'string' }
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (isDigit(ch) || (ch === '.' && isDigit(line[index + 1] ?? ''))) {
|
|
738
|
+
const re = cfg.numberUnits === true ? RE_NUMBER_UNITS : RE_NUMBER
|
|
739
|
+
re.lastIndex = index
|
|
740
|
+
const match = re.exec(line)
|
|
741
|
+
if (match !== null && match[0].length > 0) {
|
|
742
|
+
return { end: index + match[0].length, key: 'number' }
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (
|
|
747
|
+
isWordStart(ch) ||
|
|
748
|
+
(cfg.dashWord === true && ch === '-' && /[A-Za-z_]/.test(line[index + 1] ?? ''))
|
|
749
|
+
) {
|
|
750
|
+
const word = readWord(line, index, cfg)
|
|
751
|
+
if (word !== null && word.length > 0) {
|
|
752
|
+
const end = index + word.length
|
|
753
|
+
const after = line[end]
|
|
754
|
+
const before = index > 0 ? line[index - 1] : ''
|
|
755
|
+
const lookup = cfg.caseInsensitive === true ? word.toUpperCase() : word
|
|
756
|
+
let key
|
|
757
|
+
if (cfg.literals.has(lookup)) key = 'keyword'
|
|
758
|
+
else if (cfg.keywords.has(lookup)) key = 'keyword'
|
|
759
|
+
else if (cfg.types.has(lookup)) key = 'type'
|
|
760
|
+
else if (cfg.builtins.has(lookup)) key = 'function'
|
|
761
|
+
else if (cfg.selfWords.has(lookup)) key = 'variable'
|
|
762
|
+
else if (cfg.globals.has(lookup)) key = 'variable'
|
|
763
|
+
else if (after === '(') key = 'function'
|
|
764
|
+
else if (cfg.macros === true && after === '!') key = 'function'
|
|
765
|
+
else if (after === ':' && (cfg.keyColon === true ? isStrictKeyColon(line, end) : cfg.keyColonLoose === true)) {
|
|
766
|
+
key = 'property'
|
|
767
|
+
} else if (before === '.') key = 'property'
|
|
768
|
+
return { end, key }
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
RE_OPERATOR.lastIndex = index
|
|
773
|
+
const operator = RE_OPERATOR.exec(line)
|
|
774
|
+
if (operator !== null && operator[0].length > 0) {
|
|
775
|
+
return { end: index + operator[0].length, key: 'operator' }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
RE_PUNCT.lastIndex = index
|
|
779
|
+
if (RE_PUNCT.test(line)) return { end: index + 1, key: 'punctuation' }
|
|
780
|
+
|
|
781
|
+
return { end: index + 1 }
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Tokenize one already-sanitized line into contiguous segments.
|
|
786
|
+
* @param {string} line
|
|
787
|
+
* @param {string} lang
|
|
788
|
+
* @param {Record<string, string> | undefined | null} syn
|
|
789
|
+
* @param {{ block?: string | null, triple?: string | null }} state
|
|
790
|
+
* @returns {Segment[]}
|
|
791
|
+
*/
|
|
792
|
+
function scan(line, lang, syn, state) {
|
|
793
|
+
const cfg = CONFIGS[lang] ?? CONFIGS.text
|
|
794
|
+
if (cfg.plain === true) return line.length === 0 ? [] : [{ text: line }]
|
|
795
|
+
if (typeof cfg.handler === 'function') return cfg.handler(line, syn, state)
|
|
796
|
+
|
|
797
|
+
/** @type {Segment[]} */
|
|
798
|
+
const out = []
|
|
799
|
+
const n = line.length
|
|
800
|
+
let index = 0
|
|
801
|
+
while (index < n) {
|
|
802
|
+
const token = readToken(line, index, cfg, syn, state)
|
|
803
|
+
let end = token !== null && token !== undefined && Number.isFinite(token.end) ? token.end : index + 1
|
|
804
|
+
if (end <= index) end = index + 1
|
|
805
|
+
if (end > n) end = n
|
|
806
|
+
pushSegment(out, line.slice(index, end), styleOf(syn, token?.key))
|
|
807
|
+
index = end
|
|
808
|
+
}
|
|
809
|
+
return out
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/* ------------------------------------------------------------------ *
|
|
813
|
+
* Public API
|
|
814
|
+
* ------------------------------------------------------------------ */
|
|
815
|
+
|
|
816
|
+
/** Canonical identifiers this module can highlight, including `text`. */
|
|
817
|
+
export const SUPPORTED_LANGUAGES = Object.freeze([
|
|
818
|
+
'js',
|
|
819
|
+
'ts',
|
|
820
|
+
'json',
|
|
821
|
+
'bash',
|
|
822
|
+
'python',
|
|
823
|
+
'diff',
|
|
824
|
+
'css',
|
|
825
|
+
'html',
|
|
826
|
+
'sql',
|
|
827
|
+
'go',
|
|
828
|
+
'rust',
|
|
829
|
+
'yaml',
|
|
830
|
+
'markdown',
|
|
831
|
+
'text'
|
|
832
|
+
])
|
|
833
|
+
|
|
834
|
+
/** Alias table; the first whitespace-delimited token of a hint is looked up. */
|
|
835
|
+
const ALIASES = new Map(
|
|
836
|
+
Object.entries({
|
|
837
|
+
js: 'js',
|
|
838
|
+
jsx: 'js',
|
|
839
|
+
mjs: 'js',
|
|
840
|
+
cjs: 'js',
|
|
841
|
+
javascript: 'js',
|
|
842
|
+
node: 'js',
|
|
843
|
+
ts: 'ts',
|
|
844
|
+
tsx: 'ts',
|
|
845
|
+
typescript: 'ts',
|
|
846
|
+
sh: 'bash',
|
|
847
|
+
bash: 'bash',
|
|
848
|
+
zsh: 'bash',
|
|
849
|
+
shell: 'bash',
|
|
850
|
+
console: 'bash',
|
|
851
|
+
py: 'python',
|
|
852
|
+
python3: 'python',
|
|
853
|
+
python: 'python',
|
|
854
|
+
yml: 'yaml',
|
|
855
|
+
yaml: 'yaml',
|
|
856
|
+
html: 'html',
|
|
857
|
+
xml: 'html',
|
|
858
|
+
svg: 'html',
|
|
859
|
+
rs: 'rust',
|
|
860
|
+
rust: 'rust',
|
|
861
|
+
golang: 'go',
|
|
862
|
+
go: 'go',
|
|
863
|
+
md: 'markdown',
|
|
864
|
+
markdown: 'markdown',
|
|
865
|
+
patch: 'diff',
|
|
866
|
+
udiff: 'diff',
|
|
867
|
+
diff: 'diff',
|
|
868
|
+
jsonc: 'json',
|
|
869
|
+
json: 'json',
|
|
870
|
+
css: 'css',
|
|
871
|
+
sql: 'sql',
|
|
872
|
+
text: 'text',
|
|
873
|
+
txt: 'text',
|
|
874
|
+
plain: 'text'
|
|
875
|
+
})
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Canonicalize a language hint. Unknown, empty and non-string hints become
|
|
880
|
+
* `'text'`, never an error.
|
|
881
|
+
* @param {unknown} hint
|
|
882
|
+
* @returns {string}
|
|
883
|
+
*/
|
|
884
|
+
export function normalizeLanguage(hint) {
|
|
885
|
+
if (typeof hint !== 'string') return 'text'
|
|
886
|
+
const trimmed = hint.trim().toLowerCase()
|
|
887
|
+
if (trimmed.length === 0) return 'text'
|
|
888
|
+
const first = trimmed.split(/\s+/)[0]
|
|
889
|
+
return ALIASES.get(first) ?? 'text'
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Highlight a single line. The result's concatenated text always equals
|
|
894
|
+
* `sanitize(line)`; malformed input is tolerated by construction.
|
|
895
|
+
* @param {string} line
|
|
896
|
+
* @param {string} [lang]
|
|
897
|
+
* @param {Record<string, string>} [syn]
|
|
898
|
+
* @returns {Segment[]}
|
|
899
|
+
*/
|
|
900
|
+
export function highlightLine(line, lang, syn) {
|
|
901
|
+
const text = sanitize(line)
|
|
902
|
+
if (text.length === 0) return []
|
|
903
|
+
const id = normalizeLanguage(lang)
|
|
904
|
+
return scan(text, id, syn, { block: null, triple: null })
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Highlight a whole snippet. Multi-line block comments and Python triple-quoted
|
|
909
|
+
* strings carry state from line to line; the returned segments may contain
|
|
910
|
+
* `'\n'` and concatenate back to `sanitize(code)`.
|
|
911
|
+
* @param {string} code
|
|
912
|
+
* @param {string} [lang]
|
|
913
|
+
* @param {Record<string, string>} [syn]
|
|
914
|
+
* @returns {Segment[]}
|
|
915
|
+
*/
|
|
916
|
+
export function highlightCode(code, lang, syn) {
|
|
917
|
+
const text = sanitize(code)
|
|
918
|
+
if (text.length === 0) return []
|
|
919
|
+
const id = normalizeLanguage(lang)
|
|
920
|
+
if (id === 'text') return [{ text }]
|
|
921
|
+
const state = { block: null, triple: null }
|
|
922
|
+
/** @type {Segment[]} */
|
|
923
|
+
const out = []
|
|
924
|
+
const lines = text.split('\n')
|
|
925
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
926
|
+
if (i > 0) pushSegment(out, '\n')
|
|
927
|
+
for (const segment of scan(lines[i], id, syn, state)) {
|
|
928
|
+
pushSegment(out, segment.text, segment.sgr)
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return out
|
|
932
|
+
}
|