git-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.
Files changed (48) hide show
  1. package/.tracerc.example +38 -0
  2. package/README.md +136 -0
  3. package/bun.lock +511 -0
  4. package/bunchee.config.ts +11 -0
  5. package/cli/index.ts +251 -0
  6. package/cli/parser.ts +76 -0
  7. package/cli/tsconfig.json +6 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +858 -0
  10. package/dist/config.cjs +66 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +63 -0
  13. package/dist/highlight/index.cjs +770 -0
  14. package/dist/highlight/index.d.ts +26 -0
  15. package/dist/highlight/index.js +766 -0
  16. package/dist/index.cjs +849 -0
  17. package/dist/index.d.ts +52 -0
  18. package/dist/index.js +845 -0
  19. package/examples/demo/App.tsx +78 -0
  20. package/examples/demo/index.html +12 -0
  21. package/examples/demo/main.tsx +10 -0
  22. package/examples/demo/mockData.ts +170 -0
  23. package/examples/demo/styles.css +103 -0
  24. package/examples/demo/tsconfig.json +21 -0
  25. package/examples/demo/tsconfig.node.json +10 -0
  26. package/examples/demo/vite.config.ts +20 -0
  27. package/package.json +58 -0
  28. package/src/Trace.tsx +717 -0
  29. package/src/cache.ts +118 -0
  30. package/src/config.ts +51 -0
  31. package/src/entries/config.ts +7 -0
  32. package/src/entries/gitea.ts +4 -0
  33. package/src/entries/github.ts +5 -0
  34. package/src/entries/gitlab.ts +4 -0
  35. package/src/gitea.ts +58 -0
  36. package/src/github.ts +100 -0
  37. package/src/gitlab.ts +65 -0
  38. package/src/highlight/highlight.ts +119 -0
  39. package/src/highlight/index.ts +4 -0
  40. package/src/host.ts +32 -0
  41. package/src/index.ts +6 -0
  42. package/src/patterns.ts +6 -0
  43. package/src/shared.ts +108 -0
  44. package/src/themes.ts +98 -0
  45. package/src/types.ts +72 -0
  46. package/test/e2e.html +424 -0
  47. package/tsconfig.json +18 -0
  48. package/vercel.json +4 -0
@@ -0,0 +1,770 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ // @ts-check
4
+ const JSXBrackets = new Set([
5
+ '<',
6
+ '>',
7
+ '{',
8
+ '}',
9
+ '[',
10
+ ']'
11
+ ]);
12
+ const Keywords_Js = new Set([
13
+ 'for',
14
+ 'do',
15
+ 'while',
16
+ 'if',
17
+ 'else',
18
+ 'return',
19
+ 'function',
20
+ 'var',
21
+ 'let',
22
+ 'const',
23
+ 'true',
24
+ 'false',
25
+ 'undefined',
26
+ 'this',
27
+ 'new',
28
+ 'delete',
29
+ 'typeof',
30
+ 'in',
31
+ 'instanceof',
32
+ 'void',
33
+ 'break',
34
+ 'continue',
35
+ 'switch',
36
+ 'case',
37
+ 'default',
38
+ 'throw',
39
+ 'try',
40
+ 'catch',
41
+ 'finally',
42
+ 'debugger',
43
+ 'with',
44
+ 'yield',
45
+ 'async',
46
+ 'await',
47
+ 'class',
48
+ 'extends',
49
+ 'super',
50
+ 'import',
51
+ 'export',
52
+ 'from',
53
+ 'static'
54
+ ]);
55
+ const Signs = new Set([
56
+ '+',
57
+ '-',
58
+ '*',
59
+ '/',
60
+ '%',
61
+ '=',
62
+ '!',
63
+ '&',
64
+ '|',
65
+ '^',
66
+ '~',
67
+ '!',
68
+ '?',
69
+ ':',
70
+ '.',
71
+ ',',
72
+ ';',
73
+ `'`,
74
+ '"',
75
+ '.',
76
+ '(',
77
+ ')',
78
+ '[',
79
+ ']',
80
+ '#',
81
+ '@',
82
+ '\\',
83
+ ...JSXBrackets
84
+ ]);
85
+ const DefaultOptions = {
86
+ keywords: Keywords_Js,
87
+ onCommentStart: isCommentStart_Js,
88
+ onCommentEnd: isCommentEnd_Js
89
+ };
90
+ /**
91
+ *
92
+ * 0 - identifier
93
+ * 1 - keyword
94
+ * 2 - string
95
+ * 3 - Class, number and null
96
+ * 4 - property
97
+ * 5 - entity
98
+ * 6 - jsx literals
99
+ * 7 - sign
100
+ * 8 - comment
101
+ * 9 - break
102
+ * 10 - space
103
+ *
104
+ */ const TokenTypes = /** @type {const} */ [
105
+ 'identifier',
106
+ 'keyword',
107
+ 'string',
108
+ 'class',
109
+ 'property',
110
+ 'entity',
111
+ 'jsxliterals',
112
+ 'sign',
113
+ 'comment',
114
+ 'break',
115
+ 'space'
116
+ ];
117
+ const [T_IDENTIFIER, T_KEYWORD, T_STRING, T_CLS_NUMBER, T_PROPERTY, T_ENTITY, T_JSX_LITERALS, T_SIGN, T_COMMENT, T_BREAK, T_SPACE] = /** @types {const} */ TokenTypes.map((_, i)=>i);
118
+ function isSpaces(str) {
119
+ return /^[^\S\r\n]+$/g.test(str);
120
+ }
121
+ function isSign(ch) {
122
+ return Signs.has(ch);
123
+ }
124
+ function encode(str) {
125
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
126
+ }
127
+ function isWord(chr) {
128
+ return /^[\w_]+$/.test(chr) || hasUnicode(chr);
129
+ }
130
+ function isCls(str) {
131
+ const chr0 = str[0];
132
+ return isWord(chr0) && chr0 === chr0.toUpperCase() || str === 'null';
133
+ }
134
+ function hasUnicode(s) {
135
+ return /[^\u0000-\u007f]/.test(s);
136
+ }
137
+ function isAlpha(chr) {
138
+ return /^[a-zA-Z]$/.test(chr);
139
+ }
140
+ function isIdentifierChar(chr) {
141
+ return isAlpha(chr) || hasUnicode(chr);
142
+ }
143
+ function isIdentifier(str) {
144
+ return isIdentifierChar(str[0]) && (str.length === 1 || isWord(str.slice(1)));
145
+ }
146
+ function isStrTemplateChr(chr) {
147
+ return chr === '`';
148
+ }
149
+ function isSingleQuotes(chr) {
150
+ return chr === '"' || chr === "'";
151
+ }
152
+ function isStringQuotation(chr) {
153
+ return isSingleQuotes(chr) || isStrTemplateChr(chr);
154
+ }
155
+ /** @returns {0|1|2} */ function isCommentStart_Js(curr, next) {
156
+ const str = curr + next;
157
+ if (str === '/*') return 2;
158
+ return str === '//' ? 1 : 0;
159
+ }
160
+ /** @returns {0|1|2} */ function isCommentEnd_Js(prev, curr) {
161
+ return prev + curr === '*/' ? 2 : curr === '\n' ? 1 : 0;
162
+ }
163
+ function isRegexStart(str) {
164
+ return str[0] === '/' && !isCommentStart_Js(str[0], str[1]);
165
+ }
166
+ /**
167
+ * @param {string} code
168
+ * @param {{ keywords: Set<string> }} options
169
+ * @return {Array<[number, string]>}
170
+ */ function tokenize(code, options) {
171
+ const { keywords, onCommentStart, onCommentEnd } = {
172
+ ...DefaultOptions,
173
+ ...options
174
+ };
175
+ let current = '';
176
+ let type = -1;
177
+ /** @type {[number, string]} */ let last = [
178
+ -1,
179
+ ''
180
+ ];
181
+ /** @type {[number, string]} */ let beforeLast = [
182
+ -2,
183
+ ''
184
+ ];
185
+ /** @type {Array<[number, string]>} */ const tokens = [];
186
+ /** @type boolean if entered jsx tag, inside <open tag> or </close tag> */ let __jsxEnter = false;
187
+ /**
188
+ * @type {0 | 1 | 2}
189
+ * @example
190
+ * 0 for not in jsx;
191
+ * 1 for open jsx tag;
192
+ * 2 for closing jsx tag;
193
+ **/ let __jsxTag = 0;
194
+ let __jsxExpr = false;
195
+ // only match paired (open + close) tags, not self-closing tags
196
+ let __jsxStack = 0;
197
+ const __jsxChild = ()=>__jsxEnter && !__jsxExpr && !__jsxTag;
198
+ // < __content__ >
199
+ const inJsxTag = ()=>__jsxTag && !__jsxChild();
200
+ // {'__content__'}
201
+ const inJsxLiterals = ()=>!__jsxTag && __jsxChild() && !__jsxExpr && __jsxStack > 0;
202
+ /** @type {string | null} */ let __strQuote = null;
203
+ let __regexQuoteStart = false;
204
+ let __strTemplateExprStack = 0;
205
+ let __strTemplateQuoteStack = 0;
206
+ const inStringQuotes = ()=>__strQuote !== null;
207
+ const inRegexQuotes = ()=>__regexQuoteStart;
208
+ const inStrTemplateLiterals = ()=>__strTemplateQuoteStack > __strTemplateExprStack;
209
+ const inStrTemplateExpr = ()=>__strTemplateQuoteStack > 0 && __strTemplateQuoteStack === __strTemplateExprStack;
210
+ const inStringContent = ()=>inStringQuotes() || inStrTemplateLiterals();
211
+ /**
212
+ *
213
+ * @param {string} token
214
+ * @returns {number}
215
+ */ function classify(token) {
216
+ const isLineBreak = token === '\n';
217
+ // First checking if they're attributes values
218
+ if (inJsxTag()) {
219
+ if (inStringQuotes()) {
220
+ return T_STRING;
221
+ }
222
+ const [, lastToken] = last;
223
+ if (isIdentifier(token)) {
224
+ // classify jsx open tag
225
+ if (lastToken === '<' || lastToken === '</') return T_ENTITY;
226
+ }
227
+ }
228
+ // Then determine if they're jsx literals
229
+ const isJsxLiterals = inJsxLiterals();
230
+ if (isJsxLiterals) return T_JSX_LITERALS;
231
+ // Determine strings first before other types
232
+ if (inStringQuotes() || inStrTemplateLiterals()) {
233
+ return T_STRING;
234
+ } else if (keywords.has(token)) {
235
+ return last[1] === '.' ? T_IDENTIFIER : T_KEYWORD;
236
+ } else if (isLineBreak) {
237
+ return T_BREAK;
238
+ } else if (isSpaces(token)) {
239
+ return T_SPACE;
240
+ } else if (token.split('').every(isSign)) {
241
+ return T_SIGN;
242
+ } else if (isCls(token)) {
243
+ return inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER;
244
+ } else {
245
+ if (isIdentifier(token)) {
246
+ const isLastPropDot = last[1] === '.' && isIdentifier(beforeLast[1]);
247
+ if (!inStringContent() && !isLastPropDot) return T_IDENTIFIER;
248
+ if (isLastPropDot) return T_PROPERTY;
249
+ }
250
+ return T_STRING;
251
+ }
252
+ }
253
+ /**
254
+ *
255
+ * @param {number} type_
256
+ * @param {string} token_
257
+ */ const append = (type_, token_)=>{
258
+ if (token_) {
259
+ current = token_;
260
+ }
261
+ if (current) {
262
+ type = type_ || classify(current);
263
+ /** @type [number, string] */ const pair = [
264
+ type,
265
+ current
266
+ ];
267
+ if (type !== T_SPACE && type !== T_BREAK) {
268
+ beforeLast = last;
269
+ last = pair;
270
+ }
271
+ tokens.push(pair);
272
+ }
273
+ current = '';
274
+ };
275
+ for(let i = 0; i < code.length; i++){
276
+ const curr = code[i];
277
+ const prev = code[i - 1];
278
+ const next = code[i + 1];
279
+ const p_c = prev + curr // previous and current
280
+ ;
281
+ const c_n = curr + next // current and next
282
+ ;
283
+ // Determine string quotation outside of jsx literals and template literals.
284
+ // Inside jsx literals or template literals, string quotation is still part of it.
285
+ if (isSingleQuotes(curr) && !inJsxLiterals() && !inStrTemplateLiterals()) {
286
+ append();
287
+ if (prev !== `\\`) {
288
+ if (__strQuote && curr === __strQuote) {
289
+ __strQuote = null;
290
+ } else if (!__strQuote) {
291
+ __strQuote = curr;
292
+ }
293
+ }
294
+ append(T_STRING, curr);
295
+ continue;
296
+ }
297
+ if (!inStrTemplateLiterals()) {
298
+ if (prev !== '\\n' && isStrTemplateChr(curr)) {
299
+ append();
300
+ append(T_STRING, curr);
301
+ __strTemplateQuoteStack++;
302
+ continue;
303
+ }
304
+ }
305
+ if (inStrTemplateLiterals()) {
306
+ if (prev !== '\\n' && isStrTemplateChr(curr)) {
307
+ if (__strTemplateQuoteStack > 0) {
308
+ append();
309
+ __strTemplateQuoteStack--;
310
+ append(T_STRING, curr);
311
+ continue;
312
+ }
313
+ }
314
+ if (c_n === '${') {
315
+ __strTemplateExprStack++;
316
+ append(T_STRING);
317
+ append(T_SIGN, c_n);
318
+ i++;
319
+ continue;
320
+ }
321
+ }
322
+ if (inStrTemplateExpr() && curr === '}') {
323
+ append();
324
+ __strTemplateExprStack--;
325
+ append(T_SIGN, curr);
326
+ continue;
327
+ }
328
+ if (__jsxChild()) {
329
+ if (curr === '{') {
330
+ append();
331
+ append(T_SIGN, curr);
332
+ __jsxExpr = true;
333
+ continue;
334
+ }
335
+ }
336
+ if (__jsxEnter) {
337
+ // <: open tag sign
338
+ // new '<' not inside jsx
339
+ if (!__jsxTag && curr === '<') {
340
+ append();
341
+ if (next === '/') {
342
+ // close tag
343
+ __jsxTag = 2;
344
+ current = c_n;
345
+ i++;
346
+ } else {
347
+ // open tag
348
+ __jsxTag = 1;
349
+ current = curr;
350
+ }
351
+ append(T_SIGN);
352
+ continue;
353
+ }
354
+ if (__jsxTag) {
355
+ // >: open tag close sign or closing tag closing sign
356
+ // and it's not `=>` or `/>`
357
+ // `curr` could be `>` or `/`
358
+ if (curr === '>' && !'/='.includes(prev)) {
359
+ append();
360
+ if (__jsxTag === 1) {
361
+ __jsxTag = 0;
362
+ __jsxStack++;
363
+ } else {
364
+ __jsxTag = 0;
365
+ __jsxEnter = false;
366
+ }
367
+ append(T_SIGN, curr);
368
+ continue;
369
+ }
370
+ // >: tag self close sign or close tag sign
371
+ if (c_n === '/>' || c_n === '</') {
372
+ // if current token is not part of close tag sign, push it first
373
+ if (current !== '<' && current !== '/') {
374
+ append();
375
+ }
376
+ if (c_n === '/>') {
377
+ __jsxTag = 0;
378
+ } else {
379
+ // is '</'
380
+ __jsxStack--;
381
+ }
382
+ if (!__jsxStack) __jsxEnter = false;
383
+ current = c_n;
384
+ i++;
385
+ append(T_SIGN);
386
+ continue;
387
+ }
388
+ // <: open tag sign
389
+ if (curr === '<') {
390
+ append();
391
+ current = curr;
392
+ append(T_SIGN);
393
+ continue;
394
+ }
395
+ // jsx property
396
+ // `-` in data-prop
397
+ if (next === '-' && !inStringContent() && !inJsxLiterals()) {
398
+ if (current) {
399
+ append(T_PROPERTY, current + curr + next);
400
+ i++;
401
+ continue;
402
+ }
403
+ }
404
+ // `=` in property=<value>
405
+ if (next === '=' && !inStringContent()) {
406
+ // if current is not a space, ensure `prop` is a property
407
+ if (!isSpaces(curr)) {
408
+ // If there're leading spaces, append them first
409
+ if (isSpaces(current)) {
410
+ append();
411
+ }
412
+ // Now check if the accumulated token is a property
413
+ const prop = current + curr;
414
+ if (isIdentifier(prop)) {
415
+ append(T_PROPERTY, prop);
416
+ continue;
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
423
+ if (!__jsxTag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
424
+ __jsxTag = next === '/' ? 2 : 1;
425
+ // current and next char can form a jsx open or close tag
426
+ if (curr === '<' && (next === '/' || isAlpha(next))) {
427
+ if (!inStringContent() && !inJsxLiterals() && !inRegexQuotes()) {
428
+ __jsxEnter = true;
429
+ }
430
+ }
431
+ }
432
+ const isQuotationChar = isStringQuotation(curr);
433
+ const isStringTemplateLiterals = inStrTemplateLiterals();
434
+ const isRegexChar = !__jsxEnter && isRegexStart(c_n);
435
+ const isJsxLiterals = inJsxLiterals();
436
+ // string quotation
437
+ if (isQuotationChar || isStringTemplateLiterals || isSingleQuotes(__strQuote)) {
438
+ current += curr;
439
+ } else if (isRegexChar) {
440
+ append();
441
+ const [lastType, lastToken] = last;
442
+ // Special cases that are not considered as regex:
443
+ // * (expr1) / expr2: `)` before `/` operator is still in expression
444
+ // * <non comment start>/ expr: non comment start before `/` is not regex
445
+ if (isRegexChar && lastType !== -1 && !(lastType === T_SIGN && ')' !== lastToken || lastType === T_COMMENT)) {
446
+ current = curr;
447
+ append();
448
+ continue;
449
+ }
450
+ __regexQuoteStart = true;
451
+ const start = i++;
452
+ // end of line of end of file
453
+ const isEof = ()=>i >= code.length;
454
+ const isEol = ()=>isEof() || code[i] === '\n';
455
+ let foundClose = false;
456
+ // traverse to find closing regex slash
457
+ for(; !isEol(); i++){
458
+ if (code[i] === '/' && code[i - 1] !== '\\') {
459
+ foundClose = true;
460
+ // end of regex, append regex flags
461
+ while(start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()){
462
+ i++;
463
+ }
464
+ break;
465
+ }
466
+ }
467
+ __regexQuoteStart = false;
468
+ if (start !== i && foundClose) {
469
+ // If current line is fully closed with string quotes or regex slashes,
470
+ // add them to tokens
471
+ current = code.slice(start, i + 1);
472
+ append(T_STRING);
473
+ } else {
474
+ // If it doesn't match any of the above, just leave it as operator and move on
475
+ current = curr;
476
+ append();
477
+ i = start;
478
+ }
479
+ } else if (onCommentStart(curr, next)) {
480
+ append();
481
+ const start = i;
482
+ const startCommentType = onCommentStart(curr, next);
483
+ // just match the comment, commentType === true
484
+ // inline comment, commentType === 1
485
+ // block comment, commentType === 2
486
+ if (startCommentType) {
487
+ for(; i < code.length; i++){
488
+ const endCommentType = onCommentEnd(code[i - 1], code[i]);
489
+ if (endCommentType == startCommentType) break;
490
+ }
491
+ }
492
+ current = code.slice(start, i + 1);
493
+ append(T_COMMENT);
494
+ } else if (curr === ' ' || curr === '\n') {
495
+ if (curr === ' ' && (isSpaces(current) || !current || isJsxLiterals)) {
496
+ current += curr;
497
+ if (next === '<') {
498
+ append();
499
+ }
500
+ } else {
501
+ append();
502
+ current = curr;
503
+ append();
504
+ }
505
+ } else {
506
+ if (__jsxExpr && curr === '}') {
507
+ append();
508
+ current = curr;
509
+ append();
510
+ __jsxExpr = false;
511
+ } else if (// it's jsx literals and is not a jsx bracket
512
+ isJsxLiterals && !JSXBrackets.has(curr) || // it's template literal content (including quotes)
513
+ inStrTemplateLiterals() || // same type char as previous one in current token
514
+ (isWord(curr) === isWord(current[current.length - 1]) || __jsxChild()) && !Signs.has(curr)) {
515
+ current += curr;
516
+ } else {
517
+ if (p_c === '</') {
518
+ current = p_c;
519
+ }
520
+ append();
521
+ if (p_c !== '</') {
522
+ current = curr;
523
+ }
524
+ if (c_n === '</' || c_n === '/>') {
525
+ current = c_n;
526
+ append();
527
+ i++;
528
+ } else if (JSXBrackets.has(curr)) append();
529
+ }
530
+ }
531
+ }
532
+ append();
533
+ return tokens;
534
+ }
535
+ /**
536
+ * @param {Array<[number, string]>} tokens
537
+ * @return {Array<{type: string, tagName: string, children: any[], properties: Record<string, string>}>}
538
+ */ function generate(tokens) {
539
+ const lines = [];
540
+ /**
541
+ * @param {any} children
542
+ * @return {{type: string, tagName: string, children: any[], properties: Record<string, string>}}
543
+ */ const createLine = (children)=>({
544
+ type: 'element',
545
+ tagName: 'span',
546
+ children,
547
+ properties: {
548
+ className: 'sh__line'
549
+ }
550
+ });
551
+ /**
552
+ * @param {Array<[number, string]>} tokens
553
+ * @returns {void}
554
+ */ function flushLine(tokens) {
555
+ /** @type {Array<any>} */ const lineTokens = tokens.map(([type, value])=>{
556
+ const tokenType = TokenTypes[type];
557
+ return {
558
+ type: 'element',
559
+ tagName: 'span',
560
+ children: [
561
+ {
562
+ type: 'text',
563
+ value
564
+ }
565
+ ],
566
+ properties: {
567
+ className: `sh__token--${tokenType}`,
568
+ style: {
569
+ color: `var(--sh-${tokenType})`
570
+ }
571
+ }
572
+ };
573
+ });
574
+ lines.push(createLine(lineTokens));
575
+ }
576
+ /** @type {Array<[number, string]>} */ const lineTokens = [];
577
+ let lastWasBreak = false;
578
+ for(let i = 0; i < tokens.length; i++){
579
+ const token = tokens[i];
580
+ const [type, value] = token;
581
+ const isLastToken = i === tokens.length - 1;
582
+ if (type !== T_BREAK) {
583
+ // Divide multi-line token into multi-line code
584
+ if (value.includes('\n')) {
585
+ const lines = value.split('\n');
586
+ for(let j = 0; j < lines.length; j++){
587
+ lineTokens.push([
588
+ type,
589
+ lines[j]
590
+ ]);
591
+ if (j < lines.length - 1) {
592
+ flushLine(lineTokens);
593
+ lineTokens.length = 0;
594
+ }
595
+ }
596
+ } else {
597
+ lineTokens.push(token);
598
+ }
599
+ lastWasBreak = false;
600
+ } else {
601
+ if (lastWasBreak) {
602
+ // Consecutive break - create empty line
603
+ flushLine([]);
604
+ } else {
605
+ // First break after content - flush current line
606
+ flushLine(lineTokens);
607
+ lineTokens.length = 0;
608
+ }
609
+ // If this is the last token and it's a break, create an empty line
610
+ if (isLastToken) {
611
+ flushLine([]);
612
+ }
613
+ lastWasBreak = true;
614
+ }
615
+ }
616
+ // Flush remaining tokens if any
617
+ if (lineTokens.length) {
618
+ flushLine(lineTokens);
619
+ }
620
+ return lines;
621
+ }
622
+ /** @param { className: string, style?: Record<string, string> } */ const propsToString = (props)=>{
623
+ let str = `class="${props.className}"`;
624
+ if (props.style) {
625
+ const style = Object.entries(props.style).map(([key, value])=>`${key}:${value}`).join(';');
626
+ str += ` style="${style}"`;
627
+ }
628
+ return str;
629
+ };
630
+ function toHtml(lines) {
631
+ return lines.map((line)=>{
632
+ const { tagName: lineTag } = line;
633
+ const tokens = line.children.map((child)=>{
634
+ const { tagName, children, properties } = child;
635
+ return `<${tagName} ${propsToString(properties)}>${encode(children[0].value)}</${tagName}>`;
636
+ }).join('');
637
+ return `<${lineTag} class="${line.properties.className}">${tokens}</${lineTag}>`;
638
+ }).join('\n');
639
+ }
640
+ /**
641
+ *
642
+ * @param {string} code
643
+ * @param {
644
+ * {
645
+ * keywords: Set<string>
646
+ * onCommentStart?: (curr: string, next: string) => number | boolean
647
+ * onCommentEnd?: (curr: string, prev: string) => number | boolean
648
+ * } | undefined} options
649
+ * @returns {string}
650
+ */ function highlight(code, options) {
651
+ const tokens = tokenize(code, options);
652
+ const lines = generate(tokens);
653
+ const output = toHtml(lines);
654
+ return output;
655
+ }
656
+ // namespace
657
+ /** @type {const} */ ({
658
+ TokenMap: new Map(TokenTypes.map((type, i)=>[
659
+ type,
660
+ i
661
+ ]))
662
+ });
663
+
664
+ // Syntax highlighting using sugar-high (~1KB, zero dependencies)
665
+ const MAX_CACHE_SIZE = 100;
666
+ /**
667
+ * Simple LRU (Least Recently Used) cache implementation
668
+ * Evicts oldest entries when capacity is reached
669
+ */ class LRUCache {
670
+ get(key) {
671
+ if (!this.cache.has(key)) return undefined;
672
+ const value = this.cache.get(key);
673
+ // Move to end (most recently used)
674
+ this.cache.delete(key);
675
+ this.cache.set(key, value);
676
+ return value;
677
+ }
678
+ set(key, value) {
679
+ // Remove oldest entry if at capacity
680
+ if (this.cache.size >= MAX_CACHE_SIZE) {
681
+ const firstKey = this.cache.keys().next().value;
682
+ if (firstKey !== undefined) {
683
+ this.cache.delete(firstKey);
684
+ }
685
+ }
686
+ this.cache.set(key, value);
687
+ }
688
+ clear() {
689
+ this.cache.clear();
690
+ }
691
+ constructor(){
692
+ this.cache = new Map();
693
+ }
694
+ }
695
+ // LRU cache for highlighted code to avoid re-calculating
696
+ const highlightCache = new LRUCache();
697
+ /**
698
+ * Highlight code using sugar-high
699
+ * Returns HTML string with token classes (sh__token--*)
700
+ * Results are cached for performance - same input produces same output
701
+ *
702
+ * SECURITY: sugar-high tokenizes code but does NOT escape HTML.
703
+ * Caller must escape user input BEFORE calling this function.
704
+ */ function highlightCode(code) {
705
+ if (!code) return '';
706
+ const cached = highlightCache.get(code);
707
+ if (cached) return cached;
708
+ const result = highlight(code);
709
+ highlightCache.set(code, result);
710
+ return result;
711
+ }
712
+ /**
713
+ * Clear the highlight cache (useful for testing or memory management)
714
+ */ function clearHighlightCache() {
715
+ highlightCache.clear();
716
+ }
717
+ /**
718
+ * Get language from file extension
719
+ * Useful for language-specific handling
720
+ */ const LANGUAGE_MAP = {
721
+ 'js': 'javascript',
722
+ 'jsx': 'javascript',
723
+ 'ts': 'typescript',
724
+ 'tsx': 'typescript',
725
+ 'py': 'python',
726
+ 'rs': 'rust',
727
+ 'go': 'go',
728
+ 'java': 'java',
729
+ 'c': 'c',
730
+ 'h': 'c',
731
+ 'cpp': 'cpp',
732
+ 'cc': 'cpp',
733
+ 'cxx': 'cpp',
734
+ 'hpp': 'cpp',
735
+ 'cs': 'csharp',
736
+ 'php': 'php',
737
+ 'rb': 'ruby',
738
+ 'swift': 'swift',
739
+ 'kt': 'kotlin',
740
+ 'scala': 'scala',
741
+ 'sh': 'shell',
742
+ 'bash': 'shell',
743
+ 'zsh': 'shell',
744
+ 'fish': 'shell',
745
+ 'json': 'json',
746
+ 'yaml': 'yaml',
747
+ 'yml': 'yaml',
748
+ 'toml': 'toml',
749
+ 'xml': 'xml',
750
+ 'html': 'html',
751
+ 'htm': 'html',
752
+ 'css': 'css',
753
+ 'scss': 'scss',
754
+ 'sass': 'sass',
755
+ 'md': 'markdown',
756
+ 'mdx': 'markdown',
757
+ 'sql': 'sql',
758
+ 'graphql': 'graphql',
759
+ 'gql': 'graphql',
760
+ 'vue': 'vue',
761
+ 'svelte': 'svelte'
762
+ };
763
+ function getLanguageFromPath(filePath) {
764
+ const ext = filePath.split('.').pop()?.toLowerCase();
765
+ return LANGUAGE_MAP[ext || ''] || 'text';
766
+ }
767
+
768
+ exports.clearHighlightCache = clearHighlightCache;
769
+ exports.getLanguageFromPath = getLanguageFromPath;
770
+ exports.highlightCode = highlightCode;