Handle OSC and ascii
This commit is contained in:
parent
9fff95e786
commit
45b232bcc7
3 changed files with 144 additions and 54 deletions
|
|
@ -424,3 +424,61 @@ test("handles only ANSI codes with no text", () => {
|
|||
// Opens and closes span even with no text
|
||||
expect(result).toBe('<span style="color:#f85149"></span>');
|
||||
});
|
||||
|
||||
// OSC sequences (Operating System Command)
|
||||
test("strips OSC terminal title with BEL", () => {
|
||||
// ESC ] 0 ; title BEL
|
||||
const result = ansiToHtml("\x1b]0;My Terminal Title\x07text after");
|
||||
expect(result).toBe("text after");
|
||||
});
|
||||
|
||||
test("strips OSC terminal title with ST", () => {
|
||||
// ESC ] 0 ; title ESC \
|
||||
const result = ansiToHtml("\x1b]0;My Terminal Title\x1b\\text after");
|
||||
expect(result).toBe("text after");
|
||||
});
|
||||
|
||||
test("strips OSC with emoji in title", () => {
|
||||
const result = ansiToHtml("\x1b]0;★ Claude Code\x07hello");
|
||||
expect(result).toBe("hello");
|
||||
});
|
||||
|
||||
test("strips multiple OSC sequences", () => {
|
||||
const result = ansiToHtml("\x1b]0;title1\x07text\x1b]0;title2\x07more");
|
||||
expect(result).toBe("textmore");
|
||||
});
|
||||
|
||||
// Private mode sequences (DEC)
|
||||
test("strips bracketed paste mode enable", () => {
|
||||
// ESC [ ? 2004 h
|
||||
const result = ansiToHtml("\x1b[?2004htext");
|
||||
expect(result).toBe("text");
|
||||
});
|
||||
|
||||
test("strips bracketed paste mode disable", () => {
|
||||
// ESC [ ? 2004 l
|
||||
const result = ansiToHtml("\x1b[?2004ltext");
|
||||
expect(result).toBe("text");
|
||||
});
|
||||
|
||||
test("strips mixed private mode and SGR", () => {
|
||||
const result = ansiToHtml("\x1b[?2004h\x1b[31mred\x1b[0m\x1b[?2004l");
|
||||
expect(result).toBe('<span style="color:#f85149">red</span>');
|
||||
});
|
||||
|
||||
test("strips cursor visibility sequences", () => {
|
||||
// ESC [ ? 25 h (show cursor) and ESC [ ? 25 l (hide cursor)
|
||||
const result = ansiToHtml("\x1b[?25lhidden cursor\x1b[?25h");
|
||||
expect(result).toBe("hidden cursor");
|
||||
});
|
||||
|
||||
test("handles Claude Code typical output", () => {
|
||||
// Simulated Claude Code startup with title, bracketed paste, and colors
|
||||
const input =
|
||||
"\x1b]0;★ Claude Code\x07\x1b[?2004h\x1b[1;33mClaude Code\x1b[0m v2.1.15\x1b[?2004l";
|
||||
const result = ansiToHtml(input);
|
||||
expect(result).toContain("Claude Code");
|
||||
expect(result).toContain("v2.1.15");
|
||||
expect(result).not.toContain("2004");
|
||||
expect(result).not.toContain("]0;");
|
||||
});
|
||||
|
|
|
|||
130
src/ansi.ts
130
src/ansi.ts
|
|
@ -189,60 +189,88 @@ export function ansiToHtml(text: string): string {
|
|||
|
||||
while (i < text.length) {
|
||||
// Check for ESC character (char code 27)
|
||||
if (text.charCodeAt(i) === 27 && text[i + 1] === "[") {
|
||||
// Parse CSI sequence: ESC [ params command
|
||||
let j = i + 2;
|
||||
let params = "";
|
||||
while (j < text.length && /[0-9;]/.test(text[j] || "")) {
|
||||
params += text[j];
|
||||
j++;
|
||||
}
|
||||
const command = text[j] || "";
|
||||
j++;
|
||||
|
||||
if (command === "m") {
|
||||
// SGR - Select Graphic Rendition
|
||||
const codes = params ? params.split(";").map(Number) : [0];
|
||||
for (let k = 0; k < codes.length; k++) {
|
||||
const code = codes[k];
|
||||
if (code === 0) {
|
||||
state = resetState();
|
||||
} else if (code === 1) state.bold = true;
|
||||
else if (code === 2) state.dim = true;
|
||||
else if (code === 3) state.italic = true;
|
||||
else if (code === 4) state.underline = true;
|
||||
else if (code === 7) state.inverse = true;
|
||||
else if (code === 22) {
|
||||
state.bold = false;
|
||||
state.dim = false;
|
||||
} else if (code === 23) state.italic = false;
|
||||
else if (code === 24) state.underline = false;
|
||||
else if (code === 27) state.inverse = false;
|
||||
else if (code === 39) state.fg = null;
|
||||
else if (code === 49) state.bg = null;
|
||||
else if (code === 38) {
|
||||
const extColor = parseExtendedColor(codes, k);
|
||||
if (extColor) {
|
||||
state.fg = extColor.color;
|
||||
k += extColor.skip;
|
||||
}
|
||||
} else if (code === 48) {
|
||||
const extColor = parseExtendedColor(codes, k);
|
||||
if (extColor) {
|
||||
state.bg = extColor.color;
|
||||
k += extColor.skip;
|
||||
}
|
||||
} else if (code !== undefined && colors[code]) {
|
||||
state.fg = colors[code];
|
||||
} else if (code !== undefined && bgColors[code]) {
|
||||
state.bg = bgColors[code];
|
||||
if (text.charCodeAt(i) === 27) {
|
||||
// OSC sequence: ESC ] ... BEL or ESC ] ... ESC \
|
||||
// Used for terminal title, hyperlinks, etc.
|
||||
if (text[i + 1] === "]") {
|
||||
let j = i + 2;
|
||||
// Skip until BEL (0x07) or ST (ESC \)
|
||||
while (j < text.length) {
|
||||
if (text.charCodeAt(j) === 0x07) {
|
||||
j++;
|
||||
break;
|
||||
}
|
||||
if (text.charCodeAt(j) === 27 && text[j + 1] === "\\") {
|
||||
j += 2;
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
applyStyle();
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// Skip other escape sequences (H, f, J, K, etc.) - they don't affect our line-based output
|
||||
i = j;
|
||||
|
||||
// CSI sequence: ESC [ params command
|
||||
if (text[i + 1] === "[") {
|
||||
let j = i + 2;
|
||||
let params = "";
|
||||
// Include ? for private mode sequences like ESC[?2004h (bracketed paste)
|
||||
while (j < text.length && /[0-9;?]/.test(text[j] || "")) {
|
||||
params += text[j];
|
||||
j++;
|
||||
}
|
||||
const command = text[j] || "";
|
||||
j++;
|
||||
|
||||
// Only process SGR (m command) for styling, skip others (cursor, clear, etc.)
|
||||
if (command === "m" && !params.includes("?")) {
|
||||
// SGR - Select Graphic Rendition
|
||||
const codes = params ? params.split(";").map(Number) : [0];
|
||||
for (let k = 0; k < codes.length; k++) {
|
||||
const code = codes[k];
|
||||
if (code === 0) {
|
||||
state = resetState();
|
||||
} else if (code === 1) state.bold = true;
|
||||
else if (code === 2) state.dim = true;
|
||||
else if (code === 3) state.italic = true;
|
||||
else if (code === 4) state.underline = true;
|
||||
else if (code === 7) state.inverse = true;
|
||||
else if (code === 22) {
|
||||
state.bold = false;
|
||||
state.dim = false;
|
||||
} else if (code === 23) state.italic = false;
|
||||
else if (code === 24) state.underline = false;
|
||||
else if (code === 27) state.inverse = false;
|
||||
else if (code === 39) state.fg = null;
|
||||
else if (code === 49) state.bg = null;
|
||||
else if (code === 38) {
|
||||
const extColor = parseExtendedColor(codes, k);
|
||||
if (extColor) {
|
||||
state.fg = extColor.color;
|
||||
k += extColor.skip;
|
||||
}
|
||||
} else if (code === 48) {
|
||||
const extColor = parseExtendedColor(codes, k);
|
||||
if (extColor) {
|
||||
state.bg = extColor.color;
|
||||
k += extColor.skip;
|
||||
}
|
||||
} else if (code !== undefined && colors[code]) {
|
||||
state.fg = colors[code];
|
||||
} else if (code !== undefined && bgColors[code]) {
|
||||
state.bg = bgColors[code];
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle();
|
||||
}
|
||||
// Skip all CSI sequences (SGR handled above, others like cursor/clear ignored)
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unknown ESC sequence - skip the ESC character
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
10
src/seed.ts
10
src/seed.ts
|
|
@ -12,9 +12,13 @@ initDb();
|
|||
|
||||
const existing = getDeviceBySecret(secret);
|
||||
if (existing) {
|
||||
console.log(`device already exists: id=${existing.id} secret=${existing.secret} name=${existing.name}`);
|
||||
process.exit(0);
|
||||
console.log(
|
||||
`device already exists: id=${existing.id} secret=${existing.secret} name=${existing.name}`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const device = createDevice(secret, name);
|
||||
console.log(`created device: id=${device.id} secret=${device.secret} name=${device.name}`);
|
||||
console.log(
|
||||
`created device: id=${device.id} secret=${device.secret} name=${device.name}`,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue