@xterm/addon-serialize 0.15.0-beta.27 → 0.15.0-beta.271

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