jsmdcui 0.6.3 → 0.8.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +216 -33
  3. package/clean.sh +9 -0
  4. package/demo.resized.jpg +0 -0
  5. package/{image-processor.md → demos/image-processor.md} +1 -0
  6. package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
  7. package/demos/maze.md +144 -0
  8. package/{select.md → demos/select.md} +2 -0
  9. package/demos/todo-zh.md +190 -0
  10. package/demos/todo.md +191 -0
  11. package/edit +9 -0
  12. package/package.json +1 -1
  13. package/runmd.mjs +77 -16
  14. package/runtime/help/help.md +216 -33
  15. package/runtime/jsplugins/cdp/cdp.js +5 -2
  16. package/runtime/jsplugins/chapter/chapter.js +4 -2
  17. package/runtime/jsplugins/example/example.js +10 -7
  18. package/runtime/syntax/markdown.yaml +1 -0
  19. package/single-exe/packAssets.sh +1 -1
  20. package/src/cui/fence-events.mjs +106 -0
  21. package/src/cui/id-collision.mjs +10 -28
  22. package/src/cui/kitty-debug.mjs +25 -0
  23. package/src/cui/kitty-images.mjs +200 -0
  24. package/src/cui/rpc.mjs +169 -10
  25. package/src/cui/server.mjs +14 -3
  26. package/src/cui/task-checkbox.mjs +44 -0
  27. package/src/index.js +615 -101
  28. package/src/platform/terminal.js +5 -0
  29. package/src/plugins/js-bridge.js +354 -21
  30. package/src/screen/screen.js +108 -1
  31. package/tests/cat-markdown.test.js +6 -5
  32. package/tests/demo.test.js +99 -9
  33. package/tests/fence-events.test.js +405 -0
  34. package/tests/heading-list-selector.test.js +346 -0
  35. package/tests/id-collision.test.js +14 -2
  36. package/tests/js-plugin-prompts.test.js +46 -0
  37. package/tests/key-event.md +10 -0
  38. package/tests/kitty-demo.md +184 -0
  39. package/tests/kitty-images.test.js +169 -0
  40. package/tests/platform-terminal.test.js +8 -0
  41. package/tests/task-checkbox.test.js +33 -0
  42. package/tests/textarea.md +8 -0
  43. package/tests/wui-responsive-images.test.js +35 -0
  44. package/tests/wui.test.js +16 -1
  45. package/tui +4 -2
  46. package/wui +6 -2
@@ -0,0 +1,200 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { logKittyPlacement } from "./kitty-debug.mjs";
4
+ import { fetchHttpBytes as defaultFetchHttpBytes } from "../platform/commands.js";
5
+
6
+ const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
7
+
8
+ function imageSize(buffer) {
9
+ if (buffer.length >= 24 && buffer.subarray(0, 8).equals(PNG)) {
10
+ return { mime: "image/png", width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
11
+ }
12
+ const gif = buffer.toString("ascii", 0, 6);
13
+ if (buffer.length >= 10 && (gif === "GIF87a" || gif === "GIF89a")) {
14
+ return { mime: "image/gif", width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
15
+ }
16
+ if (buffer.length >= 26 && buffer.toString("ascii", 0, 2) === "BM") {
17
+ return { mime: "image/bmp", width: Math.abs(buffer.readInt32LE(18)), height: Math.abs(buffer.readInt32LE(22)) };
18
+ }
19
+ if (buffer.length >= 16 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
20
+ const kind = buffer.toString("ascii", 12, 16);
21
+ if (kind === "VP8 " && buffer.length >= 30) {
22
+ return {
23
+ mime: "image/webp",
24
+ width: buffer.readUInt16LE(26) & 0x3fff,
25
+ height: buffer.readUInt16LE(28) & 0x3fff,
26
+ };
27
+ }
28
+ if (kind === "VP8L" && buffer.length >= 25) {
29
+ const bits = buffer[21] | (buffer[22] << 8) | (buffer[23] << 16) | (buffer[24] << 24);
30
+ return {
31
+ mime: "image/webp",
32
+ width: (bits & 0x3fff) + 1,
33
+ height: ((bits >> 14) & 0x3fff) + 1,
34
+ };
35
+ }
36
+ if (kind === "VP8X" && buffer.length >= 30) {
37
+ return {
38
+ mime: "image/webp",
39
+ width: 1 + buffer.readUIntLE(24, 3),
40
+ height: 1 + buffer.readUIntLE(27, 3),
41
+ };
42
+ }
43
+ }
44
+ if (buffer.length >= 4 && buffer[0] === 0xff && buffer[1] === 0xd8) {
45
+ let offset = 2;
46
+ while (offset + 8 < buffer.length) {
47
+ if (buffer[offset] !== 0xff) { offset++; continue; }
48
+ const marker = buffer[offset + 1];
49
+ offset += 2;
50
+ if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7)) continue;
51
+ if (offset + 2 > buffer.length) break;
52
+ const length = buffer.readUInt16BE(offset);
53
+ if (length < 2 || offset + length > buffer.length) break;
54
+ const sof = (marker >= 0xc0 && marker <= 0xc3) ||
55
+ (marker >= 0xc5 && marker <= 0xc7) ||
56
+ (marker >= 0xc9 && marker <= 0xcb) ||
57
+ (marker >= 0xcd && marker <= 0xcf);
58
+ if (sof && length >= 7) {
59
+ return { mime: "image/jpeg", width: buffer.readUInt16BE(offset + 5), height: buffer.readUInt16BE(offset + 3) };
60
+ }
61
+ offset += length;
62
+ }
63
+ }
64
+ const svg = buffer.subarray(0, Math.min(buffer.length, 8192)).toString("utf8");
65
+ if (svg.includes("<svg")) {
66
+ const width = svg.match(/\bwidth=["']([0-9.]+)(?:px)?["']/i);
67
+ const height = svg.match(/\bheight=["']([0-9.]+)(?:px)?["']/i);
68
+ if (width && height) {
69
+ return { mime: "image/svg+xml", width: Math.round(Number(width[1])), height: Math.round(Number(height[1])) };
70
+ }
71
+ const viewBox = svg.match(/\bviewBox=["'][^"']*?([0-9.]+)[ ,]+([0-9.]+)\s*["']/i);
72
+ if (viewBox) {
73
+ return { mime: "image/svg+xml", width: Math.round(Number(viewBox[1])), height: Math.round(Number(viewBox[2])) };
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ function isHttpUrl(value) {
80
+ return /^https?:\/\//i.test(String(value ?? ""));
81
+ }
82
+
83
+ function imageSource(href, markdownPath, allowUrl) {
84
+ try {
85
+ if (href.startsWith("file:")) return { kind: "local", value: fileURLToPath(href) };
86
+ if (process.platform === "win32" && /^[A-Za-z]:[\\/]/.test(href)) {
87
+ return { kind: "local", value: resolve(href) };
88
+ }
89
+ if (isHttpUrl(href)) {
90
+ return allowUrl ? { kind: "remote", value: new URL(href).href } : null;
91
+ }
92
+ if (href.startsWith("//")) {
93
+ if (!allowUrl) return null;
94
+ const protocol = isHttpUrl(markdownPath) ? new URL(markdownPath).protocol : "https:";
95
+ return { kind: "remote", value: new URL(`${protocol}${href}`).href };
96
+ }
97
+ if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(href)) return null;
98
+ if (isHttpUrl(markdownPath)) {
99
+ return allowUrl ? { kind: "remote", value: new URL(href, markdownPath).href } : null;
100
+ }
101
+ const decoded = decodeURIComponent(href.replace(/[?#].*$/, ""));
102
+ return {
103
+ kind: "local",
104
+ value: resolve(markdownPath ? dirname(markdownPath) : process.cwd(), decoded),
105
+ };
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ function stableImageId(pathname, line, data) {
112
+ let hash = 2166136261;
113
+ for (const byte of Buffer.concat([Buffer.from(`${pathname}\0${line}\0`), data])) {
114
+ hash ^= byte;
115
+ hash = Math.imul(hash, 16777619);
116
+ }
117
+ return (hash >>> 0) % 2147483646 + 1;
118
+ }
119
+
120
+ async function convertToKittyPng(data) {
121
+ return Buffer.from(await new Bun.Image(data).png().toBuffer());
122
+ }
123
+
124
+ export function fitKittyImageToWidth(image, maxCols) {
125
+ const originalCols = Math.max(1, Math.trunc(Number(image?.cols) || 1));
126
+ const originalRows = Math.max(1, Math.trunc(Number(image?.rows) || 1));
127
+ const availableCols = Math.max(1, Math.trunc(Number(maxCols) || 1));
128
+ const cols = Math.min(originalCols, availableCols);
129
+ const rows = Math.max(1, Math.round(originalRows * cols / originalCols));
130
+ return { cols, rows };
131
+ }
132
+
133
+ export async function prepareKittyImages(ansiText, markdownPath, terminalCols = 80, options = {}) {
134
+ const inputLines = String(ansiText).split("\n");
135
+ const outputLines = [];
136
+ const images = [];
137
+ const maxCols = Math.max(1, Math.trunc(Number(terminalCols) || 80));
138
+ const oscImage = /\x1b\]8;;([^\x1b]*)\x1b\\(?=[^\n]*📷)/;
139
+
140
+ for (let sourceLine = 0; sourceLine < inputLines.length; sourceLine++) {
141
+ const line = inputLines[sourceLine];
142
+ const match = line.match(oscImage);
143
+ const source = match ? imageSource(match[1], markdownPath, options.allowUrl === true) : null;
144
+ if (!source) {
145
+ outputLines.push(line);
146
+ continue;
147
+ }
148
+ try {
149
+ let data;
150
+ if (source.kind === "remote") {
151
+ const fetchBytes = options.fetchHttpBytes ?? defaultFetchHttpBytes;
152
+ data = Buffer.from(await fetchBytes(source.value));
153
+ } else {
154
+ const file = Bun.file(source.value);
155
+ if (!(await file.exists())) throw new Error("missing image");
156
+ data = Buffer.from(await file.arrayBuffer());
157
+ }
158
+ let size = imageSize(data);
159
+ if (!size?.width || !size?.height) throw new Error("unsupported image");
160
+ if (options.kittyMode === "compat" && size.mime !== "image/png") {
161
+ data = await convertToKittyPng(data);
162
+ size = imageSize(data);
163
+ if (size?.mime !== "image/png" || !size.width || !size.height)
164
+ throw new Error("PNG conversion failed");
165
+ }
166
+ const estimatedCols = Math.max(1, Math.round(size.width / 8));
167
+ const cols = Math.min(maxCols, estimatedCols);
168
+ const rows = Math.max(1, Math.round((cols * size.height) / size.width / 2));
169
+ const lineIndex = outputLines.length;
170
+ outputLines.push(line, ...Array(Math.max(0, rows - 1)).fill(""));
171
+ images.push({
172
+ id: stableImageId(source.value, sourceLine, data),
173
+ line: lineIndex,
174
+ cols,
175
+ rows,
176
+ pixelWidth: size.width,
177
+ pixelHeight: size.height,
178
+ mime: size.mime,
179
+ data,
180
+ path: source.value,
181
+ });
182
+ logKittyPlacement("image-prepared", {
183
+ path: source.value,
184
+ sourceKind: source.kind,
185
+ sourceLine,
186
+ renderedLine: lineIndex,
187
+ imageId: images.at(-1).id,
188
+ intrinsicWidth: size.width,
189
+ intrinsicHeight: size.height,
190
+ cols,
191
+ rows,
192
+ terminalCols: maxCols + 1,
193
+ });
194
+ } catch {
195
+ // An unavailable or unsupported image remains the normal Markdown link.
196
+ outputLines.push(line);
197
+ }
198
+ }
199
+ return { rendered: outputLines.join("\n"), images };
200
+ }
package/src/cui/rpc.mjs CHANGED
@@ -71,16 +71,13 @@ function isWebHeading(element)
71
71
  return /^h[1-6]$/i.test(String(element?.tagName ?? ""));
72
72
  }
73
73
 
74
- function firstHeadingTaskList(heading)
74
+ function firstHeadingList(heading)
75
75
  {
76
76
  for (let sibling = heading?.nextElementSibling; sibling; sibling = sibling.nextElementSibling) {
77
77
  if (isWebHeading(sibling) || String(sibling.tagName ?? "").toLowerCase() === "section")
78
78
  break;
79
- const task = sibling.matches?.("li.task-list-item")
80
- ? sibling
81
- : sibling.querySelector?.("li.task-list-item");
82
- if (!task) continue;
83
- const list = task.closest?.("ul, ol");
79
+ if (sibling.matches?.("ul, ol")) return sibling;
80
+ const list = sibling.querySelector?.("ul, ol");
84
81
  if (list) return list;
85
82
  }
86
83
  return null;
@@ -109,7 +106,7 @@ function webTaskItemValue(item, checkbox)
109
106
  function webHeadingValue(heading)
110
107
  {
111
108
  const single = String(heading?.id ?? "").startsWith("select");
112
- const list = firstHeadingTaskList(heading);
109
+ const list = firstHeadingList(heading);
113
110
  if (!list) return single ? null : [];
114
111
 
115
112
  const selected = [];
@@ -124,6 +121,114 @@ function webHeadingValue(heading)
124
121
  return single ? null : selected;
125
122
  }
126
123
 
124
+ function directWebTaskItems(list)
125
+ {
126
+ const items = [];
127
+ for (const item of list?.children ?? []) {
128
+ if (!item.matches?.("li.task-list-item")) continue;
129
+ if (directTaskCheckbox(item)) items.push(item);
130
+ }
131
+ return items;
132
+ }
133
+
134
+ function webTaskItemSnapshot(item)
135
+ {
136
+ const checkbox = directTaskCheckbox(item);
137
+ return {
138
+ value: webTaskItemValue(item, checkbox),
139
+ checked: Boolean(checkbox?.checked),
140
+ };
141
+ }
142
+
143
+ function normalizedTaskItem(input)
144
+ {
145
+ if (input && typeof input === "object") {
146
+ return {
147
+ value: String(input.value ?? input.label ?? "").replace(/\r?\n/g, " "),
148
+ checked: Boolean(input.checked),
149
+ };
150
+ }
151
+ return {
152
+ value: String(input ?? "").replace(/\r?\n/g, " "),
153
+ checked: false,
154
+ };
155
+ }
156
+
157
+ function normalizedSpliceRange(length, argumentCount, start, deleteCount)
158
+ {
159
+ if (argumentCount === 0) return { start: 0, deleteCount: 0 };
160
+ let relativeStart = Number(start);
161
+ if (Number.isNaN(relativeStart)) relativeStart = 0;
162
+ relativeStart = Math.trunc(relativeStart);
163
+ const actualStart = relativeStart < 0
164
+ ? Math.max(length + relativeStart, 0)
165
+ : Math.min(relativeStart, length);
166
+ if (argumentCount === 1) return { start: actualStart, deleteCount: length - actualStart };
167
+ let requestedDelete = Number(deleteCount);
168
+ if (Number.isNaN(requestedDelete)) requestedDelete = 0;
169
+ requestedDelete = Math.max(0, Math.trunc(requestedDelete));
170
+ return {
171
+ start: actualStart,
172
+ deleteCount: Math.min(requestedDelete, length - actualStart),
173
+ };
174
+ }
175
+
176
+ function appendWebTaskItem(list, input, before = null)
177
+ {
178
+ const documentObject = list?.ownerDocument;
179
+ if (!documentObject?.createElement || !documentObject?.createTextNode) return false;
180
+
181
+ const itemValue = normalizedTaskItem(input);
182
+ const item = documentObject.createElement("li");
183
+ item.classList?.add("task-list-item");
184
+ const label = documentObject.createElement("label");
185
+ const checkbox = documentObject.createElement("input");
186
+ checkbox.type = "checkbox";
187
+ checkbox.classList?.add("task-list-item-checkbox");
188
+ checkbox.checked = itemValue.checked;
189
+ label.append(checkbox, documentObject.createTextNode(itemValue.value));
190
+ item.append(label);
191
+ list.insertBefore(item, before);
192
+ return true;
193
+ }
194
+
195
+ function mutateWebHeadingList(heading, method, args)
196
+ {
197
+ const list = firstHeadingList(heading);
198
+ if (!list) {
199
+ if (method === "push" || method === "unshift") return 0;
200
+ if (method === "splice") return [];
201
+ return undefined;
202
+ }
203
+
204
+ const items = directWebTaskItems(list);
205
+ if (method === "splice") {
206
+ const range = normalizedSpliceRange(items.length, args.length, args[0], args[1]);
207
+ const removed = items
208
+ .slice(range.start, range.start + range.deleteCount)
209
+ .map((item) => webTaskItemValue(item, directTaskCheckbox(item)));
210
+ const before = items[range.start] ?? null;
211
+ for (const input of args.slice(2)) appendWebTaskItem(list, input, before);
212
+ for (const item of items.slice(range.start, range.start + range.deleteCount)) item.remove?.();
213
+ return removed;
214
+ }
215
+ if (method === "pop" || method === "shift") {
216
+ const item = method === "pop" ? items.at(-1) : items[0];
217
+ if (!item) return undefined;
218
+ const value = webTaskItemValue(item, directTaskCheckbox(item));
219
+ item.remove?.();
220
+ return value;
221
+ }
222
+
223
+ if (method === "push") {
224
+ for (const input of args) appendWebTaskItem(list, input);
225
+ } else {
226
+ const before = items[0] ?? list.firstChild ?? null;
227
+ for (const input of args) appendWebTaskItem(list, input, before);
228
+ }
229
+ return directWebTaskItems(list).length;
230
+ }
231
+
127
232
  function resizeWebTextarea(element)
128
233
  {
129
234
  if (!element || String(element.tagName ?? "").toLowerCase() !== "textarea") return;
@@ -194,6 +299,57 @@ export function createWebDollar(documentObject = globalThis.document)
194
299
  return args.length > 0 ? selection : "";
195
300
  }
196
301
  },
302
+ push(...items) {
303
+ try {
304
+ const element = findWebDollarElement(documentObject, selectorText, selector);
305
+ return isWebHeading(element) ? mutateWebHeadingList(element, "push", items) : 0;
306
+ } catch {
307
+ return 0;
308
+ }
309
+ },
310
+ pop() {
311
+ try {
312
+ const element = findWebDollarElement(documentObject, selectorText, selector);
313
+ return isWebHeading(element) ? mutateWebHeadingList(element, "pop", []) : undefined;
314
+ } catch {
315
+ return undefined;
316
+ }
317
+ },
318
+ shift() {
319
+ try {
320
+ const element = findWebDollarElement(documentObject, selectorText, selector);
321
+ return isWebHeading(element) ? mutateWebHeadingList(element, "shift", []) : undefined;
322
+ } catch {
323
+ return undefined;
324
+ }
325
+ },
326
+ unshift(...items) {
327
+ try {
328
+ const element = findWebDollarElement(documentObject, selectorText, selector);
329
+ return isWebHeading(element) ? mutateWebHeadingList(element, "unshift", items) : 0;
330
+ } catch {
331
+ return 0;
332
+ }
333
+ },
334
+ splice(...args) {
335
+ try {
336
+ const element = findWebDollarElement(documentObject, selectorText, selector);
337
+ return isWebHeading(element) ? mutateWebHeadingList(element, "splice", args) : [];
338
+ } catch {
339
+ return [];
340
+ }
341
+ },
342
+ slice(...args) {
343
+ try {
344
+ const element = findWebDollarElement(documentObject, selectorText, selector);
345
+ if (!isWebHeading(element)) return [];
346
+ const list = firstHeadingList(element);
347
+ if (!list) return [];
348
+ return directWebTaskItems(list).map(webTaskItemSnapshot).slice(...args);
349
+ } catch {
350
+ return [];
351
+ }
352
+ },
197
353
  };
198
354
  return selection;
199
355
  };
@@ -295,15 +451,18 @@ function safeFrontValue(value)
295
451
  };
296
452
  }
297
453
 
298
- export async function evalFront(mod, text)
454
+ export async function evalFront(mod, text, scope = {})
299
455
  {
300
456
  try{
301
457
 
302
458
  text = text.replace(/^javascript:/, "");
303
459
 
304
- const entries = Object.entries(mod).filter(([name]) => name !== "$");
460
+ const entryMap = new Map(Object.entries(mod).filter(([name]) => name !== "$"));
305
461
  if (typeof globalThis.$ === "function")
306
- entries.push(["$", globalThis.$]);
462
+ entryMap.set("$", globalThis.$);
463
+ for (const [name, value] of Object.entries(scope))
464
+ entryMap.set(name, value);
465
+ const entries = [...entryMap];
307
466
  const names = entries.map(([name]) => name);
308
467
  const values = entries.map(([, value]) => safeFrontValue(value));
309
468
 
@@ -50,7 +50,7 @@ export function main()
50
50
 
51
51
  const basePath = "/" + crypto.randomUUID()
52
52
  const pathname = basePath + "/"
53
- const server = Bun.serve({
53
+ const serverOptions = {
54
54
  port: Number(process.env.PORT || 3000),
55
55
  development: {
56
56
  hmr: true,
@@ -101,9 +101,20 @@ const server = Bun.serve({
101
101
  fetch() {
102
102
  return new Response("Not Found", { status: 404 });
103
103
  },
104
- });
104
+ };
105
+
106
+ let server;
107
+ try {
108
+ server = Bun.serve(serverOptions);
109
+ } catch (error) {
110
+ const addressInUse = error?.code === "EADDRINUSE"
111
+ || error?.cause?.code === "EADDRINUSE"
112
+ || /EADDRINUSE|address already in use/i.test(String(error?.message || error));
113
+ if (serverOptions.port !== 3000 || !addressInUse) throw error;
114
+ server = Bun.serve({ ...serverOptions, port: 0 });
115
+ }
105
116
 
106
- console.log(mda("- Bun RPC server listening on"));
117
+ console.error(mda("- Bun RPC server listening on"));
107
118
  console.log(`http://localhost:${server.port}${pathname}`);
108
119
 
109
120
 
@@ -12,5 +12,49 @@ export function toggleTaskCheckboxBeforeColumn(line, column) {
12
12
  return {
13
13
  line: text.slice(0, checkboxAt) + replacement + text.slice(checkboxAt + 1),
14
14
  toggled: true,
15
+ checkboxAt,
16
+ checked: replacement === "☒",
15
17
  };
16
18
  }
19
+
20
+ export function updateAnsiTaskCheckbox(ansiLine, checkboxAt, checked) {
21
+ const input = String(ansiLine ?? "");
22
+ const target = checked ? "☒" : "☐";
23
+ const sgr = `\x1b[${checked ? "32" : "2"}m`;
24
+ let plainIndex = 0;
25
+
26
+ for (let i = 0; i < input.length;) {
27
+ if (input[i] === "\x1b" && input[i + 1] === "[") {
28
+ let end = i + 2;
29
+ while (end < input.length) {
30
+ const code = input.charCodeAt(end);
31
+ if (code >= 0x40 && code <= 0x7e) { end++; break; }
32
+ end++;
33
+ }
34
+ i = end;
35
+ continue;
36
+ }
37
+ if (input[i] === "\x1b" && input[i + 1] === "]") {
38
+ const bel = input.indexOf("\x07", i + 2);
39
+ const st = input.indexOf("\x1b\\", i + 2);
40
+ if (bel < 0 && st < 0) break;
41
+ i = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st + 2;
42
+ continue;
43
+ }
44
+
45
+ const codePoint = input.codePointAt(i);
46
+ const charLength = codePoint > 0xffff ? 2 : 1;
47
+ if (plainIndex === checkboxAt && (input[i] === "☐" || input[i] === "☒")) {
48
+ const prefix = input.slice(0, i);
49
+ const immediateSgr = prefix.match(/\x1b\[[0-9;]*m$/);
50
+ const beforeStyle = immediateSgr
51
+ ? prefix.slice(0, prefix.length - immediateSgr[0].length) + sgr
52
+ : prefix;
53
+ return beforeStyle + target + input.slice(i + charLength);
54
+ }
55
+ plainIndex += charLength;
56
+ i += charLength;
57
+ }
58
+
59
+ return input;
60
+ }