improvements for html serialize

This commit is contained in:
Simon Lamon
2022-02-02 06:27:01 +00:00
parent 6dc813bdff
commit 09a77998ba
2 changed files with 88 additions and 61 deletions
@@ -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<IHtmlSerializeOptions>): 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<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);
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 += '<html><head><meta name=\'generator\' content=\'xtermjs\'/>'
+ '<meta http-equiv=\'Content-Type\' content=\'text/html; charset=UTF-8\'/></head><body><!--StartFragment--><pre>';
// TODO: fetch options and remove hardcoded values
this._htmlContent += '<div style=\'color: #ffffff; background-color: #000000; font-family: Fira Code, courier-new, courier, monospace; font-size: 15px;\'>';
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 += '<div style=\'' + globalStyleDefinitions.join(' ') + '\'>';
}
protected _afterSerialize(): void {
+36 -45
View File
@@ -59,27 +59,27 @@ interface IDemoAddon<T extends AddonType> {
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<T> } = {
const addons: { [T in AddonType]: IDemoAddon<T>} = {
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.";
}
);
}
});
}