jsmdcui 0.4.0 → 0.6.3
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 +225 -0
- package/README.md +267 -25
- package/image-processor.md +360 -0
- package/image-processor.zh-TW.md +360 -0
- package/package.json +1 -1
- package/runmd.mjs +147 -10
- package/runtime/help/help.md +267 -25
- package/runtime/syntax/markdown.yaml +12 -0
- package/select.md +61 -0
- package/single-exe/packAssets.sh +1 -1
- package/src/config/defaults.js +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/rpc.mjs +207 -2
- package/src/cui/task-checkbox.mjs +16 -0
- package/src/index.js +256 -22
- package/src/plugins/js-bridge.js +283 -0
- package/testapp.md +23 -0
- package/tests/demo.test.js +110 -1
- package/tests/id-collision.test.js +146 -0
- package/tests/wui.test.js +30 -0
package/src/cui/rpc.mjs
CHANGED
|
@@ -9,6 +9,208 @@ let apilist = new Map()
|
|
|
9
9
|
|
|
10
10
|
export const jss = JSON.stringify
|
|
11
11
|
|
|
12
|
+
function parseDollarIdentity(input, { selector = false } = {})
|
|
13
|
+
{
|
|
14
|
+
const text = String(input ?? "").trim();
|
|
15
|
+
const match = text.match(/^([A-Za-z_][\w:-]*)?(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/);
|
|
16
|
+
if (!match || (!match[1] && !match[2] && !match[3])) return null;
|
|
17
|
+
if (!selector && !match[1]) return null;
|
|
18
|
+
return {
|
|
19
|
+
tag: match[1] || null,
|
|
20
|
+
id: match[2] || null,
|
|
21
|
+
classes: match[3] ? match[3].slice(1).split(".") : [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function matchesDollarIdentity(identity, selector)
|
|
26
|
+
{
|
|
27
|
+
if (selector.tag && identity.tag !== selector.tag) return false;
|
|
28
|
+
if (selector.id && identity.id !== selector.id) return false;
|
|
29
|
+
return selector.classes.every(name => identity.classes.includes(name));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findMarkdownCodeElement(documentObject, selector)
|
|
33
|
+
{
|
|
34
|
+
for (const code of documentObject?.querySelectorAll?.("pre > code") ?? []) {
|
|
35
|
+
for (const className of code.classList ?? []) {
|
|
36
|
+
if (!className.startsWith("language-")) continue;
|
|
37
|
+
const identity = parseDollarIdentity(className.slice("language-".length));
|
|
38
|
+
if (identity && matchesDollarIdentity(identity, selector)) return code;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findWebDollarElement(documentObject, selectorText, selector)
|
|
45
|
+
{
|
|
46
|
+
try {
|
|
47
|
+
const direct = documentObject?.querySelector?.(String(selectorText));
|
|
48
|
+
if (direct) return direct;
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
for (const element of documentObject?.querySelectorAll?.("[data-mdcui-tag]") ?? []) {
|
|
52
|
+
const identity = {
|
|
53
|
+
tag: element.getAttribute?.("data-mdcui-tag") || null,
|
|
54
|
+
id: element.id || null,
|
|
55
|
+
classes: [...(element.classList ?? [])],
|
|
56
|
+
};
|
|
57
|
+
if (matchesDollarIdentity(identity, selector)) return element;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return findMarkdownCodeElement(documentObject, selector);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function webDollarValue(element)
|
|
64
|
+
{
|
|
65
|
+
if (element && "value" in element) return String(element.value ?? "");
|
|
66
|
+
return String(element?.textContent ?? "").replace(/\n$/, "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isWebHeading(element)
|
|
70
|
+
{
|
|
71
|
+
return /^h[1-6]$/i.test(String(element?.tagName ?? ""));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function firstHeadingTaskList(heading)
|
|
75
|
+
{
|
|
76
|
+
for (let sibling = heading?.nextElementSibling; sibling; sibling = sibling.nextElementSibling) {
|
|
77
|
+
if (isWebHeading(sibling) || String(sibling.tagName ?? "").toLowerCase() === "section")
|
|
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");
|
|
84
|
+
if (list) return list;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function directTaskCheckbox(item)
|
|
90
|
+
{
|
|
91
|
+
for (const checkbox of item?.querySelectorAll?.('input[type="checkbox"]') ?? []) {
|
|
92
|
+
if (checkbox.closest?.("li.task-list-item") === item) return checkbox;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function webTaskItemValue(item, checkbox)
|
|
98
|
+
{
|
|
99
|
+
const label = checkbox?.closest?.("label");
|
|
100
|
+
if (label && label.closest?.("li.task-list-item") === item)
|
|
101
|
+
return String(label.textContent ?? "").trim();
|
|
102
|
+
|
|
103
|
+
const copy = item.cloneNode?.(true);
|
|
104
|
+
for (const nested of copy?.querySelectorAll?.("ul, ol") ?? []) nested.remove?.();
|
|
105
|
+
for (const input of copy?.querySelectorAll?.('input[type="checkbox"]') ?? []) input.remove?.();
|
|
106
|
+
return String(copy?.textContent ?? "").trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function webHeadingValue(heading)
|
|
110
|
+
{
|
|
111
|
+
const single = String(heading?.id ?? "").startsWith("select");
|
|
112
|
+
const list = firstHeadingTaskList(heading);
|
|
113
|
+
if (!list) return single ? null : [];
|
|
114
|
+
|
|
115
|
+
const selected = [];
|
|
116
|
+
for (const item of list.children ?? []) {
|
|
117
|
+
if (!item.matches?.("li.task-list-item")) continue;
|
|
118
|
+
const checkbox = directTaskCheckbox(item);
|
|
119
|
+
if (!checkbox?.checked) continue;
|
|
120
|
+
const value = webTaskItemValue(item, checkbox);
|
|
121
|
+
if (single) return value;
|
|
122
|
+
selected.push(value);
|
|
123
|
+
}
|
|
124
|
+
return single ? null : selected;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function resizeWebTextarea(element)
|
|
128
|
+
{
|
|
129
|
+
if (!element || String(element.tagName ?? "").toLowerCase() !== "textarea") return;
|
|
130
|
+
try {
|
|
131
|
+
element.style.height = "auto";
|
|
132
|
+
element.style.height = `${element.scrollHeight}px`;
|
|
133
|
+
const lineHeight = Number.parseFloat(
|
|
134
|
+
element.ownerDocument?.defaultView?.getComputedStyle?.(element)?.lineHeight,
|
|
135
|
+
);
|
|
136
|
+
if (Number.isFinite(lineHeight) && lineHeight > 0)
|
|
137
|
+
element.rows = Math.max(1, Math.ceil(element.scrollHeight / lineHeight));
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installWebTextareaResize(target)
|
|
142
|
+
{
|
|
143
|
+
const documentObject = target?.document;
|
|
144
|
+
if (!documentObject || documentObject.__mdcuiTextareaResizeInstalled) return;
|
|
145
|
+
documentObject.__mdcuiTextareaResizeInstalled = true;
|
|
146
|
+
const resizeAll = () => {
|
|
147
|
+
for (const element of documentObject.querySelectorAll?.("textarea[data-mdcui-tag]") ?? [])
|
|
148
|
+
resizeWebTextarea(element);
|
|
149
|
+
};
|
|
150
|
+
documentObject.addEventListener?.("input", event => {
|
|
151
|
+
if (event.target?.matches?.("textarea[data-mdcui-tag]"))
|
|
152
|
+
resizeWebTextarea(event.target);
|
|
153
|
+
});
|
|
154
|
+
target.addEventListener?.("resize", resizeAll);
|
|
155
|
+
if (documentObject.readyState === "loading")
|
|
156
|
+
documentObject.addEventListener?.("DOMContentLoaded", resizeAll, { once: true });
|
|
157
|
+
else
|
|
158
|
+
queueMicrotask(resizeAll);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createWebDollar(documentObject = globalThis.document)
|
|
162
|
+
{
|
|
163
|
+
return function $(selectorText) {
|
|
164
|
+
const selector = parseDollarIdentity(selectorText, { selector: true });
|
|
165
|
+
const selection = {
|
|
166
|
+
html() {
|
|
167
|
+
try {
|
|
168
|
+
const element = findWebDollarElement(documentObject, selectorText, selector);
|
|
169
|
+
return element ? String(element.innerHTML ?? "") : "";
|
|
170
|
+
} catch {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
val(...args) {
|
|
175
|
+
try {
|
|
176
|
+
const element = findWebDollarElement(documentObject, selectorText, selector);
|
|
177
|
+
if (!element) return args.length > 0 ? selection : "";
|
|
178
|
+
if (isWebHeading(element)) {
|
|
179
|
+
if (args.length > 0) return selection;
|
|
180
|
+
return webHeadingValue(element);
|
|
181
|
+
}
|
|
182
|
+
if (!selector) return args.length > 0 ? selection : "";
|
|
183
|
+
if (args.length > 0) {
|
|
184
|
+
const value = String(args[0] ?? "");
|
|
185
|
+
if ("value" in element) {
|
|
186
|
+
element.value = value;
|
|
187
|
+
resizeWebTextarea(element);
|
|
188
|
+
}
|
|
189
|
+
else element.textContent = value;
|
|
190
|
+
return selection;
|
|
191
|
+
}
|
|
192
|
+
return webDollarValue(element);
|
|
193
|
+
} catch {
|
|
194
|
+
return args.length > 0 ? selection : "";
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
return selection;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function installWebDollar(target = globalThis)
|
|
203
|
+
{
|
|
204
|
+
if (!target?.document) return target?.$;
|
|
205
|
+
const $ = createWebDollar(target.document);
|
|
206
|
+
target.$ = $;
|
|
207
|
+
installWebTextareaResize(target);
|
|
208
|
+
return $;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof globalThis.document !== "undefined")
|
|
212
|
+
installWebDollar(globalThis);
|
|
213
|
+
|
|
12
214
|
|
|
13
215
|
export async function evalBack(backmod, qjson)
|
|
14
216
|
{
|
|
@@ -99,8 +301,11 @@ try{
|
|
|
99
301
|
|
|
100
302
|
text = text.replace(/^javascript:/, "");
|
|
101
303
|
|
|
102
|
-
const
|
|
103
|
-
|
|
304
|
+
const entries = Object.entries(mod).filter(([name]) => name !== "$");
|
|
305
|
+
if (typeof globalThis.$ === "function")
|
|
306
|
+
entries.push(["$", globalThis.$]);
|
|
307
|
+
const names = entries.map(([name]) => name);
|
|
308
|
+
const values = entries.map(([, value]) => safeFrontValue(value));
|
|
104
309
|
|
|
105
310
|
try {
|
|
106
311
|
return await new AsyncFunction(...names, `return await (${text})`)(...values);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function toggleTaskCheckboxBeforeColumn(line, column) {
|
|
2
|
+
const text = String(line ?? "");
|
|
3
|
+
const end = Math.max(0, Math.min(text.length, Math.trunc(Number(column) || 0) + 1));
|
|
4
|
+
const prefix = text.slice(0, end);
|
|
5
|
+
const uncheckedAt = prefix.lastIndexOf("☐");
|
|
6
|
+
const checkedAt = prefix.lastIndexOf("☒");
|
|
7
|
+
const checkboxAt = Math.max(uncheckedAt, checkedAt);
|
|
8
|
+
|
|
9
|
+
if (checkboxAt < 0) return { line: text, toggled: false };
|
|
10
|
+
|
|
11
|
+
const replacement = text[checkboxAt] === "☐" ? "☒" : "☐";
|
|
12
|
+
return {
|
|
13
|
+
line: text.slice(0, checkboxAt) + replacement + text.slice(checkboxAt + 1),
|
|
14
|
+
toggled: true,
|
|
15
|
+
};
|
|
16
|
+
}
|