From 1b72d1e4e4c1e16f4ac9835c0338301806fab0ac Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 31 Jan 2026 11:53:38 -0500 Subject: [PATCH] Trim empty rows --- src/terminal.test.ts | 67 +++++++++++++++++++++++++++++++++++++ src/terminal.ts | 78 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/src/terminal.test.ts b/src/terminal.test.ts index 1bc5a7c..2f6c681 100644 --- a/src/terminal.test.ts +++ b/src/terminal.test.ts @@ -97,4 +97,71 @@ describe("terminal", () => { expect(html).toMatch(/ { + 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(/
{ + 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"); + + 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("
"); + + // Should have minimal row divs (ideally 0) + const rowCount = (html.match(/
{ + 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(/
...
...
+ * Each row is:
content
+ */ +function trimHtmlOutput(html: string): string { + // Find the row container div + const wrapperStart = html.indexOf("
"); + + if (wrapperStart === -1 || wrapperEnd === -1) return html; + + const firstDivEnd = html.indexOf(">", wrapperStart) + 1; + const rowsHtml = html.substring(firstDivEnd, wrapperEnd); + + // Parse rows: each is
...
+ const rows: string[] = []; + let pos = 0; + while (pos < rowsHtml.length) { + const divStart = rowsHtml.indexOf("
", pos); + if (divStart === -1) break; + const divEnd = rowsHtml.indexOf("
", 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>)/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; } /**