160 lines
4.9 KiB
JavaScript
160 lines
4.9 KiB
JavaScript
/* global module, require */
|
||
|
||
const {
|
||
buildLineCellRenderRuns,
|
||
createBlankCell,
|
||
createContinuationCell,
|
||
createTerminalCell,
|
||
measureCharDisplayColumns
|
||
} = require("./terminalCursorModel.js");
|
||
|
||
/**
|
||
* 终端快照样式做最小化存储:
|
||
* 1. 仅保留当前渲染真正需要的 fg/bg/bold/underline;
|
||
* 2. 同一份样式进入 style table 去重,line runs 只保留索引;
|
||
* 3. 这样比直接存整行 cell 更省空间,也避免恢复时回退成纯文本。
|
||
*/
|
||
function normalizeSnapshotStyle(style) {
|
||
const source = style && typeof style === "object" ? style : null;
|
||
if (!source) return null;
|
||
const fg = String(source.fg || "").trim();
|
||
const bg = String(source.bg || "").trim();
|
||
const bold = source.bold === true;
|
||
const underline = source.underline === true;
|
||
if (!fg && !bg && !bold && !underline) {
|
||
return null;
|
||
}
|
||
return {
|
||
fg,
|
||
bg,
|
||
bold,
|
||
underline
|
||
};
|
||
}
|
||
|
||
function buildSnapshotStyleSignature(style) {
|
||
const normalized = normalizeSnapshotStyle(style);
|
||
if (!normalized) return "";
|
||
return `${normalized.fg || ""}|${normalized.bg || ""}|${normalized.bold ? 1 : 0}|${normalized.underline ? 1 : 0}`;
|
||
}
|
||
|
||
function cloneSnapshotStyle(style) {
|
||
const normalized = normalizeSnapshotStyle(style);
|
||
return normalized ? { ...normalized } : null;
|
||
}
|
||
|
||
function measureTextDisplayColumns(text) {
|
||
const value = String(text || "");
|
||
if (!value) return 0;
|
||
let columns = 0;
|
||
for (let index = 0; index < value.length; ) {
|
||
const codePoint = value.codePointAt(index);
|
||
if (!Number.isFinite(codePoint)) break;
|
||
const ch = String.fromCodePoint(codePoint);
|
||
index += ch.length;
|
||
const width = measureCharDisplayColumns(ch);
|
||
if (width > 0) {
|
||
columns += width;
|
||
}
|
||
}
|
||
return columns;
|
||
}
|
||
|
||
function appendStyledTextCells(cells, text, style) {
|
||
const value = String(text || "");
|
||
if (!value) return;
|
||
for (let index = 0; index < value.length; ) {
|
||
const codePoint = value.codePointAt(index);
|
||
if (!Number.isFinite(codePoint)) break;
|
||
const ch = String.fromCodePoint(codePoint);
|
||
index += ch.length;
|
||
const width = measureCharDisplayColumns(ch);
|
||
if (width <= 0) {
|
||
for (let ownerIndex = cells.length - 1; ownerIndex >= 0; ownerIndex -= 1) {
|
||
if (cells[ownerIndex] && !cells[ownerIndex].continuation) {
|
||
cells[ownerIndex].text = `${cells[ownerIndex].text || ""}${ch}`;
|
||
break;
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
cells.push(createTerminalCell(ch, cloneSnapshotStyle(style), width));
|
||
for (let rest = width - 1; rest > 0; rest -= 1) {
|
||
cells.push(createContinuationCell(cloneSnapshotStyle(style)));
|
||
}
|
||
}
|
||
}
|
||
|
||
function serializeTerminalSnapshotRows(rowsInput) {
|
||
const rows = Array.isArray(rowsInput) ? rowsInput : [];
|
||
const styleTable = [];
|
||
const styleIndexBySignature = new Map();
|
||
const styledLines = rows.map((lineCells) => {
|
||
const runs = buildLineCellRenderRuns(Array.isArray(lineCells) ? lineCells : []);
|
||
return runs.map((run) => {
|
||
const entry = {};
|
||
const text = String((run && run.text) || "");
|
||
const columns = Math.max(0, Math.round(Number(run && run.columns) || 0));
|
||
if (text) {
|
||
entry.t = text;
|
||
}
|
||
if (columns > 0) {
|
||
entry.c = columns;
|
||
}
|
||
if (run && run.fixed) {
|
||
entry.f = 1;
|
||
}
|
||
const styleSignature = buildSnapshotStyleSignature(run && run.style);
|
||
if (styleSignature) {
|
||
let styleIndex = styleIndexBySignature.get(styleSignature);
|
||
if (!Number.isInteger(styleIndex)) {
|
||
styleIndex = styleTable.length;
|
||
styleIndexBySignature.set(styleSignature, styleIndex);
|
||
styleTable.push(cloneSnapshotStyle(run.style));
|
||
}
|
||
entry.s = styleIndex;
|
||
}
|
||
return entry;
|
||
});
|
||
});
|
||
return {
|
||
styleTable,
|
||
styledLines
|
||
};
|
||
}
|
||
|
||
function deserializeTerminalSnapshotRows(linesInput, styleTableInput) {
|
||
const styleTable = Array.isArray(styleTableInput) ? styleTableInput.map((style) => cloneSnapshotStyle(style)) : [];
|
||
const lines = Array.isArray(linesInput) ? linesInput : [];
|
||
return lines.map((lineRuns) => {
|
||
const cells = [];
|
||
const runs = Array.isArray(lineRuns) ? lineRuns : [];
|
||
runs.forEach((run) => {
|
||
const source = run && typeof run === "object" ? run : {};
|
||
const text = String(source.t || "");
|
||
const columns = Math.max(
|
||
0,
|
||
Math.round(Number(source.c !== undefined ? source.c : measureTextDisplayColumns(text)) || 0)
|
||
);
|
||
const styleIndex = Number(source.s);
|
||
const style =
|
||
Number.isInteger(styleIndex) && styleIndex >= 0 && styleIndex < styleTable.length
|
||
? styleTable[styleIndex]
|
||
: null;
|
||
if (!text) {
|
||
for (let index = 0; index < columns; index += 1) {
|
||
cells.push(createBlankCell(cloneSnapshotStyle(style)));
|
||
}
|
||
return;
|
||
}
|
||
appendStyledTextCells(cells, text, style);
|
||
});
|
||
return cells;
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
deserializeTerminalSnapshotRows,
|
||
serializeTerminalSnapshotRows
|
||
};
|