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.
- package/CHANGELOG.md +121 -0
- package/README.md +216 -33
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/{image-processor.md → demos/image-processor.md} +1 -0
- package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
- package/demos/maze.md +144 -0
- package/{select.md → demos/select.md} +2 -0
- package/demos/todo-zh.md +190 -0
- package/demos/todo.md +191 -0
- package/edit +9 -0
- package/package.json +1 -1
- package/runmd.mjs +77 -16
- package/runtime/help/help.md +216 -33
- package/runtime/jsplugins/cdp/cdp.js +5 -2
- package/runtime/jsplugins/chapter/chapter.js +4 -2
- package/runtime/jsplugins/example/example.js +10 -7
- package/runtime/syntax/markdown.yaml +1 -0
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/fence-events.mjs +106 -0
- package/src/cui/id-collision.mjs +10 -28
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +200 -0
- package/src/cui/rpc.mjs +169 -10
- package/src/cui/server.mjs +14 -3
- package/src/cui/task-checkbox.mjs +44 -0
- package/src/index.js +615 -101
- package/src/platform/terminal.js +5 -0
- package/src/plugins/js-bridge.js +354 -21
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +99 -9
- package/tests/fence-events.test.js +405 -0
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +14 -2
- package/tests/js-plugin-prompts.test.js +46 -0
- package/tests/key-event.md +10 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +169 -0
- package/tests/platform-terminal.test.js +8 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/textarea.md +8 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +16 -1
- package/tui +4 -2
- 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
|
|
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
|
-
|
|
80
|
-
|
|
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 =
|
|
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
|
|
460
|
+
const entryMap = new Map(Object.entries(mod).filter(([name]) => name !== "$"));
|
|
305
461
|
if (typeof globalThis.$ === "function")
|
|
306
|
-
|
|
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
|
|
package/src/cui/server.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function main()
|
|
|
50
50
|
|
|
51
51
|
const basePath = "/" + crypto.randomUUID()
|
|
52
52
|
const pathname = basePath + "/"
|
|
53
|
-
const
|
|
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.
|
|
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
|
+
}
|