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
@@ -47,9 +47,10 @@ On other platforms, follow the [official Bun installation guide](https://bun.com
47
47
  Choose either of these two ways to run jsmdcui.
48
48
 
49
49
  > **Important:** Opening or rendering a local `.md` file writes or overwrites
50
- > 5 generated files beside it. Starting `--wui` without a file writes the
51
- > 5 files generated from `testapp.md` in the current directory. The source
52
- > Markdown is not changed, but you should run the demo in a directory where
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
53
54
  > overwriting generated files is safe.
54
55
 
55
56
  ### Route 1: Run with npx
@@ -72,9 +73,10 @@ npx jsmdcui
72
73
  npx jsmdcui --demo
73
74
  ```
74
75
 
76
+ List every bundled demo and its command-line option:
77
+
75
78
  ```sh
76
- # Heading and task-list selector demo
77
- npx jsmdcui --demo-select
79
+ npx jsmdcui --demo-list
78
80
  ```
79
81
 
80
82
  ```sh
@@ -87,6 +89,11 @@ npx jsmdcui --demo-imgtool
87
89
  npx jsmdcui --demo-imgtool-zh
88
90
  ```
89
91
 
92
+ ```sh
93
+ # Maze game demo
94
+ npx jsmdcui --demo-maze
95
+ ```
96
+
90
97
  ```sh
91
98
  # WUI(Web User Interface) Demo
92
99
  npx jsmdcui --wui
@@ -121,22 +128,31 @@ bun src/index.js --wui testapp.md
121
128
 
122
129
  ### Usage table
123
130
 
124
- - The command table below uses the cloned-source form.
131
+ - The command table below assumes you're running from a cloned repository
125
132
  - If you use npx, replace `bun src/index.js` with `npx jsmdcui`
126
133
 
134
+ - I've also provided short aliases
135
+ * bun ./tui = bun src/index.js
136
+ * bun ./wui = bun src/index.js --wui
137
+
127
138
  | Command | Result |
128
139
  | --- | --- |
129
140
  | `bun src/index.js app.md` | Render `app.md` as a read-only terminal UI and write five generated files beside it. |
141
+ | `bun src/index.js --kitty app.md` | Display Markdown images with Kitty graphics and the jsgotty MIME extension. |
142
+ | `bun src/index.js --kitty-compat app.md` | Convert Markdown images to PNG with `Bun.Image` and display them using the standard Kitty graphics protocol without the non-standard MIME `U` field. |
143
+ | `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. |
144
+ | `JSMDCUI_KITTY_DEBUG=1 bun src/index.js --kitty app.md` | Enable Kitty image placement logging to `kitty-placement.log`. |
130
145
  | `bun src/index.js --check app.md` | Check heading and fenced-block IDs for collisions, print line-by-line details, and exit. |
131
146
  | `bun src/index.js --edit app.md` | Open `app.md` as editable UTF-8 source, overriding automatic Markdown UI detection. |
132
147
  | `bun src/index.js --cat app.md` | Render the terminal version to stdout, write five generated files beside it, and exit. |
133
148
  | `bun src/index.js --testapp.md` | Write the bundled `testapp.md` source to stdout and exit. |
134
149
  | `bun src/index.js --export-readme` | Write or overwrite `./README.md` with the bundled README source and exit. |
150
+ | `bun src/index.js --demo-list` | List `testapp.md` and every bundled `demos/*.md` example with its command-line option, then exit. |
135
151
  | `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. |
136
- | `bun src/index.js --demo-select` | Use local `select.md` when present, otherwise write the bundled selector demo; open it in the terminal UI and write five generated files beside it. |
137
- | `bun src/index.js --demo-imgtool` | Use local `image-processor.md` when present, otherwise write the bundled Bun.Image processor; open it in the terminal UI and write five generated files beside it. |
138
- | `bun src/index.js --demo-imgtool-zh` | Use local `image-processor.zh-TW.md` when present, otherwise write the bundled Traditional Chinese Bun.Image processor; open it in the terminal UI and write five generated files beside it. |
139
- | `bun src/index.js --allow-url URL.md` | Download HTTP(S) Markdown to the current directory, write 5 generated files, and allow its embedded code to run. Only use trusted URLs. |
152
+ | `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. |
153
+ | `bun src/index.js --demo-imgtool` | Compatibility alias for `--demo-image-processor`. |
154
+ | `bun src/index.js --demo-imgtool-zh` | Compatibility alias for `--demo-image-processor.zh-TW`. |
155
+ | `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. |
140
156
  | `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. |
141
157
  | `bun src/index.js --wui app.md` | Write five generated files beside `app.md`, then print and serve a random URL. |
142
158
  | `PORT=8080 bun src/index.js --wui app.md` | Start the browser UI on another port. |
@@ -228,20 +244,90 @@ native `<textarea>` with the declared ID and classes. Long text wraps
228
244
  automatically, and the field height is recalculated when the user types, the
229
245
  window is resized, or frontend code calls `.val(value)`.
230
246
 
247
+ In the TUI, `text` remains a single-line control while `textarea` supports
248
+ native multiline editing. Enter splits the current body row and grows the
249
+ frame, Backspace at the start of a later row joins it to the previous row, and
250
+ Delete at the end of a row joins the following row. When the expanded control
251
+ does not fit on screen, the document viewport scrolls to keep the cursor
252
+ visible. The closing border and following Markdown content move with the
253
+ resized control.
254
+
255
+ A named `text` or `textarea` block can run inline front-end code before it
256
+ handles a key by placing a quoted HTML-style `@keydown` attribute after its
257
+ identity:
258
+
259
+ ````md
260
+ ```text#command.field @keydown="handleCommand(event)"
261
+ Initial value
262
+ ```
263
+
264
+ ```js front
265
+ export function handleCommand(event) {
266
+ if (event.key !== 'Enter') return;
267
+ event.preventDefault();
268
+ alert(`Command: ${$('#command').val()}`);
269
+ }
270
+ ```
271
+ ````
272
+
273
+ The block must have a unique ID. In the WUI, jsmdcui writes the code directly
274
+ to the generated textarea's native `onkeydown` attribute. Mobile browsers may
275
+ report ordinary software-keyboard letters as `Unidentified`, so the WUI also
276
+ generates an `onbeforeinput` fallback. The unidentified event is hidden from
277
+ the handler; `beforeinput.data` becomes `event.key`, then the same keydown code
278
+ runs once. The fallback also preserves `ctrlKey`, `shiftKey`, `altKey`, and
279
+ `metaKey` from the unidentified keydown. A short zero-delay timer distinguishes
280
+ it from the `beforeinput` that normally follows an already identified keydown,
281
+ preventing duplicate calls.
282
+
283
+ On Android browsers, `Alt-E`, `Alt-N`, `Alt-U`, and `Alt-I` may be consumed or
284
+ transformed by the software keyboard and therefore cannot always be observed
285
+ reliably by a keydown handler. Avoid relying on these combinations for portable
286
+ WUI controls.
287
+
288
+ In the TUI, the full source info string is kept in an event table before Bun
289
+ renders it. The keydown statements run with a synthetic `event` before the
290
+ text control handles the key. Both interfaces expose `event.key`, modifier
291
+ flags, `event.target.id`, and `event.target.value`. Use `event`, the native
292
+ inline-handler variable, rather than Vue's `$event` alias. Double quotes
293
+ delimit the handler; use single-quoted JavaScript strings inside it or escape
294
+ an embedded double quote as `\"`.
295
+
296
+ Both interfaces add a non-enumerable `event.toJSON()` method with matching
297
+ keyboard, modifier, prevention, and target fields. `JSON.stringify(event)`
298
+ calls it automatically, so the resulting JSON is portable between the TUI and
299
+ WUI.
300
+
301
+ A keydown handler can call `event.preventDefault()` to stop text insertion or
302
+ cursor movement. The `.prevent` modifier applies this automatically:
303
+
304
+ ````md
305
+ ```text#command @keydown.prevent="handleCommand(event)"
306
+ ```
307
+ ````
308
+
309
+ `@keydown` is the only keyboard event exposed by jsmdcui. Traditional terminal
310
+ input does not report physical key releases reliably, so jsmdcui does not
311
+ provide or emulate `@keyup` in either interface.
312
+
231
313
  In the terminal TUI, only content after the protected `│ ` or `| ` prefix can
232
- be edited. The frame prefix cannot be deleted, Enter cannot insert a newline,
233
- Delete at the end of a row cannot join the next row, and multiline paste is
234
- blocked. Activate the lower-left frame corner to add a row. Activate the
235
- upper-left frame corner to remove the trailing row only when it is empty;
236
- non-empty content is never removed.
314
+ be edited, and the frame prefix cannot be deleted. Multiline paste remains
315
+ blocked. For single-line `text` controls, activate the lower-left frame corner
316
+ to add a row. Activate the upper-left frame corner to remove the trailing row
317
+ only when it is empty; non-empty content is never removed.
318
+
319
+ TUI text-control changes made through `.val(value)` participate in the same
320
+ history as direct editing. `Ctrl-Z` undoes the replacement and `Ctrl-Y` redoes
321
+ it, including multiline frame resizing and its associated rendered metadata.
237
322
 
238
323
  ### Heading task lists and selector API
239
324
 
240
325
  Headings can act as form-group selectors. jsmdcui uses the IDs generated by
241
326
  `Bun.markdown.html(..., { headings: { ids: true } })`; for example,
242
- `## Select Color` becomes `#select-color`. A heading selection reads the first
243
- task list directly following that heading and stops at the next heading. Nested
244
- task items are not included in the outer list's value.
327
+ `## Select Color` becomes `#select-color`. A heading selection reads direct
328
+ task items from the first list following that heading and stops at the next
329
+ heading. In the TUI, the first rendered `☐` or `☒` establishes that list and
330
+ its indentation. Nested task items are not included in the outer list's value.
245
331
 
246
332
  Heading IDs share the same selector namespace as all explicitly named fenced
247
333
  blocks, not only `text` and `textarea`, so avoid name collisions between them.
@@ -306,14 +392,65 @@ $('#select-color').val() // "Green"
306
392
  $('#features').val() // ["Search", "Offline mode"]
307
393
  ```
308
394
 
309
- The bundled `select.md` is a multilevel runnable example. Use `--demo-select`
395
+ The first direct task list belonging to a heading can also be changed with
396
+ Array-style methods. String arguments create unchecked items. Pass an object
397
+ to choose the initial checked state:
398
+
399
+ ```js
400
+ $('#features').push('Export')
401
+ $('#features').unshift({ value: 'Import', checked: true })
402
+ $('#features').splice(1, 2, 'Replacement')
403
+ $('#features').slice(0, 2)
404
+ $('#features').pop()
405
+ $('#features').shift()
406
+ ```
407
+
408
+ Like the corresponding `Array.prototype` methods, `.push(...items)` and
409
+ `.unshift(...items)` accept multiple items and return the new number of direct
410
+ items. `.pop()` and `.shift()` return the removed item's visible label, or
411
+ `undefined` when the list is empty. Nested task items are part of their parent
412
+ item: they are not counted separately, and are removed together with that
413
+ parent. These methods change the rendered TUI/WUI state; they do not rewrite
414
+ the source Markdown file. A heading must already have a task list before items
415
+ can be added.
416
+
417
+ `.splice(start, deleteCount, ...items)` follows `Array.prototype.splice()`:
418
+ negative indexes count from the end, omitting `deleteCount` removes through the
419
+ end, and the return value is an array containing the removed visible labels.
420
+
421
+ `.slice(start, end)` is read-only and follows `Array.prototype.slice()`. It
422
+ returns fresh item snapshots, including unchecked items, so changing the
423
+ returned array or its objects does not change the rendered list:
424
+
425
+ ```js
426
+ $('#features').slice()
427
+ // [
428
+ // { value: 'Search', checked: true },
429
+ // { value: 'Notifications', checked: false },
430
+ // ]
431
+ ```
432
+
433
+ `demos/todo.md` and `demos/todo-zh.md` are runnable Todo examples that
434
+ demonstrate `.push()`,
435
+ `.splice()`, `.slice()`, and `{ value, checked }` snapshots using editable text
436
+ controls instead of `prompt()` dialogs. Materialize one in the current
437
+ directory with its demo flag, or open the source-tree copy directly:
438
+
439
+ ```sh
440
+ bun src/index.js --demo-todo
441
+ bun src/index.js --demo-todo-zh
442
+ bun src/index.js demos/todo.md
443
+ bun src/index.js --wui demos/todo-zh.md
444
+ ```
445
+
446
+ The bundled `demos/select.md` is a multilevel runnable example. Use `--demo-select`
310
447
  to write it into the current directory when missing and open it in the TUI, or
311
448
  open it explicitly in either interface:
312
449
 
313
450
  ```sh
314
451
  bun src/index.js --demo-select
315
- bun src/index.js select.md
316
- bun src/index.js --wui select.md
452
+ bun src/index.js demos/select.md
453
+ bun src/index.js --wui demos/select.md
317
454
  ```
318
455
 
319
456
  The available selector methods are:
@@ -321,23 +458,29 @@ The available selector methods are:
321
458
  | Method | TUI | WUI |
322
459
  | --- | --- | --- |
323
460
  | `.val()` | Read text blocks or heading task-list values. | Read textareas/controls or heading task-list values. |
324
- | `.val(value)` | Replace text-block contents and resize multiline values. | Set textarea/control values and resize textareas. |
461
+ | `.val(value)` | Replace text-block contents, resize multiline values, and record Undo/Redo history. | Set textarea/control values and resize textareas. |
325
462
  | `.html()` | Return a selected heading's inner HTML. | Return any successfully selected DOM element's `innerHTML`. |
326
463
  | `.line()` | Return a heading's current 1-based TUI row, or `0` if missing. | Not available. |
464
+ | `.push(...items)` | Append unchecked strings or `{ value, checked }` task items; return the new direct-item count. | Same. |
465
+ | `.pop()` | Remove and return the last direct task item's label, or `undefined`. | Same. |
466
+ | `.shift()` | Remove and return the first direct task item's label, or `undefined`. | Same. |
467
+ | `.unshift(...items)` | Prepend unchecked strings or `{ value, checked }` task items; return the new direct-item count. | Same. |
468
+ | `.splice(start, deleteCount, ...items)` | Remove and insert direct task items; return the removed labels as an array. | Same. |
469
+ | `.slice(start, end)` | Return `{ value, checked }` snapshots without changing the direct task items. | Same. |
327
470
 
328
471
  TUI heading rows are recalculated after text-block rows are added, removed, or
329
472
  replaced with multiline `.val(value)` content.
330
473
 
331
- The three UI building blocks are:
474
+ The 3 UI building blocks are:
332
475
 
333
- - Regular Markdown provides headings, text, lists, task checkboxes, code, and
476
+ - 1. `Regular Markdown` provides headings, text, lists, task checkboxes, code, and
334
477
  links.
335
- - A `js front` block contains UI code. Exported functions can use
478
+ - 2. `js front` block contains UI code. Exported functions can use
336
479
  `alert`, `confirm`, `prompt`, and the generated `rpc` client.
337
- - A front module may export `async function onMdcuiExit({ reason, path, $ })`.
338
- The terminal UI awaits it before closing an `mdcui` buffer. Modified
339
- `mdcui` buffers close without a save prompt.
340
- - A `js back` block exports trusted backend functions. In the browser WUI,
480
+ * A front module may export `async function onMdcuiExit({ reason, path, $ })`.
481
+ * The terminal UI awaits it before closing an `mdcui` buffer.
482
+ * Modified `mdcui` buffers close without a save prompt.
483
+ - 3. `js back` block exports trusted backend functions. In the browser WUI,
341
484
  `rpc` publishes only exported functions whose exported names do not start
342
485
  with `_`. Call a published function from the front end with
343
486
  `await rpc.functionName(arg1, arg2)`.
@@ -398,6 +541,19 @@ The terminal automatically reflows the Markdown when its width changes.
398
541
  Only `javascript:` links execute in the TUI; ordinary web links behave as
399
542
  normal links in the browser.
400
543
 
544
+ Local Markdown images are displayed automatically in terminals that support
545
+ the Kitty graphics protocol. Relative image paths are resolved from the
546
+ Markdown file's directory. jsmdcui reads the image dimensions, reserves the
547
+ corresponding terminal rows, and updates the placement when the document is
548
+ scrolled, resized, or shown in a split pane. Unsupported or missing images, as
549
+ well as remote images not authorized with `--allow-url`, retain Bun's normal
550
+ linked `📷` fallback. To download trusted remote Markdown and display its
551
+ supported HTTP(S) images with Kitty graphics, combine the options:
552
+
553
+ ```sh
554
+ bun src/index.js --kitty --allow-url https://example.com/app.md
555
+ ```
556
+
401
557
  ## Browser interaction
402
558
 
403
559
  The WUI uses normal browser mouse and keyboard behavior. Clicking a
@@ -407,15 +563,35 @@ its associated text. `alert`, `confirm`, and `prompt` use the browser's built-in
407
563
  dialogs. Checkbox changes exist only in the current page: refreshing does not
408
564
  preserve them and does not update the Markdown file.
409
565
 
410
- The WUI uses port `3000` by default and accepts connections through the
411
- machine's available network interfaces. The printed `localhost` URL is for the
412
- same machine. From another device on the same network, replace `localhost` with
413
- the server machine's IP address and keep the same port and full path.
566
+ The WUI first tries port `3000` and accepts connections through the machine's
567
+ available network interfaces. If port `3000` is already in use, the operating
568
+ system selects an available port instead; the printed URL always contains the
569
+ actual port. Set `PORT` to request another fixed port. The printed `localhost`
570
+ URL is for the same machine. From another device on the same network, replace
571
+ `localhost` with the server machine's IP address and keep the printed port and
572
+ full path.
414
573
 
415
574
  Each server start prints a new random path. The old URL stops working after the
416
575
  server is stopped or restarted. Keep the process running while using the page,
417
576
  and press `Ctrl-C` in its terminal to stop it.
418
577
 
578
+ ## Editing Markdown source
579
+
580
+ From a cloned repository, use the `edit` launcher to open a Markdown file as
581
+ ordinary editable UTF-8 source instead of rendering it as a Markdown UI:
582
+
583
+ ```sh
584
+ bun ./edit app.md
585
+ ```
586
+
587
+ This is equivalent to:
588
+
589
+ ```sh
590
+ bun src/index.js --edit app.md
591
+ ```
592
+
593
+ Additional file and cursor arguments are forwarded unchanged.
594
+
419
595
  ## Generated files
420
596
 
421
597
  Opening a local Markdown UI generates these files beside the source file:
@@ -431,6 +607,13 @@ app.md-server.js
431
607
  They are regenerated from `app.md`, so edit the Markdown source rather than the
432
608
  generated files. The source directory must be writable.
433
609
 
610
+ From the project directory, remove generated `*.md.*` and `*.md-*` companion
611
+ files while keeping the Markdown source files:
612
+
613
+ ```sh
614
+ bun ./clean.sh
615
+ ```
616
+
434
617
  ### Generated heading sections
435
618
 
436
619
  WUI output wraps each h1-h6 and its content in a hierarchical `<section>`.
@@ -44,7 +44,10 @@ Other micro APIs:
44
44
  e.g. for command "js 1+1": args.raw = "js 1+1", args.raw.slice(3) = "1+1"
45
45
  micro.RegisterAction(name, fn) — register bindable action
46
46
  micro.TermMessage(msg) — show msg in editor status row
47
- micro.alert(msg) — suspend editor, print msg, wait for Enter
47
+ micro.alert(msg) — synchronous; suspend editor, print msg, wait for Enter
48
+ e.g. micro.alert("Done") (do not await)
49
+ micro.confirm(msg) — synchronous boolean result
50
+ micro.prompt(msg, default?) — synchronous string or null result
48
51
  micro.Log(...args) — console.log passthrough
49
52
  micro.GetOption(name) micro.SetOption(name, value)
50
53
  micro.cmd.save() — call any editor command via proxy
@@ -169,7 +172,7 @@ micro.on("init", () => {
169
172
  const addr = isPublic ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;
170
173
  micro.TermMessage(`CDP@${addr} server running 伺服器啟動了`)
171
174
 
172
- //await micro.alert(CdpServer)
175
+ //micro.alert(CdpServer)
173
176
  } // server not running
174
177
  else
175
178
  {
@@ -43,7 +43,10 @@ Other micro APIs:
43
43
  micro.MakeCommand(name, fn) — register Ctrl+E command; fn(bp, args[])
44
44
  micro.RegisterAction(name, fn) — register bindable action
45
45
  micro.TermMessage(msg) — show msg in editor status row
46
- micro.alert(msg) — suspend editor, print msg, wait for Enter
46
+ micro.alert(msg) — synchronous; suspend editor, print msg, wait for Enter
47
+ e.g. micro.alert("Done") (do not await)
48
+ micro.confirm(msg) — synchronous boolean result
49
+ micro.prompt(msg, default?) — synchronous string or null result
47
50
  micro.Log(...args) — console.log passthrough
48
51
  micro.GetOption(name) micro.SetOption(name, value)
49
52
  micro.cmd.save() — call any editor command via proxy
@@ -105,4 +108,3 @@ micro.on("init", () => {
105
108
 
106
109
  });
107
110
 
108
-
@@ -45,7 +45,10 @@ Other micro APIs:
45
45
  e.g. for command "js 1+1": args.raw = "js 1+1", args.raw.slice(3) = "1+1"
46
46
  micro.RegisterAction(name, fn) — register bindable action
47
47
  micro.TermMessage(msg) — show msg in editor status row
48
- micro.alert(msg) — suspend editor, print msg, wait for Enter
48
+ micro.alert(msg) — synchronous; suspend editor, print msg, wait for Enter
49
+ e.g. micro.alert("Done") (do not await)
50
+ micro.confirm(msg) — synchronous boolean result
51
+ micro.prompt(msg, default?) — synchronous string or null result
49
52
  micro.Log(...args) — console.log passthrough
50
53
  micro.GetOption(name) micro.SetOption(name, value)
51
54
  micro.cmd.save() — call any editor command via proxy
@@ -58,14 +61,14 @@ Other micro APIs:
58
61
 
59
62
  micro.on("init", () => {
60
63
  // Register a custom Ctrl+E command
61
- micro.MakeCommand("showpath", async (bp, args) =>
64
+ micro.MakeCommand("showpath", (bp, args) =>
62
65
  {
63
66
  //micro.TermMessage("Hello from JS plugin! Args: " + args.join(", "));
64
67
 
65
- //await micro.alert(micro.getLine())
68
+ //micro.alert(micro.getLine())
66
69
  const path = bp?.Buf?.Path || "(no path)";
67
70
  const loc = bp?.CursorLocation?.() || "+1.0:1";
68
- await micro.alert(`${path}\n${loc}`)
71
+ micro.alert(`${path}\n${loc}`)
69
72
 
70
73
  });
71
74
 
@@ -75,12 +78,12 @@ micro.on("init", () => {
75
78
  // args.raw is the full original input string, e.g. "js console.log('hi')"
76
79
  // slice(3) skips the "js " prefix (2-char name + 1 space)
77
80
  const scriptText = args.raw?.slice(3) ?? args.join(" ");
78
- await micro.alert(await eval(scriptText));
81
+ micro.alert(await eval(scriptText));
79
82
  });
80
83
 
81
84
 
82
85
  // greet: shows a message via micro.alert (leaves editor, shows text, Enter returns)
83
- micro.MakeCommand("greet", async (bp, args) =>
86
+ micro.MakeCommand("greet", (bp, args) =>
84
87
  {
85
88
  const name = args.length ?
86
89
  args.join(" ") : "world";
@@ -89,7 +92,7 @@ micro.on("init", () => {
89
92
  '# Hello\n- '+name
90
93
  );
91
94
 
92
- await micro.alert(s);
95
+ micro.alert(s);
93
96
  });
94
97
 
95
98
  // Register a custom action (can be bound to a key in keybindings.json)
@@ -52,6 +52,7 @@ rules:
52
52
  # keywords
53
53
  # javascript alert confirm prompt rpc
54
54
  # front back
55
+ # export function keydown
55
56
  # console log error JSON stringify parse
56
57
  # async await Promise
57
58
  # textarea text password search
@@ -4,4 +4,4 @@ sd=$(dirname "$0")
4
4
 
5
5
  cd "$sd"/..
6
6
 
7
- tar -cvf single-exe/assets.tar runtime README.md CHANGELOG.md src/cui/server.mjs src/cui/rpc.mjs testapp.md select.md image-processor.md image-processor.zh-TW.md
7
+ tar -cvf single-exe/assets.tar demos runtime README.md CHANGELOG.md src/cui/server.mjs src/cui/rpc.mjs testapp.md
@@ -0,0 +1,106 @@
1
+ const IDENTITY_RE = /^([A-Za-z_][\w:-]*)(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/;
2
+
3
+ function parseIdentity(value) {
4
+ const match = String(value ?? "").match(IDENTITY_RE);
5
+ if (!match) return null;
6
+ return {
7
+ tag: match[1],
8
+ id: match[2] || null,
9
+ classes: match[3] ? match[3].slice(1).split(".") : [],
10
+ };
11
+ }
12
+
13
+ function parseEventAttributes(text) {
14
+ const events = new Map();
15
+ let offset = 0;
16
+ while (offset < text.length) {
17
+ while (/\s/.test(text[offset] ?? "")) offset++;
18
+ if (offset >= text.length || text[offset] !== "@") break;
19
+ offset++;
20
+ const nameStart = offset;
21
+ while (/[A-Za-z0-9_.:-]/.test(text[offset] ?? "")) offset++;
22
+ const name = text.slice(nameStart, offset);
23
+ while (/\s/.test(text[offset] ?? "")) offset++;
24
+ if (!name || text[offset] !== "=") break;
25
+ offset++;
26
+ while (/\s/.test(text[offset] ?? "")) offset++;
27
+ if (text[offset] !== '"') break;
28
+ offset++;
29
+
30
+ let code = "";
31
+ let closed = false;
32
+ while (offset < text.length) {
33
+ const ch = text[offset++];
34
+ if (ch === '"') {
35
+ closed = true;
36
+ break;
37
+ }
38
+ if (ch === "\\" && offset < text.length) {
39
+ const next = text[offset++];
40
+ code += next === '"' || next === "\\" ? next : `\\${next}`;
41
+ } else {
42
+ code += ch;
43
+ }
44
+ }
45
+ if (!closed) break;
46
+ const [eventName, ...modifiers] = name.split(".");
47
+ if (eventName === "keydown")
48
+ events.set(eventName, { code, modifiers: [...new Set(modifiers.filter(Boolean))] });
49
+ }
50
+ return events;
51
+ }
52
+
53
+ export function parseFenceDeclarations(markdown) {
54
+ const lines = String(markdown ?? "").split(/\r?\n/);
55
+ const declarations = [];
56
+ let fence = null;
57
+
58
+ for (let index = 0; index < lines.length; index++) {
59
+ const line = lines[index];
60
+ if (fence) {
61
+ const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
62
+ if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length)
63
+ fence = null;
64
+ continue;
65
+ }
66
+
67
+ const opening = line.match(/^ {0,3}(`{3,}|~{3,})([^]*)$/);
68
+ if (!opening) continue;
69
+ fence = { marker: opening[1][0], length: opening[1].length };
70
+ const info = String(opening[2] ?? "").trim();
71
+ const firstSpace = info.search(/\s/);
72
+ const identityText = firstSpace < 0 ? info : info.slice(0, firstSpace);
73
+ const attributesText = firstSpace < 0 ? "" : info.slice(firstSpace);
74
+ const identity = parseIdentity(identityText);
75
+ if (!identity) continue;
76
+ declarations.push({
77
+ ...identity,
78
+ identity: identityText,
79
+ events: parseEventAttributes(attributesText),
80
+ line: index + 1,
81
+ source: line.trim(),
82
+ });
83
+ }
84
+
85
+ return declarations;
86
+ }
87
+
88
+ export function fenceEventMap(markdown) {
89
+ const result = new Map();
90
+ for (const declaration of parseFenceDeclarations(markdown)) {
91
+ if (
92
+ !["text", "textarea"].includes(declaration.tag)
93
+ || !declaration.id
94
+ || declaration.events.size === 0
95
+ || result.has(declaration.id)
96
+ ) continue;
97
+ result.set(declaration.id, declaration);
98
+ }
99
+ return result;
100
+ }
101
+
102
+ export function inlineFenceEventCode(handler) {
103
+ if (!handler) return "";
104
+ const prefix = handler.modifiers.includes("prevent") ? "event.preventDefault();" : "";
105
+ return prefix + handler.code;
106
+ }
@@ -1,3 +1,5 @@
1
+ import { parseFenceDeclarations } from "./fence-events.mjs";
2
+
1
3
  function markdownHeadingDeclarations(markdown) {
2
4
  const lines = String(markdown).split(/\r?\n/);
3
5
  const declarations = [];
@@ -51,34 +53,14 @@ function markdownHeadingDeclarations(markdown) {
51
53
  }
52
54
 
53
55
  function fencedBlockDeclarations(markdown) {
54
- const lines = String(markdown).split(/\r?\n/);
55
- const declarations = [];
56
- let fence = null;
57
-
58
- for (let index = 0; index < lines.length; index++) {
59
- const line = lines[index];
60
- if (!fence) {
61
- const opening = line.match(/^ {0,3}(`{3,}|~{3,})\s*(\S+)?\s*$/);
62
- if (!opening) continue;
63
- fence = { marker: opening[1][0], length: opening[1].length };
64
- const identity = String(opening[2] ?? "").match(/^([A-Za-z_][\w:-]*)(?:#([A-Za-z_][\w:-]*))?(?:\.[A-Za-z_][\w:-]*)*$/);
65
- if (identity?.[2]) {
66
- declarations.push({
67
- id: identity[2],
68
- kind: `${identity[1]} fenced block`,
69
- line: index + 1,
70
- source: line.trim(),
71
- });
72
- }
73
- continue;
74
- }
75
-
76
- const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
77
- if (closing && closing[1][0] === fence.marker && closing[1].length >= fence.length)
78
- fence = null;
79
- }
80
-
81
- return declarations;
56
+ return parseFenceDeclarations(markdown)
57
+ .filter((declaration) => declaration.id)
58
+ .map((declaration) => ({
59
+ id: declaration.id,
60
+ kind: `${declaration.tag} fenced block`,
61
+ line: declaration.line,
62
+ source: declaration.source,
63
+ }));
82
64
  }
83
65
 
84
66
  export function checkMarkdownIdCollisions(markdown) {
@@ -0,0 +1,25 @@
1
+ import { appendFileSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export const KITTY_DEBUG_LOG = resolve(import.meta.dir, "../..", "kitty-placement.log");
5
+ export const KITTY_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(
6
+ String(process.env.JSMDCUI_KITTY_DEBUG ?? ""),
7
+ );
8
+
9
+ if (KITTY_DEBUG_ENABLED) {
10
+ try {
11
+ writeFileSync(KITTY_DEBUG_LOG, "");
12
+ } catch {}
13
+ }
14
+
15
+ export function logKittyPlacement(stage, details = {}) {
16
+ if (!KITTY_DEBUG_ENABLED) return;
17
+ try {
18
+ appendFileSync(KITTY_DEBUG_LOG, JSON.stringify({
19
+ time: new Date().toISOString(),
20
+ pid: process.pid,
21
+ stage,
22
+ ...details,
23
+ }) + "\n");
24
+ } catch {}
25
+ }