yoctomarkdown 0.0.2 → 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.
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Create a new tagged npm release (patch/minor/major)
3
+ ---
4
+
5
+ Create a new npm release for this repo. The bump type is: $1 (default to "patch" if not provided).
6
+
7
+ Steps to follow exactly:
8
+
9
+ 1. Normalize bump type to one of `patch|minor|major`; if empty, use `patch`.
10
+ 2. Verify git state before doing anything:
11
+ - current branch must be `main`
12
+ - working tree must be clean
13
+ - run `git fetch origin --tags` and ensure `HEAD` equals `origin/main`
14
+ If any check fails, stop and report.
15
+ 3. Run the full repo check suit — stop and report failures.
16
+ 4. Read `package.json` and capture both `name` and current `version`.
17
+ 5. Compute the next semver version by bumping the selected part.
18
+ 6. Guardrails before mutating files:
19
+ - ensure git tag `v<new-version>` does not exist locally or on origin
20
+ - ensure npm version does not already exist: `npm view <package-name>@<new-version> version`
21
+ If either exists, stop and report.
22
+ 7. Update the `version` field in `package.json` to `<new-version>`.
23
+ 8. Run `bun run build`.
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 "<small-changelog>"`.
26
+ 11. Push explicitly to main and the exact tag:
27
+ - `git push origin main`
28
+ - `git push origin v<new-version>`
29
+ 12. Post-push verification: run `npm view <package-name>@<new-version> version`.
30
+ - If it already exists, report that publish already happened and do **not** ask user to publish again.
31
+ - If it does not exist, run `npm publish`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yoctomarkdown",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -18,7 +18,7 @@
18
18
  "yoctocolors": "^2.1.2"
19
19
  },
20
20
  "bin": {
21
- "yoctomarkdown": "./src/cli.ts"
21
+ "yoctomarkdown": "src/cli.ts"
22
22
  },
23
23
  "scripts": {
24
24
  "typecheck": "tsc --noEmit",
package/src/index.ts CHANGED
@@ -129,6 +129,14 @@ export function createHighlighter(options?: Options): Highlighter {
129
129
 
130
130
  let hasPartial = false;
131
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
+ }
139
+
132
140
  return {
133
141
  write(chunk: string | Uint8Array): string {
134
142
  const text =
@@ -138,17 +146,14 @@ export function createHighlighter(options?: Options): Highlighter {
138
146
  const lines = buffer.split("\n");
139
147
  buffer = lines.pop() ?? "";
140
148
 
141
- let out = "";
142
- if (hasPartial) {
143
- out += "\x1b[2K\r";
144
- }
149
+ let out = redrawPartialPrefix();
145
150
 
146
151
  if (lines.length > 0) {
147
152
  out += `${lines.map((line) => parseLine(line, false)).join("\n")}\n`;
148
153
  }
149
154
 
150
155
  if (buffer.length > 0) {
151
- out += parseLine(buffer, true);
156
+ out += renderPartial(buffer, true);
152
157
  hasPartial = true;
153
158
  } else {
154
159
  hasPartial = false;
@@ -160,7 +165,12 @@ export function createHighlighter(options?: Options): Highlighter {
160
165
  if (buffer.length > 0) {
161
166
  const out = parseLine(buffer, false);
162
167
  buffer = "";
163
- return (hasPartial ? "\x1b[2K\r" : "") + out;
168
+ if (hasPartial) {
169
+ const redrawPrefix = redrawPartialPrefix();
170
+ hasPartial = false;
171
+ return redrawPrefix + out;
172
+ }
173
+ return out;
164
174
  }
165
175
  return "";
166
176
  },
@@ -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)", () => {
@@ -81,4 +178,21 @@ describe("Programmatic Usage: createHighlighter (Streaming)", () => {
81
178
 
82
179
  expectStreamMatch(res, markdown);
83
180
  });
181
+
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;
187
+
188
+ const highlighter = createHighlighter({ theme: "none" });
189
+ let res = prefix;
190
+ res += highlighter.write(markdown.slice(0, splitAt));
191
+ res += highlighter.write(markdown.slice(splitAt));
192
+ res += highlighter.end();
193
+
194
+ expect(renderTerminalOutput(res, 120)).toBe(
195
+ renderTerminalOutput(`${prefix}${markdown}`, 120),
196
+ );
197
+ });
84
198
  });