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/);
|
expect(html).toMatch(/<span|<div|Test content/);
|
||||||
disposeTerminal(term);
|
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 {
|
export function serializeAsHTML(session: TerminalSession): string {
|
||||||
return session.serialize.serializeAsHTML({
|
const html = session.serialize.serializeAsHTML({
|
||||||
onlySelection: false,
|
onlySelection: false,
|
||||||
includeGlobalBackground: true,
|
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