reslop 0.0.1 → 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.
@@ -0,0 +1,1234 @@
1
+ 'use strict';
2
+
3
+ const { mapLang } = require('./detect.js');
4
+
5
+ const SQUOTE = '\x27';
6
+ const QUOTES = `${SQUOTE}"`;
7
+ const ANY_QUOTE = `${QUOTES}\``;
8
+ const BASH_QUOTE = `${SQUOTE}\``;
9
+
10
+ const emit = (out, style, text) => {
11
+ if (!text) return;
12
+ out.push({ text, style: style || 'plain' });
13
+ };
14
+
15
+ const tokensText = (out) => {
16
+ let s = '';
17
+ for (const t of out) s += t.text;
18
+ return s;
19
+ };
20
+
21
+ const STORAGE = [
22
+ 'const',
23
+ 'let',
24
+ 'var',
25
+ 'function',
26
+ 'class',
27
+ 'extends',
28
+ 'static',
29
+ 'async',
30
+ 'get',
31
+ 'set',
32
+ 'constructor',
33
+ 'new',
34
+ 'typeof',
35
+ 'instanceof',
36
+ 'void',
37
+ 'delete',
38
+ 'yield',
39
+ 'await',
40
+ 'import',
41
+ 'export',
42
+ 'from',
43
+ 'as',
44
+ 'default',
45
+ 'type',
46
+ 'interface',
47
+ 'implements',
48
+ 'enum',
49
+ 'namespace',
50
+ 'module',
51
+ 'declare',
52
+ 'abstract',
53
+ 'readonly',
54
+ 'private',
55
+ 'public',
56
+ 'protected',
57
+ 'override',
58
+ 'satisfies',
59
+ ];
60
+
61
+ const CONTROL = [
62
+ 'if',
63
+ 'else',
64
+ 'for',
65
+ 'while',
66
+ 'do',
67
+ 'switch',
68
+ 'case',
69
+ 'break',
70
+ 'continue',
71
+ 'return',
72
+ 'throw',
73
+ 'try',
74
+ 'catch',
75
+ 'finally',
76
+ 'with',
77
+ 'of',
78
+ 'in',
79
+ ];
80
+
81
+ const LITERALS = [
82
+ 'true',
83
+ 'false',
84
+ 'null',
85
+ 'undefined',
86
+ 'NaN',
87
+ 'Infinity',
88
+ 'this',
89
+ 'super',
90
+ ];
91
+
92
+ const TYPES = [
93
+ 'string',
94
+ 'number',
95
+ 'boolean',
96
+ 'symbol',
97
+ 'bigint',
98
+ 'object',
99
+ 'any',
100
+ 'unknown',
101
+ 'never',
102
+ 'void',
103
+ 'keyof',
104
+ 'infer',
105
+ 'unique',
106
+ 'asserts',
107
+ 'is',
108
+ ];
109
+
110
+ const BUILTINS = [
111
+ 'console',
112
+ 'Math',
113
+ 'JSON',
114
+ 'Date',
115
+ 'Array',
116
+ 'Object',
117
+ 'String',
118
+ 'Number',
119
+ 'Boolean',
120
+ 'Map',
121
+ 'Set',
122
+ 'WeakMap',
123
+ 'WeakSet',
124
+ 'Promise',
125
+ 'Error',
126
+ 'RegExp',
127
+ 'Symbol',
128
+ 'Proxy',
129
+ 'Reflect',
130
+ 'Intl',
131
+ 'Buffer',
132
+ 'process',
133
+ 'module',
134
+ 'exports',
135
+ 'require',
136
+ 'global',
137
+ 'globalThis',
138
+ 'window',
139
+ 'document',
140
+ 'parseInt',
141
+ 'parseFloat',
142
+ 'isNaN',
143
+ 'isFinite',
144
+ 'encodeURI',
145
+ 'decodeURI',
146
+ 'encodeURIComponent',
147
+ 'decodeURIComponent',
148
+ 'setTimeout',
149
+ 'setInterval',
150
+ 'clearTimeout',
151
+ 'clearInterval',
152
+ 'fetch',
153
+ 'URL',
154
+ 'URLSearchParams',
155
+ ];
156
+
157
+ const OP3 = ['===', '!==', '>>>', '**=', '&&=', '||=', '??='];
158
+
159
+ const OP2 = [
160
+ '==',
161
+ '!=',
162
+ '<=',
163
+ '>=',
164
+ '&&',
165
+ '||',
166
+ '??',
167
+ '=>',
168
+ '++',
169
+ '--',
170
+ '<<',
171
+ '>>',
172
+ '**',
173
+ '+=',
174
+ '-=',
175
+ '*=',
176
+ '/=',
177
+ '%=',
178
+ '&=',
179
+ '|=',
180
+ '^=',
181
+ ];
182
+
183
+ const BASH_KEYWORDS = [
184
+ 'if',
185
+ 'then',
186
+ 'else',
187
+ 'elif',
188
+ 'fi',
189
+ 'for',
190
+ 'while',
191
+ 'until',
192
+ 'do',
193
+ 'done',
194
+ 'case',
195
+ 'esac',
196
+ 'function',
197
+ 'select',
198
+ 'in',
199
+ 'time',
200
+ 'coproc',
201
+ ];
202
+
203
+ const BASH_BUILTINS = [
204
+ 'echo',
205
+ 'printf',
206
+ 'cd',
207
+ 'pwd',
208
+ 'exit',
209
+ 'export',
210
+ 'source',
211
+ 'alias',
212
+ 'unalias',
213
+ 'local',
214
+ 'declare',
215
+ 'typeset',
216
+ 'readonly',
217
+ 'unset',
218
+ 'shift',
219
+ 'read',
220
+ 'test',
221
+ 'true',
222
+ 'false',
223
+ 'exec',
224
+ 'eval',
225
+ 'set',
226
+ 'trap',
227
+ 'wait',
228
+ 'kill',
229
+ 'type',
230
+ 'command',
231
+ 'builtin',
232
+ 'return',
233
+ 'break',
234
+ 'continue',
235
+ 'pushd',
236
+ 'popd',
237
+ 'dirs',
238
+ 'getopts',
239
+ 'mapfile',
240
+ 'readarray',
241
+ 'let',
242
+ 'umask',
243
+ 'ulimit',
244
+ 'fg',
245
+ 'bg',
246
+ 'jobs',
247
+ 'hash',
248
+ 'help',
249
+ 'enable',
250
+ 'caller',
251
+ 'bind',
252
+ 'compgen',
253
+ 'complete',
254
+ ];
255
+
256
+ const LOG_DATETIME_RE = new RegExp(
257
+ '^(\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}' +
258
+ '(?:\\.\\d+)?(?:Z|[+-]\\d{2}:?\\d{2})?)',
259
+ );
260
+
261
+ const LOG_TAG_STYLE = Object.assign(Object.create(null), {
262
+ error: 'logError',
263
+ err: 'logError',
264
+ warn: 'logWarn',
265
+ warning: 'logWarn',
266
+ info: 'logInfo',
267
+ debug: 'logDebug',
268
+ log: 'plain',
269
+ });
270
+
271
+ const JSON_LITERAL = Object.assign(Object.create(null), {
272
+ true: 'literal',
273
+ false: 'literal',
274
+ null: 'literal',
275
+ });
276
+
277
+ const CSS_NEXT_STYLE = Object.assign(Object.create(null), {
278
+ ':': 'property',
279
+ '{': 'tag',
280
+ });
281
+
282
+ const EXPECT_STYLE = Object.assign(Object.create(null), {
283
+ function: 'function',
284
+ class: 'className',
285
+ type: 'className',
286
+ });
287
+
288
+ const NEXT_EXPECT = {
289
+ function: 'function',
290
+ class: 'class',
291
+ interface: 'class',
292
+ enum: 'class',
293
+ type: 'type',
294
+ };
295
+
296
+ const wordStyle = (words, style) => {
297
+ const table = Object.create(null);
298
+ for (const word of words) table[word] = style;
299
+ return table;
300
+ };
301
+
302
+ const BASH_WORD_STYLE = Object.assign(
303
+ Object.create(null),
304
+ wordStyle(BASH_KEYWORDS, 'control'),
305
+ wordStyle(BASH_BUILTINS, 'function'),
306
+ );
307
+
308
+ const typeOverlay = TYPES.filter((word) => STORAGE.includes(word));
309
+
310
+ const WORD_STYLE = Object.assign(
311
+ Object.create(null),
312
+ wordStyle(CONTROL, 'control'),
313
+ wordStyle(LITERALS, 'literal'),
314
+ wordStyle(BUILTINS, 'className'),
315
+ wordStyle(STORAGE, 'storage'),
316
+ wordStyle(typeOverlay, 'type'),
317
+ );
318
+
319
+ const TS_WORD_STYLE = Object.assign(
320
+ Object.create(null),
321
+ WORD_STYLE,
322
+ wordStyle(TYPES, 'type'),
323
+ );
324
+
325
+ const peekIdent = (source, i) => {
326
+ if (!/[A-Za-z_$]/.test(source[i] || '')) return null;
327
+ let j = i + 1;
328
+ while (j < source.length && /[A-Za-z0-9_$]/.test(source[j])) j += 1;
329
+ return source.slice(i, j);
330
+ };
331
+
332
+ const skipWs = (source, i) => {
333
+ while (i < source.length && /\s/.test(source[i])) i += 1;
334
+ return i;
335
+ };
336
+
337
+ const readUntilNl = (source, i) => {
338
+ let j = i;
339
+ const n = source.length;
340
+ while (j < n && source[j] !== '\n') j += 1;
341
+ return { text: source.slice(i, j), end: j };
342
+ };
343
+
344
+ const readBlockComment = (source, i) => {
345
+ let j = i + 2;
346
+ const n = source.length;
347
+ while (j < n && !(source[j] === '*' && source[j + 1] === '/')) {
348
+ j += 1;
349
+ }
350
+ j = Math.min(j + 2, n);
351
+ return { text: source.slice(i, j), end: j };
352
+ };
353
+
354
+ const skipQuoted = (source, j) => {
355
+ const q = source[j];
356
+ j += 1;
357
+ const n = source.length;
358
+ while (j < n && source[j] !== q) {
359
+ if (source[j] === '\\') j += 1;
360
+ j += 1;
361
+ }
362
+ j += 1;
363
+ return j;
364
+ };
365
+
366
+ const readJsNumber = (source, i) => {
367
+ const n = source.length;
368
+ let j = i;
369
+ if (source.startsWith('0x', i) || source.startsWith('0X', i)) {
370
+ j += 2;
371
+ while (j < n && /[0-9a-fA-F_]/.test(source[j])) j += 1;
372
+ } else if (source.startsWith('0b', i) || source.startsWith('0B', i)) {
373
+ j += 2;
374
+ while (j < n && /[01_]/.test(source[j])) j += 1;
375
+ } else if (source.startsWith('0o', i) || source.startsWith('0O', i)) {
376
+ j += 2;
377
+ while (j < n && /[0-7_]/.test(source[j])) j += 1;
378
+ } else {
379
+ while (j < n && /[0-9_n.]/.test(source[j])) j += 1;
380
+ if ('eE'.includes(source[j])) {
381
+ j += 1;
382
+ if ('+-'.includes(source[j])) j += 1;
383
+ while (j < n && /[0-9_]/.test(source[j])) j += 1;
384
+ }
385
+ }
386
+ return { text: source.slice(i, j), end: j };
387
+ };
388
+
389
+ const readJsOperator = (source, i) => {
390
+ const two = source.slice(i, i + 2);
391
+ const three = source.slice(i, i + 3);
392
+ let j = i + 1;
393
+ if (OP3.includes(three)) j = i + 3;
394
+ else if (OP2.includes(two)) j = i + 2;
395
+ return { text: source.slice(i, j), end: j };
396
+ };
397
+
398
+ const plainTail = (out) => tokensText(out).slice(-8);
399
+
400
+ const styleExpected = ({ expectName }) => EXPECT_STYLE[expectName];
401
+
402
+ const styleKeyword = ({ word, ts }) => {
403
+ const table = ts ? TS_WORD_STYLE : WORD_STYLE;
404
+ if (!Object.hasOwn(table, word)) return null;
405
+ return table[word];
406
+ };
407
+
408
+ const styleConstant = ({ word }) =>
409
+ /^[A-Z][A-Z0-9_]+$/.test(word) ? 'constant' : null;
410
+
411
+ const stylePascal = ({ word }) => (/^[A-Z]/.test(word) ? 'className' : null);
412
+
413
+ const styleCall = ({ after }) => (after === '(' ? 'function' : null);
414
+
415
+ const styleAssign = ({ after, source, k }) => {
416
+ if (after !== '=') return null;
417
+ const k2 = skipWs(source, k + 1);
418
+ const rhs = source.slice(k2, k2 + 8);
419
+ if (rhs.startsWith('async')) return 'function';
420
+ if (rhs.startsWith('function')) return 'function';
421
+ if (source[k2] === '(') return 'function';
422
+ return 'variable';
423
+ };
424
+
425
+ const styleProperty = ({ out }) => {
426
+ const prev = plainTail(out);
427
+ return /\.$/.test(prev) ? 'property' : null;
428
+ };
429
+
430
+ const styleVariable = () => 'variable';
431
+
432
+ const IDENT_RULES = [
433
+ styleExpected,
434
+ styleKeyword,
435
+ styleConstant,
436
+ stylePascal,
437
+ styleCall,
438
+ styleAssign,
439
+ styleProperty,
440
+ styleVariable,
441
+ ];
442
+
443
+ const identStyle = (ctx) => {
444
+ for (const rule of IDENT_RULES) {
445
+ const style = rule(ctx);
446
+ if (style) return style;
447
+ }
448
+ return 'plain';
449
+ };
450
+
451
+ const readString = (source, i) => {
452
+ const q = source[i];
453
+ let j = i + 1;
454
+ const n = source.length;
455
+ while (j < n) {
456
+ if (source[j] === '\\') {
457
+ j += 2;
458
+ continue;
459
+ }
460
+ if (source[j] === q) {
461
+ j += 1;
462
+ break;
463
+ }
464
+ if (source[j] === '\n') break;
465
+ j += 1;
466
+ }
467
+ return { text: source.slice(i, j), end: j };
468
+ };
469
+
470
+ const readRegex = (source, i) => {
471
+ let j = i + 1;
472
+ const n = source.length;
473
+ while (j < n) {
474
+ if (source[j] === '\\') {
475
+ j += 2;
476
+ continue;
477
+ }
478
+ if (source[j] === '[') {
479
+ j += 1;
480
+ while (j < n && source[j] !== ']') {
481
+ if (source[j] === '\\') j += 1;
482
+ j += 1;
483
+ }
484
+ j += 1;
485
+ continue;
486
+ }
487
+ if (source[j] === '/') {
488
+ j += 1;
489
+ while (j < n && /[a-z]/i.test(source[j])) j += 1;
490
+ break;
491
+ }
492
+ if (source[j] === '\n') break;
493
+ j += 1;
494
+ }
495
+ return { text: source.slice(i, j), end: j };
496
+ };
497
+
498
+ const readTemplateInterp = (source, j) => {
499
+ let depth = 1;
500
+ const start = j;
501
+ const n = source.length;
502
+ while (j < n && depth > 0) {
503
+ if (ANY_QUOTE.includes(source[j])) {
504
+ j = skipQuoted(source, j);
505
+ continue;
506
+ }
507
+ if (source[j] === '{') depth += 1;
508
+ if (source[j] === '}') {
509
+ depth -= 1;
510
+ if (depth === 0) break;
511
+ }
512
+ j += 1;
513
+ }
514
+ return { expr: source.slice(start, j), end: j };
515
+ };
516
+
517
+ const isRegexContext = (out) => {
518
+ const prev = tokensText(out).trimEnd();
519
+ if (!prev) return true;
520
+ const afterOperator = /[=(:,[?!&|{};]$/.test(prev);
521
+ const afterKeyword = /(?:return|case|throw|=>|typeof|void)$/.test(prev);
522
+ return afterOperator || afterKeyword;
523
+ };
524
+
525
+ class JsHighlighter {
526
+ highlightJsFamily(source, ts = false) {
527
+ const out = [];
528
+ let i = 0;
529
+ const n = source.length;
530
+ let expectName = null;
531
+
532
+ while (i < n) {
533
+ const ch = source[i];
534
+ const next = source[i + 1];
535
+
536
+ if (ch === '/' && next === '/') {
537
+ const { text, end } = readUntilNl(source, i);
538
+ emit(out, 'comment', text);
539
+ i = end;
540
+ continue;
541
+ }
542
+
543
+ if (ch === '/' && next === '*') {
544
+ const { text, end } = readBlockComment(source, i);
545
+ emit(out, 'comment', text);
546
+ i = end;
547
+ continue;
548
+ }
549
+
550
+ if (ch === '`') {
551
+ const r = this.highlightTemplate(source, i);
552
+ for (const t of r.tokens) out.push(t);
553
+ i = r.end;
554
+ continue;
555
+ }
556
+
557
+ if (QUOTES.includes(ch)) {
558
+ const { text, end } = readString(source, i);
559
+ emit(out, 'string', text);
560
+ i = end;
561
+ continue;
562
+ }
563
+
564
+ if (ch === '/' && isRegexContext(out)) {
565
+ const { text, end } = readRegex(source, i);
566
+ emit(out, 'regex', text);
567
+ i = end;
568
+ continue;
569
+ }
570
+
571
+ if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(next || ''))) {
572
+ const { text, end } = readJsNumber(source, i);
573
+ emit(out, 'number', text);
574
+ i = end;
575
+ continue;
576
+ }
577
+
578
+ if (ch === '@') {
579
+ let j = i + 1;
580
+ while (j < n && /[A-Za-z0-9_$]/.test(source[j])) j += 1;
581
+ emit(out, 'decorator', source.slice(i, j));
582
+ i = j;
583
+ expectName = null;
584
+ continue;
585
+ }
586
+
587
+ if (/[A-Za-z_$]/.test(ch)) {
588
+ const word = peekIdent(source, i);
589
+ const end = i + word.length;
590
+ const k = skipWs(source, end);
591
+ const after = source[k];
592
+ const ctx = { word, ts, after, source, k, out, expectName };
593
+ emit(out, identStyle(ctx), word);
594
+ if (EXPECT_STYLE[expectName]) expectName = null;
595
+ else expectName = NEXT_EXPECT[word] ?? null;
596
+ i = end;
597
+ continue;
598
+ }
599
+
600
+ if (/[=<>!+\-*/%&|^~?:]/.test(ch)) {
601
+ const { text, end } = readJsOperator(source, i);
602
+ emit(out, 'operator', text);
603
+ i = end;
604
+ expectName = null;
605
+ continue;
606
+ }
607
+ if (/[{}()[\];,.]/.test(ch)) {
608
+ emit(out, 'punct', ch);
609
+ i += 1;
610
+ continue;
611
+ }
612
+ emit(out, 'plain', ch);
613
+ i += 1;
614
+ }
615
+ return out;
616
+ }
617
+
618
+ highlightTemplate(source, i) {
619
+ const n = source.length;
620
+ let j = i + 1;
621
+ const parts = [];
622
+ emit(parts, 'template', '`');
623
+ let buf = '';
624
+ const flush = () => {
625
+ if (buf) {
626
+ emit(parts, 'template', buf);
627
+ buf = '';
628
+ }
629
+ };
630
+ while (j < n) {
631
+ if (source[j] === '\\') {
632
+ flush();
633
+ emit(parts, 'escape', source.slice(j, j + 2));
634
+ j += 2;
635
+ continue;
636
+ }
637
+ if (source[j] === '`') {
638
+ flush();
639
+ emit(parts, 'template', '`');
640
+ j += 1;
641
+ break;
642
+ }
643
+ if (source[j] === '$' && source[j + 1] === '{') {
644
+ flush();
645
+ emit(parts, 'interpolation', '${');
646
+ j += 2;
647
+ const interp = readTemplateInterp(source, j);
648
+ const inner = this.highlightJsFamily(interp.expr, false);
649
+ for (const t of inner) parts.push(t);
650
+ j = interp.end;
651
+ if (source[j] === '}') {
652
+ emit(parts, 'interpolation', '}');
653
+ j += 1;
654
+ }
655
+ continue;
656
+ }
657
+ buf += source[j];
658
+ j += 1;
659
+ }
660
+ flush();
661
+ return { tokens: parts, end: j };
662
+ }
663
+ }
664
+
665
+ const jsHighlighter = new JsHighlighter();
666
+
667
+ const highlightJsFamily = (source, ts = false) =>
668
+ jsHighlighter.highlightJsFamily(source, ts);
669
+
670
+ const highlightJson = (source) => {
671
+ const out = [];
672
+ let i = 0;
673
+ const n = source.length;
674
+ while (i < n) {
675
+ const ch = source[i];
676
+ if (ch === '"') {
677
+ const { text, end } = readString(source, i);
678
+ let k = end;
679
+ while (k < n && /\s/.test(source[k])) k += 1;
680
+ const kind = source[k] === ':' ? 'property' : 'string';
681
+ emit(out, kind, text);
682
+ i = end;
683
+ continue;
684
+ }
685
+ if (/[0-9-]/.test(ch)) {
686
+ let j = i;
687
+ while (j < n && /[0-9.eE+-]/.test(source[j])) j += 1;
688
+ emit(out, 'number', source.slice(i, j));
689
+ i = j;
690
+ continue;
691
+ }
692
+ if (/[a-z]/.test(ch)) {
693
+ let j = i;
694
+ while (j < n && /[a-z]/.test(source[j])) j += 1;
695
+ const word = source.slice(i, j);
696
+ emit(out, JSON_LITERAL[word] ?? 'plain', word);
697
+ i = j;
698
+ continue;
699
+ }
700
+ if (/[{}[\],:]/.test(ch)) {
701
+ emit(out, 'punct', ch);
702
+ i += 1;
703
+ continue;
704
+ }
705
+ emit(out, 'plain', ch);
706
+ i += 1;
707
+ }
708
+ return out;
709
+ };
710
+
711
+ const cssWordStyle = (next) => CSS_NEXT_STYLE[next] ?? 'plain';
712
+
713
+ const highlightCss = (source) => {
714
+ const out = [];
715
+ let i = 0;
716
+ const n = source.length;
717
+ while (i < n) {
718
+ const ch = source[i];
719
+ const next = source[i + 1];
720
+ if (ch === '/' && next === '*') {
721
+ const { text, end } = readBlockComment(source, i);
722
+ emit(out, 'comment', text);
723
+ i = end;
724
+ continue;
725
+ }
726
+ if (QUOTES.includes(ch)) {
727
+ const { text, end } = readString(source, i);
728
+ emit(out, 'string', text);
729
+ i = end;
730
+ continue;
731
+ }
732
+ if (ch === '@') {
733
+ let j = i + 1;
734
+ while (j < n && /[A-Za-z0-9-]/.test(source[j])) j += 1;
735
+ emit(out, 'keyword', source.slice(i, j));
736
+ i = j;
737
+ continue;
738
+ }
739
+ if ('#.'.includes(ch)) {
740
+ let j = i + 1;
741
+ while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
742
+ const kind = ch === '#' ? 'constant' : 'className';
743
+ emit(out, kind, source.slice(i, j));
744
+ i = j;
745
+ continue;
746
+ }
747
+ if (/[0-9]/.test(ch)) {
748
+ let j = i;
749
+ while (j < n && /[0-9.%]/.test(source[j])) j += 1;
750
+ emit(out, 'number', source.slice(i, j));
751
+ i = j;
752
+ continue;
753
+ }
754
+ if (/[A-Za-z_-]/.test(ch)) {
755
+ let j = i + 1;
756
+ while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
757
+ const word = source.slice(i, j);
758
+ let k = j;
759
+ while (k < n && /\s/.test(source[k])) k += 1;
760
+ emit(out, cssWordStyle(source[k]), word);
761
+ i = j;
762
+ continue;
763
+ }
764
+ if (/[{};:,]/.test(ch)) {
765
+ emit(out, 'punct', ch);
766
+ i += 1;
767
+ continue;
768
+ }
769
+ emit(out, 'plain', ch);
770
+ i += 1;
771
+ }
772
+ return out;
773
+ };
774
+
775
+ const colorHtmlTag = (tag) => {
776
+ const out = [];
777
+ let i = 1;
778
+ const n = tag.length;
779
+ emit(out, 'punct', '<');
780
+ if (tag[i] === '/') {
781
+ emit(out, 'punct', '/');
782
+ i += 1;
783
+ }
784
+ let j = i;
785
+ while (j < n && /[A-Za-z0-9:-]/.test(tag[j])) j += 1;
786
+ emit(out, 'tag', tag.slice(i, j));
787
+ i = j;
788
+ while (i < n) {
789
+ if (tag[i] === '>' || (tag[i] === '/' && tag[i + 1] === '>')) {
790
+ emit(out, 'punct', tag.slice(i));
791
+ break;
792
+ }
793
+ if (/\s/.test(tag[i])) {
794
+ emit(out, 'plain', tag[i]);
795
+ i += 1;
796
+ continue;
797
+ }
798
+ if (tag[i] === '"' || tag[i] === SQUOTE) {
799
+ const q = tag[i];
800
+ let k = i + 1;
801
+ while (k < n && tag[k] !== q) k += 1;
802
+ k = Math.min(k + 1, n);
803
+ emit(out, 'string', tag.slice(i, k));
804
+ i = k;
805
+ continue;
806
+ }
807
+ if (/[A-Za-z_:]/.test(tag[i])) {
808
+ let k = i + 1;
809
+ while (k < n && /[A-Za-z0-9_:.-]/.test(tag[k])) k += 1;
810
+ emit(out, 'attr', tag.slice(i, k));
811
+ i = k;
812
+ continue;
813
+ }
814
+ emit(out, 'punct', tag[i]);
815
+ i += 1;
816
+ }
817
+ return out;
818
+ };
819
+
820
+ const highlightHtml = (source) => {
821
+ const out = [];
822
+ let i = 0;
823
+ const n = source.length;
824
+ while (i < n) {
825
+ if (source.startsWith('<!--', i)) {
826
+ let j = source.indexOf('-->', i + 4);
827
+ j = j === -1 ? n : j + 3;
828
+ emit(out, 'comment', source.slice(i, j));
829
+ i = j;
830
+ continue;
831
+ }
832
+ if (source[i] === '<') {
833
+ let j = i + 1;
834
+ while (j < n && source[j] !== '>') {
835
+ if (source[j] === '"' || source[j] === SQUOTE) {
836
+ j = skipQuoted(source, j);
837
+ continue;
838
+ }
839
+ j += 1;
840
+ }
841
+ if (j < n) j += 1;
842
+ for (const t of colorHtmlTag(source.slice(i, j))) out.push(t);
843
+ i = j;
844
+ continue;
845
+ }
846
+ let j = i + 1;
847
+ while (j < n && source[j] !== '<' && !source.startsWith('<!--', j)) {
848
+ j += 1;
849
+ }
850
+ emit(out, 'plain', source.slice(i, j));
851
+ i = j;
852
+ }
853
+ return out;
854
+ };
855
+
856
+ const splitCsv = (line) => {
857
+ const cells = [];
858
+ let cur = '';
859
+ let inQ = false;
860
+ for (let i = 0; i < line.length; i += 1) {
861
+ const ch = line[i];
862
+ if (ch === '"') {
863
+ if (inQ && line[i + 1] === '"') {
864
+ cur += '"';
865
+ i += 1;
866
+ } else {
867
+ inQ = !inQ;
868
+ cur += ch;
869
+ }
870
+ continue;
871
+ }
872
+ if (ch === ',' && !inQ) {
873
+ cells.push(cur);
874
+ cur = '';
875
+ continue;
876
+ }
877
+ cur += ch;
878
+ }
879
+ cells.push(cur);
880
+ return cells;
881
+ };
882
+
883
+ const highlightCsvCell = (cell, row, col) => {
884
+ if (row === 0) return { text: cell, style: 'function' };
885
+ const num = /^-?\d+(\.\d+)?$/.test(cell.trim());
886
+ if (num) return { text: cell, style: 'number' };
887
+ if (col % 2 === 0) return { text: cell, style: 'string' };
888
+ return { text: cell, style: 'variable' };
889
+ };
890
+
891
+ const highlightCsvLine = (line, row) => {
892
+ if (!line) return [];
893
+ const cells = splitCsv(line);
894
+ const out = [];
895
+ for (let col = 0; col < cells.length; col += 1) {
896
+ if (col) emit(out, 'punct', ',');
897
+ const cell = highlightCsvCell(cells[col], row, col);
898
+ emit(out, cell.style, cell.text);
899
+ }
900
+ return out;
901
+ };
902
+
903
+ const highlightCsv = (source) => {
904
+ const lines = source.split(/\r?\n/);
905
+ const out = [];
906
+ for (let row = 0; row < lines.length; row += 1) {
907
+ if (row) emit(out, 'plain', '\n');
908
+ for (const t of highlightCsvLine(lines[row], row === 0 ? 1 : row)) {
909
+ out.push(t);
910
+ }
911
+ }
912
+ return out;
913
+ };
914
+
915
+ const readBashVar = (source, i) => {
916
+ if (source[i + 1] === '{') {
917
+ let j = i + 2;
918
+ while (j < source.length && source[j] !== '}') j += 1;
919
+ if (j < source.length) j += 1;
920
+ return { text: source.slice(i, j), end: j };
921
+ }
922
+ if (/[0-9#?$!*@-]/.test(source[i + 1] || '')) {
923
+ return { text: source.slice(i, i + 2), end: i + 2 };
924
+ }
925
+ let j = i + 1;
926
+ while (j < source.length && /[A-Za-z0-9_]/.test(source[j])) j += 1;
927
+ if (j === i + 1) return { text: '$', end: i + 1 };
928
+ return { text: source.slice(i, j), end: j };
929
+ };
930
+
931
+ const takeQuoted = (source, i) => {
932
+ const q = source[i];
933
+ let j = i + 1;
934
+ const n = source.length;
935
+ while (j < n && source[j] !== q) j += 1;
936
+ if (j < n) j += 1;
937
+ return { text: source.slice(i, j), end: j };
938
+ };
939
+
940
+ const takeBashDouble = (source, i, out) => {
941
+ let j = i + 1;
942
+ let chunk = '"';
943
+ const n = source.length;
944
+ while (j < n && source[j] !== '"') {
945
+ if (source[j] === '\\' && j + 1 < n) {
946
+ chunk += source.slice(j, j + 2);
947
+ j += 2;
948
+ continue;
949
+ }
950
+ if (source[j] === '$') {
951
+ emit(out, 'string', chunk);
952
+ chunk = '';
953
+ const { text, end } = readBashVar(source, j);
954
+ emit(out, 'variable', text);
955
+ j = end;
956
+ continue;
957
+ }
958
+ chunk += source[j];
959
+ j += 1;
960
+ }
961
+ if (j < n) {
962
+ chunk += '"';
963
+ j += 1;
964
+ }
965
+ if (chunk) emit(out, 'string', chunk);
966
+ return j;
967
+ };
968
+
969
+ const bashWordStyle = (word, nextCh) => {
970
+ const known = BASH_WORD_STYLE[word];
971
+ if (known) return known;
972
+ if (nextCh === '=') return 'variable';
973
+ return 'plain';
974
+ };
975
+
976
+ const bashOpLen = (ch, next) => ('|&><'.includes(ch) && next === ch ? 2 : 1);
977
+
978
+ const highlightBash = (source) => {
979
+ const out = [];
980
+ let i = 0;
981
+ const n = source.length;
982
+ while (i < n) {
983
+ const ch = source[i];
984
+ const next = source[i + 1];
985
+
986
+ if (ch === '#' && (i === 0 || /\s/.test(source[i - 1]))) {
987
+ const { text, end } = readUntilNl(source, i);
988
+ emit(out, 'comment', text);
989
+ i = end;
990
+ continue;
991
+ }
992
+
993
+ if (BASH_QUOTE.includes(ch)) {
994
+ const { text, end } = takeQuoted(source, i);
995
+ emit(out, 'string', text);
996
+ i = end;
997
+ continue;
998
+ }
999
+
1000
+ if (ch === '"') {
1001
+ i = takeBashDouble(source, i, out);
1002
+ continue;
1003
+ }
1004
+
1005
+ if (ch === '$') {
1006
+ const { text, end } = readBashVar(source, i);
1007
+ emit(out, 'variable', text);
1008
+ i = end;
1009
+ continue;
1010
+ }
1011
+
1012
+ if (ch === '-' && /[A-Za-z0-9]/.test(next || '')) {
1013
+ let j = i + 1;
1014
+ while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
1015
+ emit(out, 'attr', source.slice(i, j));
1016
+ i = j;
1017
+ continue;
1018
+ }
1019
+
1020
+ if (/[A-Za-z_]/.test(ch)) {
1021
+ let j = i + 1;
1022
+ while (j < n && /[A-Za-z0-9_]/.test(source[j])) j += 1;
1023
+ const word = source.slice(i, j);
1024
+ emit(out, bashWordStyle(word, source[j]), word);
1025
+ i = j;
1026
+ continue;
1027
+ }
1028
+
1029
+ if (/[0-9]/.test(ch)) {
1030
+ let j = i + 1;
1031
+ while (j < n && /[0-9]/.test(source[j])) j += 1;
1032
+ emit(out, 'number', source.slice(i, j));
1033
+ i = j;
1034
+ continue;
1035
+ }
1036
+
1037
+ if ('|&;<>(){}'.includes(ch)) {
1038
+ const j = i + bashOpLen(ch, next);
1039
+ emit(out, 'operator', source.slice(i, j));
1040
+ i = j;
1041
+ continue;
1042
+ }
1043
+
1044
+ emit(out, 'plain', ch);
1045
+ i += 1;
1046
+ }
1047
+ return out;
1048
+ };
1049
+
1050
+ const highlightLogLine = (line) => {
1051
+ if (!line) return [];
1052
+ const out = [];
1053
+ let rest = line;
1054
+ const dm = rest.match(LOG_DATETIME_RE);
1055
+ if (dm) {
1056
+ emit(out, 'logDate', dm[1]);
1057
+ rest = rest.slice(dm[1].length);
1058
+ }
1059
+ const tag = rest.match(/^(\s*)(\[[^\]]+\])/);
1060
+ if (tag) {
1061
+ emit(out, 'plain', tag[1]);
1062
+ const name = tag[2].replace(/^\[|\]$/g, '').toLowerCase();
1063
+ emit(out, LOG_TAG_STYLE[name] || 'plain', tag[2]);
1064
+ rest = rest.slice(tag[0].length);
1065
+ }
1066
+ if (rest.includes('\t')) {
1067
+ const parts = rest.split('\t');
1068
+ for (let i = 0; i < parts.length; i += 1) {
1069
+ if (i) emit(out, 'plain', '\t');
1070
+ emit(out, `logCol${i}`, parts[i]);
1071
+ }
1072
+ return out;
1073
+ }
1074
+ if (rest) emit(out, 'logCol0', rest);
1075
+ return out;
1076
+ };
1077
+
1078
+ const highlightLog = (source) => {
1079
+ const lines = source.split('\n');
1080
+ const out = [];
1081
+ for (let i = 0; i < lines.length; i += 1) {
1082
+ if (i) emit(out, 'plain', '\n');
1083
+ for (const t of highlightLogLine(lines[i])) out.push(t);
1084
+ }
1085
+ return out;
1086
+ };
1087
+
1088
+ const highlightDotLine = (line) => {
1089
+ if (!line) return [];
1090
+ const out = [];
1091
+ const t = line.trimStart();
1092
+ if (t.startsWith('#') || t.startsWith(';')) {
1093
+ emit(out, 'comment', line);
1094
+ return out;
1095
+ }
1096
+ if (t.startsWith('!')) {
1097
+ const lead = line.slice(0, line.length - t.length);
1098
+ emit(out, 'plain', lead);
1099
+ emit(out, 'operator', '!');
1100
+ emit(out, 'plain', t.slice(1));
1101
+ return out;
1102
+ }
1103
+ const eq = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_.-]*)(\s*=\s*)(.*)$/);
1104
+ if (eq) {
1105
+ emit(out, 'plain', eq[1]);
1106
+ emit(out, 'variable', eq[2]);
1107
+ emit(out, 'operator', eq[3]);
1108
+ emit(out, 'string', eq[4]);
1109
+ return out;
1110
+ }
1111
+ emit(out, 'plain', line);
1112
+ return out;
1113
+ };
1114
+
1115
+ const highlightDot = (source) => {
1116
+ const lines = source.split('\n');
1117
+ const out = [];
1118
+ for (let i = 0; i < lines.length; i += 1) {
1119
+ if (i) emit(out, 'plain', '\n');
1120
+ for (const t of highlightDotLine(lines[i])) out.push(t);
1121
+ }
1122
+ return out;
1123
+ };
1124
+
1125
+ const highlightTxtLine = (line) => {
1126
+ if (!line) return [];
1127
+ const out = [];
1128
+ const re = /(https?:\/\/[^\s<>"']+|www\.[^\s<>"']+)/g;
1129
+ let last = 0;
1130
+ let m = re.exec(line);
1131
+ while (m !== null) {
1132
+ if (m.index > last) emit(out, 'plain', line.slice(last, m.index));
1133
+ emit(out, 'string', m[0]);
1134
+ last = m.index + m[0].length;
1135
+ m = re.exec(line);
1136
+ }
1137
+ if (last < line.length) emit(out, 'plain', line.slice(last));
1138
+ if (!out.length) emit(out, 'plain', line);
1139
+ return out;
1140
+ };
1141
+
1142
+ const highlightTxt = (source) => {
1143
+ const lines = source.split('\n');
1144
+ const out = [];
1145
+ for (let i = 0; i < lines.length; i += 1) {
1146
+ if (i) emit(out, 'plain', '\n');
1147
+ for (const t of highlightTxtLine(lines[i])) out.push(t);
1148
+ }
1149
+ return out;
1150
+ };
1151
+
1152
+ const highlightJs = (source) => highlightJsFamily(source, false);
1153
+ const highlightTs = (source) => highlightJsFamily(source, true);
1154
+
1155
+ const HIGHLIGHTERS = {
1156
+ js: highlightJs,
1157
+ mjs: highlightJs,
1158
+ ts: highlightTs,
1159
+ json: highlightJson,
1160
+ css: highlightCss,
1161
+ html: highlightHtml,
1162
+ csv: highlightCsv,
1163
+ bash: highlightBash,
1164
+ sh: highlightBash,
1165
+ shell: highlightBash,
1166
+ zsh: highlightBash,
1167
+ log: highlightLog,
1168
+ dot: highlightDot,
1169
+ txt: highlightTxt,
1170
+ py: highlightTxt,
1171
+ md: highlightTxt,
1172
+ };
1173
+
1174
+ const tokenize = (lang, source) => {
1175
+ const text = source ?? '';
1176
+ const key = lang === 'dts' ? 'ts' : mapLang(lang);
1177
+ const fn = HIGHLIGHTERS[key] || highlightTxt;
1178
+ return fn(text);
1179
+ };
1180
+
1181
+ const overlayTokens = (tokens, spans) => {
1182
+ const syn = tokens && tokens.length ? tokens : [{ text: '', style: 'plain' }];
1183
+ const diff =
1184
+ spans && spans.length ? spans : [{ text: tokensText(syn), changed: false }];
1185
+ const pieces = [];
1186
+ let tokenIndex = 0;
1187
+ let spanIndex = 0;
1188
+ let tokenOff = 0;
1189
+ let spanOff = 0;
1190
+ while (tokenIndex < syn.length && spanIndex < diff.length) {
1191
+ const token = syn[tokenIndex];
1192
+ const span = diff[spanIndex];
1193
+ const tokenText = token.text ?? '';
1194
+ const spanText = span.text ?? '';
1195
+ const tokenLeft = tokenText.length - tokenOff;
1196
+ const spanLeft = spanText.length - spanOff;
1197
+ const n = Math.min(tokenLeft, spanLeft);
1198
+ if (n > 0) {
1199
+ const name = typeof token.style === 'string' ? token.style : 'plain';
1200
+ pieces.push({
1201
+ text: tokenText.slice(tokenOff, tokenOff + n),
1202
+ style: name || 'plain',
1203
+ changed: span.changed === true,
1204
+ });
1205
+ }
1206
+ tokenOff += n;
1207
+ spanOff += n;
1208
+ if (tokenOff >= tokenText.length) {
1209
+ tokenIndex += 1;
1210
+ tokenOff = 0;
1211
+ }
1212
+ if (spanOff >= spanText.length) {
1213
+ spanIndex += 1;
1214
+ spanOff = 0;
1215
+ }
1216
+ }
1217
+ while (spanIndex < diff.length) {
1218
+ const span = diff[spanIndex];
1219
+ const text = (span.text ?? '').slice(spanOff);
1220
+ if (text) {
1221
+ pieces.push({ text, style: 'plain', changed: span.changed === true });
1222
+ }
1223
+ spanIndex += 1;
1224
+ spanOff = 0;
1225
+ }
1226
+ return pieces;
1227
+ };
1228
+
1229
+ module.exports = {
1230
+ tokenize,
1231
+ overlayTokens,
1232
+ tokensText,
1233
+ HIGHLIGHTERS,
1234
+ };