From 6dc813bdff4abca8bea4c67bfb609f6a50e737ac Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Sat, 29 Jan 2022 18:44:32 +0000 Subject: [PATCH 01/11] html serialization --- .../src/SerializeAddon.ts | 159 +++++++++++++++++- .../xterm-addon-serialize/src/tsconfig.json | 6 + demo/client.ts | 75 ++++++--- demo/index.html | 4 + 4 files changed, 219 insertions(+), 25 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 24020b74..8a1c9659 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -6,7 +6,7 @@ */ import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; - +import { IColorSet } from 'browser/Types'; function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); @@ -406,6 +406,13 @@ export class SerializeAddon implements ITerminalAddon { return handler.serialize(maxRows - correctRows, maxRows); } + private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { + const maxRows = buffer.length; + const handler = new HTMLSerializeHandler(buffer, terminal); + const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return handler.serialize(maxRows - correctRows, maxRows); + } + private _serializeModes(terminal: Terminal): string { let content = ''; const modes = terminal.modes; @@ -460,6 +467,14 @@ export class SerializeAddon implements ITerminalAddon { return content; } + public htmlserialize(options?: IHtmlSerializeOptions): string { + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + + return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); + } + public dispose(): void { } } @@ -469,3 +484,145 @@ interface ISerializeOptions { excludeModes?: boolean; excludeAltBuffer?: boolean; } + +interface IHtmlSerializeOptions { + scrollback?: number; +} + +export class HTMLSerializeHandler extends BaseSerializeHandler { + private _currentRow: string = ''; + + private _htmlContent = ''; + + private _inverseStyle = 'color: #000000; background-color: #BFBFBF;'; + private _blinkStyle = 'text-decoration: blink;'; + private _boldStyle = 'font-weight: bold;'; + private _italicStyle = 'font-style: italic;'; + private _underlineStyle = 'text-decoration: underline;'; + private _strikethroughStyle = 'text-decoration: line-through;'; + private _invisibleStyle = 'visibility: hidden;'; + private _dimStyle = 'opacity: 0.5;'; + + private _colors: IColorSet; + + constructor( + buffer: IBuffer, + private readonly _terminal: Terminal + ) { + super(buffer); + + // https://github.com/xtermjs/xterm.js/issues/3601 + this._colors = (_terminal as any)._core._colorManager.colors; + } + + private _padStart(target: string, targetLength: number, padString: string): string { + targetLength = targetLength >> 0; + padString = String(typeof padString !== 'undefined' ? padString : ' '); + if (target.length > targetLength) { + return target; + } + + targetLength = targetLength - target.length; + if (targetLength > padString.length) { + padString += padString.repeat(targetLength / padString.length); + } + return padString.slice(0, targetLength) + target; + } + + protected _beforeSerialize(rows: number, start: number, end: number): void { + this._htmlContent += '' + + '
';
+    // TODO: fetch options and remove hardcoded values
+    this._htmlContent += '
'; + } + + protected _afterSerialize(): void { + this._htmlContent += '
'; + this._htmlContent += '
'; + } + + protected _rowEnd(row: number, isLastRow: boolean): void { + this._htmlContent += '' + this._currentRow + '
'; + this._currentRow = ''; + } + + private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined { + const color = isFg ? cell.getFgColor() : cell.getBgColor(); + if (isFg ? cell.isFgRGB() : cell.isBgRGB()) { + const rgb = [ + color >>> 0xFF0000 & 255, + color >>> 0xFF00 & 255, + color & 255 + ]; + return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join(''); + } + if (isFg ? cell.isFgPalette() : cell.isBgPalette()) { + return this._colors.ansi[color].css; + } + return undefined; + } + + private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined { + const content: string[] = []; + + const fgChanged = !equalFg(cell, oldCell); + const bgChanged = !equalBg(cell, oldCell); + const flagsChanged = !equalFlags(cell, oldCell); + + if (fgChanged || bgChanged || flagsChanged) { + const fgHexColor = this._getHexColor(cell, true); + if (fgHexColor) { + content.push('color: ' + fgHexColor + ';'); + } + + const bgHexColor = this._getHexColor(cell, false); + if (bgHexColor) { + content.push('background-color: ' + bgHexColor + ';'); + } + + if (cell.isInverse()) { content.push(this._inverseStyle); } + if (cell.isBold()) { content.push(this._boldStyle); } + if (cell.isUnderline()) { content.push(this._underlineStyle); } + if (cell.isBlink()) { content.push(this._blinkStyle); } + if (cell.isInvisible()) { content.push(this._invisibleStyle); } + if (cell.isItalic()) { content.push(this._italicStyle); } + if (cell.isDim()) { content.push(this._dimStyle); } + if (cell.isStrikethrough()) { content.push(this._strikethroughStyle); } + + return content; + } + + return undefined; + } + + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + // a width 0 cell don't need to be count because it is just a placeholder after a CJK character; + const isPlaceHolderCell = cell.getWidth() === 0; + if (isPlaceHolderCell) { + return; + } + + // this cell don't have content + const isEmptyCell = cell.getChars() === ''; + + const styleDefinitions = this._diffStyle(cell, oldCell); + + // handles style change + if (styleDefinitions) { + this._currentRow += styleDefinitions.length === 0 ? + `` : + ``; + } + + // handles actual content + if (isEmptyCell) { + this._currentRow += ' '; + } else { + this._currentRow += cell.getChars(); + } + } + + protected _serializeString(): string { + return this._htmlContent; + } +} diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json index ef4a14ab..38ef6a7f 100644 --- a/addons/xterm-addon-serialize/src/tsconfig.json +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -14,6 +14,9 @@ "paths": { "common/*": [ "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" ] }, "strict": true, @@ -28,6 +31,9 @@ "references": [ { "path": "../../../src/common" + }, + { + "path": "../../../src/browser" } ] } diff --git a/demo/client.ts b/demo/client.ts index 58d719a1..cc73cfa4 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -59,27 +59,27 @@ interface IDemoAddon { name: T; canChange: boolean; ctor: - T extends 'attach' ? typeof AttachAddon : - T extends 'fit' ? typeof FitAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'web-links' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - typeof WebglAddon; - instance?: - T extends 'attach' ? AttachAddon : - T extends 'fit' ? FitAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'web-links' ? WebLinksAddon : - T extends 'webgl' ? WebglAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - never; + T extends 'attach' ? typeof AttachAddon : + T extends 'fit' ? typeof FitAddon : + T extends 'search' ? typeof SearchAddon : + T extends 'serialize' ? typeof SerializeAddon : + T extends 'web-links' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + typeof WebglAddon; + instance?: + T extends 'attach' ? AttachAddon : + T extends 'fit' ? FitAddon : + T extends 'search' ? SearchAddon : + T extends 'serialize' ? SerializeAddon : + T extends 'web-links' ? WebLinksAddon : + T extends 'webgl' ? WebglAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + never; } -const addons: { [T in AddonType]: IDemoAddon} = { +const addons: { [T in AddonType]: IDemoAddon } = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, search: { name: 'search', ctor: SearchAddon, canChange: true }, @@ -147,6 +147,7 @@ if (document.location.pathname === '/test') { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); + document.getElementById('htmlserialize').addEventListener('click', htmlserializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); } @@ -186,7 +187,7 @@ function createTerminal(): void { const rows = size.rows; const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - fetch(url, {method: 'POST'}); + fetch(url, { method: 'POST' }); }); protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; @@ -216,7 +217,7 @@ function createTerminal(): void { // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => { + fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }).then((res) => { res.text().then((processId) => { pid = processId; socketURL += processId; @@ -261,7 +262,7 @@ function runFakeTerminal(): void { if (ev.keyCode === 13) { term.prompt(); } else if (ev.keyCode === 8) { - // Do not delete the prompt + // Do not delete the prompt if (term._core.buffer.x > 2) { term.write('\b \b'); } @@ -353,7 +354,7 @@ function initOptions(term: TerminalType): void { } else if (o === 'scrollSensitivity') { term.options.scrollSensitivity = parseFloat(input.value); updateTerminalSize(); - } else if(o === 'scrollback') { + } else if (o === 'scrollback') { term.options.scrollback = parseInt(input.value); setTimeout(() => updateTerminalSize(), 5); } else { @@ -380,7 +381,7 @@ function initAddons(term: TerminalType): void { if (!addon.canChange) { checkbox.disabled = true; } - if(name === 'unicode11' && checkbox.checked) { + if (name === 'unicode11' && checkbox.checked) { term.unicode.activeVersion = '11'; } addDomListener(checkbox, 'change', () => { @@ -447,6 +448,32 @@ function serializeButtonHandler(): void { } } +function htmlserializeButtonHandler(): void { + const output = addons.serialize.instance.htmlserialize(); + document.getElementById('htmlserialize-output').innerText = output; + + var type = "text/html"; + var blob = new Blob([output], { type }); + // @ts-ignore + var data = [new ClipboardItem({ [type]: blob })]; + + const permissionName = "clipboard-write" as PermissionName; + navigator.permissions.query({name: permissionName }).then(result => { + if (result.state == "granted" || result.state == "prompt") { + navigator.clipboard.write(data).then( + () => { + document.getElementById("htmlserialize-output-result").innerText + = "Copied to clipboard"; + }, + () => { + document.getElementById("htmlserialize-output-result").innerText + = "Can't copy to clipboard."; + } + ); + } + }); +} + function writeCustomGlyphHandler() { term.write('\n\r'); diff --git a/demo/index.html b/demo/index.html index 9c86783b..1ddce948 100644 --- a/demo/index.html +++ b/demo/index.html @@ -49,6 +49,10 @@
+ + + +
From 09a77998baed5f9c52206a3bda07010a52227b05 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 06:27:01 +0000 Subject: [PATCH 02/11] improvements for html serialize --- .../src/SerializeAddon.ts | 68 ++++++++++++---- demo/client.ts | 81 +++++++++---------- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 8a1c9659..cebd16cc 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -5,7 +5,7 @@ * (EXPERIMENTAL) This Addon is still under development */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm'; import { IColorSet } from 'browser/Types'; function constrain(value: number, low: number, high: number): number { @@ -19,18 +19,25 @@ abstract class BaseSerializeHandler { ) { } - public serialize(startRow: number, endRow: number): string { + public serialize(range: IBufferRange): string { // we need two of them to flip between old and new cell const cell1 = this._buffer.getNullCell(); const cell2 = this._buffer.getNullCell(); let oldCell = cell1; + const startRow = range.start.x; + const endRow = range.end.x; + const startColumn = range.start.y; + const endColumn = range.end.y; + this._beforeSerialize(endRow - startRow, startRow, endRow); - for (let row = startRow; row < endRow; row++) { + for (let row = startRow; row <= endRow; row++) { const line = this._buffer.getLine(row); if (line) { - for (let col = 0; col < line.length; col++) { + const startLineColumn = row !== range.start.x ? 0 : startColumn; + const endLineColumn = row !== range.end.x ? line.length : endColumn; + for (let col = startLineColumn; col < endLineColumn; col++) { const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); if (!c) { console.warn(`Can't get cell at row=${row}, col=${col}`); @@ -40,7 +47,7 @@ abstract class BaseSerializeHandler { oldCell = c; } } - this._rowEnd(row, row === endRow - 1); + this._rowEnd(row, row === endRow); } this._afterSerialize(); @@ -400,17 +407,37 @@ export class SerializeAddon implements ITerminalAddon { } private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length; + const maxRows = buffer.length - 1; const handler = new StringSerializeHandler(buffer, terminal); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); - return handler.serialize(maxRows - correctRows, maxRows); + return handler.serialize({ + start: { x: maxRows - correctRows, y: 0 }, + end: { x: maxRows, y: 0 } + }); } - private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length; + private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, options: Partial): string { const handler = new HTMLSerializeHandler(buffer, terminal); - const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); - return handler.serialize(maxRows - correctRows, maxRows); + const onlySelection = options.onlySelection ?? true; + if (!onlySelection) { + const maxRows = buffer.length; + const scrollback = options.scrollback; + const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return handler.serialize({ + start: { x: maxRows - correctRows, y: 0 }, + end: { x: maxRows, y: 0 } + }); + } + + const selection = this._terminal?.getSelectionPosition(); + if (selection !== undefined) { + return handler.serialize({ + start: { x: selection.startRow, y: selection.startColumn }, + end: { x: selection.endRow, y: selection.endColumn } + }); + } + + return ''; } private _serializeModes(terminal: Terminal): string { @@ -467,12 +494,12 @@ export class SerializeAddon implements ITerminalAddon { return content; } - public htmlserialize(options?: IHtmlSerializeOptions): string { + public htmlserialize(options?: Partial): string { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); + return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options || {}); } public dispose(): void { } @@ -486,7 +513,8 @@ interface ISerializeOptions { } interface IHtmlSerializeOptions { - scrollback?: number; + scrollback: number; + onlySelection: boolean; } export class HTMLSerializeHandler extends BaseSerializeHandler { @@ -532,8 +560,16 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { protected _beforeSerialize(rows: number, start: number, end: number): void { this._htmlContent += '' + '
';
-    // TODO: fetch options and remove hardcoded values
-    this._htmlContent += '
'; + + const foreground = this._terminal.options.theme?.foreground ?? '#ffffff'; + const background = this._terminal.options.theme?.background ?? '#000000'; + + const globalStyleDefinitions = []; + globalStyleDefinitions.push('color: ' + foreground + ';'); + globalStyleDefinitions.push('background-color: ' + background + ';'); + globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';'); + globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;'); + this._htmlContent += '
'; } protected _afterSerialize(): void { diff --git a/demo/client.ts b/demo/client.ts index cc73cfa4..81309c7b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -59,27 +59,27 @@ interface IDemoAddon { name: T; canChange: boolean; ctor: - T extends 'attach' ? typeof AttachAddon : - T extends 'fit' ? typeof FitAddon : - T extends 'search' ? typeof SearchAddon : - T extends 'serialize' ? typeof SerializeAddon : - T extends 'web-links' ? typeof WebLinksAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - typeof WebglAddon; - instance?: - T extends 'attach' ? AttachAddon : - T extends 'fit' ? FitAddon : - T extends 'search' ? SearchAddon : - T extends 'serialize' ? SerializeAddon : - T extends 'web-links' ? WebLinksAddon : - T extends 'webgl' ? WebglAddon : - T extends 'unicode11' ? typeof Unicode11Addon : - T extends 'ligatures' ? typeof LigaturesAddon : - never; + T extends 'attach' ? typeof AttachAddon : + T extends 'fit' ? typeof FitAddon : + T extends 'search' ? typeof SearchAddon : + T extends 'serialize' ? typeof SerializeAddon : + T extends 'web-links' ? typeof WebLinksAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + typeof WebglAddon; + instance?: + T extends 'attach' ? AttachAddon : + T extends 'fit' ? FitAddon : + T extends 'search' ? SearchAddon : + T extends 'serialize' ? SerializeAddon : + T extends 'web-links' ? WebLinksAddon : + T extends 'webgl' ? WebglAddon : + T extends 'unicode11' ? typeof Unicode11Addon : + T extends 'ligatures' ? typeof LigaturesAddon : + never; } -const addons: { [T in AddonType]: IDemoAddon } = { +const addons: { [T in AddonType]: IDemoAddon} = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, search: { name: 'search', ctor: SearchAddon, canChange: true }, @@ -147,7 +147,7 @@ if (document.location.pathname === '/test') { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); - document.getElementById('htmlserialize').addEventListener('click', htmlserializeButtonHandler); + document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); } @@ -187,7 +187,7 @@ function createTerminal(): void { const rows = size.rows; const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - fetch(url, { method: 'POST' }); + fetch(url, {method: 'POST'}); }); protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; @@ -217,7 +217,7 @@ function createTerminal(): void { // Set terminal size again to set the specific dimensions on the demo updateTerminalSize(); - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }).then((res) => { + fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => { res.text().then((processId) => { pid = processId; socketURL += processId; @@ -262,7 +262,7 @@ function runFakeTerminal(): void { if (ev.keyCode === 13) { term.prompt(); } else if (ev.keyCode === 8) { - // Do not delete the prompt + // Do not delete the prompt if (term._core.buffer.x > 2) { term.write('\b \b'); } @@ -354,7 +354,7 @@ function initOptions(term: TerminalType): void { } else if (o === 'scrollSensitivity') { term.options.scrollSensitivity = parseFloat(input.value); updateTerminalSize(); - } else if (o === 'scrollback') { + } else if(o === 'scrollback') { term.options.scrollback = parseInt(input.value); setTimeout(() => updateTerminalSize(), 5); } else { @@ -381,7 +381,7 @@ function initAddons(term: TerminalType): void { if (!addon.canChange) { checkbox.disabled = true; } - if (name === 'unicode11' && checkbox.checked) { + if(name === 'unicode11' && checkbox.checked) { term.unicode.activeVersion = '11'; } addDomListener(checkbox, 'change', () => { @@ -448,30 +448,21 @@ function serializeButtonHandler(): void { } } -function htmlserializeButtonHandler(): void { +function htmlSerializeButtonHandler(): void { const output = addons.serialize.instance.htmlserialize(); document.getElementById('htmlserialize-output').innerText = output; - var type = "text/html"; - var blob = new Blob([output], { type }); - // @ts-ignore - var data = [new ClipboardItem({ [type]: blob })]; - - const permissionName = "clipboard-write" as PermissionName; - navigator.permissions.query({name: permissionName }).then(result => { - if (result.state == "granted" || result.state == "prompt") { - navigator.clipboard.write(data).then( - () => { - document.getElementById("htmlserialize-output-result").innerText + // Deprecated, but the most supported for now. + function listener(e) { + e.clipboardData.setData("text/html", output); + e.clipboardData.setData("text/plain", output); + e.preventDefault(); + } + document.addEventListener("copy", listener); + document.execCommand("copy"); + document.removeEventListener("copy", listener); + document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard"; - }, - () => { - document.getElementById("htmlserialize-output-result").innerText - = "Can't copy to clipboard."; - } - ); - } - }); } From 9473777e4341085717e1ee8ad7b9a90579b476ef Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 17:18:35 +0000 Subject: [PATCH 03/11] Fix serialize tests --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index cebd16cc..bfbb8ab2 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -412,7 +412,7 @@ export class SerializeAddon implements ITerminalAddon { const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows, y: 0 } + end: { x: maxRows, y: terminal.cols } }); } From adedfed2bdb63c3c9343e480fd4329d75da179c4 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 17:44:26 +0000 Subject: [PATCH 04/11] Fix serialize tests --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index bfbb8ab2..c02c717e 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -407,7 +407,7 @@ export class SerializeAddon implements ITerminalAddon { } private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { - const maxRows = buffer.length - 1; + const maxRows = buffer.length; const handler = new StringSerializeHandler(buffer, terminal); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ @@ -425,7 +425,7 @@ export class SerializeAddon implements ITerminalAddon { const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows, y: 0 } + end: { x: maxRows, y: terminal.cols } }); } From bc9e6b49e88500c25d323178397b5e2717a2f87c Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 18:57:41 +0000 Subject: [PATCH 05/11] Fix serialize tests --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index c02c717e..e1db6f8e 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -412,7 +412,7 @@ export class SerializeAddon implements ITerminalAddon { const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows, y: terminal.cols } + end: { x: maxRows - 1, y: terminal.cols } }); } @@ -425,7 +425,7 @@ export class SerializeAddon implements ITerminalAddon { const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows, y: terminal.cols } + end: { x: maxRows - 1, y: terminal.cols } }); } From 93ac9efe92bf7dc2ca588b96494d30d3a50f38b7 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Wed, 2 Feb 2022 19:21:02 +0000 Subject: [PATCH 06/11] Use alternate buffer if alternate buffer is active --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index e1db6f8e..3da4ce9b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -416,7 +416,8 @@ export class SerializeAddon implements ITerminalAddon { }); } - private _htmlserializeBuffer(terminal: Terminal, buffer: IBuffer, options: Partial): string { + private _htmlserializeBuffer(terminal: Terminal, options: Partial): string { + const buffer = terminal.buffer.active; const handler = new HTMLSerializeHandler(buffer, terminal); const onlySelection = options.onlySelection ?? true; if (!onlySelection) { @@ -499,7 +500,7 @@ export class SerializeAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } - return this._htmlserializeBuffer(this._terminal, this._terminal.buffer.normal, options || {}); + return this._htmlserializeBuffer(this._terminal, options || {}); } public dispose(): void { } From 16257386b1668918316cf5afd796c49112233a0a Mon Sep 17 00:00:00 2001 From: silamon <32477463+silamon@users.noreply.github.com> Date: Thu, 3 Feb 2022 18:55:07 +0100 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 8 ++++---- demo/client.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 3da4ce9b..b7cb00a9 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -546,7 +546,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { private _padStart(target: string, targetLength: number, padString: string): string { targetLength = targetLength >> 0; - padString = String(typeof padString !== 'undefined' ? padString : ' '); + padString = padString ?? ' '; if (target.length > targetLength) { return target; } @@ -587,9 +587,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { const color = isFg ? cell.getFgColor() : cell.getBgColor(); if (isFg ? cell.isFgRGB() : cell.isBgRGB()) { const rgb = [ - color >>> 0xFF0000 & 255, - color >>> 0xFF00 & 255, - color & 255 + (color >> 16) & 255, + (color >> 8) & 255, + (color ) & 255 ]; return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join(''); } diff --git a/demo/client.ts b/demo/client.ts index 81309c7b..e6b2d385 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -461,8 +461,7 @@ function htmlSerializeButtonHandler(): void { document.addEventListener("copy", listener); document.execCommand("copy"); document.removeEventListener("copy", listener); - document.getElementById("htmlserialize-output-result").innerText - = "Copied to clipboard"; + document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard"; } From 4d70606febacd6bd1e9b2b8ec0d4cfc216661f9d Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Thu, 3 Feb 2022 18:00:16 +0000 Subject: [PATCH 08/11] Apply feedback from code review --- .../src/SerializeAddon.ts | 56 +++++++++---------- .../typings/xterm-addon-serialize.d.ts | 33 ++++++++++- demo/client.ts | 5 +- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b7cb00a9..20377cec 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -416,9 +416,9 @@ export class SerializeAddon implements ITerminalAddon { }); } - private _htmlserializeBuffer(terminal: Terminal, options: Partial): string { + private _serializeBufferAsHTML(terminal: Terminal, options: Partial): string { const buffer = terminal.buffer.active; - const handler = new HTMLSerializeHandler(buffer, terminal); + const handler = new HTMLSerializeHandler(buffer, terminal, options); const onlySelection = options.onlySelection ?? true; if (!onlySelection) { const maxRows = buffer.length; @@ -495,12 +495,12 @@ export class SerializeAddon implements ITerminalAddon { return content; } - public htmlserialize(options?: Partial): string { + public serializeAsHTML(options?: Partial): string { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - return this._htmlserializeBuffer(this._terminal, options || {}); + return this._serializeBufferAsHTML(this._terminal, options || {}); } public dispose(): void { } @@ -513,9 +513,10 @@ interface ISerializeOptions { excludeAltBuffer?: boolean; } -interface IHtmlSerializeOptions { +interface IHTMLSerializeOptions { scrollback: number; onlySelection: boolean; + includeGlobalBackground: boolean; } export class HTMLSerializeHandler extends BaseSerializeHandler { @@ -523,20 +524,12 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { private _htmlContent = ''; - private _inverseStyle = 'color: #000000; background-color: #BFBFBF;'; - private _blinkStyle = 'text-decoration: blink;'; - private _boldStyle = 'font-weight: bold;'; - private _italicStyle = 'font-style: italic;'; - private _underlineStyle = 'text-decoration: underline;'; - private _strikethroughStyle = 'text-decoration: line-through;'; - private _invisibleStyle = 'visibility: hidden;'; - private _dimStyle = 'opacity: 0.5;'; - private _colors: IColorSet; constructor( buffer: IBuffer, - private readonly _terminal: Terminal + private readonly _terminal: Terminal, + private readonly _options: Partial ) { super(buffer); @@ -559,11 +552,14 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { } protected _beforeSerialize(rows: number, start: number, end: number): void { - this._htmlContent += '' - + '
';
+    this._htmlContent += '
';
 
-    const foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
-    const background = this._terminal.options.theme?.background ?? '#000000';
+    let foreground = '#000000';
+    let background = '#ffffff';
+    if (this._options.includeGlobalBackground ?? true) {
+      foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
+      background = this._terminal.options.theme?.background ?? '#000000';
+    }
 
     const globalStyleDefinitions = [];
     globalStyleDefinitions.push('color: ' + foreground + ';');
@@ -579,7 +575,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
   }
 
   protected _rowEnd(row: number, isLastRow: boolean): void {
-    this._htmlContent += '' + this._currentRow + '
'; + this._htmlContent += '
' + this._currentRow + '
'; this._currentRow = ''; } @@ -617,14 +613,14 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { content.push('background-color: ' + bgHexColor + ';'); } - if (cell.isInverse()) { content.push(this._inverseStyle); } - if (cell.isBold()) { content.push(this._boldStyle); } - if (cell.isUnderline()) { content.push(this._underlineStyle); } - if (cell.isBlink()) { content.push(this._blinkStyle); } - if (cell.isInvisible()) { content.push(this._invisibleStyle); } - if (cell.isItalic()) { content.push(this._italicStyle); } - if (cell.isDim()) { content.push(this._dimStyle); } - if (cell.isStrikethrough()) { content.push(this._strikethroughStyle); } + if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); } + if (cell.isBold()) { content.push('font-weight: bold;'); } + if (cell.isUnderline()) { content.push('text-decoration: underline;'); } + if (cell.isBlink()) { content.push('text-decoration: blink;'); } + 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; } @@ -647,8 +643,8 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { // handles style change if (styleDefinitions) { this._currentRow += styleDefinitions.length === 0 ? - `` : - ``; + '' : + ''; } // handles actual content diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts index f50fc4de..63e2c206 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -24,14 +24,23 @@ declare module 'xterm-addon-serialize' { * the state. The cursor will also be positioned to the correct cell. When restoring a terminal * it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering * incomplete frames. - * + * * It's recommended that you write the serialized data into a terminal of the same size in which * it originated from and then resize it after if needed. - * + * * @param options Custom options to allow control over what gets serialized. */ public serialize(options?: ISerializeOptions): string; + /** + * Serializes terminal rows into a HTML string. The output of this function can be written + * to the OS clipboard. If an application supports pasting HTML, the content of the terminal + * is pasted with style options retained. + * + * @param options Custom options to allow control over what gets serialized. + */ + public serializeAsHTML(options?: Partial): string; + /** * Disposes the addon. */ @@ -56,4 +65,24 @@ declare module 'xterm-addon-serialize' { */ excludeAltBuffer?: boolean; } + + export interface IHTMLSerializeOptions { + /** + * The number of rows in the scrollback buffer to serialize, starting from the bottom of the + * scrollback buffer. When not specified, all available rows in the scrollback buffer will be + * serialized. + */ + scrollback: number; + + /** + * Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML. + * True by default. + */ + onlySelection: boolean; + + /** + * Whether to include the global background of the terminal. True by default. + */ + includeGlobalBackground: boolean; + } } diff --git a/demo/client.ts b/demo/client.ts index e6b2d385..1b3987bd 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -449,12 +449,11 @@ function serializeButtonHandler(): void { } function htmlSerializeButtonHandler(): void { - const output = addons.serialize.instance.htmlserialize(); + const output = addons.serialize.instance.serializeAsHTML(); document.getElementById('htmlserialize-output').innerText = output; // Deprecated, but the most supported for now. - function listener(e) { - e.clipboardData.setData("text/html", output); + function listener(e: any) { e.clipboardData.setData("text/plain", output); e.preventDefault(); } From 4c1722da2ca968763bfd4d2e708dca2f6e41f5c8 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Thu, 3 Feb 2022 18:30:28 +0000 Subject: [PATCH 09/11] fixup text/plain removal --- demo/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index 1b3987bd..5202db76 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -454,7 +454,7 @@ function htmlSerializeButtonHandler(): void { // Deprecated, but the most supported for now. function listener(e: any) { - e.clipboardData.setData("text/plain", output); + e.clipboardData.setData("text/html", output); e.preventDefault(); } document.addEventListener("copy", listener); From 4ca26f7e7bc2e52f3cc97eb8cd77d01678423046 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Sat, 12 Feb 2022 09:42:08 +0000 Subject: [PATCH 10/11] text improvements --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 4 ++-- .../typings/xterm-addon-serialize.d.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 20377cec..d48816c5 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -419,7 +419,7 @@ export class SerializeAddon implements ITerminalAddon { private _serializeBufferAsHTML(terminal: Terminal, options: Partial): string { const buffer = terminal.buffer.active; const handler = new HTMLSerializeHandler(buffer, terminal, options); - const onlySelection = options.onlySelection ?? true; + const onlySelection = options.onlySelection ?? false; if (!onlySelection) { const maxRows = buffer.length; const scrollback = options.scrollback; @@ -556,7 +556,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { let foreground = '#000000'; let background = '#ffffff'; - if (this._options.includeGlobalBackground ?? true) { + if (this._options.includeGlobalBackground ?? false) { foreground = this._terminal.options.theme?.foreground ?? '#ffffff'; background = this._terminal.options.theme?.background ?? '#000000'; } diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts index 63e2c206..4cb6283d 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -33,9 +33,9 @@ declare module 'xterm-addon-serialize' { public serialize(options?: ISerializeOptions): string; /** - * Serializes terminal rows into a HTML string. The output of this function can be written - * to the OS clipboard. If an application supports pasting HTML, the content of the terminal - * is pasted with style options retained. + * Serializes terminal content as HTML, which can be written to the clipboard using the + * `text/html` mimetype. For applications that support it, the pasted text should then retain + * its colors/styles. * * @param options Custom options to allow control over what gets serialized. */ @@ -70,18 +70,18 @@ declare module 'xterm-addon-serialize' { /** * The number of rows in the scrollback buffer to serialize, starting from the bottom of the * scrollback buffer. When not specified, all available rows in the scrollback buffer will be - * serialized. + * serialized. This setting is ignored if {@link IHTMLSerializeOptions.onlySelection} is true. */ scrollback: number; /** * Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML. - * True by default. + * False by default. */ onlySelection: boolean; /** - * Whether to include the global background of the terminal. True by default. + * Whether to include the global background of the terminal. False by default. */ includeGlobalBackground: boolean; } From 92a61e6506593c25e11adc393758ea730f0e3ed3 Mon Sep 17 00:00:00 2001 From: Simon Lamon Date: Sat, 19 Feb 2022 13:17:26 +0000 Subject: [PATCH 11/11] tests for serializeAsHTML --- .../src/SerializeAddon.test.ts | 205 ++++++++++++++++++ .../src/SerializeAddon.ts | 2 +- 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 addons/xterm-addon-serialize/src/SerializeAddon.test.ts diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts new file mode 100644 index 00000000..caa29053 --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { SerializeAddon } from './SerializeAddon'; +import { Terminal } from 'browser/public/Terminal'; +import { ColorManager } from 'browser/ColorManager'; +import { SelectionModel } from 'browser/selection/SelectionModel'; +import { IBufferService } from 'common/services/Services'; + +function sgr(...seq: string[]): string { + return `\x1b[${seq.join(';')}m`; +} + +function writeP(terminal: Terminal, data: string | Uint8Array): Promise { + return new Promise(r => terminal.write(data, r)); +} + +class TestSelectionService { + private _model: SelectionModel; + private _hasSelection: boolean = false; + + constructor( + bufferService: IBufferService + ) { + this._model = new SelectionModel(bufferService); + } + + public get model(): SelectionModel { return this._model; } + + public get hasSelection(): boolean { return this._hasSelection; } + + public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; } + public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; } + + public setSelection(col: number, row: number, length: number): void { + this._model.selectionStart = [col, row]; + this._model.selectionStartLength = length; + this._hasSelection = true; + } +} + +describe('xterm-addon-serialize html', () => { + let cm: ColorManager; + let dom: jsdom.JSDOM; + let document: Document; + let window: jsdom.DOMWindow; + + let serializeAddon: SerializeAddon; + let terminal: Terminal; + let selectionService: any; + + before(() => { + serializeAddon = new SerializeAddon(); + }); + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + window = dom.window; + document = window.document; + + (window as any).HTMLCanvasElement.prototype.getContext = () => ({ + createLinearGradient(): any { + return null; + }, + + fillRect(): void { }, + + getImageData(): any { + return { data: [0, 0, 0, 0xFF] }; + } + }); + + terminal = new Terminal({ cols: 10, rows: 2 }); + terminal.loadAddon(serializeAddon); + + selectionService = new TestSelectionService((terminal as any)._core._bufferService); + cm = new ColorManager(document, false); + (terminal as any)._core._colorManager = cm; + (terminal as any)._core._selectionService = selectionService; + }); + + it('empty terminal with selection turned off', () => { + const output = serializeAddon.serializeAsHTML(); + assert.notEqual(output, ''); + assert.equal((output.match(new RegExp('
{10}<\/div>', 'g')) || []).length, 2); + }); + + it('empty terminal with no selection', () => { + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal(output, ''); + }); + + it('basic terminal with selection', async () => { + await writeP(terminal, ' terminal '); + terminal.select(1, 0, 8); + + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal((output.match(new RegExp('
terminal<\/span><\/div>', 'g')) || []).length, 1, output); + }); + + it('cells with bold styling', async () => { + await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with italic styling', async () => { + await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with inverse styling', async () => { + await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with underline styling', async () => { + await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with invisible styling', async () => { + await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with dim styling', async () => { + await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with strikethrough styling', async () => { + await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with combined styling', async () => { + await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp(' <\/span>', 'g')) || []).length, 1, output); + assert.equal((output.match(new RegExp('termi<\/span>', 'g')) || []).length, 1, output); + assert.equal((output.match(new RegExp('nal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with color styling', async () => { + await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('cells with background styling', async () => { + await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); + }); + + it('empty terminal with default options', async () => { + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(new RegExp('color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output); + }); + + it('empty terminal with custom options', async () => { + terminal.options.fontFamily = 'verdana'; + terminal.options.fontSize = 20; + terminal.options.theme = { + foreground: '#ff00ff', + background: '#00ff00' + }; + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(new RegExp('color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;', 'g')) || []).length, 1, output); + }); + + it('empty terminal with background included', async () => { + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(new RegExp('color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output); + }); +}); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index d48816c5..25a42e65 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -575,7 +575,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { } protected _rowEnd(row: number, isLastRow: boolean): void { - this._htmlContent += '
' + this._currentRow + '
'; + this._htmlContent += '
' + this._currentRow + '
'; this._currentRow = ''; }