reslop 0.1.4 → 0.1.6

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/lib/highlight.js CHANGED
@@ -1,1320 +1,52 @@
1
1
  'use strict';
2
2
 
3
- const { mapLang } = require('./detect.js');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
4
5
 
5
- const SQUOTE = '\x27';
6
- const QUOTES = `${SQUOTE}"`;
7
- const ANY_QUOTE = `${QUOTES}\``;
8
- const BASH_QUOTE = `${SQUOTE}\``;
6
+ const { tokensText } = require('./highlight/core.js');
9
7
 
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 = {
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 = {
272
- true: 'literal',
273
- false: 'literal',
274
- null: 'literal',
275
- };
276
-
277
- const CSS_NEXT_STYLE = {
278
- ':': 'property',
279
- '{': 'tag',
280
- };
281
-
282
- const EXPECT_STYLE = {
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 = {};
298
- for (const word of words) table[word] = style;
299
- return table;
300
- };
301
-
302
- const BASH_WORD_STYLE = {
303
- ...wordStyle(BASH_KEYWORDS, 'control'),
304
- ...wordStyle(BASH_BUILTINS, 'function'),
305
- };
306
-
307
- const typeOverlay = TYPES.filter((word) => STORAGE.includes(word));
308
-
309
- const WORD_STYLE = {
310
- ...wordStyle(CONTROL, 'control'),
311
- ...wordStyle(LITERALS, 'literal'),
312
- ...wordStyle(BUILTINS, 'className'),
313
- ...wordStyle(STORAGE, 'storage'),
314
- ...wordStyle(typeOverlay, 'type'),
315
- };
316
-
317
- const TS_WORD_STYLE = {
318
- ...WORD_STYLE,
319
- ...wordStyle(TYPES, 'type'),
320
- };
321
-
322
- const peekIdent = (source, i) => {
323
- if (!/[A-Za-z_$]/.test(source[i] || '')) return null;
324
- let j = i + 1;
325
- while (j < source.length && /[A-Za-z0-9_$]/.test(source[j])) j += 1;
326
- return source.slice(i, j);
327
- };
328
-
329
- const skipWs = (source, i) => {
330
- while (i < source.length && /\s/.test(source[i])) i += 1;
331
- return i;
332
- };
333
-
334
- const readUntilNl = (source, i) => {
335
- let j = i;
336
- const n = source.length;
337
- while (j < n && source[j] !== '\n') j += 1;
338
- return { text: source.slice(i, j), end: j };
339
- };
340
-
341
- const readBlockComment = (source, i) => {
342
- let j = i + 2;
343
- const n = source.length;
344
- while (j < n && !(source[j] === '*' && source[j + 1] === '/')) {
345
- j += 1;
346
- }
347
- j = Math.min(j + 2, n);
348
- return { text: source.slice(i, j), end: j };
349
- };
350
-
351
- const skipQuoted = (source, j) => {
352
- const q = source[j];
353
- j += 1;
354
- const n = source.length;
355
- while (j < n && source[j] !== q) {
356
- if (source[j] === '\\') j += 1;
357
- j += 1;
358
- }
359
- j += 1;
360
- return j;
361
- };
362
-
363
- const readJsNumber = (source, i) => {
364
- const n = source.length;
365
- let j = i;
366
- if (source.startsWith('0x', i) || source.startsWith('0X', i)) {
367
- j += 2;
368
- while (j < n && /[0-9a-fA-F_]/.test(source[j])) j += 1;
369
- } else if (source.startsWith('0b', i) || source.startsWith('0B', i)) {
370
- j += 2;
371
- while (j < n && /[01_]/.test(source[j])) j += 1;
372
- } else if (source.startsWith('0o', i) || source.startsWith('0O', i)) {
373
- j += 2;
374
- while (j < n && /[0-7_]/.test(source[j])) j += 1;
375
- } else {
376
- while (j < n && /[0-9_n.]/.test(source[j])) j += 1;
377
- if ('eE'.includes(source[j])) {
378
- j += 1;
379
- if ('+-'.includes(source[j])) j += 1;
380
- while (j < n && /[0-9_]/.test(source[j])) j += 1;
381
- }
382
- }
383
- return { text: source.slice(i, j), end: j };
384
- };
385
-
386
- const readJsOperator = (source, i) => {
387
- const two = source.slice(i, i + 2);
388
- const three = source.slice(i, i + 3);
389
- let j = i + 1;
390
- if (OP3.includes(three)) j = i + 3;
391
- else if (OP2.includes(two)) j = i + 2;
392
- return { text: source.slice(i, j), end: j };
393
- };
394
-
395
- const plainTail = (out) => tokensText(out).slice(-8);
396
-
397
- const styleExpected = ({ expectName }) => EXPECT_STYLE[expectName];
398
-
399
- const styleKeyword = ({ word, ts }) => {
400
- const table = ts ? TS_WORD_STYLE : WORD_STYLE;
401
- if (!Object.hasOwn(table, word)) return null;
402
- return table[word];
403
- };
404
-
405
- const styleConstant = ({ word }) =>
406
- /^[A-Z][A-Z0-9_]+$/.test(word) ? 'constant' : null;
407
-
408
- const stylePascal = ({ word }) => (/^[A-Z]/.test(word) ? 'className' : null);
409
-
410
- const styleCall = ({ after }) => (after === '(' ? 'function' : null);
411
-
412
- const styleAssign = ({ after, source, k }) => {
413
- if (after !== '=') return null;
414
- const k2 = skipWs(source, k + 1);
415
- const rhs = source.slice(k2, k2 + 8);
416
- if (rhs.startsWith('async')) return 'function';
417
- if (rhs.startsWith('function')) return 'function';
418
- if (source[k2] === '(') return 'function';
419
- return 'variable';
420
- };
421
-
422
- const styleProperty = ({ out }) => {
423
- const prev = plainTail(out);
424
- return /\.$/.test(prev) ? 'property' : null;
425
- };
426
-
427
- const IDENT_RULES = [
428
- styleExpected,
429
- styleKeyword,
430
- styleConstant,
431
- stylePascal,
432
- styleCall,
433
- styleAssign,
434
- styleProperty,
435
- ];
436
-
437
- const identStyle = (ctx) => {
438
- for (const rule of IDENT_RULES) {
439
- const style = rule(ctx);
440
- if (style) return style;
441
- }
442
- return 'variable';
443
- };
444
-
445
- const readString = (source, i) => {
446
- const q = source[i];
447
- let j = i + 1;
448
- const n = source.length;
449
- while (j < n) {
450
- if (source[j] === '\\') {
451
- j += 2;
452
- continue;
453
- }
454
- if (source[j] === q) {
455
- j += 1;
456
- break;
457
- }
458
- if (source[j] === '\n') break;
459
- j += 1;
460
- }
461
- return { text: source.slice(i, j), end: j };
462
- };
463
-
464
- const readRegex = (source, i) => {
465
- let j = i + 1;
466
- const n = source.length;
467
- while (j < n) {
468
- if (source[j] === '\\') {
469
- j += 2;
470
- continue;
471
- }
472
- if (source[j] === '[') {
473
- j += 1;
474
- while (j < n && source[j] !== ']') {
475
- if (source[j] === '\\') j += 1;
476
- j += 1;
477
- }
478
- j += 1;
479
- continue;
480
- }
481
- if (source[j] === '/') {
482
- j += 1;
483
- while (j < n && /[a-z]/i.test(source[j])) j += 1;
484
- break;
485
- }
486
- if (source[j] === '\n') break;
487
- j += 1;
488
- }
489
- return { text: source.slice(i, j), end: j };
490
- };
491
-
492
- const readTemplateInterp = (source, j) => {
493
- let depth = 1;
494
- const start = j;
495
- const n = source.length;
496
- while (j < n && depth > 0) {
497
- if (ANY_QUOTE.includes(source[j])) {
498
- j = skipQuoted(source, j);
499
- continue;
500
- }
501
- if (source[j] === '{') depth += 1;
502
- if (source[j] === '}') {
503
- depth -= 1;
504
- if (depth === 0) break;
505
- }
506
- j += 1;
507
- }
508
- return { expr: source.slice(start, j), end: j };
509
- };
510
-
511
- const isRegexContext = (out) => {
512
- const prev = tokensText(out).trimEnd();
513
- if (!prev) return true;
514
- const afterOperator = /[=(:,[?!&|{};]$/.test(prev);
515
- const afterKeyword = /(?:return|case|throw|=>|typeof|void)$/.test(prev);
516
- return afterOperator || afterKeyword;
517
- };
518
-
519
- const highlightTemplate = (source, i, highlightExpr) => {
520
- const n = source.length;
521
- let j = i + 1;
522
- const parts = [];
523
- emit(parts, 'template', '`');
524
- let buf = '';
525
- const flush = () => {
526
- if (buf) {
527
- emit(parts, 'template', buf);
528
- buf = '';
529
- }
530
- };
531
- while (j < n) {
532
- if (source[j] === '\\') {
533
- flush();
534
- emit(parts, 'escape', source.slice(j, j + 2));
535
- j += 2;
536
- continue;
537
- }
538
- if (source[j] === '`') {
539
- flush();
540
- emit(parts, 'template', '`');
541
- j += 1;
542
- break;
543
- }
544
- if (source[j] === '$' && source[j + 1] === '{') {
545
- flush();
546
- emit(parts, 'interpolation', '${');
547
- j += 2;
548
- const interp = readTemplateInterp(source, j);
549
- const inner = highlightExpr(interp.expr);
550
- for (const t of inner) parts.push(t);
551
- j = interp.end;
552
- if (source[j] === '}') {
553
- emit(parts, 'interpolation', '}');
554
- j += 1;
555
- }
556
- continue;
557
- }
558
- buf += source[j];
559
- j += 1;
560
- }
561
- flush();
562
- return { tokens: parts, end: j };
563
- };
8
+ const PLUGIN_DIR = path.join(__dirname, 'highlight');
9
+ const SKIP = new Set(['core.js']);
564
10
 
565
- const isJsxTagStart = (source, i) => {
566
- if (source[i] !== '<') return false;
567
- if (i > 0 && /[A-Za-z0-9_$]/.test(source[i - 1])) return false;
568
- const next = source[i + 1];
569
- if (next === '>' || next === '/') return true;
570
- if (!/[A-Za-z_$]/.test(next || '')) return false;
571
- let j = i + 1;
572
- while (j < source.length && /[A-Za-z0-9_$.:-]/.test(source[j])) j += 1;
573
- const after = source[skipWs(source, j)];
574
- if (after === '>' || after === '/' || after === '{') return true;
575
- return /[A-Za-z_$]/.test(after || '');
576
- };
577
-
578
- const jsxNameStyle = (name) => {
579
- if (/^[A-Z]/.test(name) || name.includes('.')) return 'className';
580
- return 'tag';
581
- };
582
-
583
- const readJsxName = (source, i) => {
584
- let j = i;
585
- while (j < source.length && /[A-Za-z0-9_$.:-]/.test(source[j])) j += 1;
586
- return { text: source.slice(i, j), end: j };
587
- };
588
-
589
- const highlightJsxExpr = (source, i, inner) => {
590
- const out = [];
591
- emit(out, 'interpolation', '{');
592
- const interp = readTemplateInterp(source, i + 1);
593
- const tokens = inner(interp.expr);
594
- for (const t of tokens) out.push(t);
595
- let end = interp.end;
596
- if (source[end] === '}') {
597
- emit(out, 'interpolation', '}');
598
- end += 1;
599
- }
600
- return { tokens: out, end };
601
- };
11
+ const isPluginFile = (name) => name.endsWith('.js') && !SKIP.has(name);
602
12
 
603
- const highlightJsxTagTail = (source, i, inner, out) => {
604
- const n = source.length;
605
- let selfClosing = false;
606
- while (i < n) {
607
- const ch = source[i];
608
- if (ch === '>') {
609
- emit(out, 'punct', '>');
610
- i += 1;
611
- break;
612
- }
613
- if (ch === '/' && source[i + 1] === '>') {
614
- emit(out, 'punct', '/>');
615
- i += 2;
616
- selfClosing = true;
617
- break;
618
- }
619
- if (/\s/.test(ch)) {
620
- emit(out, 'plain', ch);
621
- i += 1;
622
- continue;
623
- }
624
- if (ch === '{') {
625
- const expr = highlightJsxExpr(source, i, inner);
626
- for (const t of expr.tokens) out.push(t);
627
- i = expr.end;
628
- continue;
629
- }
630
- if (QUOTES.includes(ch)) {
631
- const { text, end } = readString(source, i);
632
- emit(out, 'string', text);
633
- i = end;
634
- continue;
635
- }
636
- if (ch === '=') {
637
- emit(out, 'punct', '=');
638
- i += 1;
639
- continue;
640
- }
641
- if (/[A-Za-z_$]/.test(ch)) {
642
- const { text, end } = readJsxName(source, i);
643
- emit(out, 'attr', text);
644
- i = end;
645
- continue;
646
- }
647
- emit(out, 'punct', ch);
648
- i += 1;
649
- }
650
- return { end: i, selfClosing };
651
- };
13
+ const loadPlugin = (file) => require(path.join(PLUGIN_DIR, file));
652
14
 
653
- const highlightJsxElement = (source, i, inner) => {
654
- const out = [];
655
- const n = source.length;
656
- emit(out, 'punct', '<');
657
- i += 1;
658
- let closing = false;
659
- if (source[i] === '/') {
660
- emit(out, 'punct', '/');
661
- i += 1;
662
- closing = true;
663
- }
664
- const nameRead = readJsxName(source, i);
665
- const name = nameRead.text;
666
- if (name) {
667
- emit(out, jsxNameStyle(name), name);
668
- i = nameRead.end;
669
- }
670
- const tail = highlightJsxTagTail(source, i, inner, out);
671
- i = tail.end;
672
- if (closing || tail.selfClosing) return { tokens: out, end: i };
673
- while (i < n) {
674
- if (source.startsWith('</', i)) {
675
- const close = highlightJsxElement(source, i, inner);
676
- for (const t of close.tokens) out.push(t);
677
- return { tokens: out, end: close.end };
678
- }
679
- if (source[i] === '{') {
680
- const expr = highlightJsxExpr(source, i, inner);
681
- for (const t of expr.tokens) out.push(t);
682
- i = expr.end;
683
- continue;
684
- }
685
- if (source[i] === '<' && isJsxTagStart(source, i)) {
686
- const child = highlightJsxElement(source, i, inner);
687
- for (const t of child.tokens) out.push(t);
688
- i = child.end;
689
- continue;
690
- }
691
- let j = i;
692
- while (j < n && source[j] !== '<' && source[j] !== '{') j += 1;
693
- if (j === i) {
694
- emit(out, 'plain', source[i]);
695
- i += 1;
696
- continue;
697
- }
698
- emit(out, 'plain', source.slice(i, j));
699
- i = j;
700
- }
701
- return { tokens: out, end: i };
15
+ const loadPlugins = () => {
16
+ const names = fs.readdirSync(PLUGIN_DIR).filter(isPluginFile);
17
+ names.sort();
18
+ return names.map(loadPlugin);
702
19
  };
703
20
 
704
- const highlightJsFamily = (source, options = {}) => {
705
- const ts = options.ts === true;
706
- const jsx = options.jsx === true;
707
- const inner = (expr) => highlightJsFamily(expr, options);
708
- const out = [];
709
- let i = 0;
710
- const n = source.length;
711
- let expectName = null;
712
-
713
- while (i < n) {
714
- const ch = source[i];
715
- const next = source[i + 1];
716
-
717
- if (ch === '/' && next === '/') {
718
- const { text, end } = readUntilNl(source, i);
719
- emit(out, 'comment', text);
720
- i = end;
721
- continue;
722
- }
723
-
724
- if (ch === '/' && next === '*') {
725
- const { text, end } = readBlockComment(source, i);
726
- emit(out, 'comment', text);
727
- i = end;
728
- continue;
729
- }
730
-
731
- if (ch === '`') {
732
- const r = highlightTemplate(source, i, inner);
733
- for (const t of r.tokens) out.push(t);
734
- i = r.end;
735
- continue;
21
+ const indexPlugins = (plugins) => {
22
+ const highlighters = {};
23
+ const lang = {};
24
+ for (const plugin of plugins) {
25
+ for (const id of plugin.langs) {
26
+ highlighters[id] = plugin.highlight;
27
+ lang[id] = id;
736
28
  }
737
-
738
- if (QUOTES.includes(ch)) {
739
- const { text, end } = readString(source, i);
740
- emit(out, 'string', text);
741
- i = end;
742
- continue;
743
- }
744
-
745
- if (ch === '/' && isRegexContext(out)) {
746
- const { text, end } = readRegex(source, i);
747
- emit(out, 'regex', text);
748
- i = end;
749
- continue;
750
- }
751
-
752
- if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(next || ''))) {
753
- const { text, end } = readJsNumber(source, i);
754
- emit(out, 'number', text);
755
- i = end;
756
- continue;
757
- }
758
-
759
- if (ch === '@') {
760
- let j = i + 1;
761
- while (j < n && /[A-Za-z0-9_$]/.test(source[j])) j += 1;
762
- emit(out, 'decorator', source.slice(i, j));
763
- i = j;
764
- expectName = null;
765
- continue;
766
- }
767
-
768
- if (/[A-Za-z_$]/.test(ch)) {
769
- const word = peekIdent(source, i);
770
- const end = i + word.length;
771
- const k = skipWs(source, end);
772
- const after = source[k];
773
- const ctx = { word, ts, after, source, k, out, expectName };
774
- emit(out, identStyle(ctx), word);
775
- if (EXPECT_STYLE[expectName]) expectName = null;
776
- else expectName = NEXT_EXPECT[word] ?? null;
777
- i = end;
778
- continue;
779
- }
780
-
781
- if (jsx && ch === '<' && isJsxTagStart(source, i)) {
782
- const node = highlightJsxElement(source, i, inner);
783
- for (const t of node.tokens) out.push(t);
784
- i = node.end;
785
- expectName = null;
786
- continue;
787
- }
788
-
789
- if (/[=<>!+\-*/%&|^~?:]/.test(ch)) {
790
- const { text, end } = readJsOperator(source, i);
791
- emit(out, 'operator', text);
792
- i = end;
793
- expectName = null;
794
- continue;
795
- }
796
- if (/[{}()[\];,.]/.test(ch)) {
797
- emit(out, 'punct', ch);
798
- i += 1;
799
- continue;
800
- }
801
- emit(out, 'plain', ch);
802
- i += 1;
803
- }
804
- return out;
805
- };
806
-
807
- const highlightJson = (source) => {
808
- const out = [];
809
- let i = 0;
810
- const n = source.length;
811
- while (i < n) {
812
- const ch = source[i];
813
- if (ch === '"') {
814
- const { text, end } = readString(source, i);
815
- let k = end;
816
- while (k < n && /\s/.test(source[k])) k += 1;
817
- const kind = source[k] === ':' ? 'property' : 'string';
818
- emit(out, kind, text);
819
- i = end;
820
- continue;
821
- }
822
- if (/[0-9-]/.test(ch)) {
823
- let j = i;
824
- while (j < n && /[0-9.eE+-]/.test(source[j])) j += 1;
825
- emit(out, 'number', source.slice(i, j));
826
- i = j;
827
- continue;
828
- }
829
- if (/[a-z]/.test(ch)) {
830
- let j = i;
831
- while (j < n && /[a-z]/.test(source[j])) j += 1;
832
- const word = source.slice(i, j);
833
- emit(out, JSON_LITERAL[word] ?? 'plain', word);
834
- i = j;
835
- continue;
836
- }
837
- if (/[{}[\],:]/.test(ch)) {
838
- emit(out, 'punct', ch);
839
- i += 1;
840
- continue;
841
- }
842
- emit(out, 'plain', ch);
843
- i += 1;
844
- }
845
- return out;
846
- };
847
-
848
- const cssWordStyle = (next) => CSS_NEXT_STYLE[next] ?? 'plain';
849
-
850
- const highlightCss = (source) => {
851
- const out = [];
852
- let i = 0;
853
- const n = source.length;
854
- while (i < n) {
855
- const ch = source[i];
856
- const next = source[i + 1];
857
- if (ch === '/' && next === '*') {
858
- const { text, end } = readBlockComment(source, i);
859
- emit(out, 'comment', text);
860
- i = end;
861
- continue;
862
- }
863
- if (QUOTES.includes(ch)) {
864
- const { text, end } = readString(source, i);
865
- emit(out, 'string', text);
866
- i = end;
867
- continue;
868
- }
869
- if (ch === '@') {
870
- let j = i + 1;
871
- while (j < n && /[A-Za-z0-9-]/.test(source[j])) j += 1;
872
- emit(out, 'keyword', source.slice(i, j));
873
- i = j;
874
- continue;
875
- }
876
- if ('#.'.includes(ch)) {
877
- let j = i + 1;
878
- while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
879
- const kind = ch === '#' ? 'constant' : 'className';
880
- emit(out, kind, source.slice(i, j));
881
- i = j;
882
- continue;
883
- }
884
- if (/[0-9]/.test(ch)) {
885
- let j = i;
886
- while (j < n && /[0-9.%]/.test(source[j])) j += 1;
887
- emit(out, 'number', source.slice(i, j));
888
- i = j;
889
- continue;
890
- }
891
- if (/[A-Za-z_-]/.test(ch)) {
892
- let j = i + 1;
893
- while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
894
- const word = source.slice(i, j);
895
- let k = j;
896
- while (k < n && /\s/.test(source[k])) k += 1;
897
- emit(out, cssWordStyle(source[k]), word);
898
- i = j;
899
- continue;
900
- }
901
- if (/[{};:,]/.test(ch)) {
902
- emit(out, 'punct', ch);
903
- i += 1;
904
- continue;
905
- }
906
- emit(out, 'plain', ch);
907
- i += 1;
908
- }
909
- return out;
910
- };
911
-
912
- const colorHtmlTag = (tag) => {
913
- const out = [];
914
- let i = 1;
915
- const n = tag.length;
916
- emit(out, 'punct', '<');
917
- if (tag[i] === '/') {
918
- emit(out, 'punct', '/');
919
- i += 1;
920
- }
921
- let j = i;
922
- while (j < n && /[A-Za-z0-9:-]/.test(tag[j])) j += 1;
923
- emit(out, 'tag', tag.slice(i, j));
924
- i = j;
925
- while (i < n) {
926
- if (tag[i] === '>' || (tag[i] === '/' && tag[i + 1] === '>')) {
927
- emit(out, 'punct', tag.slice(i));
928
- break;
929
- }
930
- if (/\s/.test(tag[i])) {
931
- emit(out, 'plain', tag[i]);
932
- i += 1;
933
- continue;
934
- }
935
- if (tag[i] === '"' || tag[i] === SQUOTE) {
936
- const q = tag[i];
937
- let k = i + 1;
938
- while (k < n && tag[k] !== q) k += 1;
939
- k = Math.min(k + 1, n);
940
- emit(out, 'string', tag.slice(i, k));
941
- i = k;
942
- continue;
943
- }
944
- if (/[A-Za-z_:]/.test(tag[i])) {
945
- let k = i + 1;
946
- while (k < n && /[A-Za-z0-9_:.-]/.test(tag[k])) k += 1;
947
- emit(out, 'attr', tag.slice(i, k));
948
- i = k;
949
- continue;
950
- }
951
- emit(out, 'punct', tag[i]);
952
- i += 1;
953
- }
954
- return out;
955
- };
956
-
957
- const highlightHtml = (source) => {
958
- const out = [];
959
- let i = 0;
960
- const n = source.length;
961
- while (i < n) {
962
- if (source.startsWith('<!--', i)) {
963
- let j = source.indexOf('-->', i + 4);
964
- j = j === -1 ? n : j + 3;
965
- emit(out, 'comment', source.slice(i, j));
966
- i = j;
967
- continue;
968
- }
969
- if (source[i] === '<') {
970
- let j = i + 1;
971
- while (j < n && source[j] !== '>') {
972
- if (source[j] === '"' || source[j] === SQUOTE) {
973
- j = skipQuoted(source, j);
974
- continue;
975
- }
976
- j += 1;
977
- }
978
- if (j < n) j += 1;
979
- for (const t of colorHtmlTag(source.slice(i, j))) out.push(t);
980
- i = j;
981
- continue;
982
- }
983
- let j = i + 1;
984
- while (j < n && source[j] !== '<' && !source.startsWith('<!--', j)) {
985
- j += 1;
986
- }
987
- emit(out, 'plain', source.slice(i, j));
988
- i = j;
989
- }
990
- return out;
991
- };
992
-
993
- const splitCsv = (line) => {
994
- const cells = [];
995
- let cur = '';
996
- let inQ = false;
997
- for (let i = 0; i < line.length; i += 1) {
998
- const ch = line[i];
999
- if (ch === '"') {
1000
- if (inQ && line[i + 1] === '"') {
1001
- cur += '"';
1002
- i += 1;
1003
- } else {
1004
- inQ = !inQ;
1005
- cur += ch;
1006
- }
1007
- continue;
1008
- }
1009
- if (ch === ',' && !inQ) {
1010
- cells.push(cur);
1011
- cur = '';
1012
- continue;
1013
- }
1014
- cur += ch;
1015
- }
1016
- cells.push(cur);
1017
- return cells;
1018
- };
1019
-
1020
- const highlightCsvCell = (cell, row, col) => {
1021
- if (row === 0) return { text: cell, style: 'function' };
1022
- const num = /^-?\d+(\.\d+)?$/.test(cell.trim());
1023
- if (num) return { text: cell, style: 'number' };
1024
- if (col % 2 === 0) return { text: cell, style: 'string' };
1025
- return { text: cell, style: 'variable' };
1026
- };
1027
-
1028
- const highlightCsvLine = (line, row) => {
1029
- if (!line) return [];
1030
- const cells = splitCsv(line);
1031
- const out = [];
1032
- for (let col = 0; col < cells.length; col += 1) {
1033
- if (col) emit(out, 'punct', ',');
1034
- const cell = highlightCsvCell(cells[col], row, col);
1035
- emit(out, cell.style, cell.text);
1036
- }
1037
- return out;
1038
- };
1039
-
1040
- const highlightCsv = (source) => {
1041
- const lines = source.split(/\r?\n/);
1042
- const out = [];
1043
- for (let row = 0; row < lines.length; row += 1) {
1044
- if (row) emit(out, 'plain', '\n');
1045
- for (const t of highlightCsvLine(lines[row], row === 0 ? 1 : row)) {
1046
- out.push(t);
1047
- }
1048
- }
1049
- return out;
1050
- };
1051
-
1052
- const readBashVar = (source, i) => {
1053
- if (source[i + 1] === '{') {
1054
- let j = i + 2;
1055
- while (j < source.length && source[j] !== '}') j += 1;
1056
- if (j < source.length) j += 1;
1057
- return { text: source.slice(i, j), end: j };
1058
- }
1059
- if (/[0-9#?$!*@-]/.test(source[i + 1] || '')) {
1060
- return { text: source.slice(i, i + 2), end: i + 2 };
1061
- }
1062
- let j = i + 1;
1063
- while (j < source.length && /[A-Za-z0-9_]/.test(source[j])) j += 1;
1064
- if (j === i + 1) return { text: '$', end: i + 1 };
1065
- return { text: source.slice(i, j), end: j };
1066
- };
1067
-
1068
- const takeQuoted = (source, i) => {
1069
- const q = source[i];
1070
- let j = i + 1;
1071
- const n = source.length;
1072
- while (j < n && source[j] !== q) j += 1;
1073
- if (j < n) j += 1;
1074
- return { text: source.slice(i, j), end: j };
1075
- };
1076
-
1077
- const takeBashDouble = (source, i, out) => {
1078
- let j = i + 1;
1079
- let chunk = '"';
1080
- const n = source.length;
1081
- while (j < n && source[j] !== '"') {
1082
- if (source[j] === '\\' && j + 1 < n) {
1083
- chunk += source.slice(j, j + 2);
1084
- j += 2;
1085
- continue;
1086
- }
1087
- if (source[j] === '$') {
1088
- emit(out, 'string', chunk);
1089
- chunk = '';
1090
- const { text, end } = readBashVar(source, j);
1091
- emit(out, 'variable', text);
1092
- j = end;
1093
- continue;
1094
- }
1095
- chunk += source[j];
1096
- j += 1;
1097
- }
1098
- if (j < n) {
1099
- chunk += '"';
1100
- j += 1;
1101
- }
1102
- if (chunk) emit(out, 'string', chunk);
1103
- return j;
1104
- };
1105
-
1106
- const bashWordStyle = (word, nextCh) => {
1107
- const known = BASH_WORD_STYLE[word];
1108
- if (known) return known;
1109
- if (nextCh === '=') return 'variable';
1110
- return 'plain';
1111
- };
1112
-
1113
- const bashOpLen = (ch, next) => ('|&><'.includes(ch) && next === ch ? 2 : 1);
1114
-
1115
- const highlightBash = (source) => {
1116
- const out = [];
1117
- let i = 0;
1118
- const n = source.length;
1119
- while (i < n) {
1120
- const ch = source[i];
1121
- const next = source[i + 1];
1122
-
1123
- if (ch === '#' && (i === 0 || /\s/.test(source[i - 1]))) {
1124
- const { text, end } = readUntilNl(source, i);
1125
- emit(out, 'comment', text);
1126
- i = end;
1127
- continue;
1128
- }
1129
-
1130
- if (BASH_QUOTE.includes(ch)) {
1131
- const { text, end } = takeQuoted(source, i);
1132
- emit(out, 'string', text);
1133
- i = end;
1134
- continue;
1135
- }
1136
-
1137
- if (ch === '"') {
1138
- i = takeBashDouble(source, i, out);
1139
- continue;
1140
- }
1141
-
1142
- if (ch === '$') {
1143
- const { text, end } = readBashVar(source, i);
1144
- emit(out, 'variable', text);
1145
- i = end;
1146
- continue;
1147
- }
1148
-
1149
- if (ch === '-' && /[A-Za-z0-9]/.test(next || '')) {
1150
- let j = i + 1;
1151
- while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
1152
- emit(out, 'attr', source.slice(i, j));
1153
- i = j;
1154
- continue;
1155
- }
1156
-
1157
- if (/[A-Za-z_]/.test(ch)) {
1158
- let j = i + 1;
1159
- while (j < n && /[A-Za-z0-9_]/.test(source[j])) j += 1;
1160
- const word = source.slice(i, j);
1161
- emit(out, bashWordStyle(word, source[j]), word);
1162
- i = j;
1163
- continue;
1164
- }
1165
-
1166
- if (/[0-9]/.test(ch)) {
1167
- let j = i + 1;
1168
- while (j < n && /[0-9]/.test(source[j])) j += 1;
1169
- emit(out, 'number', source.slice(i, j));
1170
- i = j;
1171
- continue;
1172
- }
1173
-
1174
- if ('|&;<>(){}'.includes(ch)) {
1175
- const j = i + bashOpLen(ch, next);
1176
- emit(out, 'operator', source.slice(i, j));
1177
- i = j;
1178
- continue;
1179
- }
1180
-
1181
- emit(out, 'plain', ch);
1182
- i += 1;
1183
- }
1184
- return out;
1185
- };
1186
-
1187
- const highlightLogLine = (line) => {
1188
- if (!line) return [];
1189
- const out = [];
1190
- let rest = line;
1191
- const dm = rest.match(LOG_DATETIME_RE);
1192
- if (dm) {
1193
- emit(out, 'logDate', dm[1]);
1194
- rest = rest.slice(dm[1].length);
1195
- }
1196
- const tag = rest.match(/^(\s*)(\[[^\]]+\])/);
1197
- if (tag) {
1198
- emit(out, 'plain', tag[1]);
1199
- const name = tag[2].replace(/^\[|\]$/g, '').toLowerCase();
1200
- emit(out, LOG_TAG_STYLE[name] || 'plain', tag[2]);
1201
- rest = rest.slice(tag[0].length);
1202
- }
1203
- if (rest.includes('\t')) {
1204
- const parts = rest.split('\t');
1205
- for (let i = 0; i < parts.length; i += 1) {
1206
- if (i) emit(out, 'plain', '\t');
1207
- emit(out, `logCol${i}`, parts[i]);
1208
- }
1209
- return out;
1210
- }
1211
- if (rest) emit(out, 'logCol0', rest);
1212
- return out;
1213
- };
1214
-
1215
- const highlightLog = (source) => {
1216
- const lines = source.split('\n');
1217
- const out = [];
1218
- for (let i = 0; i < lines.length; i += 1) {
1219
- if (i) emit(out, 'plain', '\n');
1220
- for (const t of highlightLogLine(lines[i])) out.push(t);
1221
- }
1222
- return out;
1223
- };
1224
-
1225
- const highlightDotLine = (line) => {
1226
- if (!line) return [];
1227
- const out = [];
1228
- const t = line.trimStart();
1229
- if (t.startsWith('#') || t.startsWith(';')) {
1230
- emit(out, 'comment', line);
1231
- return out;
1232
- }
1233
- if (t.startsWith('!')) {
1234
- const lead = line.slice(0, line.length - t.length);
1235
- emit(out, 'plain', lead);
1236
- emit(out, 'operator', '!');
1237
- emit(out, 'plain', t.slice(1));
1238
- return out;
1239
- }
1240
- const eq = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_.-]*)(\s*=\s*)(.*)$/);
1241
- if (eq) {
1242
- emit(out, 'plain', eq[1]);
1243
- emit(out, 'variable', eq[2]);
1244
- emit(out, 'operator', eq[3]);
1245
- emit(out, 'string', eq[4]);
1246
- return out;
1247
- }
1248
- emit(out, 'plain', line);
1249
- return out;
1250
- };
1251
-
1252
- const highlightDot = (source) => {
1253
- const lines = source.split('\n');
1254
- const out = [];
1255
- for (let i = 0; i < lines.length; i += 1) {
1256
- if (i) emit(out, 'plain', '\n');
1257
- for (const t of highlightDotLine(lines[i])) out.push(t);
1258
- }
1259
- return out;
1260
- };
1261
-
1262
- const highlightTxtLine = (line) => {
1263
- if (!line) return [];
1264
- const out = [];
1265
- const re = /(https?:\/\/[^\s<>"']+|www\.[^\s<>"']+)/g;
1266
- let last = 0;
1267
- let m = re.exec(line);
1268
- while (m !== null) {
1269
- if (m.index > last) emit(out, 'plain', line.slice(last, m.index));
1270
- emit(out, 'string', m[0]);
1271
- last = m.index + m[0].length;
1272
- m = re.exec(line);
1273
- }
1274
- if (last < line.length) emit(out, 'plain', line.slice(last));
1275
- if (!out.length) emit(out, 'plain', line);
1276
- return out;
1277
- };
1278
-
1279
- const highlightTxt = (source) => {
1280
- const lines = source.split('\n');
1281
- const out = [];
1282
- for (let i = 0; i < lines.length; i += 1) {
1283
- if (i) emit(out, 'plain', '\n');
1284
- for (const t of highlightTxtLine(lines[i])) out.push(t);
29
+ const aliases = plugin.aliases ?? {};
30
+ for (const name of Object.keys(aliases)) lang[name] = aliases[name];
1285
31
  }
1286
- return out;
32
+ return { highlighters, lang };
1287
33
  };
1288
34
 
1289
- const highlightJs = (source) => highlightJsFamily(source, {});
1290
- const highlightTs = (source) => highlightJsFamily(source, { ts: true });
1291
- const highlightJsx = (source) => highlightJsFamily(source, { jsx: true });
1292
- const highlightTsx = (source) =>
1293
- highlightJsFamily(source, { ts: true, jsx: true });
35
+ const PLUGINS = loadPlugins();
36
+ const registry = indexPlugins(PLUGINS);
37
+ const HIGHLIGHTERS = registry.highlighters;
38
+ const LANG = registry.lang;
1294
39
 
1295
- const HIGHLIGHTERS = {
1296
- js: highlightJs,
1297
- mjs: highlightJs,
1298
- ts: highlightTs,
1299
- jsx: highlightJsx,
1300
- tsx: highlightTsx,
1301
- json: highlightJson,
1302
- css: highlightCss,
1303
- html: highlightHtml,
1304
- csv: highlightCsv,
1305
- bash: highlightBash,
1306
- log: highlightLog,
1307
- dot: highlightDot,
1308
- txt: highlightTxt,
1309
- py: highlightTxt,
1310
- md: highlightTxt,
40
+ const mapLang = (name) => {
41
+ if (!name) return 'txt';
42
+ return LANG[name.toLowerCase()] || 'txt';
1311
43
  };
1312
44
 
1313
45
  const tokenize = (lang, source) => {
1314
46
  const text = source ?? '';
1315
- const key = lang === 'dts' ? 'ts' : mapLang(lang);
1316
- const fn = HIGHLIGHTERS[key] || highlightTxt;
1317
- return fn(text);
47
+ const key = mapLang(lang);
48
+ const highlight = HIGHLIGHTERS[key] || HIGHLIGHTERS.txt;
49
+ return highlight(text, key);
1318
50
  };
1319
51
 
1320
52
  const overlayTokens = (tokens, spans) => {
@@ -1369,4 +101,5 @@ module.exports = {
1369
101
  tokensText,
1370
102
  tokenize,
1371
103
  overlayTokens,
104
+ mapLang,
1372
105
  };