@xterm/addon-serialize 0.15.0-beta.17 → 0.15.0-beta.170

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.
@@ -9,6 +9,7 @@ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } fro
9
9
  import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
10
10
  import { IAttributeData, IColor } from 'common/Types';
11
11
  import { DEFAULT_ANSI_COLORS } from 'browser/Types';
12
+ import { UnderlineStyle } from 'common/buffer/Constants';
12
13
 
13
14
  function constrain(value: number, low: number, high: number): number {
14
15
  return Math.max(low, Math.min(value, high));
@@ -82,10 +83,32 @@ function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boole
82
83
  && cell1.getBgColor() === cell2.getBgColor();
83
84
  }
84
85
 
86
+ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
87
+ // If neither cell has underline, consider them equal regardless of internal underline color
88
+ // values
89
+ if (!cell1.isUnderline() && !cell2.isUnderline()) {
90
+ return true;
91
+ }
92
+ if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
93
+ return false;
94
+ }
95
+ const cell1Default = cell1.isUnderlineColorDefault();
96
+ const cell2Default = cell2.isUnderlineColorDefault();
97
+ if (cell1Default && cell2Default) {
98
+ return true;
99
+ }
100
+ if (cell1Default !== cell2Default) {
101
+ return false;
102
+ }
103
+ return cell1.getUnderlineColor() === cell2.getUnderlineColor()
104
+ && cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
105
+ }
106
+
85
107
  function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
86
108
  return cell1.isInverse() === cell2.isInverse()
87
109
  && cell1.isBold() === cell2.isBold()
88
110
  && cell1.isUnderline() === cell2.isUnderline()
111
+ && equalUnderline(cell1, cell2)
89
112
  && cell1.isOverline() === cell2.isOverline()
90
113
  && cell1.isBlink() === cell2.isBlink()
91
114
  && cell1.isInvisible() === cell2.isInvisible()
@@ -94,6 +117,16 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
94
117
  && cell1.isStrikethrough() === cell2.isStrikethrough();
95
118
  }
96
119
 
120
+ function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
121
+ const cell1AsBufferCell = cell1 as IBufferCell;
122
+ if (typeof cell1AsBufferCell.attributesEquals === 'function') {
123
+ return cell1AsBufferCell.attributesEquals(cell2);
124
+ }
125
+ return equalFg(cell1, cell2)
126
+ && equalBg(cell1, cell2)
127
+ && equalFlags(cell1, cell2);
128
+ }
129
+
97
130
  class StringSerializeHandler extends BaseSerializeHandler {
98
131
  private _rowIndex: number = 0;
99
132
  private _allRows: string[] = new Array<string>();
@@ -243,6 +276,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
243
276
 
244
277
  private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
245
278
  const sgrSeq: number[] = [];
279
+ if (attributesEquals(cell, oldCell)) {
280
+ return sgrSeq;
281
+ }
246
282
  const fgChanged = !equalFg(cell, oldCell);
247
283
  const bgChanged = !equalBg(cell, oldCell);
248
284
  const flagsChanged = !equalFlags(cell, oldCell);
@@ -274,7 +310,28 @@ class StringSerializeHandler extends BaseSerializeHandler {
274
310
  if (flagsChanged) {
275
311
  if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
276
312
  if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
277
- if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
313
+ if (!equalUnderline(cell, oldCell)) {
314
+ const style = cell.getUnderlineStyle();
315
+ if (style === UnderlineStyle.NONE) {
316
+ sgrSeq.push(24);
317
+ } else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
318
+ sgrSeq.push(4);
319
+ } else {
320
+ // Use SGR 4:X format for underline styles
321
+ sgrSeq.push('4:' + style as unknown as number);
322
+ // Handle underline color
323
+ if (!cell.isUnderlineColorDefault()) {
324
+ const color = cell.getUnderlineColor();
325
+ if (cell.isUnderlineColorRGB()) {
326
+ sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
327
+ } else {
328
+ sgrSeq.push('58:5:' + color as unknown as number);
329
+ }
330
+ }
331
+ }
332
+ } else if (cell.isUnderline() !== oldCell.isUnderline()) {
333
+ sgrSeq.push(cell.isUnderline() ? 4 : 24);
334
+ }
278
335
  if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
279
336
  if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
280
337
  if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
@@ -422,7 +479,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
422
479
  }
423
480
  }
424
481
 
425
- export class SerializeAddon implements ITerminalAddon , ISerializeApi {
482
+ export class SerializeAddon implements ITerminalAddon, ISerializeApi {
426
483
  private _terminal: Terminal | undefined;
427
484
 
428
485
  public activate(terminal: Terminal): void {
@@ -478,6 +535,25 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
478
535
  return '';
479
536
  }
480
537
 
538
+ /**
539
+ * Serializes the scroll region (DECSTBM) if it's not set to the full terminal size.
540
+ * Uses internal API access since scroll region is not exposed in the public API.
541
+ */
542
+ private _serializeScrollRegion(terminal: Terminal): string {
543
+ // HACK: Internal API access since scroll region is not exposed in the public API
544
+ const buffer = (terminal as any)._core.buffer;
545
+ const scrollTop: number = buffer.scrollTop;
546
+ const scrollBottom: number = buffer.scrollBottom;
547
+
548
+ // Only serialize if scroll region is not the default (full terminal size)
549
+ if (scrollTop !== 0 || scrollBottom !== terminal.rows - 1) {
550
+ // DECSTBM uses 1-based indices: CSI Ps ; Ps r
551
+ return `\x1b[${scrollTop + 1};${scrollBottom + 1}r`;
552
+ }
553
+
554
+ return '';
555
+ }
556
+
481
557
  private _serializeModes(terminal: Terminal): string {
482
558
  let content = '';
483
559
  const modes = terminal.modes;
@@ -505,6 +581,12 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
505
581
  }
506
582
  }
507
583
 
584
+ // Cursor visibility (DECTCEM)
585
+ // Default: visible
586
+ if (!modes.showCursor) {
587
+ content += '\x1b[?25l';
588
+ }
589
+
508
590
  return content;
509
591
  }
510
592
 
@@ -527,9 +609,10 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
527
609
  }
528
610
  }
529
611
 
530
- // Modes
612
+ // Modes and scroll region
531
613
  if (!options?.excludeModes) {
532
614
  content += this._serializeModes(this._terminal);
615
+ content += this._serializeScrollRegion(this._terminal);
533
616
  }
534
617
 
535
618
  return content;
@@ -540,7 +623,7 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
540
623
  throw new Error('Cannot use addon until it has been loaded');
541
624
  }
542
625
 
543
- return this._serializeBufferAsHTML(this._terminal, options || {});
626
+ return this._serializeBufferAsHTML(this._terminal, options ?? {});
544
627
  }
545
628
 
546
629
  public dispose(): void { }
@@ -569,20 +652,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
569
652
  }
570
653
  }
571
654
 
572
- private _padStart(target: string, targetLength: number, padString: string): string {
573
- targetLength = targetLength >> 0;
574
- padString = padString ?? ' ';
575
- if (target.length > targetLength) {
576
- return target;
577
- }
578
-
579
- targetLength -= target.length;
580
- if (targetLength > padString.length) {
581
- padString += padString.repeat(targetLength / padString.length);
582
- }
583
- return padString.slice(0, targetLength) + target;
584
- }
585
-
586
655
  protected _beforeSerialize(rows: number, start: number, end: number): void {
587
656
  this._htmlContent += '<html><body><!--StartFragment--><pre>';
588
657
 
@@ -619,7 +688,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
619
688
  (color >> 8) & 255,
620
689
  (color ) & 255
621
690
  ];
622
- return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
691
+ return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
623
692
  }
624
693
  if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
625
694
  return this._ansiColors[color].css;
@@ -627,9 +696,47 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
627
696
  return undefined;
628
697
  }
629
698
 
699
+ private _getUnderlineColor(cell: IBufferCell): string | undefined {
700
+ if (cell.isUnderlineColorDefault()) {
701
+ return undefined;
702
+ }
703
+ const color = cell.getUnderlineColor();
704
+ if (cell.isUnderlineColorRGB()) {
705
+ const rgb = [
706
+ (color >> 16) & 255,
707
+ (color >> 8) & 255,
708
+ (color ) & 255
709
+ ];
710
+ return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
711
+ }
712
+ // Palette color
713
+ return this._ansiColors[color].css;
714
+ }
715
+
716
+ private _getUnderlineStyle(cell: IBufferCell): string {
717
+ switch (cell.getUnderlineStyle()) {
718
+ case UnderlineStyle.SINGLE:
719
+ return 'underline';
720
+ case UnderlineStyle.DOUBLE:
721
+ return 'underline double';
722
+ case UnderlineStyle.CURLY:
723
+ return 'underline wavy';
724
+ case UnderlineStyle.DOTTED:
725
+ return 'underline dotted';
726
+ case UnderlineStyle.DASHED:
727
+ return 'underline dashed';
728
+ default:
729
+ return 'underline';
730
+ }
731
+ }
732
+
630
733
  private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
631
734
  const content: string[] = [];
632
735
 
736
+ if (attributesEquals(cell, oldCell)) {
737
+ return undefined;
738
+ }
739
+
633
740
  const fgChanged = !equalFg(cell, oldCell);
634
741
  const bgChanged = !equalBg(cell, oldCell);
635
742
  const flagsChanged = !equalFlags(cell, oldCell);
@@ -647,14 +754,36 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
647
754
 
648
755
  if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
649
756
  if (cell.isBold()) { content.push('font-weight: bold;'); }
650
- if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
651
- else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
652
- else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
653
- if (cell.isBlink()) { content.push('text-decoration: blink;'); }
757
+
758
+ // Handle text-decoration (underline, overline, strikethrough, blink)
759
+ const decorations: string[] = [];
760
+ if (cell.isUnderline()) {
761
+ decorations.push(this._getUnderlineStyle(cell));
762
+ }
763
+ if (cell.isOverline()) {
764
+ decorations.push('overline');
765
+ }
766
+ if (cell.isStrikethrough()) {
767
+ decorations.push('line-through');
768
+ }
769
+ if (cell.isBlink()) {
770
+ decorations.push('blink');
771
+ }
772
+ if (decorations.length > 0) {
773
+ content.push('text-decoration: ' + decorations.join(' ') + ';');
774
+ }
775
+
776
+ // Handle underline color
777
+ if (cell.isUnderline()) {
778
+ const underlineColor = this._getUnderlineColor(cell);
779
+ if (underlineColor) {
780
+ content.push('text-decoration-color: ' + underlineColor + ';');
781
+ }
782
+ }
783
+
654
784
  if (cell.isInvisible()) { content.push('visibility: hidden;'); }
655
785
  if (cell.isItalic()) { content.push('font-style: italic;'); }
656
786
  if (cell.isDim()) { content.push('opacity: 0.5;'); }
657
- if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
658
787
 
659
788
  return content;
660
789
  }