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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env jsmdcui
|
|
2
|
+
# Bun.Image Processor
|
|
3
|
+
|
|
4
|
+
先把本機圖片路徑貼到下方(例如 `/home/me/photo.jpg`)。輸出檔會放在原圖旁邊,檔名為 `original.resized.jpg` 或 `original.resized.png`;原圖不會被覆寫。
|
|
5
|
+
|
|
6
|
+
```text#image-path
|
|
7
|
+
demo.jpg
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- [讀取圖片 metadata](javascript:readMetadata())
|
|
11
|
+
|
|
12
|
+
```text#image-metadata
|
|
13
|
+
尚未讀取 metadata
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 尺寸
|
|
17
|
+
|
|
18
|
+
寬度(必填,正整數):
|
|
19
|
+
|
|
20
|
+
```text#resize-width
|
|
21
|
+
800
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
高度(留白會保持原始長寬比):
|
|
25
|
+
|
|
26
|
+
```text#resize-height
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Select Fit
|
|
31
|
+
|
|
32
|
+
- [ ] fill(精確填滿寬高,可能變形)
|
|
33
|
+
- [x] inside(保持比例,縮放到指定範圍內)
|
|
34
|
+
|
|
35
|
+
## Select Filter
|
|
36
|
+
|
|
37
|
+
- [x] lanczos3(照片通用,預設)
|
|
38
|
+
- [ ] lanczos2(較柔和、較少光暈)
|
|
39
|
+
- [ ] mitchell(平滑漸層)
|
|
40
|
+
- [ ] cubic(較銳利)
|
|
41
|
+
- [ ] mks2013(Magic Kernel Sharp)
|
|
42
|
+
- [ ] mks2021(Magic Kernel Sharp)
|
|
43
|
+
- [ ] bilinear(快速、柔和)
|
|
44
|
+
- [ ] linear(快速、柔和)
|
|
45
|
+
- [ ] box(大倍率整數縮小)
|
|
46
|
+
- [ ] nearest(像素圖、硬邊緣)
|
|
47
|
+
|
|
48
|
+
## Select Without Enlargement
|
|
49
|
+
|
|
50
|
+
- [x] yes(不放大較小的原圖)
|
|
51
|
+
- [ ] no(允許放大)
|
|
52
|
+
|
|
53
|
+
## 方向與翻轉
|
|
54
|
+
|
|
55
|
+
## Select Auto Orient
|
|
56
|
+
|
|
57
|
+
- [x] yes(依 JPEG EXIF 自動校正方向)
|
|
58
|
+
- [ ] no
|
|
59
|
+
|
|
60
|
+
## Select Rotate
|
|
61
|
+
|
|
62
|
+
- [x] 0°
|
|
63
|
+
- [ ] 90°
|
|
64
|
+
- [ ] 180°
|
|
65
|
+
- [ ] 270°
|
|
66
|
+
|
|
67
|
+
## Select Flip
|
|
68
|
+
|
|
69
|
+
- [ ] yes(上下翻轉)
|
|
70
|
+
- [x] no
|
|
71
|
+
|
|
72
|
+
## Select Flop
|
|
73
|
+
|
|
74
|
+
- [ ] yes(左右翻轉)
|
|
75
|
+
- [x] no
|
|
76
|
+
|
|
77
|
+
## 色彩
|
|
78
|
+
|
|
79
|
+
亮度倍率(`1` 不變):
|
|
80
|
+
|
|
81
|
+
```text#brightness
|
|
82
|
+
1
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
飽和度倍率(`0` 灰階、`1` 不變、大於 `1` 增豔):
|
|
86
|
+
|
|
87
|
+
```text#saturation
|
|
88
|
+
1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Select Output Format
|
|
92
|
+
|
|
93
|
+
- [x] JPEG (.jpg)
|
|
94
|
+
- [ ] PNG (.png)
|
|
95
|
+
|
|
96
|
+
## JPEG 選項
|
|
97
|
+
|
|
98
|
+
品質(`1`–`100`,預設 `80`):
|
|
99
|
+
|
|
100
|
+
```text#jpeg-quality
|
|
101
|
+
80
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Select Progressive JPEG
|
|
105
|
+
|
|
106
|
+
- [ ] yes
|
|
107
|
+
- [x] no
|
|
108
|
+
|
|
109
|
+
## PNG 選項
|
|
110
|
+
|
|
111
|
+
壓縮等級(`0`–`9`,預設 `6`):
|
|
112
|
+
|
|
113
|
+
```text#png-compression
|
|
114
|
+
6
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Select PNG Palette
|
|
118
|
+
|
|
119
|
+
- [ ] yes(索引色 PNG)
|
|
120
|
+
- [x] no(全彩 PNG)
|
|
121
|
+
|
|
122
|
+
調色盤色數(啟用 Palette 時使用,`2`–`256`):
|
|
123
|
+
|
|
124
|
+
```text#png-colors
|
|
125
|
+
256
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Select PNG Dither
|
|
129
|
+
|
|
130
|
+
- [x] yes
|
|
131
|
+
- [ ] no
|
|
132
|
+
|
|
133
|
+
- [Resize and write image](javascript:resizeAndWrite())
|
|
134
|
+
|
|
135
|
+
## 寫入狀態
|
|
136
|
+
|
|
137
|
+
```text#write-status
|
|
138
|
+
尚未執行
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```js front
|
|
142
|
+
function firstWord(value) {
|
|
143
|
+
return String(value ?? '').trim().split(/\s+/)[0].toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function includesChoice(id, choices, fallback) {
|
|
147
|
+
const value = String($('#' + id).val() ?? '').toLowerCase();
|
|
148
|
+
return choices.find(choice => value.includes(choice)) ?? fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function yes(id) {
|
|
152
|
+
return String($('#' + id).val() ?? '').toLowerCase().includes('yes');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function numberText(id) {
|
|
156
|
+
return $('#' + id).val().trim();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function describeError(error) {
|
|
160
|
+
if (error == null) return '未知錯誤(沒有錯誤內容)';
|
|
161
|
+
if (typeof error === 'string') return error;
|
|
162
|
+
const details = [];
|
|
163
|
+
if (error?.code) details.push(`錯誤代碼:${error.code}`);
|
|
164
|
+
if (error?.name && error.name !== 'Error') details.push(`類型:${error.name}`);
|
|
165
|
+
if (error?.message) details.push(`訊息:${error.message}`);
|
|
166
|
+
if (error?.error && error.error !== error) details.push(`錯誤:${describeError(error.error)}`);
|
|
167
|
+
if (error?.cause && error.cause !== error) details.push(`原因:${describeError(error.cause)}`);
|
|
168
|
+
if (error?.stack && String(error.stack) !== String(error.message ?? ''))
|
|
169
|
+
details.push(`Stack:${error.stack}`);
|
|
170
|
+
if (details.length) return details.join('\n');
|
|
171
|
+
try {
|
|
172
|
+
const json = JSON.stringify(error, null, 2);
|
|
173
|
+
if (json && json !== '{}') return json;
|
|
174
|
+
} catch {}
|
|
175
|
+
return String(error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function readMetadata() {
|
|
179
|
+
const output = $('#image-metadata');
|
|
180
|
+
output.val('正在讀取 metadata…');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const inputPath = $('#image-path').val().trim();
|
|
184
|
+
const result = await rpc.readImageMetadata(inputPath);
|
|
185
|
+
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
186
|
+
const error = result && typeof result === 'object' && 'error' in result
|
|
187
|
+
? result.error
|
|
188
|
+
: result;
|
|
189
|
+
output.val(`讀取 metadata 失敗:\n${describeError(error)}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
output.val(`圖片路徑:${result.inputPath}\n${JSON.stringify(result.metadata, null, 2)}`);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
output.val(`讀取 metadata 失敗:\n${describeError(error)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function resizeAndWrite() {
|
|
199
|
+
const status = $('#write-status');
|
|
200
|
+
status.val('處理中…');
|
|
201
|
+
let options;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
options = {
|
|
205
|
+
inputPath: $('#image-path').val().trim(),
|
|
206
|
+
width: numberText('resize-width'),
|
|
207
|
+
height: numberText('resize-height'),
|
|
208
|
+
fit: includesChoice('select-fit', ['fill', 'inside'], 'inside'),
|
|
209
|
+
filter: includesChoice('select-filter', [
|
|
210
|
+
'nearest', 'box', 'bilinear', 'cubic', 'mitchell',
|
|
211
|
+
'lanczos2', 'lanczos3', 'mks2013', 'mks2021', 'linear',
|
|
212
|
+
], 'lanczos3'),
|
|
213
|
+
withoutEnlargement: yes('select-without-enlargement'),
|
|
214
|
+
autoOrient: yes('select-auto-orient'),
|
|
215
|
+
rotate: Number.parseInt(firstWord($('#select-rotate').val()), 10),
|
|
216
|
+
flip: yes('select-flip'),
|
|
217
|
+
flop: yes('select-flop'),
|
|
218
|
+
brightness: numberText('brightness'),
|
|
219
|
+
saturation: numberText('saturation'),
|
|
220
|
+
format: includesChoice('select-output-format', ['png', 'jpeg'], 'jpeg'),
|
|
221
|
+
jpegQuality: numberText('jpeg-quality'),
|
|
222
|
+
progressive: yes('select-progressive-jpeg'),
|
|
223
|
+
pngCompression: numberText('png-compression'),
|
|
224
|
+
pngPalette: yes('select-png-palette'),
|
|
225
|
+
pngColors: numberText('png-colors'),
|
|
226
|
+
pngDither: yes('select-png-dither'),
|
|
227
|
+
};
|
|
228
|
+
const result = await rpc.resizeImage(options);
|
|
229
|
+
|
|
230
|
+
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
231
|
+
const error = result && typeof result === 'object' && 'error' in result
|
|
232
|
+
? result.error
|
|
233
|
+
: result;
|
|
234
|
+
status.val(`寫入失敗:\n${describeError(error)}\n讀取選項:\n${JSON.stringify(options, null, 2)}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
status.val(`成功寫入:${result.outputPath}\n${result.width}×${result.height},${result.bytes} bytes\n讀取選項:\n${JSON.stringify(options, null, 2)}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const optionText = options ? `\n讀取選項:\n${JSON.stringify(options, null, 2)}` : '';
|
|
241
|
+
status.val(`寫入失敗:\n${describeError(error)}${optionText}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```js back
|
|
247
|
+
import { dirname, extname, join, basename, resolve } from 'node:path';
|
|
248
|
+
|
|
249
|
+
function integer(value, name, { min = 1, max = Number.MAX_SAFE_INTEGER, optional = false } = {}) {
|
|
250
|
+
if (optional && String(value ?? '').trim() === '') return undefined;
|
|
251
|
+
const result = Number(value);
|
|
252
|
+
if (!Number.isInteger(result) || result < min || result > max)
|
|
253
|
+
throw new Error(`${name} 必須是 ${min}–${max} 的整數`);
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function finite(value, name, { min = 0 } = {}) {
|
|
258
|
+
const result = Number(value);
|
|
259
|
+
if (!Number.isFinite(result) || result < min)
|
|
260
|
+
throw new Error(`${name} 必須是至少 ${min} 的數字`);
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function describeBackendError(error) {
|
|
265
|
+
if (error == null) return '未知錯誤(沒有錯誤內容)';
|
|
266
|
+
if (typeof error === 'string') return error;
|
|
267
|
+
const details = [];
|
|
268
|
+
if (error?.code) details.push(`[${error.code}]`);
|
|
269
|
+
if (error?.name && error.name !== 'Error') details.push(error.name);
|
|
270
|
+
if (error?.message) details.push(error.message);
|
|
271
|
+
if (error?.cause && error.cause !== error)
|
|
272
|
+
details.push(`原因:${describeBackendError(error.cause)}`);
|
|
273
|
+
if (error?.stack) details.push(`Stack:\n${error.stack}`);
|
|
274
|
+
if (details.length) return details.join('\n');
|
|
275
|
+
try {
|
|
276
|
+
const json = JSON.stringify(error, null, 2);
|
|
277
|
+
if (json && json !== '{}') return json;
|
|
278
|
+
} catch {}
|
|
279
|
+
return String(error);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function readImageMetadata(inputText) {
|
|
283
|
+
try {
|
|
284
|
+
const pathText = String(inputText ?? '').trim();
|
|
285
|
+
if (!pathText) throw new Error('請先貼上圖片路徑');
|
|
286
|
+
|
|
287
|
+
const inputPath = resolve(pathText);
|
|
288
|
+
const inputFile = Bun.file(inputPath);
|
|
289
|
+
if (!await inputFile.exists()) throw new Error(`找不到輸入檔:${inputPath}`);
|
|
290
|
+
|
|
291
|
+
const metadata = await new Bun.Image(inputFile).metadata();
|
|
292
|
+
return { ok: true, inputPath, metadata };
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return { ok: false, error: describeBackendError(error) };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function resizeImage(options = {}) {
|
|
299
|
+
try {
|
|
300
|
+
const inputText = String(options.inputPath ?? '').trim();
|
|
301
|
+
if (!inputText) throw new Error('請先貼上圖片路徑');
|
|
302
|
+
|
|
303
|
+
const inputPath = resolve(inputText);
|
|
304
|
+
const inputFile = Bun.file(inputPath);
|
|
305
|
+
if (!await inputFile.exists()) throw new Error(`找不到輸入檔:${inputPath}`);
|
|
306
|
+
|
|
307
|
+
const width = integer(options.width, '寬度');
|
|
308
|
+
const height = integer(options.height, '高度', { optional: true });
|
|
309
|
+
const fit = options.fit === 'fill' ? 'fill' : 'inside';
|
|
310
|
+
const filters = new Set([
|
|
311
|
+
'nearest', 'box', 'bilinear', 'linear', 'cubic', 'mitchell',
|
|
312
|
+
'lanczos2', 'lanczos3', 'mks2013', 'mks2021',
|
|
313
|
+
]);
|
|
314
|
+
const filter = filters.has(options.filter) ? options.filter : 'lanczos3';
|
|
315
|
+
const rotate = [0, 90, 180, 270].includes(options.rotate) ? options.rotate : 0;
|
|
316
|
+
const brightness = finite(options.brightness, '亮度');
|
|
317
|
+
const saturation = finite(options.saturation, '飽和度');
|
|
318
|
+
const format = options.format === 'png' ? 'png' : 'jpeg';
|
|
319
|
+
|
|
320
|
+
let image = new Bun.Image(inputFile, { autoOrient: options.autoOrient !== false });
|
|
321
|
+
if (rotate) image = image.rotate(rotate);
|
|
322
|
+
if (options.flip) image = image.flip();
|
|
323
|
+
if (options.flop) image = image.flop();
|
|
324
|
+
image = image.resize(width, height, {
|
|
325
|
+
fit,
|
|
326
|
+
filter,
|
|
327
|
+
withoutEnlargement: Boolean(options.withoutEnlargement),
|
|
328
|
+
});
|
|
329
|
+
if (brightness !== 1 || saturation !== 1)
|
|
330
|
+
image = image.modulate({ brightness, saturation });
|
|
331
|
+
|
|
332
|
+
const originalName = basename(inputPath, extname(inputPath));
|
|
333
|
+
const outputPath = join(dirname(inputPath), `${originalName}.resized.${format === 'jpeg' ? 'jpg' : 'png'}`);
|
|
334
|
+
|
|
335
|
+
if (format === 'jpeg') {
|
|
336
|
+
image = image.jpeg({
|
|
337
|
+
quality: integer(options.jpegQuality, 'JPEG 品質', { min: 1, max: 100 }),
|
|
338
|
+
progressive: Boolean(options.progressive),
|
|
339
|
+
});
|
|
340
|
+
} else {
|
|
341
|
+
const palette = Boolean(options.pngPalette);
|
|
342
|
+
const pngOptions = {
|
|
343
|
+
compressionLevel: integer(options.pngCompression, 'PNG 壓縮等級', { min: 0, max: 9 }),
|
|
344
|
+
palette,
|
|
345
|
+
};
|
|
346
|
+
if (palette) {
|
|
347
|
+
pngOptions.colors = integer(options.pngColors, 'PNG 調色盤色數', { min: 2, max: 256 });
|
|
348
|
+
pngOptions.dither = Boolean(options.pngDither);
|
|
349
|
+
}
|
|
350
|
+
image = image.png(pngOptions);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const bytes = await image.write(outputPath);
|
|
354
|
+
const metadata = await new Bun.Image(outputPath).metadata();
|
|
355
|
+
return { ok: true, outputPath, bytes, width: metadata.width, height: metadata.height };
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return { ok: false, error: describeBackendError(error) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
```
|
package/package.json
CHANGED
package/runmd.mjs
CHANGED
|
@@ -27,16 +27,23 @@ async function readTemplate(pathname)
|
|
|
27
27
|
await Bun.file(path.join(REPO_ROOT, pathname)).text()
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async function readMarkdownInput(mdpath)
|
|
30
|
+
export async function readMarkdownInput(mdpath)
|
|
31
31
|
{
|
|
32
32
|
const file = Bun.file(mdpath);
|
|
33
33
|
if (await file.exists()) return await file.text();
|
|
34
34
|
const assetName = path.basename(mdpath);
|
|
35
35
|
const internalText = readInternalAssetText(assetName);
|
|
36
|
-
if (internalText != null)
|
|
36
|
+
if (internalText != null) {
|
|
37
|
+
await Bun.write(mdpath, internalText);
|
|
38
|
+
return internalText;
|
|
39
|
+
}
|
|
37
40
|
const fallbackPath = path.join(REPO_ROOT, assetName);
|
|
38
41
|
const fallback = Bun.file(fallbackPath);
|
|
39
|
-
if (await fallback.exists())
|
|
42
|
+
if (await fallback.exists()) {
|
|
43
|
+
const fallbackText = await fallback.text();
|
|
44
|
+
await Bun.write(mdpath, fallbackText);
|
|
45
|
+
return fallbackText;
|
|
46
|
+
}
|
|
40
47
|
throw new Error(`md file not found: ${mdpath}`);
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -189,6 +196,110 @@ export function createTui(md,TERMINAL_WIDTH=30) // ANSI Colors
|
|
|
189
196
|
)
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
function parseWuiControlIdentity(info)
|
|
200
|
+
{
|
|
201
|
+
const match = String(info ?? "").match(/^([A-Za-z_][\w:-]*)(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/);
|
|
202
|
+
if (!match || !["text", "textarea"].includes(match[1])) return null;
|
|
203
|
+
return {
|
|
204
|
+
tag: match[1],
|
|
205
|
+
id: match[2] || "",
|
|
206
|
+
classes: match[3] ? match[3].slice(1).split(".") : [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function escapeHtmlAttribute(value)
|
|
211
|
+
{
|
|
212
|
+
return String(value)
|
|
213
|
+
.replaceAll("&", "&")
|
|
214
|
+
.replaceAll('"', """)
|
|
215
|
+
.replaceAll("<", "<")
|
|
216
|
+
.replaceAll(">", ">");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function convertWuiTextareas(html)
|
|
220
|
+
{
|
|
221
|
+
return String(html).replace(
|
|
222
|
+
/<pre><code class="language-([^"]+)">([^]*?)<\/code><\/pre>/g,
|
|
223
|
+
(whole, info, content) => {
|
|
224
|
+
const identity = parseWuiControlIdentity(info);
|
|
225
|
+
if (!identity) return whole;
|
|
226
|
+
const value = content.replace(/\n$/, "");
|
|
227
|
+
const contentLines = value.split("\n");
|
|
228
|
+
const cols = Math.max(1, ...contentLines.map(line => [...line].length));
|
|
229
|
+
const attrs = [
|
|
230
|
+
`data-mdcui-tag="${identity.tag}"`,
|
|
231
|
+
`data-mdcui-language="${escapeHtmlAttribute(info)}"`,
|
|
232
|
+
identity.id ? `id="${escapeHtmlAttribute(identity.id)}"` : "",
|
|
233
|
+
`class="${escapeHtmlAttribute([
|
|
234
|
+
`language-${info}`,
|
|
235
|
+
...identity.classes,
|
|
236
|
+
].join(" "))}"`,
|
|
237
|
+
`rows="${Math.max(1, contentLines.length)}"`,
|
|
238
|
+
`cols="${cols}"`,
|
|
239
|
+
'wrap="soft"',
|
|
240
|
+
'style="box-sizing:border-box;max-width:100%;width:100%;resize:vertical;overflow-y:hidden"',
|
|
241
|
+
].filter(Boolean).join(" ");
|
|
242
|
+
return `<textarea ${attrs}>${value}</textarea>`;
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function wrapWuiHeadingSections(html)
|
|
248
|
+
{
|
|
249
|
+
const input = String(html);
|
|
250
|
+
const bodyStart = input.match(/<body\b[^>]*>/i);
|
|
251
|
+
if (bodyStart?.index != null) {
|
|
252
|
+
const contentStart = bodyStart.index + bodyStart[0].length;
|
|
253
|
+
const bodyEnd = input.slice(contentStart).search(/<\/body\s*>/i);
|
|
254
|
+
if (bodyEnd >= 0) {
|
|
255
|
+
const contentEnd = contentStart + bodyEnd;
|
|
256
|
+
return input.slice(0, contentStart) +
|
|
257
|
+
wrapWuiHeadingSections(input.slice(contentStart, contentEnd)) +
|
|
258
|
+
input.slice(contentEnd);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let markerPrefix = "MDCUI_HEADING_OPAQUE_";
|
|
263
|
+
while (input.includes(markerPrefix)) markerPrefix = "_" + markerPrefix;
|
|
264
|
+
const opaqueHtml = [];
|
|
265
|
+
const searchable = input.replace(
|
|
266
|
+
/<!--[^]*?-->|<(script|style|pre|code|textarea|template)\b[^>]*>[^]*?<\/\1\s*>/gi,
|
|
267
|
+
(whole) => {
|
|
268
|
+
const marker = `\0${markerPrefix}${opaqueHtml.length}\0`;
|
|
269
|
+
opaqueHtml.push({ marker, html: whole });
|
|
270
|
+
return marker;
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const heading = /<h([1-6])\b[^>]*>[^]*?<\/h\1\s*>/gi;
|
|
275
|
+
const openLevels = [];
|
|
276
|
+
let output = "";
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
let match;
|
|
279
|
+
|
|
280
|
+
while ((match = heading.exec(searchable)) !== null) {
|
|
281
|
+
const level = Number(match[1]);
|
|
282
|
+
output += searchable.slice(cursor, match.index);
|
|
283
|
+
while (openLevels.length && openLevels.at(-1) >= level) {
|
|
284
|
+
output += "</section>\n";
|
|
285
|
+
openLevels.pop();
|
|
286
|
+
}
|
|
287
|
+
output += `<section>\n${match[0]}`;
|
|
288
|
+
openLevels.push(level);
|
|
289
|
+
cursor = heading.lastIndex;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
output += searchable.slice(cursor);
|
|
293
|
+
while (openLevels.length) {
|
|
294
|
+
output += "</section>\n";
|
|
295
|
+
openLevels.pop();
|
|
296
|
+
}
|
|
297
|
+
for (const opaque of opaqueHtml) {
|
|
298
|
+
output = output.replace(opaque.marker, opaque.html);
|
|
299
|
+
}
|
|
300
|
+
return output;
|
|
301
|
+
}
|
|
302
|
+
|
|
192
303
|
export async function createWui(md,mdpath) // HTML
|
|
193
304
|
{
|
|
194
305
|
|
|
@@ -209,16 +320,42 @@ export async function createWui(md,mdpath) // HTML
|
|
|
209
320
|
'class="task-list-item-checkbox" disabled',
|
|
210
321
|
'class="task-list-item-checkbox"'
|
|
211
322
|
)
|
|
323
|
+
|
|
324
|
+
const taskItemStart = '(<li\\b(?=[^>]*\\bclass="[^"]*\\btask-list-item\\b[^"]*")[^>]*>\\s*)'
|
|
325
|
+
const taskCheckbox = '(<input\\b(?=[^>]*\\btype="checkbox")[^>]*>)'
|
|
326
|
+
md = md.replace(
|
|
327
|
+
new RegExp(taskItemStart + taskCheckbox + '([^<]*(?:<p>[^]*?<\\/p>[^<]*)*)', 'g'),
|
|
328
|
+
(whole, itemStart, checkbox, content) => {
|
|
329
|
+
if (!content.trim()) return whole
|
|
330
|
+
return `${itemStart}<label>${checkbox}${content}</label>`
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
md = convertWuiTextareas(md)
|
|
335
|
+
md = wrapWuiHeadingSections(md)
|
|
212
336
|
|
|
213
337
|
const mdb = path.basename(mdpath);
|
|
214
338
|
|
|
215
|
-
|
|
216
|
-
md
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
339
|
+
const moduleScript = `<scr`+`ipt type="module" src="./${mdb}.front.js"></scr`+`ipt>`;
|
|
340
|
+
if (!/^\s*<!doctype html>/i.test(md)) {
|
|
341
|
+
md = `<!doctype html>
|
|
342
|
+
<html lang="zh-TW">
|
|
343
|
+
<head>
|
|
344
|
+
<meta charset="utf-8">
|
|
345
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
346
|
+
<title>${escapeHtmlAttribute(mdb)}</title>
|
|
347
|
+
</head>
|
|
348
|
+
<body>
|
|
349
|
+
${md}
|
|
350
|
+
${moduleScript}
|
|
351
|
+
</body>
|
|
352
|
+
</html>
|
|
353
|
+
`;
|
|
354
|
+
} else if (/<\/body\s*>/i.test(md)) {
|
|
355
|
+
md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
|
|
356
|
+
} else {
|
|
357
|
+
md += `\n${moduleScript}\n`;
|
|
358
|
+
}
|
|
222
359
|
|
|
223
360
|
|
|
224
361
|
await Bun.write(mdpath+".html",md)
|