yoctomarkdown 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@ Steps to follow exactly:
22
22
  7. Update the `version` field in `package.json` to `<new-version>`.
23
23
  8. Run `bun run build`.
24
24
  9. Commit all staged and unstaged changes with message: `chore: release v<new-version>`.
25
- 10. Create annotated tag: `git tag -a v<new-version> -m "<new-version>\n<small-changelog>"`.
25
+ 10. Create annotated tag: `git tag -a v<new-version> -m "<small-changelog>"`.
26
26
  11. Push explicitly to main and the exact tag:
27
27
  - `git push origin main`
28
28
  - `git push origin v<new-version>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yoctomarkdown",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {
package/src/index.ts CHANGED
@@ -128,7 +128,14 @@ export function createHighlighter(options?: Options): Highlighter {
128
128
  }
129
129
 
130
130
  let hasPartial = false;
131
- let partialLength = 0;
131
+
132
+ function redrawPartialPrefix(): string {
133
+ return hasPartial ? "\x1b8\x1b[J" : "";
134
+ }
135
+
136
+ function renderPartial(line: string, isPartial: boolean): string {
137
+ return `\x1b7${parseLine(line, isPartial)}`;
138
+ }
132
139
 
133
140
  return {
134
141
  write(chunk: string | Uint8Array): string {
@@ -139,30 +146,17 @@ export function createHighlighter(options?: Options): Highlighter {
139
146
  const lines = buffer.split("\n");
140
147
  buffer = lines.pop() ?? "";
141
148
 
142
- let out = "";
143
- if (hasPartial) {
144
- const columns =
145
- typeof process !== "undefined" && process.stdout?.columns
146
- ? process.stdout.columns
147
- : 80;
148
- const linesToClear = Math.ceil(partialLength / columns) || 1;
149
- out += "\x1b[2K\r";
150
- if (linesToClear > 1) {
151
- out += "\x1b[1A\x1b[2K".repeat(linesToClear - 1);
152
- }
153
- }
149
+ let out = redrawPartialPrefix();
154
150
 
155
151
  if (lines.length > 0) {
156
152
  out += `${lines.map((line) => parseLine(line, false)).join("\n")}\n`;
157
153
  }
158
154
 
159
155
  if (buffer.length > 0) {
160
- out += parseLine(buffer, true);
156
+ out += renderPartial(buffer, true);
161
157
  hasPartial = true;
162
- partialLength = buffer.length;
163
158
  } else {
164
159
  hasPartial = false;
165
- partialLength = 0;
166
160
  }
167
161
 
168
162
  return out;
@@ -172,16 +166,9 @@ export function createHighlighter(options?: Options): Highlighter {
172
166
  const out = parseLine(buffer, false);
173
167
  buffer = "";
174
168
  if (hasPartial) {
175
- const columns =
176
- typeof process !== "undefined" && process.stdout?.columns
177
- ? process.stdout.columns
178
- : 80;
179
- const linesToClear = Math.ceil(partialLength / columns) || 1;
180
- let clearSeq = "\x1b[2K\r";
181
- if (linesToClear > 1) {
182
- clearSeq += "\x1b[1A\x1b[2K".repeat(linesToClear - 1);
183
- }
184
- return clearSeq + out;
169
+ const redrawPrefix = redrawPartialPrefix();
170
+ hasPartial = false;
171
+ return redrawPrefix + out;
185
172
  }
186
173
  return out;
187
174
  }
@@ -32,10 +32,107 @@ describe("Programmatic Usage: highlightSync", () => {
32
32
  });
33
33
  });
34
34
 
35
+ function renderTerminalOutput(
36
+ output: string,
37
+ columns = Number.POSITIVE_INFINITY,
38
+ ) {
39
+ const screen: string[][] = [[]];
40
+ let row = 0;
41
+ let column = 0;
42
+ let savedCursor: { row: number; column: number } | null = null;
43
+
44
+ function ensureRow(targetRow: number) {
45
+ while (screen.length <= targetRow) {
46
+ screen.push([]);
47
+ }
48
+ }
49
+
50
+ function writeChar(char: string) {
51
+ if (column >= columns) {
52
+ row += 1;
53
+ column = 0;
54
+ }
55
+
56
+ ensureRow(row);
57
+ if (!screen[row]) {
58
+ screen[row] = [];
59
+ }
60
+ const currentLine = screen[row];
61
+ if (!currentLine) {
62
+ return;
63
+ }
64
+ currentLine[column] = char;
65
+ column += 1;
66
+ }
67
+
68
+ for (let i = 0; i < output.length; i += 1) {
69
+ const rest = output.slice(i);
70
+
71
+ if (rest.startsWith("\x1b7")) {
72
+ savedCursor = { row, column };
73
+ i += 1;
74
+ continue;
75
+ }
76
+
77
+ if (rest.startsWith("\x1b8")) {
78
+ if (savedCursor) {
79
+ row = savedCursor.row;
80
+ column = savedCursor.column;
81
+ }
82
+ i += 1;
83
+ continue;
84
+ }
85
+
86
+ if (rest.startsWith("\x1b[J")) {
87
+ ensureRow(row);
88
+ screen[row] = (screen[row] ?? []).slice(0, column);
89
+ for (let clearRow = row + 1; clearRow < screen.length; clearRow += 1) {
90
+ screen[clearRow] = [];
91
+ }
92
+ i += 2;
93
+ continue;
94
+ }
95
+
96
+ if (rest.startsWith("\x1b[2K")) {
97
+ ensureRow(row);
98
+ screen[row] = [];
99
+ i += 3;
100
+ continue;
101
+ }
102
+
103
+ if (rest.startsWith("\x1b[1A")) {
104
+ row = Math.max(0, row - 1);
105
+ i += 3;
106
+ continue;
107
+ }
108
+
109
+ const char = output[i];
110
+ if (char === "\n") {
111
+ row += 1;
112
+ column = 0;
113
+ ensureRow(row);
114
+ continue;
115
+ }
116
+
117
+ if (char === "\r") {
118
+ column = 0;
119
+ continue;
120
+ }
121
+
122
+ if (char !== undefined && char !== "\x1b") {
123
+ writeChar(char);
124
+ }
125
+ }
126
+
127
+ return screen
128
+ .map((line) => line.join("").replace(/\s+$/u, ""))
129
+ .join("\n")
130
+ .replace(/\n+$/u, "");
131
+ }
132
+
35
133
  function expectStreamMatch(res: string, markdown: string) {
36
134
  const expected = highlightSync(markdown, { theme: "none" });
37
- // biome-ignore lint/suspicious/noControlCharactersInRegex: clear line ansi escape
38
- expect(res.replace(/[^\n]*\x1b\[2K\r/g, "")).toBe(expected);
135
+ expect(renderTerminalOutput(res)).toBe(renderTerminalOutput(expected));
39
136
  }
40
137
 
41
138
  describe("Programmatic Usage: createHighlighter (Streaming)", () => {
@@ -82,33 +179,20 @@ describe("Programmatic Usage: createHighlighter (Streaming)", () => {
82
179
  expectStreamMatch(res, markdown);
83
180
  });
84
181
 
85
- test("should clear multiple lines when wrapped", () => {
86
- // Mock columns
87
- const originalColumns = process.stdout.columns;
88
- Object.defineProperty(process.stdout, "columns", {
89
- value: 10,
90
- configurable: true,
91
- });
182
+ test("should redraw wrapped partial lines after an external prefix", () => {
183
+ const prefix = "◆ ";
184
+ const markdown =
185
+ "No `\\x1b[2K` or `\\r` sequences in the streamed text. The ANSI colour codes remain (intentional — those are static style codes from yoctocolors, not cursor-control sequences).\n";
186
+ const splitAt = 134;
92
187
 
93
188
  const highlighter = createHighlighter({ theme: "none" });
94
- // 25 chars, columns = 10, so it takes 3 visual lines
95
- const chunk1 = "1234567890123456789012345";
96
- const chunk2 = "67890\n";
97
-
98
- let res = highlighter.write(chunk1);
99
- res += highlighter.write(chunk2);
189
+ let res = prefix;
190
+ res += highlighter.write(markdown.slice(0, splitAt));
191
+ res += highlighter.write(markdown.slice(splitAt));
100
192
  res += highlighter.end();
101
193
 
102
- // The first chunk is written, then cleared. Since it takes 3 lines,
103
- // clear sequence should have \x1b[1A\x1b[2K repeated twice.
104
- expect(res).toContain("\x1b[1A\x1b[2K\x1b[1A\x1b[2K");
105
-
106
- // Restore
107
- if (originalColumns !== undefined) {
108
- Object.defineProperty(process.stdout, "columns", {
109
- value: originalColumns,
110
- configurable: true,
111
- });
112
- }
194
+ expect(renderTerminalOutput(res, 120)).toBe(
195
+ renderTerminalOutput(`${prefix}${markdown}`, 120),
196
+ );
113
197
  });
114
198
  });