diff --git a/src/ansi.test.ts b/src/ansi.test.ts
index 0fa1611..c6fc227 100644
--- a/src/ansi.test.ts
+++ b/src/ansi.test.ts
@@ -285,14 +285,35 @@ test("multiple newlines with styling", () => {
);
});
-test("carriage return is stripped", () => {
+test("carriage return in \\r\\n is treated as line ending", () => {
const result = ansiToHtml("line1\r\nline2");
expect(result).toBe("line1\nline2");
});
-test("standalone carriage return is stripped", () => {
+test("standalone carriage return overwrites from line start", () => {
const result = ansiToHtml("text\rmore");
- expect(result).toBe("textmore");
+ expect(result).toBe("more");
+});
+
+test("multiple carriage returns overwrite progressively", () => {
+ const result = ansiToHtml("loading...\rloading...\rdone ");
+ // Note: trailing spaces are trimmed by trimLineEndPreserveAnsi
+ expect(result).toBe("done");
+});
+
+test("carriage return with newlines preserves lines", () => {
+ const result = ansiToHtml("line1\nloading...\rdone\nline3");
+ expect(result).toBe("line1\ndone\nline3");
+});
+
+test("carriage return with styling preserves style", () => {
+ const result = ansiToHtml("\x1b[31mloading...\rdone\x1b[0m");
+ expect(result).toBe('done');
+});
+
+test("carriage return at start of line", () => {
+ const result = ansiToHtml("\rtext");
+ expect(result).toBe("text");
});
// 9. trimLineEndPreserveAnsi tests
diff --git a/src/ansi.ts b/src/ansi.ts
index d787844..ce2260e 100644
--- a/src/ansi.ts
+++ b/src/ansi.ts
@@ -274,20 +274,45 @@ export function ansiToHtml(text: string): string {
continue;
}
- // Track newlines in the content
- if (text[i] === "\n" || text[i] === "\r") {
- if (text[i] === "\n") {
- // Close span before newline to prevent background color bleeding
- if (inSpan) {
- result += "";
- }
- result += "\n";
- // Reopen span after newline if we had styling
- if (inSpan) {
- result += ``;
- }
+ // Track newlines and carriage returns
+ if (text[i] === "\n") {
+ // Close span before newline to prevent background color bleeding
+ if (inSpan) {
+ result += "";
}
- // Skip carriage return - we only care about line feeds
+ result += "\n";
+ // Reopen span after newline if we had styling
+ if (inSpan) {
+ result += ``;
+ }
+ i++;
+ continue;
+ }
+
+ // Carriage return: overwrite from start of current line
+ if (text[i] === "\r") {
+ // Check if this is \r\n - if so, just skip the \r
+ if (text[i + 1] === "\n") {
+ i++;
+ continue;
+ }
+
+ // Standalone \r: truncate back to last newline (or start)
+ // Need to handle open spans carefully
+ const lastNewline = result.lastIndexOf("\n");
+ if (lastNewline >= 0) {
+ // Truncate after the last newline
+ result = result.substring(0, lastNewline + 1);
+ } else {
+ // No newline found, truncate everything
+ result = "";
+ }
+
+ // If we had a span open, we need to reopen it after truncation
+ if (inSpan) {
+ result += ``;
+ }
+
i++;
continue;
}