@oh-my-pi/pi-utils 18.7.0 → 18.8.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 (42) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/types/ar/index.d.ts +1 -0
  3. package/dist/types/ar/zip-package.d.ts +21 -0
  4. package/dist/types/docx/converter.d.ts +2 -0
  5. package/dist/types/dom/core.d.ts +15 -4
  6. package/dist/types/dom/parser.d.ts +5 -3
  7. package/dist/types/dom/selector.d.ts +7 -1
  8. package/dist/types/logger/rotating-file.d.ts +4 -2
  9. package/dist/types/marked/core.d.ts +7 -6
  10. package/dist/types/marked-list.d.ts +38 -0
  11. package/dist/types/math-delimiters.d.ts +10 -0
  12. package/dist/types/readability/types.d.ts +2 -0
  13. package/dist/types/turndown/types.d.ts +7 -0
  14. package/dist/types/vterm/buffer.d.ts +16 -5
  15. package/package.json +2 -2
  16. package/src/ar/index.ts +1 -0
  17. package/src/ar/zip-package.ts +57 -0
  18. package/src/chalk.ts +66 -23
  19. package/src/docx/converter.ts +26 -12
  20. package/src/dom/core.ts +354 -111
  21. package/src/dom/parser.ts +42 -19
  22. package/src/dom/selector.ts +328 -95
  23. package/src/fetch-retry.ts +2 -0
  24. package/src/json-parse.ts +21 -6
  25. package/src/logger/rotating-file.ts +5 -3
  26. package/src/logger.ts +39 -23
  27. package/src/marked/core.ts +42 -18
  28. package/src/marked-list.ts +73 -0
  29. package/src/math-delimiters.ts +16 -0
  30. package/src/prompt.ts +3 -1
  31. package/src/ptree.ts +28 -35
  32. package/src/readability/readability.ts +349 -39
  33. package/src/readability/types.ts +2 -0
  34. package/src/stream.ts +34 -13
  35. package/src/temp.ts +1 -10
  36. package/src/template.ts +7 -4
  37. package/src/turndown/gfm.ts +16 -5
  38. package/src/turndown/html.ts +28 -8
  39. package/src/turndown/service.ts +43 -15
  40. package/src/turndown/types.ts +7 -0
  41. package/src/vterm/buffer.ts +31 -11
  42. package/src/vterm/terminal.ts +185 -92
@@ -2,10 +2,10 @@ import {
2
2
  BufferLine,
3
3
  type BufferState,
4
4
  BufferView,
5
- blankCell,
6
5
  type CellAttributes,
7
6
  type CellData,
8
- defaultAttributes,
7
+ DEFAULT_CELL_ATTRIBUTES,
8
+ sharedBlankCell,
9
9
  } from "./buffer";
10
10
 
11
11
  const segmenter = new Intl.Segmenter();
@@ -39,7 +39,7 @@ export interface TerminalBuffers {
39
39
  interface SavedCursor {
40
40
  x: number;
41
41
  y: number;
42
- attrs: CellAttributes;
42
+ attrs: Readonly<CellAttributes>;
43
43
  }
44
44
 
45
45
  function createState(columns: number, rows: number): BufferState {
@@ -52,8 +52,36 @@ function createState(columns: number, rows: number): BufferState {
52
52
  };
53
53
  }
54
54
 
55
- function cloneCell(cell: CellData): CellData {
56
- return { chars: cell.chars, width: cell.width, attrs: { ...cell.attrs } };
55
+ function sameAttributes(a: Readonly<CellAttributes>, b: Readonly<CellAttributes>): boolean {
56
+ return (
57
+ a.bold === b.bold &&
58
+ a.dim === b.dim &&
59
+ a.italic === b.italic &&
60
+ a.underline === b.underline &&
61
+ a.inverse === b.inverse &&
62
+ a.strikethrough === b.strikethrough &&
63
+ a.overline === b.overline &&
64
+ a.fgMode === b.fgMode &&
65
+ a.fg === b.fg &&
66
+ a.bgMode === b.bgMode &&
67
+ a.bg === b.bg
68
+ );
69
+ }
70
+
71
+ /** Parses CSI parameters (`;`/`:` separated); null when the body holds anything but digits. */
72
+ function parseCsiParams(body: string): number[] | null {
73
+ const params: number[] = [];
74
+ let value = 0;
75
+ for (let index = 0; index < body.length; index++) {
76
+ const code = body.charCodeAt(index);
77
+ if (code >= 0x30 && code <= 0x39) value = value * 10 + (code - 0x30);
78
+ else if (code === 0x3b || code === 0x3a) {
79
+ params.push(value);
80
+ value = 0;
81
+ } else return null;
82
+ }
83
+ params.push(value);
84
+ return params;
57
85
  }
58
86
 
59
87
  /** Behavior-compatible reimplementation of @xterm/headless's used surface. */
@@ -72,7 +100,12 @@ export class Terminal {
72
100
  #alternate: BufferState;
73
101
  #active: BufferState;
74
102
  #usingAlternate = false;
75
- #attrs = defaultAttributes();
103
+ // Renditions are immutable and shared by every cell printed with them;
104
+ // SGR builds a new frozen object instead of mutating this one.
105
+ #attrs: Readonly<CellAttributes> = DEFAULT_CELL_ATTRIBUTES;
106
+ // Frozen printable-ASCII cells for #attrs, indexed by char code - 0x20.
107
+ #asciiCells: CellData[] = [];
108
+ #asciiCellsAttrs: Readonly<CellAttributes> | undefined;
76
109
  #scrollTop = 0;
77
110
  #scrollBottom: number;
78
111
  #originMode = false;
@@ -149,13 +182,30 @@ export class Terminal {
149
182
  }
150
183
 
151
184
  #parse(text: string): void {
152
- let printable = "";
153
- const flush = () => {
154
- if (!printable) return;
155
- for (const part of segmenter.segment(printable)) this.#print(part.segment);
156
- printable = "";
157
- };
158
- for (const char of text) {
185
+ const length = text.length;
186
+ let index = 0;
187
+ while (index < length) {
188
+ if (this.#state === "ground") {
189
+ let end = index;
190
+ while (end < length) {
191
+ const code = text.charCodeAt(end);
192
+ if (code < 0x20 || code === 0x7f) break;
193
+ end++;
194
+ }
195
+ if (end > index) {
196
+ this.#printRun(text, index, end);
197
+ index = end;
198
+ continue;
199
+ }
200
+ const char = text[index]!;
201
+ index++;
202
+ if (char === "\x1b") this.#state = "escape";
203
+ else this.#handleControl(char);
204
+ continue;
205
+ }
206
+ const code = text.codePointAt(index)!;
207
+ const char = code > 0xffff ? text.slice(index, index + 2) : text[index]!;
208
+ index += char.length;
159
209
  if (this.#state === "string") {
160
210
  if (this.#stringEscape && char === "\\") {
161
211
  this.#state = "ground";
@@ -172,30 +222,41 @@ export class Terminal {
172
222
  this.#handleEscape(char);
173
223
  continue;
174
224
  }
175
- if (this.#state === "csi") {
176
- if (char >= "@" && char <= "~") {
177
- this.#handleCsi(this.#sequence, char);
178
- this.#sequence = "";
179
- this.#state = "ground";
180
- } else if (char === "\x1b") {
181
- this.#state = "escape";
182
- this.#sequence = "";
183
- } else {
184
- this.#sequence += char;
185
- }
186
- continue;
187
- }
188
- if (char === "\x1b") {
189
- flush();
225
+ if (char >= "@" && char <= "~") {
226
+ this.#handleCsi(this.#sequence, char);
227
+ this.#sequence = "";
228
+ this.#state = "ground";
229
+ } else if (char === "\x1b") {
190
230
  this.#state = "escape";
191
- } else if (char < " " || char === "\x7f") {
192
- flush();
193
- this.#handleControl(char);
231
+ this.#sequence = "";
194
232
  } else {
195
- printable += char;
233
+ this.#sequence += char;
234
+ }
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Prints a run of non-control text. Printable ASCII is one cell wide and
240
+ * every boundary between two ASCII characters is a grapheme break, so ASCII
241
+ * stretches skip segmentation; the last ASCII character before non-ASCII text
242
+ * may start a cluster (combining mark, keycap) and goes to the segmenter.
243
+ */
244
+ #printRun(text: string, start: number, end: number): void {
245
+ let index = start;
246
+ while (index < end) {
247
+ let asciiEnd = index;
248
+ while (asciiEnd < end && text.charCodeAt(asciiEnd) < 0x80) asciiEnd++;
249
+ if (asciiEnd < end && asciiEnd > index) asciiEnd--;
250
+ for (; index < asciiEnd; index++) this.#putAscii(text.charCodeAt(index));
251
+ if (index >= end) return;
252
+ // Segment up to the first break between two ASCII characters.
253
+ let clusterEnd = index + 1;
254
+ while (clusterEnd < end && !(text.charCodeAt(clusterEnd) < 0x80 && text.charCodeAt(clusterEnd - 1) < 0x80)) {
255
+ clusterEnd++;
196
256
  }
257
+ for (const part of segmenter.segment(text.slice(index, clusterEnd))) this.#print(part.segment);
258
+ index = clusterEnd;
197
259
  }
198
- flush();
199
260
  }
200
261
 
201
262
  #handleControl(char: string): void {
@@ -262,10 +323,12 @@ export class Terminal {
262
323
  const privateMode = raw.startsWith("?");
263
324
  const greater = raw.startsWith(">");
264
325
  const body = privateMode || greater ? raw.slice(1) : raw;
265
- const params = body
266
- .replaceAll(":", ";")
267
- .split(";")
268
- .map(value => (value === "" ? 0 : Number(value)));
326
+ const params =
327
+ parseCsiParams(body) ??
328
+ body
329
+ .replaceAll(":", ";")
330
+ .split(";")
331
+ .map(value => (value === "" ? 0 : Number(value)));
269
332
  const first = params[0] ?? 0;
270
333
  const amount = Math.max(1, first);
271
334
  switch (final) {
@@ -359,12 +422,30 @@ export class Terminal {
359
422
  }
360
423
 
361
424
  #print(grapheme: string): void {
362
- let width = Bun.stringWidth(grapheme);
425
+ const width = Bun.stringWidth(grapheme);
363
426
  if (width === 0) {
364
427
  this.#appendCombining(grapheme);
365
428
  return;
366
429
  }
367
- width = Math.min(2, width);
430
+ const cellWidth = Math.min(2, width);
431
+ this.#placeCell(Object.freeze({ chars: grapheme, width: cellWidth, attrs: this.#attrs }), cellWidth);
432
+ }
433
+
434
+ #putAscii(code: number): void {
435
+ if (this.#asciiCellsAttrs !== this.#attrs) {
436
+ this.#asciiCells = [];
437
+ this.#asciiCellsAttrs = this.#attrs;
438
+ }
439
+ const slot = code - 0x20;
440
+ let cell = this.#asciiCells[slot];
441
+ if (!cell) {
442
+ cell = Object.freeze({ chars: String.fromCharCode(code), width: 1, attrs: this.#attrs });
443
+ this.#asciiCells[slot] = cell;
444
+ }
445
+ this.#placeCell(cell, 1);
446
+ }
447
+
448
+ #placeCell(cell: CellData, width: number): void {
368
449
  if (this.#pendingWrap || (width === 2 && this.#active.cursorX === this.cols - 1)) {
369
450
  if (!this.#autowrap) {
370
451
  this.#active.cursorX = this.cols - width;
@@ -377,9 +458,9 @@ export class Terminal {
377
458
  const column = Math.min(this.#active.cursorX, this.cols - 1);
378
459
  if (this.#insertMode) this.#shiftCells(line, column, width);
379
460
  this.#clearWideAt(line, column);
380
- line.cells[column] = { chars: grapheme, width, attrs: { ...this.#attrs } };
461
+ line.cells[column] = cell;
381
462
  if (width === 2 && column + 1 < this.cols) {
382
- line.cells[column + 1] = { chars: "", width: 0, attrs: { ...this.#attrs } };
463
+ line.cells[column + 1] = Object.freeze({ chars: "", width: 0, attrs: cell.attrs });
383
464
  }
384
465
  this.#active.cursorX = column + width;
385
466
  this.#pendingWrap = this.#active.cursorX >= this.cols;
@@ -396,7 +477,9 @@ export class Terminal {
396
477
  if (!line) return;
397
478
  while (column >= 0 && line.cells[column]?.width === 0) column--;
398
479
  const cell = line.cells[column];
399
- if (cell?.chars) cell.chars += mark;
480
+ // Cells are frozen and may be shared; replace instead of mutating.
481
+ if (cell?.chars)
482
+ line.cells[column] = Object.freeze({ chars: cell.chars + mark, width: cell.width, attrs: cell.attrs });
400
483
  }
401
484
 
402
485
  #lineFeed(wrapped: boolean): void {
@@ -489,8 +572,8 @@ export class Terminal {
489
572
  }
490
573
 
491
574
  #eraseRange(line: BufferLine, start: number, end: number): void {
492
- for (let column = Math.max(0, start); column < Math.min(this.cols, end); column++)
493
- line.cells[column] = blankCell(this.#attrs);
575
+ const blank = sharedBlankCell(this.#attrs);
576
+ for (let column = Math.max(0, start); column < Math.min(this.cols, end); column++) line.cells[column] = blank;
494
577
  this.#repairWideCells(line);
495
578
  }
496
579
 
@@ -521,14 +604,18 @@ export class Terminal {
521
604
  }
522
605
 
523
606
  #shiftCells(line: BufferLine, column: number, count: number): void {
524
- line.cells.splice(column, 0, ...Array.from({ length: count }, () => blankCell(this.#attrs)));
607
+ // Inserting more than a row's worth only pushes everything past the edge.
608
+ const blank = sharedBlankCell(this.#attrs);
609
+ const blanks = Array.from({ length: Math.min(count, this.cols) }, () => blank);
610
+ line.cells.splice(column, 0, ...blanks);
525
611
  line.cells.length = this.cols;
526
612
  }
527
613
 
528
614
  #deleteCells(count: number): void {
529
615
  const line = this.#currentLine();
530
616
  line.cells.splice(this.#active.cursorX, count);
531
- while (line.cells.length < this.cols) line.cells.push(blankCell(this.#attrs));
617
+ const blank = sharedBlankCell(this.#attrs);
618
+ while (line.cells.length < this.cols) line.cells.push(blank);
532
619
  this.#repairWideCells(line);
533
620
  }
534
621
 
@@ -537,64 +624,67 @@ export class Terminal {
537
624
  }
538
625
 
539
626
  #clearWideAt(line: BufferLine, column: number): void {
540
- if (line.cells[column]?.width === 0 && column > 0) line.cells[column - 1] = blankCell(this.#attrs);
541
- if (line.cells[column]?.width === 2 && column + 1 < this.cols) line.cells[column + 1] = blankCell(this.#attrs);
627
+ const width = line.cells[column]?.width;
628
+ if (width === 0 && column > 0) line.cells[column - 1] = sharedBlankCell(this.#attrs);
629
+ if (width === 2 && column + 1 < this.cols) line.cells[column + 1] = sharedBlankCell(this.#attrs);
542
630
  }
543
631
 
544
632
  #repairWideCells(line: BufferLine): void {
545
633
  for (let column = 0; column < this.cols; column++) {
546
634
  const cell = line.cells[column]!;
547
635
  if (cell.width === 0 && (column === 0 || line.cells[column - 1]?.width !== 2))
548
- line.cells[column] = blankCell(this.#attrs);
636
+ line.cells[column] = sharedBlankCell(this.#attrs);
549
637
  if (cell.width === 2 && (column === this.cols - 1 || line.cells[column + 1]?.width !== 0))
550
- line.cells[column] = blankCell(this.#attrs);
638
+ line.cells[column] = sharedBlankCell(this.#attrs);
551
639
  }
552
640
  }
553
641
 
554
642
  #setRendition(values: number[]): void {
555
643
  const params = values.length === 0 ? [0] : values;
644
+ // Copy-on-write: cells keep referencing the previous rendition object.
645
+ const attrs: CellAttributes = { ...this.#attrs };
556
646
  for (let index = 0; index < params.length; index++) {
557
647
  const code = params[index] ?? 0;
558
- if (code === 0) this.#attrs = defaultAttributes();
559
- else if (code === 1) this.#attrs.bold = true;
560
- else if (code === 2) this.#attrs.dim = true;
561
- else if (code === 3) this.#attrs.italic = true;
562
- else if (code === 4 || code === 21) this.#attrs.underline = true;
563
- else if (code === 7) this.#attrs.inverse = true;
564
- else if (code === 9) this.#attrs.strikethrough = true;
648
+ if (code === 0) Object.assign(attrs, DEFAULT_CELL_ATTRIBUTES);
649
+ else if (code === 1) attrs.bold = true;
650
+ else if (code === 2) attrs.dim = true;
651
+ else if (code === 3) attrs.italic = true;
652
+ else if (code === 4 || code === 21) attrs.underline = true;
653
+ else if (code === 7) attrs.inverse = true;
654
+ else if (code === 9) attrs.strikethrough = true;
565
655
  else if (code === 22) {
566
- this.#attrs.bold = false;
567
- this.#attrs.dim = false;
568
- } else if (code === 23) this.#attrs.italic = false;
569
- else if (code === 24) this.#attrs.underline = false;
570
- else if (code === 27) this.#attrs.inverse = false;
571
- else if (code === 29) this.#attrs.strikethrough = false;
572
- else if (code === 53) this.#attrs.overline = true;
573
- else if (code === 55) this.#attrs.overline = false;
656
+ attrs.bold = false;
657
+ attrs.dim = false;
658
+ } else if (code === 23) attrs.italic = false;
659
+ else if (code === 24) attrs.underline = false;
660
+ else if (code === 27) attrs.inverse = false;
661
+ else if (code === 29) attrs.strikethrough = false;
662
+ else if (code === 53) attrs.overline = true;
663
+ else if (code === 55) attrs.overline = false;
574
664
  else if (code >= 30 && code <= 37) {
575
- this.#attrs.fgMode = 1;
576
- this.#attrs.fg = code - 30;
665
+ attrs.fgMode = 1;
666
+ attrs.fg = code - 30;
577
667
  } else if (code >= 40 && code <= 47) {
578
- this.#attrs.bgMode = 1;
579
- this.#attrs.bg = code - 40;
668
+ attrs.bgMode = 1;
669
+ attrs.bg = code - 40;
580
670
  } else if (code >= 90 && code <= 97) {
581
- this.#attrs.fgMode = 1;
582
- this.#attrs.fg = code - 82;
671
+ attrs.fgMode = 1;
672
+ attrs.fg = code - 82;
583
673
  } else if (code >= 100 && code <= 107) {
584
- this.#attrs.bgMode = 1;
585
- this.#attrs.bg = code - 92;
586
- } else if (code === 39) this.#attrs.fgMode = 0;
587
- else if (code === 49) this.#attrs.bgMode = 0;
674
+ attrs.bgMode = 1;
675
+ attrs.bg = code - 92;
676
+ } else if (code === 39) attrs.fgMode = 0;
677
+ else if (code === 49) attrs.bgMode = 0;
588
678
  else if (code === 38 || code === 48) {
589
679
  const foreground = code === 38;
590
680
  const mode = params[index + 1];
591
681
  if (mode === 5 && params[index + 2] !== undefined) {
592
682
  if (foreground) {
593
- this.#attrs.fgMode = 1;
594
- this.#attrs.fg = params[index + 2]! & 0xff;
683
+ attrs.fgMode = 1;
684
+ attrs.fg = params[index + 2]! & 0xff;
595
685
  } else {
596
- this.#attrs.bgMode = 1;
597
- this.#attrs.bg = params[index + 2]! & 0xff;
686
+ attrs.bgMode = 1;
687
+ attrs.bg = params[index + 2]! & 0xff;
598
688
  }
599
689
  index += 2;
600
690
  } else if (mode === 2 && params[index + 4] !== undefined) {
@@ -603,16 +693,18 @@ export class Terminal {
603
693
  ((params[index + 3]! & 0xff) << 8) |
604
694
  (params[index + 4]! & 0xff);
605
695
  if (foreground) {
606
- this.#attrs.fgMode = 2;
607
- this.#attrs.fg = color;
696
+ attrs.fgMode = 2;
697
+ attrs.fg = color;
608
698
  } else {
609
- this.#attrs.bgMode = 2;
610
- this.#attrs.bg = color;
699
+ attrs.bgMode = 2;
700
+ attrs.bg = color;
611
701
  }
612
702
  index += 4;
613
703
  }
614
704
  }
615
705
  }
706
+ if (sameAttributes(attrs, this.#attrs)) return;
707
+ this.#attrs = sameAttributes(attrs, DEFAULT_CELL_ATTRIBUTES) ? DEFAULT_CELL_ATTRIBUTES : Object.freeze(attrs);
616
708
  }
617
709
 
618
710
  #setScrollRegion(params: number[]): void {
@@ -643,21 +735,20 @@ export class Terminal {
643
735
  }
644
736
 
645
737
  #saveCursor(): void {
646
- this.#saved = { x: this.#active.cursorX, y: this.#active.cursorY, attrs: { ...this.#attrs } };
738
+ this.#saved = { x: this.#active.cursorX, y: this.#active.cursorY, attrs: this.#attrs };
647
739
  }
648
740
 
649
741
  #restoreCursor(): void {
650
742
  if (!this.#saved) return;
651
743
  this.#active.cursorX = Math.min(this.cols - 1, this.#saved.x);
652
744
  this.#active.cursorY = Math.min(this.rows - 1, this.#saved.y);
653
- this.#attrs = { ...this.#saved.attrs };
745
+ this.#attrs = this.#saved.attrs;
654
746
  this.#pendingWrap = false;
655
747
  }
656
748
 
657
749
  #enterAlternate(saveCursor: boolean): void {
658
750
  if (this.#usingAlternate) return;
659
- if (saveCursor)
660
- this.#alternateSaved = { x: this.#normal.cursorX, y: this.#normal.cursorY, attrs: { ...this.#attrs } };
751
+ if (saveCursor) this.#alternateSaved = { x: this.#normal.cursorX, y: this.#normal.cursorY, attrs: this.#attrs };
661
752
  this.#alternate = createState(this.cols, this.rows);
662
753
  this.#active = this.#alternate;
663
754
  this.#usingAlternate = true;
@@ -673,7 +764,7 @@ export class Terminal {
673
764
  if (restoreCursor && this.#alternateSaved) {
674
765
  this.#normal.cursorX = this.#alternateSaved.x;
675
766
  this.#normal.cursorY = this.#alternateSaved.y;
676
- this.#attrs = { ...this.#alternateSaved.attrs };
767
+ this.#attrs = this.#alternateSaved.attrs;
677
768
  }
678
769
  this.#scrollTop = 0;
679
770
  this.#scrollBottom = this.rows - 1;
@@ -681,7 +772,7 @@ export class Terminal {
681
772
  }
682
773
 
683
774
  #reset(): void {
684
- this.#attrs = defaultAttributes();
775
+ this.#attrs = DEFAULT_CELL_ATTRIBUTES;
685
776
  this.#normal = createState(this.cols, this.rows);
686
777
  this.#alternate = createState(this.cols, this.rows);
687
778
  this.#usingAlternate = false;
@@ -704,7 +795,8 @@ export class Terminal {
704
795
  const group = groups.at(-1)!;
705
796
  const used = line.isWrapped || state.lines[row + 1]?.isWrapped ? line.cells.length : this.#usedColumns(line);
706
797
  if (row === absoluteCursor) group.cursorOffset = group.cells.length + state.cursorX;
707
- for (let column = 0; column < used; column++) group.cells.push(cloneCell(line.cells[column]!));
798
+ // Cells are immutable, so the reflowed grid reuses them.
799
+ for (let column = 0; column < used; column++) group.cells.push(line.cells[column]!);
708
800
  }
709
801
  const lines: BufferLine[] = [];
710
802
  let cursorAbsolute = 0;
@@ -725,8 +817,9 @@ export class Terminal {
725
817
  continue;
726
818
  }
727
819
  if (cell.width === 2 && target === columns - 1) break;
728
- line.cells[target] = cloneCell(cell);
729
- if (cell.width === 2) line.cells[target + 1] = { chars: "", width: 0, attrs: { ...cell.attrs } };
820
+ line.cells[target] = cell;
821
+ if (cell.width === 2)
822
+ line.cells[target + 1] = Object.freeze({ chars: "", width: 0, attrs: cell.attrs });
730
823
  target += cell.width;
731
824
  source += cell.width;
732
825
  }