@xterm/addon-serialize 0.15.0-beta.28 → 0.15.0-beta.281
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/src/SerializeAddon.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (cell.
|
|
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
|
}
|