html serialization

This commit is contained in:
Simon Lamon
2022-01-29 19:49:57 +01:00
parent c8123d911e
commit 6dc813bdff
4 changed files with 219 additions and 25 deletions
@@ -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 += '<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;\'>';
}
protected _afterSerialize(): void {
this._htmlContent += '</div>';
this._htmlContent += '</pre><!--EndFragment--></body></html>';
}
protected _rowEnd(row: number, isLastRow: boolean): void {
this._htmlContent += '<span>' + this._currentRow + '</span><br/>';
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 ?
`</span><span>` :
`</span><span style='${styleDefinitions.join(' ')}'>`;
}
// handles actual content
if (isEmptyCell) {
this._currentRow += ' ';
} else {
this._currentRow += cell.getChars();
}
}
protected _serializeString(): string {
return this._htmlContent;
}
}