Trim empty rows
This commit is contained in:
parent
ab61445bcc
commit
1b72d1e4e4
2 changed files with 143 additions and 2 deletions
|
|
@ -97,4 +97,71 @@ describe("terminal", () => {
|
|||
expect(html).toMatch(/<span|<div|Test content/);
|
||||
disposeTerminal(term);
|
||||
});
|
||||
|
||||
test("trims empty trailing rows from HTML output", async () => {
|
||||
const term = createTerminal(80, 24);
|
||||
await writeSync(term, "Line 1\r\n");
|
||||
await writeSync(term, "Line 2\r\n");
|
||||
await writeSync(term, "Line 3\r\n");
|
||||
const html = serializeAsHTML(term);
|
||||
|
||||
// Count rows in output - should be 3, not 24
|
||||
const rowCount = (html.match(/<div><span/g) || []).length;
|
||||
expect(rowCount).toBe(3);
|
||||
|
||||
// Content should be present
|
||||
expect(html).toContain("Line 1");
|
||||
expect(html).toContain("Line 2");
|
||||
expect(html).toContain("Line 3");
|
||||
|
||||
disposeTerminal(term);
|
||||
});
|
||||
|
||||
test("trims trailing whitespace within rows", async () => {
|
||||
const term = createTerminal(80, 24);
|
||||
await writeSync(term, "Hello\r\n");
|
||||
const html = serializeAsHTML(term);
|
||||
|
||||
// Should not have 80 spaces after "Hello"
|
||||
expect(html).not.toMatch(/Hello\s{10,}/);
|
||||
// Content should end cleanly
|
||||
expect(html).toContain("Hello</span>");
|
||||
|
||||
disposeTerminal(term);
|
||||
});
|
||||
|
||||
test("handles empty terminal gracefully", async () => {
|
||||
const term = createTerminal(80, 24);
|
||||
// No writes - terminal is empty
|
||||
const html = serializeAsHTML(term);
|
||||
|
||||
// Should produce valid minimal HTML, not 24 empty rows
|
||||
expect(html).toContain("<div style=");
|
||||
expect(html).toContain("</pre>");
|
||||
|
||||
// Should have minimal row divs (ideally 0)
|
||||
const rowCount = (html.match(/<div><span/g) || []).length;
|
||||
expect(rowCount).toBe(0);
|
||||
|
||||
disposeTerminal(term);
|
||||
});
|
||||
|
||||
test("preserves content with ANSI colors after trimming", async () => {
|
||||
const term = createTerminal(80, 24);
|
||||
await writeSync(term, "\x1b[32mGreen\x1b[0m text\r\n");
|
||||
await writeSync(term, "Normal line\r\n");
|
||||
const html = serializeAsHTML(term);
|
||||
|
||||
// Should have 2 rows
|
||||
const rowCount = (html.match(/<div><span/g) || []).length;
|
||||
expect(rowCount).toBe(2);
|
||||
|
||||
// Color styling should be preserved
|
||||
expect(html).toContain("Green");
|
||||
expect(html).toContain("Normal line");
|
||||
// Check for color styling (green is typically #4e9a06 or similar)
|
||||
expect(html).toMatch(/style='[^']*color[^']*'/);
|
||||
|
||||
disposeTerminal(term);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,13 +27,87 @@ export function createTerminal(cols: number, rows: number): TerminalSession {
|
|||
}
|
||||
|
||||
/**
|
||||
* Serialize terminal screen buffer as HTML
|
||||
* Serialize terminal screen buffer as HTML, trimming empty trailing rows
|
||||
* and trailing whitespace within each row.
|
||||
*
|
||||
* xterm's serializeAsHTML outputs ALL rows (e.g., 50 for a 50-row terminal),
|
||||
* padding empty lines with spaces. This creates a large black box below actual
|
||||
* content. We trim these to show only content-bearing rows.
|
||||
*/
|
||||
export function serializeAsHTML(session: TerminalSession): string {
|
||||
return session.serialize.serializeAsHTML({
|
||||
const html = session.serialize.serializeAsHTML({
|
||||
onlySelection: false,
|
||||
includeGlobalBackground: true,
|
||||
});
|
||||
|
||||
return trimHtmlOutput(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim empty trailing rows and trailing whitespace from xterm HTML output.
|
||||
*
|
||||
* Structure: <html><body><!--StartFragment--><pre><div style='...'><div><span>...</span></div>...</div></pre><!--EndFragment--></body></html>
|
||||
* Each row is: <div><span>content</span></div>
|
||||
*/
|
||||
function trimHtmlOutput(html: string): string {
|
||||
// Find the row container div
|
||||
const wrapperStart = html.indexOf("<div style=");
|
||||
const wrapperEnd = html.lastIndexOf("</div></pre>");
|
||||
|
||||
if (wrapperStart === -1 || wrapperEnd === -1) return html;
|
||||
|
||||
const firstDivEnd = html.indexOf(">", wrapperStart) + 1;
|
||||
const rowsHtml = html.substring(firstDivEnd, wrapperEnd);
|
||||
|
||||
// Parse rows: each is <div><span>...</span></div>
|
||||
const rows: string[] = [];
|
||||
let pos = 0;
|
||||
while (pos < rowsHtml.length) {
|
||||
const divStart = rowsHtml.indexOf("<div>", pos);
|
||||
if (divStart === -1) break;
|
||||
const divEnd = rowsHtml.indexOf("</div>", divStart);
|
||||
if (divEnd === -1) break;
|
||||
rows.push(rowsHtml.substring(divStart, divEnd + 6));
|
||||
pos = divEnd + 6;
|
||||
}
|
||||
|
||||
// Find last non-empty row (working backwards)
|
||||
let lastNonEmpty = -1;
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
const row = rows[i];
|
||||
if (!row) continue;
|
||||
// Strip HTML tags and check for non-whitespace content
|
||||
const text = row
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /g, " ")
|
||||
.trim();
|
||||
if (text) {
|
||||
lastNonEmpty = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastNonEmpty === -1) {
|
||||
// All rows empty - return minimal HTML
|
||||
const prefix = html.substring(0, firstDivEnd);
|
||||
const suffix = html.substring(wrapperEnd);
|
||||
return prefix + suffix;
|
||||
}
|
||||
|
||||
// Trim trailing whitespace within each span of retained rows
|
||||
const trimmedRows = rows.slice(0, lastNonEmpty + 1).map((row) => {
|
||||
return row.replace(
|
||||
/(<span[^>]*>)([^<]*)(<\/span>)/g,
|
||||
(_, open, content, close) => {
|
||||
const trimmed = content.replace(/[\s\u00a0]+$/, "");
|
||||
return open + trimmed + close;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const prefix = html.substring(0, firstDivEnd);
|
||||
const suffix = html.substring(wrapperEnd);
|
||||
return prefix + trimmedRows.join("") + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue