mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5528 from Tyriar/4528
Serialize rich underline styles
This commit is contained in:
@@ -112,6 +112,47 @@ describe('SerializeAddon', () => {
|
||||
}), 'world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('underline styles', () => {
|
||||
it('should serialize single underline with style', async () => {
|
||||
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize double underline', async () => {
|
||||
await writeP(terminal, sgr('4:2') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:2mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize curly underline', async () => {
|
||||
await writeP(terminal, sgr('4:3') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:3mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize dotted underline', async () => {
|
||||
await writeP(terminal, sgr('4:4') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:4mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize dashed underline', async () => {
|
||||
await writeP(terminal, sgr('4:5') + 'test' + sgr('24'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[4:5mtest\u001b[0m');
|
||||
});
|
||||
|
||||
it('should serialize underline with RGB color', async () => {
|
||||
await writeP(terminal, sgr('4', '58;2;255;128;64') + 'test' + sgr('24'));
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(result.includes('4:1'), result);
|
||||
assert.ok(result.includes('58:2::255:128:64'), result);
|
||||
});
|
||||
|
||||
it('should serialize underline with palette color', async () => {
|
||||
await writeP(terminal, sgr('4', '58;5;46') + 'test' + sgr('24'));
|
||||
const result = serializeAddon.serialize();
|
||||
assert.ok(result.includes('4:1'), result);
|
||||
assert.ok(result.includes('58:5:46'), result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
@@ -192,6 +233,50 @@ describe('SerializeAddon', () => {
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with double underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with curly underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dotted underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dashed underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline color (palette)', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4', '58;5;46') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.ok(output.includes('text-decoration: underline;'), output);
|
||||
assert.ok(output.includes('text-decoration-color: #00ff00;'), output);
|
||||
});
|
||||
|
||||
it('cells with underline color (RGB)', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4', '58;2;255;128;64') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.ok(output.includes('text-decoration: underline;'), output);
|
||||
assert.ok(output.includes('text-decoration-color: #ff8040;'), output);
|
||||
});
|
||||
|
||||
it('cells with invisible styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } fro
|
||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
@@ -82,10 +83,24 @@ function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boole
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
// If neither cell has underline, consider them equal regardless of internal underline color
|
||||
// values
|
||||
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
||||
return true;
|
||||
}
|
||||
const cell1Data = cell1 as unknown as IAttributeData;
|
||||
const cell2Data = cell2 as unknown as IAttributeData;
|
||||
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
|
||||
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
|
||||
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& equalUnderline(cell1, cell2)
|
||||
&& cell1.isOverline() === cell2.isOverline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
@@ -274,7 +289,27 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (flagsChanged) {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
|
||||
if (!equalUnderline(cell, oldCell)) {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
const style = cellData.getUnderlineStyle();
|
||||
if (style === UnderlineStyle.NONE) {
|
||||
sgrSeq.push(24);
|
||||
} else {
|
||||
// Use SGR 4:X format for underline styles
|
||||
sgrSeq.push('4:' + style as unknown as number);
|
||||
// Handle underline color
|
||||
if (!cellData.isUnderlineColorDefault()) {
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
||||
} else {
|
||||
sgrSeq.push('58:5:' + color as unknown as number);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cell.isUnderline() !== oldCell.isUnderline()) {
|
||||
sgrSeq.push(cell.isUnderline() ? 4 : 24);
|
||||
}
|
||||
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
@@ -627,6 +662,42 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
if (cellData.isUnderlineColorDefault()) {
|
||||
return undefined;
|
||||
}
|
||||
const color = cellData.getUnderlineColor();
|
||||
if (cellData.isUnderlineColorRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
}
|
||||
// Palette color
|
||||
return this._ansiColors[color].css;
|
||||
}
|
||||
|
||||
private _getUnderlineStyle(cell: IBufferCell): string {
|
||||
const cellData = cell as unknown as IAttributeData;
|
||||
switch (cellData.getUnderlineStyle()) {
|
||||
case UnderlineStyle.SINGLE:
|
||||
return 'underline';
|
||||
case UnderlineStyle.DOUBLE:
|
||||
return 'underline double';
|
||||
case UnderlineStyle.CURLY:
|
||||
return 'underline wavy';
|
||||
case UnderlineStyle.DOTTED:
|
||||
return 'underline dotted';
|
||||
case UnderlineStyle.DASHED:
|
||||
return 'underline dashed';
|
||||
default:
|
||||
return 'underline';
|
||||
}
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
@@ -647,14 +718,36 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
|
||||
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
|
||||
// Handle text-decoration (underline, overline, strikethrough, blink)
|
||||
const decorations: string[] = [];
|
||||
if (cell.isUnderline()) {
|
||||
decorations.push(this._getUnderlineStyle(cell));
|
||||
}
|
||||
if (cell.isOverline()) {
|
||||
decorations.push('overline');
|
||||
}
|
||||
if (cell.isStrikethrough()) {
|
||||
decorations.push('line-through');
|
||||
}
|
||||
if (cell.isBlink()) {
|
||||
decorations.push('blink');
|
||||
}
|
||||
if (decorations.length > 0) {
|
||||
content.push('text-decoration: ' + decorations.join(' ') + ';');
|
||||
}
|
||||
|
||||
// Handle underline color
|
||||
if (cell.isUnderline()) {
|
||||
const underlineColor = this._getUnderlineColor(cell);
|
||||
if (underlineColor) {
|
||||
content.push('text-decoration-color: ' + underlineColor + ';');
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user