jsmdcui 0.5.0 → 0.7.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 +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
package/runmd.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { readInternalAssetText } from './src/runtime/assets.js'
|
|
|
6
6
|
import { REPO_ROOT } from './single-exe/compiled.js'
|
|
7
7
|
|
|
8
8
|
const csl=console.log
|
|
9
|
+
const cse=console.error
|
|
9
10
|
const mda=Bun.markdown.ansi
|
|
10
11
|
const mdh=Bun.markdown.html
|
|
11
12
|
const jss=JSON.stringify
|
|
@@ -18,7 +19,7 @@ const TEST_COL=5
|
|
|
18
19
|
function logWroteFile(label,path)
|
|
19
20
|
{
|
|
20
21
|
if(!process.stdin.isRaw)
|
|
21
|
-
|
|
22
|
+
cse(mda(`- Wrote to ${label} file: ${path}`))
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function readTemplate(pathname)
|
|
@@ -27,16 +28,23 @@ async function readTemplate(pathname)
|
|
|
27
28
|
await Bun.file(path.join(REPO_ROOT, pathname)).text()
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async function readMarkdownInput(mdpath)
|
|
31
|
+
export async function readMarkdownInput(mdpath)
|
|
31
32
|
{
|
|
32
33
|
const file = Bun.file(mdpath);
|
|
33
34
|
if (await file.exists()) return await file.text();
|
|
34
35
|
const assetName = path.basename(mdpath);
|
|
35
36
|
const internalText = readInternalAssetText(assetName);
|
|
36
|
-
if (internalText != null)
|
|
37
|
+
if (internalText != null) {
|
|
38
|
+
await Bun.write(mdpath, internalText);
|
|
39
|
+
return internalText;
|
|
40
|
+
}
|
|
37
41
|
const fallbackPath = path.join(REPO_ROOT, assetName);
|
|
38
42
|
const fallback = Bun.file(fallbackPath);
|
|
39
|
-
if (await fallback.exists())
|
|
43
|
+
if (await fallback.exists()) {
|
|
44
|
+
const fallbackText = await fallback.text();
|
|
45
|
+
await Bun.write(mdpath, fallbackText);
|
|
46
|
+
return fallbackText;
|
|
47
|
+
}
|
|
40
48
|
throw new Error(`md file not found: ${mdpath}`);
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -55,32 +63,35 @@ export async function main(tuiWidth=30)
|
|
|
55
63
|
|
|
56
64
|
// 3. Create Terminal UI
|
|
57
65
|
let tui = createTui(md,tuiWidth)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
cse(mda("\n# TUI"))
|
|
67
|
+
cse(tui)
|
|
68
|
+
cse(mda('## TUI raw'))
|
|
69
|
+
cse(jss(tui))
|
|
62
70
|
|
|
63
71
|
|
|
64
72
|
// 4. Create Web UI
|
|
65
73
|
let wui = await createWui(md,mdpath)
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
cse(mda('\n# HTML'))
|
|
75
|
+
cse(wui)
|
|
68
76
|
|
|
69
77
|
|
|
78
|
+
/*
|
|
70
79
|
// 5. Get character from point for TUI
|
|
71
80
|
let ch = charFromPoint(tui,TEST_ROW,TEST_COL)
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
cse(mda(
|
|
74
83
|
'# Slicing row,col: '+TEST_ROW+','+TEST_COL
|
|
75
84
|
))
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
cse(jss(ch))
|
|
87
|
+
|
|
88
|
+
*/
|
|
78
89
|
|
|
79
90
|
|
|
80
91
|
const serverPath = mdpath + "-server.js"
|
|
81
92
|
const svmod = await import(pathToFileURL(path.resolve(serverPath)).href)
|
|
82
93
|
|
|
83
|
-
|
|
94
|
+
cse("\n\n"+mda('# Server'))
|
|
84
95
|
svmod.main();
|
|
85
96
|
}
|
|
86
97
|
|
|
@@ -237,6 +248,62 @@ export function convertWuiTextareas(html)
|
|
|
237
248
|
);
|
|
238
249
|
}
|
|
239
250
|
|
|
251
|
+
export function wrapWuiHeadingSections(html)
|
|
252
|
+
{
|
|
253
|
+
const input = String(html);
|
|
254
|
+
const bodyStart = input.match(/<body\b[^>]*>/i);
|
|
255
|
+
if (bodyStart?.index != null) {
|
|
256
|
+
const contentStart = bodyStart.index + bodyStart[0].length;
|
|
257
|
+
const bodyEnd = input.slice(contentStart).search(/<\/body\s*>/i);
|
|
258
|
+
if (bodyEnd >= 0) {
|
|
259
|
+
const contentEnd = contentStart + bodyEnd;
|
|
260
|
+
return input.slice(0, contentStart) +
|
|
261
|
+
wrapWuiHeadingSections(input.slice(contentStart, contentEnd)) +
|
|
262
|
+
input.slice(contentEnd);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let markerPrefix = "MDCUI_HEADING_OPAQUE_";
|
|
267
|
+
while (input.includes(markerPrefix)) markerPrefix = "_" + markerPrefix;
|
|
268
|
+
const opaqueHtml = [];
|
|
269
|
+
const searchable = input.replace(
|
|
270
|
+
/<!--[^]*?-->|<(script|style|pre|code|textarea|template)\b[^>]*>[^]*?<\/\1\s*>/gi,
|
|
271
|
+
(whole) => {
|
|
272
|
+
const marker = `\0${markerPrefix}${opaqueHtml.length}\0`;
|
|
273
|
+
opaqueHtml.push({ marker, html: whole });
|
|
274
|
+
return marker;
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const heading = /<h([1-6])\b[^>]*>[^]*?<\/h\1\s*>/gi;
|
|
279
|
+
const openLevels = [];
|
|
280
|
+
let output = "";
|
|
281
|
+
let cursor = 0;
|
|
282
|
+
let match;
|
|
283
|
+
|
|
284
|
+
while ((match = heading.exec(searchable)) !== null) {
|
|
285
|
+
const level = Number(match[1]);
|
|
286
|
+
output += searchable.slice(cursor, match.index);
|
|
287
|
+
while (openLevels.length && openLevels.at(-1) >= level) {
|
|
288
|
+
output += "</section>\n";
|
|
289
|
+
openLevels.pop();
|
|
290
|
+
}
|
|
291
|
+
output += `<section>\n${match[0]}`;
|
|
292
|
+
openLevels.push(level);
|
|
293
|
+
cursor = heading.lastIndex;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
output += searchable.slice(cursor);
|
|
297
|
+
while (openLevels.length) {
|
|
298
|
+
output += "</section>\n";
|
|
299
|
+
openLevels.pop();
|
|
300
|
+
}
|
|
301
|
+
for (const opaque of opaqueHtml) {
|
|
302
|
+
output = output.replace(opaque.marker, opaque.html);
|
|
303
|
+
}
|
|
304
|
+
return output;
|
|
305
|
+
}
|
|
306
|
+
|
|
240
307
|
export async function createWui(md,mdpath) // HTML
|
|
241
308
|
{
|
|
242
309
|
|
|
@@ -258,18 +325,37 @@ export async function createWui(md,mdpath) // HTML
|
|
|
258
325
|
'class="task-list-item-checkbox"'
|
|
259
326
|
)
|
|
260
327
|
|
|
328
|
+
const taskItemStart = '(<li\\b(?=[^>]*\\bclass="[^"]*\\btask-list-item\\b[^"]*")[^>]*>\\s*)'
|
|
329
|
+
const taskCheckbox = '(<input\\b(?=[^>]*\\btype="checkbox")[^>]*>)'
|
|
330
|
+
md = md.replace(
|
|
331
|
+
new RegExp(taskItemStart + taskCheckbox + '([^<]*(?:<p>[^]*?<\\/p>[^<]*)*)', 'g'),
|
|
332
|
+
(whole, itemStart, checkbox, content) => {
|
|
333
|
+
if (!content.trim()) return whole
|
|
334
|
+
return `${itemStart}<label>${checkbox}${content}</label>`
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
261
338
|
md = convertWuiTextareas(md)
|
|
339
|
+
md = wrapWuiHeadingSections(md)
|
|
262
340
|
|
|
263
341
|
const mdb = path.basename(mdpath);
|
|
264
342
|
|
|
343
|
+
const responsiveImageStyle = `<style>
|
|
344
|
+
img {
|
|
345
|
+
max-width: 100%;
|
|
346
|
+
height: auto;
|
|
347
|
+
}
|
|
348
|
+
</style>`;
|
|
265
349
|
const moduleScript = `<scr`+`ipt type="module" src="./${mdb}.front.js"></scr`+`ipt>`;
|
|
266
|
-
|
|
350
|
+
const isFullHtmlDocument = /^\s*<!doctype html>/i.test(md);
|
|
351
|
+
if (!isFullHtmlDocument) {
|
|
267
352
|
md = `<!doctype html>
|
|
268
353
|
<html lang="zh-TW">
|
|
269
354
|
<head>
|
|
270
355
|
<meta charset="utf-8">
|
|
271
356
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
272
357
|
<title>${escapeHtmlAttribute(mdb)}</title>
|
|
358
|
+
${responsiveImageStyle}
|
|
273
359
|
</head>
|
|
274
360
|
<body>
|
|
275
361
|
${md}
|
|
@@ -277,10 +363,17 @@ ${moduleScript}
|
|
|
277
363
|
</body>
|
|
278
364
|
</html>
|
|
279
365
|
`;
|
|
280
|
-
} else if (/<\/body\s*>/i.test(md)) {
|
|
281
|
-
md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
|
|
282
366
|
} else {
|
|
283
|
-
md
|
|
367
|
+
if (/<\/head\s*>/i.test(md)) {
|
|
368
|
+
md = md.replace(/<\/head\s*>/i, `${responsiveImageStyle}\n</head>`);
|
|
369
|
+
} else {
|
|
370
|
+
md = `${responsiveImageStyle}\n${md}`;
|
|
371
|
+
}
|
|
372
|
+
if (/<\/body\s*>/i.test(md)) {
|
|
373
|
+
md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
|
|
374
|
+
} else {
|
|
375
|
+
md += `\n${moduleScript}\n`;
|
|
376
|
+
}
|
|
284
377
|
}
|
|
285
378
|
|
|
286
379
|
|
package/runtime/help/help.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Demo App
|
|
4
|
+
## My app
|
|
5
|
+
|
|
6
|
+
- [Say hello](javascript:sayHello())
|
|
7
|
+
- [Get server time](javascript:showServerTime())
|
|
8
|
+
- [Update text box](javascript:updateText())
|
|
9
|
+
- [Show selected](javascript:alert(JSON.stringify($('#my-app').val())))
|
|
10
|
+
- [x] task1
|
|
11
|
+
- [x] task2
|
|
12
|
+
- [ ] unselected task
|
|
13
|
+
|
|
14
|
+
```text#myid.myclass
|
|
15
|
+
Editable in both TUI and WUI
|
|
16
|
+
```
|
|
17
|
+
|
|
1
18
|
# Introduction
|
|
2
19
|
- jsmdcui stands for:
|
|
3
20
|
- JavaScript Markdown Cross-environment User Interface
|
|
@@ -10,6 +27,12 @@
|
|
|
10
27
|
- Original projects:
|
|
11
28
|
- https://github.com/jjtseng93/bunmicro
|
|
12
29
|
- https://github.com/micro-editor/micro
|
|
30
|
+
- .
|
|
31
|
+
- This README is itself a runnable app.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx jsmdcui@latest README.md
|
|
35
|
+
```
|
|
13
36
|
|
|
14
37
|
## Quick start
|
|
15
38
|
|
|
@@ -24,28 +47,54 @@ On other platforms, follow the [official Bun installation guide](https://bun.com
|
|
|
24
47
|
Choose either of these two ways to run jsmdcui.
|
|
25
48
|
|
|
26
49
|
> **Important:** Opening or rendering a local `.md` file writes or overwrites
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
50
|
+
> 5 generated files beside it.
|
|
51
|
+
> Starting `--wui` without a file or starting all the `--demo`s writes the
|
|
52
|
+
> 5 files generated from `testapp.md` in the current directory.
|
|
53
|
+
> The source Markdown is not changed, but you should run the demo in a directory where
|
|
30
54
|
> overwriting generated files is safe.
|
|
31
55
|
|
|
32
56
|
### Route 1: Run with npx
|
|
33
57
|
|
|
34
|
-
This route requires both `npx` and `bun` on your `PATH`; no source checkout is
|
|
35
|
-
required.
|
|
58
|
+
- This route requires both `npx` and `bun` on your `PATH`; no source checkout is
|
|
59
|
+
required.
|
|
60
|
+
|
|
61
|
+
#### Open the normal terminal editor
|
|
36
62
|
|
|
37
63
|
```sh
|
|
38
64
|
npx jsmdcui
|
|
39
65
|
```
|
|
40
66
|
|
|
41
|
-
|
|
67
|
+
#### Start `testapp.md` as the TUI/WUI demo
|
|
68
|
+
- If `testapp.md` is missing, the bundled copy is written to the current working directory
|
|
69
|
+
- If `testapp.md` already exists, it will be used and won't be overwritten.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# TUI(Terminal User Interface) Demo
|
|
73
|
+
npx jsmdcui --demo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
List every bundled demo and its command-line option:
|
|
42
77
|
|
|
43
78
|
```sh
|
|
79
|
+
npx jsmdcui --demo-list
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# Bun.Image processor demo
|
|
84
|
+
npx jsmdcui --demo-imgtool
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# Traditional Chinese Bun.Image processor demo
|
|
89
|
+
npx jsmdcui --demo-imgtool-zh
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# WUI(Web User Interface) Demo
|
|
44
94
|
npx jsmdcui --wui
|
|
45
95
|
```
|
|
46
96
|
|
|
47
|
-
|
|
48
|
-
instead of the bundled demo. To open your own Markdown UI, pass its path:
|
|
97
|
+
#### Open your own Markdown CUI App
|
|
49
98
|
|
|
50
99
|
```sh
|
|
51
100
|
npx jsmdcui app.md
|
|
@@ -60,30 +109,46 @@ cd jsmdcui
|
|
|
60
109
|
bun src/index.js testapp.md
|
|
61
110
|
```
|
|
62
111
|
|
|
63
|
-
The last command opens the included demo in the terminal
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
- The last command opens the included demo in the terminal
|
|
113
|
+
- Use arrow keys to move around
|
|
114
|
+
- `Enter`, `Space`, or mouse click to activate an item, and `Ctrl-Q` to quit.
|
|
115
|
+
- To open the same demo in a browser instead:
|
|
66
116
|
|
|
67
117
|
```sh
|
|
68
118
|
bun src/index.js --wui testapp.md
|
|
69
119
|
```
|
|
70
120
|
|
|
71
|
-
After starting `--wui`, open the last printed `http://...` URL in a browser.
|
|
72
|
-
Keep the command running while using the
|
|
73
|
-
|
|
121
|
+
- After starting `--wui`, open the last printed `http://...` URL in a browser.
|
|
122
|
+
- Keep the command running while using the CUI App, and press `Ctrl-C` in that terminal to stop the server.
|
|
123
|
+
|
|
124
|
+
### Usage table
|
|
74
125
|
|
|
75
|
-
The command table below
|
|
76
|
-
`bun src/index.js` with `npx jsmdcui
|
|
126
|
+
- The command table below assumes you're running from a cloned repository
|
|
127
|
+
- If you use npx, replace `bun src/index.js` with `npx jsmdcui`
|
|
128
|
+
|
|
129
|
+
- I've also provided short aliases
|
|
130
|
+
* bun ./tui = bun src/index.js
|
|
131
|
+
* bun ./wui = bun src/index.js --wui
|
|
77
132
|
|
|
78
133
|
| Command | Result |
|
|
79
134
|
| --- | --- |
|
|
80
135
|
| `bun src/index.js app.md` | Render `app.md` as a read-only terminal UI and write five generated files beside it. |
|
|
136
|
+
| `bun src/index.js --kitty app.md` | Display Markdown images with Kitty graphics and the jsgotty MIME extension. |
|
|
137
|
+
| `bun src/index.js --kitty-compat app.md` | Display Markdown images with Kitty graphics without the non-standard MIME `U` field. |
|
|
138
|
+
| `bun src/index.js --kitty --allow-url URL.md` | Download trusted HTTP(S) Markdown and its HTTP(S) images, then display supported images with Kitty graphics. |
|
|
139
|
+
| `JSMDCUI_KITTY_DEBUG=1 bun src/index.js --kitty app.md` | Enable Kitty image placement logging to `kitty-placement.log`. |
|
|
140
|
+
| `bun src/index.js --check app.md` | Check heading and fenced-block IDs for collisions, print line-by-line details, and exit. |
|
|
81
141
|
| `bun src/index.js --edit app.md` | Open `app.md` as editable UTF-8 source, overriding automatic Markdown UI detection. |
|
|
82
142
|
| `bun src/index.js --cat app.md` | Render the terminal version to stdout, write five generated files beside it, and exit. |
|
|
83
143
|
| `bun src/index.js --testapp.md` | Write the bundled `testapp.md` source to stdout and exit. |
|
|
84
|
-
| `bun src/index.js --
|
|
85
|
-
| `bun src/index.js --
|
|
86
|
-
| `bun src/index.js --
|
|
144
|
+
| `bun src/index.js --export-readme` | Write or overwrite `./README.md` with the bundled README source and exit. |
|
|
145
|
+
| `bun src/index.js --demo-list` | List `testapp.md` and every bundled `demos/*.md` example with its command-line option, then exit. |
|
|
146
|
+
| `bun src/index.js --demo` | Use local `testapp.md` when present, otherwise write the bundled demo; open it in the terminal UI and write five generated files beside it. |
|
|
147
|
+
| `bun src/index.js --demo-<filename>` | Load `demos/<filename>.md`; preserve an existing local copy or write the bundled copy, then open it and generate its five companion files. New files added under `demos/` work automatically. |
|
|
148
|
+
| `bun src/index.js --demo-imgtool` | Compatibility alias for `--demo-image-processor`. |
|
|
149
|
+
| `bun src/index.js --demo-imgtool-zh` | Compatibility alias for `--demo-image-processor.zh-TW`. |
|
|
150
|
+
| `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory and, with Kitty mode enabled, download its HTTP(S) images; write 5 generated files and allow embedded code to run. Only use trusted URLs. |
|
|
151
|
+
| `bun src/index.js --wui` | Use local `testapp.md` when present, otherwise write the bundled demo; write five generated files in the current directory, then print and serve a random URL. |
|
|
87
152
|
| `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
|
|
88
153
|
| `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
|
|
89
154
|
| `bun src/index.js` | Open the normal terminal editor with an empty buffer. |
|
|
@@ -96,11 +161,15 @@ options.
|
|
|
96
161
|
Create `app.md`:
|
|
97
162
|
|
|
98
163
|
````md
|
|
99
|
-
|
|
164
|
+
## My app
|
|
100
165
|
|
|
101
166
|
- [Say hello](javascript:sayHello())
|
|
102
167
|
- [Get server time](javascript:showServerTime())
|
|
103
168
|
- [Update text box](javascript:updateText())
|
|
169
|
+
- [Show selected](javascript:alert(JSON.stringify($('#my-app').val())))
|
|
170
|
+
- [x] task1
|
|
171
|
+
- [x] task2
|
|
172
|
+
- [ ] unselected task
|
|
104
173
|
|
|
105
174
|
```text#myid.myclass
|
|
106
175
|
Editable in both TUI and WUI
|
|
@@ -136,6 +205,11 @@ bun src/index.js app.md
|
|
|
136
205
|
bun src/index.js --wui app.md
|
|
137
206
|
```
|
|
138
207
|
|
|
208
|
+
- The resulting App UI is shown at the beginning of this README.md
|
|
209
|
+
- Run this demo app directly by:
|
|
210
|
+
* bun src/index.js README.md
|
|
211
|
+
* If you didn't clone the repo, use --export-readme to write README.md to the current folder
|
|
212
|
+
|
|
139
213
|
### Text blocks
|
|
140
214
|
|
|
141
215
|
Both `text` and `textarea` fenced blocks define editable text fields:
|
|
@@ -146,6 +220,20 @@ Initial value
|
|
|
146
220
|
```
|
|
147
221
|
````
|
|
148
222
|
|
|
223
|
+
A fenced-block declaration must include a tag. Its supported identity syntax is
|
|
224
|
+
`tag`, optionally followed by `#id` and one or more `.class` names, for
|
|
225
|
+
example `text#message.note` or `textarea#notes.readonly`. Tags, IDs, and class
|
|
226
|
+
names must begin with an ASCII letter or underscore; their remaining
|
|
227
|
+
characters may also include digits, `_`, `-`, and `:`. Declarations such as
|
|
228
|
+
`#message` and `#message.note` have no tag and are therefore not recognized as
|
|
229
|
+
selectable fenced blocks.
|
|
230
|
+
|
|
231
|
+
The currently supported fenced-block tags are only `text` and `textarea` so
|
|
232
|
+
the same control works consistently in both the TUI and WUI. Selector queries
|
|
233
|
+
may omit the tag or classes after a valid declaration, so all of
|
|
234
|
+
`$('text#message.note')`, `$('text#message')`, `$('#message.note')`, and
|
|
235
|
+
`$('#message')` can select the example above.
|
|
236
|
+
|
|
149
237
|
The same Markdown works in both interfaces. In the browser WUI it becomes a
|
|
150
238
|
native `<textarea>` with the declared ID and classes. Long text wraps
|
|
151
239
|
automatically, and the field height is recalculated when the user types, the
|
|
@@ -158,6 +246,157 @@ blocked. Activate the lower-left frame corner to add a row. Activate the
|
|
|
158
246
|
upper-left frame corner to remove the trailing row only when it is empty;
|
|
159
247
|
non-empty content is never removed.
|
|
160
248
|
|
|
249
|
+
### Heading task lists and selector API
|
|
250
|
+
|
|
251
|
+
Headings can act as form-group selectors. jsmdcui uses the IDs generated by
|
|
252
|
+
`Bun.markdown.html(..., { headings: { ids: true } })`; for example,
|
|
253
|
+
`## Select Color` becomes `#select-color`. A heading selection reads direct
|
|
254
|
+
task items from the first list following that heading and stops at the next
|
|
255
|
+
heading. In the TUI, the first rendered `☐` or `☒` establishes that list and
|
|
256
|
+
its indentation. Nested task items are not included in the outer list's value.
|
|
257
|
+
|
|
258
|
+
Heading IDs share the same selector namespace as all explicitly named fenced
|
|
259
|
+
blocks, not only `text` and `textarea`, so avoid name collisions between them.
|
|
260
|
+
For example, `## Write Status` generates `#write-status` and must not be used
|
|
261
|
+
together with a block such as `text#write-status` or
|
|
262
|
+
`textarea#write-status`.
|
|
263
|
+
Otherwise, `$('#write-status')` may select the heading instead of the block and
|
|
264
|
+
updates can appear to do nothing. Rename either declaration so every
|
|
265
|
+
selectable ID is unique.
|
|
266
|
+
|
|
267
|
+
Duplicate source headings are also treated as collisions when they generate
|
|
268
|
+
the same base ID. Although Bun would automatically rename later headings with
|
|
269
|
+
suffixes such as `-1` and `-2`, those implicit selector names are not visible
|
|
270
|
+
in the Markdown source and should not be relied on. Give each heading a name
|
|
271
|
+
that produces a unique ID before rendering.
|
|
272
|
+
|
|
273
|
+
Run `bun src/index.js --check app.md` (or `jsmdcui --check app.md`) to
|
|
274
|
+
check these IDs without opening either UI or writing generated files. The
|
|
275
|
+
command prints each collision with its source type, line number, and original
|
|
276
|
+
declaration, then exits immediately. Its exit status is `0` when all IDs are
|
|
277
|
+
unique, `1` when collisions are found, and `2` for invalid arguments or read
|
|
278
|
+
errors.
|
|
279
|
+
|
|
280
|
+
If the heading ID begins with `select`, `.val()` behaves like a single select:
|
|
281
|
+
it returns the first checked item or `null`. Other heading IDs behave like a
|
|
282
|
+
multiple select and return all checked items as an array, or `[]` when none are
|
|
283
|
+
checked.
|
|
284
|
+
|
|
285
|
+
Heading values contain the complete visible label, including any explanatory
|
|
286
|
+
text. When interpreting a selection, prefer `value.includes(...)` (or another
|
|
287
|
+
deliberate prefix/token parser) unless labels are guaranteed to be exact,
|
|
288
|
+
stable identifiers. For example, `yes(flip vertically)` should not be tested
|
|
289
|
+
with `value === "yes"`.
|
|
290
|
+
|
|
291
|
+
````md
|
|
292
|
+
## Select Color
|
|
293
|
+
|
|
294
|
+
- [ ] Red
|
|
295
|
+
- [x] Green
|
|
296
|
+
- [ ] Blue
|
|
297
|
+
|
|
298
|
+
## Features
|
|
299
|
+
|
|
300
|
+
- [x] Search
|
|
301
|
+
- [ ] Notifications
|
|
302
|
+
- [x] Offline mode
|
|
303
|
+
|
|
304
|
+
```js front
|
|
305
|
+
export function showValues() {
|
|
306
|
+
alert(JSON.stringify({
|
|
307
|
+
color: $('#select-color').val(),
|
|
308
|
+
features: $('#features').val(),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
````
|
|
313
|
+
|
|
314
|
+
The same getter works in TUI and WUI:
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
$('#select-color').val() // "Green"
|
|
318
|
+
$('#features').val() // ["Search", "Offline mode"]
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The first direct task list belonging to a heading can also be changed with
|
|
322
|
+
Array-style methods. String arguments create unchecked items. Pass an object
|
|
323
|
+
to choose the initial checked state:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
$('#features').push('Export')
|
|
327
|
+
$('#features').unshift({ value: 'Import', checked: true })
|
|
328
|
+
$('#features').splice(1, 2, 'Replacement')
|
|
329
|
+
$('#features').slice(0, 2)
|
|
330
|
+
$('#features').pop()
|
|
331
|
+
$('#features').shift()
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Like the corresponding `Array.prototype` methods, `.push(...items)` and
|
|
335
|
+
`.unshift(...items)` accept multiple items and return the new number of direct
|
|
336
|
+
items. `.pop()` and `.shift()` return the removed item's visible label, or
|
|
337
|
+
`undefined` when the list is empty. Nested task items are part of their parent
|
|
338
|
+
item: they are not counted separately, and are removed together with that
|
|
339
|
+
parent. These methods change the rendered TUI/WUI state; they do not rewrite
|
|
340
|
+
the source Markdown file. A heading must already have a task list before items
|
|
341
|
+
can be added.
|
|
342
|
+
|
|
343
|
+
`.splice(start, deleteCount, ...items)` follows `Array.prototype.splice()`:
|
|
344
|
+
negative indexes count from the end, omitting `deleteCount` removes through the
|
|
345
|
+
end, and the return value is an array containing the removed visible labels.
|
|
346
|
+
|
|
347
|
+
`.slice(start, end)` is read-only and follows `Array.prototype.slice()`. It
|
|
348
|
+
returns fresh item snapshots, including unchecked items, so changing the
|
|
349
|
+
returned array or its objects does not change the rendered list:
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
$('#features').slice()
|
|
353
|
+
// [
|
|
354
|
+
// { value: 'Search', checked: true },
|
|
355
|
+
// { value: 'Notifications', checked: false },
|
|
356
|
+
// ]
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`demos/todo.md` and `demos/todo-zh.md` are runnable Todo examples that
|
|
360
|
+
demonstrate `.push()`,
|
|
361
|
+
`.splice()`, `.slice()`, and `{ value, checked }` snapshots using editable text
|
|
362
|
+
controls instead of `prompt()` dialogs. Materialize one in the current
|
|
363
|
+
directory with its demo flag, or open the source-tree copy directly:
|
|
364
|
+
|
|
365
|
+
```sh
|
|
366
|
+
bun src/index.js --demo-todo
|
|
367
|
+
bun src/index.js --demo-todo-zh
|
|
368
|
+
bun src/index.js demos/todo.md
|
|
369
|
+
bun src/index.js --wui demos/todo-zh.md
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
The bundled `demos/select.md` is a multilevel runnable example. Use `--demo-select`
|
|
373
|
+
to write it into the current directory when missing and open it in the TUI, or
|
|
374
|
+
open it explicitly in either interface:
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
bun src/index.js --demo-select
|
|
378
|
+
bun src/index.js demos/select.md
|
|
379
|
+
bun src/index.js --wui demos/select.md
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The available selector methods are:
|
|
383
|
+
|
|
384
|
+
| Method | TUI | WUI |
|
|
385
|
+
| --- | --- | --- |
|
|
386
|
+
| `.val()` | Read text blocks or heading task-list values. | Read textareas/controls or heading task-list values. |
|
|
387
|
+
| `.val(value)` | Replace text-block contents and resize multiline values. | Set textarea/control values and resize textareas. |
|
|
388
|
+
| `.html()` | Return a selected heading's inner HTML. | Return any successfully selected DOM element's `innerHTML`. |
|
|
389
|
+
| `.line()` | Return a heading's current 1-based TUI row, or `0` if missing. | Not available. |
|
|
390
|
+
| `.push(...items)` | Append unchecked strings or `{ value, checked }` task items; return the new direct-item count. | Same. |
|
|
391
|
+
| `.pop()` | Remove and return the last direct task item's label, or `undefined`. | Same. |
|
|
392
|
+
| `.shift()` | Remove and return the first direct task item's label, or `undefined`. | Same. |
|
|
393
|
+
| `.unshift(...items)` | Prepend unchecked strings or `{ value, checked }` task items; return the new direct-item count. | Same. |
|
|
394
|
+
| `.splice(start, deleteCount, ...items)` | Remove and insert direct task items; return the removed labels as an array. | Same. |
|
|
395
|
+
| `.slice(start, end)` | Return `{ value, checked }` snapshots without changing the direct task items. | Same. |
|
|
396
|
+
|
|
397
|
+
TUI heading rows are recalculated after text-block rows are added, removed, or
|
|
398
|
+
replaced with multiline `.val(value)` content.
|
|
399
|
+
|
|
161
400
|
The three UI building blocks are:
|
|
162
401
|
|
|
163
402
|
- Regular Markdown provides headings, text, lists, task checkboxes, code, and
|
|
@@ -213,8 +452,8 @@ selection, search, and copy remain available.
|
|
|
213
452
|
| Input | Result |
|
|
214
453
|
| --- | --- |
|
|
215
454
|
| Arrow keys, `Home`, `End`, `PageUp`, `PageDown` | Move through the rendered UI. |
|
|
216
|
-
| `Enter` or `Space` | Activate the cell under the cursor. Put the cursor on a `javascript:` link to run it. |
|
|
217
|
-
| Left click | Move to and activate the clicked cell. A `javascript:` link runs immediately. |
|
|
455
|
+
| `Enter` or `Space` | Activate the cell under the cursor. Put the cursor on a `javascript:` link to run it, or on or to the right of `☐` or `☒` to toggle that task. |
|
|
456
|
+
| Left click | Move to and activate the clicked cell. A `javascript:` link runs immediately; clicking on or to the right of `☐` or `☒` toggles that task. |
|
|
218
457
|
| Mouse wheel | Scroll three rows at a time. |
|
|
219
458
|
| `Shift` + arrow keys | Select rendered text. |
|
|
220
459
|
| `Ctrl-C` | Copy the selection, or the current line when nothing is selected. |
|
|
@@ -228,13 +467,27 @@ The terminal automatically reflows the Markdown when its width changes.
|
|
|
228
467
|
Only `javascript:` links execute in the TUI; ordinary web links behave as
|
|
229
468
|
normal links in the browser.
|
|
230
469
|
|
|
470
|
+
Local Markdown images are displayed automatically in terminals that support
|
|
471
|
+
the Kitty graphics protocol. Relative image paths are resolved from the
|
|
472
|
+
Markdown file's directory. jsmdcui reads the image dimensions, reserves the
|
|
473
|
+
corresponding terminal rows, and updates the placement when the document is
|
|
474
|
+
scrolled, resized, or shown in a split pane. Unsupported or missing images, as
|
|
475
|
+
well as remote images not authorized with `--allow-url`, retain Bun's normal
|
|
476
|
+
linked `📷` fallback. To download trusted remote Markdown and display its
|
|
477
|
+
supported HTTP(S) images with Kitty graphics, combine the options:
|
|
478
|
+
|
|
479
|
+
```sh
|
|
480
|
+
bun src/index.js --kitty --allow-url https://example.com/app.md
|
|
481
|
+
```
|
|
482
|
+
|
|
231
483
|
## Browser interaction
|
|
232
484
|
|
|
233
485
|
The WUI uses normal browser mouse and keyboard behavior. Clicking a
|
|
234
486
|
`javascript:` link calls its front-end function, regular links navigate
|
|
235
|
-
normally, and task checkboxes can be toggled
|
|
236
|
-
use the browser's built-in
|
|
237
|
-
|
|
487
|
+
normally, and task checkboxes can be toggled by clicking either the checkbox or
|
|
488
|
+
its associated text. `alert`, `confirm`, and `prompt` use the browser's built-in
|
|
489
|
+
dialogs. Checkbox changes exist only in the current page: refreshing does not
|
|
490
|
+
preserve them and does not update the Markdown file.
|
|
238
491
|
|
|
239
492
|
The WUI uses port `3000` by default and accepts connections through the
|
|
240
493
|
machine's available network interfaces. The printed `localhost` URL is for the
|
|
@@ -260,6 +513,33 @@ app.md-server.js
|
|
|
260
513
|
They are regenerated from `app.md`, so edit the Markdown source rather than the
|
|
261
514
|
generated files. The source directory must be writable.
|
|
262
515
|
|
|
516
|
+
From the project directory, remove generated `*.md.*` and `*.md-*` companion
|
|
517
|
+
files while keeping the Markdown source files:
|
|
518
|
+
|
|
519
|
+
```sh
|
|
520
|
+
bun ./clean.sh
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Generated heading sections
|
|
524
|
+
|
|
525
|
+
WUI output wraps each h1-h6 and its content in a hierarchical `<section>`.
|
|
526
|
+
Lower-level headings create nested sections; the next equal or higher-level
|
|
527
|
+
heading closes the applicable sections. Content before the first heading stays
|
|
528
|
+
outside all generated sections.
|
|
529
|
+
|
|
530
|
+
```html
|
|
531
|
+
<section>
|
|
532
|
+
<h1 id="chapter">Chapter</h1>
|
|
533
|
+
<section>
|
|
534
|
+
<h2 id="topic">Topic</h2>
|
|
535
|
+
</section>
|
|
536
|
+
</section>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
This structure provides stable boundaries for CSS, DOM queries, and heading
|
|
540
|
+
task-list values. Heading-like text inside comments, scripts, styles, code,
|
|
541
|
+
textareas, and templates is protected and does not create sections.
|
|
542
|
+
|
|
263
543
|
## Security
|
|
264
544
|
|
|
265
545
|
A Markdown UI is an executable application, not a passive document. Starting a
|
package/single-exe/packAssets.sh
CHANGED