jsmdcui 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +112 -18
- package/demos/maze.md +144 -0
- package/demos/todo-zh.md +98 -0
- package/demos/todo.md +98 -0
- package/edit +9 -0
- package/package.json +1 -1
- package/runmd.mjs +44 -2
- package/runtime/help/help.md +112 -18
- package/runtime/jsplugins/cdp/cdp.js +5 -2
- package/runtime/jsplugins/chapter/chapter.js +4 -2
- package/runtime/jsplugins/example/example.js +10 -7
- package/runtime/syntax/markdown.yaml +1 -0
- package/src/cui/fence-events.mjs +106 -0
- package/src/cui/id-collision.mjs +10 -28
- package/src/cui/kitty-images.mjs +11 -1
- package/src/cui/rpc.mjs +6 -3
- package/src/cui/server.mjs +13 -2
- package/src/index.js +379 -62
- package/src/platform/terminal.js +5 -0
- package/src/plugins/js-bridge.js +114 -8
- package/tests/fence-events.test.js +405 -0
- package/tests/id-collision.test.js +11 -0
- package/tests/js-plugin-prompts.test.js +46 -0
- package/tests/key-event.md +10 -0
- package/tests/kitty-images.test.js +22 -0
- package/tests/platform-terminal.test.js +8 -0
- package/tests/textarea.md +8 -0
- package/tests/wui.test.js +16 -1
package/runmd.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
import { readInternalAssetText } from './src/runtime/assets.js'
|
|
6
|
+
import { fenceEventMap } from './src/cui/fence-events.mjs'
|
|
6
7
|
import { REPO_ROOT } from './single-exe/compiled.js'
|
|
7
8
|
|
|
8
9
|
const csl=console.log
|
|
@@ -220,7 +221,7 @@ function escapeHtmlAttribute(value)
|
|
|
220
221
|
.replaceAll(">", ">");
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
export function convertWuiTextareas(html)
|
|
224
|
+
export function convertWuiTextareas(html, eventsById = new Map())
|
|
224
225
|
{
|
|
225
226
|
return String(html).replace(
|
|
226
227
|
/<pre><code class="language-([^"]+)">([^]*?)<\/code><\/pre>/g,
|
|
@@ -230,10 +231,50 @@ export function convertWuiTextareas(html)
|
|
|
230
231
|
const value = content.replace(/\n$/, "");
|
|
231
232
|
const contentLines = value.split("\n");
|
|
232
233
|
const cols = Math.max(1, ...contentLines.map(line => [...line].length));
|
|
234
|
+
const declaration = identity.id && eventsById.get(identity.id)?.tag === identity.tag
|
|
235
|
+
? eventsById.get(identity.id)
|
|
236
|
+
: null;
|
|
237
|
+
const keydownHandler = declaration?.events.get("keydown");
|
|
238
|
+
const keydownCode = keydownHandler
|
|
239
|
+
? [
|
|
240
|
+
"const __mdcuiKeyCode=Number(event.keyCode||event.which||0);",
|
|
241
|
+
"const __mdcuiCodeLetter=/^Key[A-Z]$/.test(event.code||\"\")?event.code.charCodeAt(3):0;",
|
|
242
|
+
"const __mdcuiLetterCode=__mdcuiKeyCode>=65&&__mdcuiKeyCode<=90?__mdcuiKeyCode:__mdcuiCodeLetter;",
|
|
243
|
+
"const __mdcuiAltGraph=!!event.getModifierState&&event.getModifierState(\"AltGraph\");",
|
|
244
|
+
"const __mdcuiLetter=!__mdcuiAltGraph&&(event.ctrlKey||event.altKey||event.metaKey)&&__mdcuiLetterCode>=65&&__mdcuiLetterCode<=90;",
|
|
245
|
+
"if(__mdcuiLetter)Object.defineProperty(event,\"key\",{configurable:true,value:String.fromCharCode(__mdcuiLetterCode+(event.shiftKey?0:32))});",
|
|
246
|
+
"this.__mdcuiIdentifiedKeydown=!!event.key&&event.key!==\"Unidentified\";",
|
|
247
|
+
"this.__mdcuiUnidentifiedKeydown=event.key===\"Unidentified\"?{keyCode:__mdcuiKeyCode,ctrlKey:!!event.ctrlKey,shiftKey:!!event.shiftKey,altKey:!!event.altKey,metaKey:!!event.metaKey,altGraph:__mdcuiAltGraph}:null;",
|
|
248
|
+
"clearTimeout(this.__mdcuiKeydownReset);",
|
|
249
|
+
"this.__mdcuiKeydownReset=setTimeout(()=>{this.__mdcuiIdentifiedKeydown=false;this.__mdcuiUnidentifiedKeydown=null},0);",
|
|
250
|
+
"if(event.key!==\"Unidentified\"){\n",
|
|
251
|
+
"Object.defineProperty(event,\"toJSON\",{configurable:true,value:function(){const t=this.target||{};return{type:String(this.type||\"\"),key:String(this.key||\"\"),code:String(this.code||\"\"),raw:String(this.raw||\"\"),ctrlKey:!!this.ctrlKey,shiftKey:!!this.shiftKey,altKey:!!this.altKey,metaKey:!!this.metaKey,repeat:!!this.repeat,defaultPrevented:!!this.defaultPrevented,target:{id:String(t.id||\"\"),tagName:String(t.tagName||\"\"),className:String(t.className||\"\"),value:String(t.value??\"\")}}}});",
|
|
252
|
+
keydownHandler.modifiers.includes("prevent")
|
|
253
|
+
? "event.preventDefault();"
|
|
254
|
+
: "",
|
|
255
|
+
keydownHandler.code,
|
|
256
|
+
"\n}",
|
|
257
|
+
].join("")
|
|
258
|
+
: "";
|
|
259
|
+
const beforeInputCode = keydownHandler
|
|
260
|
+
? [
|
|
261
|
+
"if(!this.__mdcuiIdentifiedKeydown&&event.data!=null&&event.data!==\"\"){",
|
|
262
|
+
"const m=this.__mdcuiUnidentifiedKeydown||{};",
|
|
263
|
+
"const letter=!m.altGraph&&(m.ctrlKey||m.altKey||m.metaKey)&&m.keyCode>=65&&m.keyCode<=90?String.fromCharCode(m.keyCode+(m.shiftKey?0:32)):String(event.data);",
|
|
264
|
+
"Object.defineProperties(event,{key:{configurable:true,value:letter},ctrlKey:{configurable:true,value:!!m.ctrlKey},shiftKey:{configurable:true,value:!!m.shiftKey},altKey:{configurable:true,value:!!m.altKey},metaKey:{configurable:true,value:!!m.metaKey}});",
|
|
265
|
+
"this.onkeydown(event)",
|
|
266
|
+
"}",
|
|
267
|
+
].join("")
|
|
268
|
+
: "";
|
|
269
|
+
const inlineEventAttrs = [
|
|
270
|
+
keydownCode ? `onkeydown="${escapeHtmlAttribute(keydownCode)}"` : "",
|
|
271
|
+
beforeInputCode ? `onbeforeinput="${escapeHtmlAttribute(beforeInputCode)}"` : "",
|
|
272
|
+
];
|
|
233
273
|
const attrs = [
|
|
234
274
|
`data-mdcui-tag="${identity.tag}"`,
|
|
235
275
|
`data-mdcui-language="${escapeHtmlAttribute(info)}"`,
|
|
236
276
|
identity.id ? `id="${escapeHtmlAttribute(identity.id)}"` : "",
|
|
277
|
+
...inlineEventAttrs,
|
|
237
278
|
`class="${escapeHtmlAttribute([
|
|
238
279
|
`language-${info}`,
|
|
239
280
|
...identity.classes,
|
|
@@ -306,6 +347,7 @@ export function wrapWuiHeadingSections(html)
|
|
|
306
347
|
|
|
307
348
|
export async function createWui(md,mdpath) // HTML
|
|
308
349
|
{
|
|
350
|
+
const eventsById = fenceEventMap(md)
|
|
309
351
|
|
|
310
352
|
const opts = {
|
|
311
353
|
headings: { ids: true }
|
|
@@ -335,7 +377,7 @@ export async function createWui(md,mdpath) // HTML
|
|
|
335
377
|
}
|
|
336
378
|
)
|
|
337
379
|
|
|
338
|
-
md = convertWuiTextareas(md)
|
|
380
|
+
md = convertWuiTextareas(md, eventsById)
|
|
339
381
|
md = wrapWuiHeadingSections(md)
|
|
340
382
|
|
|
341
383
|
const mdb = path.basename(mdpath);
|
package/runtime/help/help.md
CHANGED
|
@@ -89,6 +89,11 @@ npx jsmdcui --demo-imgtool
|
|
|
89
89
|
npx jsmdcui --demo-imgtool-zh
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
```sh
|
|
93
|
+
# Maze game demo
|
|
94
|
+
npx jsmdcui --demo-maze
|
|
95
|
+
```
|
|
96
|
+
|
|
92
97
|
```sh
|
|
93
98
|
# WUI(Web User Interface) Demo
|
|
94
99
|
npx jsmdcui --wui
|
|
@@ -134,7 +139,7 @@ bun src/index.js --wui testapp.md
|
|
|
134
139
|
| --- | --- |
|
|
135
140
|
| `bun src/index.js app.md` | Render `app.md` as a read-only terminal UI and write five generated files beside it. |
|
|
136
141
|
| `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` |
|
|
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. |
|
|
138
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. |
|
|
139
144
|
| `JSMDCUI_KITTY_DEBUG=1 bun src/index.js --kitty app.md` | Enable Kitty image placement logging to `kitty-placement.log`. |
|
|
140
145
|
| `bun src/index.js --check app.md` | Check heading and fenced-block IDs for collisions, print line-by-line details, and exit. |
|
|
@@ -239,12 +244,81 @@ native `<textarea>` with the declared ID and classes. Long text wraps
|
|
|
239
244
|
automatically, and the field height is recalculated when the user types, the
|
|
240
245
|
window is resized, or frontend code calls `.val(value)`.
|
|
241
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
|
+
|
|
242
313
|
In the terminal TUI, only content after the protected `│ ` or `| ` prefix can
|
|
243
|
-
be edited
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
248
322
|
|
|
249
323
|
### Heading task lists and selector API
|
|
250
324
|
|
|
@@ -384,7 +458,7 @@ The available selector methods are:
|
|
|
384
458
|
| Method | TUI | WUI |
|
|
385
459
|
| --- | --- | --- |
|
|
386
460
|
| `.val()` | Read text blocks or heading task-list values. | Read textareas/controls or heading task-list values. |
|
|
387
|
-
| `.val(value)` | Replace text-block contents
|
|
461
|
+
| `.val(value)` | Replace text-block contents, resize multiline values, and record Undo/Redo history. | Set textarea/control values and resize textareas. |
|
|
388
462
|
| `.html()` | Return a selected heading's inner HTML. | Return any successfully selected DOM element's `innerHTML`. |
|
|
389
463
|
| `.line()` | Return a heading's current 1-based TUI row, or `0` if missing. | Not available. |
|
|
390
464
|
| `.push(...items)` | Append unchecked strings or `{ value, checked }` task items; return the new direct-item count. | Same. |
|
|
@@ -397,16 +471,16 @@ The available selector methods are:
|
|
|
397
471
|
TUI heading rows are recalculated after text-block rows are added, removed, or
|
|
398
472
|
replaced with multiline `.val(value)` content.
|
|
399
473
|
|
|
400
|
-
The
|
|
474
|
+
The 3 UI building blocks are:
|
|
401
475
|
|
|
402
|
-
- Regular Markdown provides headings, text, lists, task checkboxes, code, and
|
|
476
|
+
- 1. `Regular Markdown` provides headings, text, lists, task checkboxes, code, and
|
|
403
477
|
links.
|
|
404
|
-
-
|
|
478
|
+
- 2. `js front` block contains UI code. Exported functions can use
|
|
405
479
|
`alert`, `confirm`, `prompt`, and the generated `rpc` client.
|
|
406
|
-
|
|
407
|
-
The terminal UI awaits it before closing an `mdcui` buffer.
|
|
408
|
-
`mdcui` buffers close without a save prompt.
|
|
409
|
-
-
|
|
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,
|
|
410
484
|
`rpc` publishes only exported functions whose exported names do not start
|
|
411
485
|
with `_`. Call a published function from the front end with
|
|
412
486
|
`await rpc.functionName(arg1, arg2)`.
|
|
@@ -489,15 +563,35 @@ its associated text. `alert`, `confirm`, and `prompt` use the browser's built-in
|
|
|
489
563
|
dialogs. Checkbox changes exist only in the current page: refreshing does not
|
|
490
564
|
preserve them and does not update the Markdown file.
|
|
491
565
|
|
|
492
|
-
The WUI
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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.
|
|
496
573
|
|
|
497
574
|
Each server start prints a new random path. The old URL stops working after the
|
|
498
575
|
server is stopped or restarted. Keep the process running while using the page,
|
|
499
576
|
and press `Ctrl-C` in its terminal to stop it.
|
|
500
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
|
+
|
|
501
595
|
## Generated files
|
|
502
596
|
|
|
503
597
|
Opening a local Markdown UI generates these files beside the source file:
|
|
@@ -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
|
-
//
|
|
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",
|
|
64
|
+
micro.MakeCommand("showpath", (bp, args) =>
|
|
62
65
|
{
|
|
63
66
|
//micro.TermMessage("Hello from JS plugin! Args: " + args.join(", "));
|
|
64
67
|
|
|
65
|
-
//
|
|
68
|
+
//micro.alert(micro.getLine())
|
|
66
69
|
const path = bp?.Buf?.Path || "(no path)";
|
|
67
70
|
const loc = bp?.CursorLocation?.() || "+1.0:1";
|
|
68
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
95
|
+
micro.alert(s);
|
|
93
96
|
});
|
|
94
97
|
|
|
95
98
|
// Register a custom action (can be bound to a key in keybindings.json)
|
|
@@ -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
|
+
}
|
package/src/cui/id-collision.mjs
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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) {
|
package/src/cui/kitty-images.mjs
CHANGED
|
@@ -117,6 +117,10 @@ function stableImageId(pathname, line, data) {
|
|
|
117
117
|
return (hash >>> 0) % 2147483646 + 1;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
async function convertToKittyPng(data) {
|
|
121
|
+
return Buffer.from(await new Bun.Image(data).png().toBuffer());
|
|
122
|
+
}
|
|
123
|
+
|
|
120
124
|
export function fitKittyImageToWidth(image, maxCols) {
|
|
121
125
|
const originalCols = Math.max(1, Math.trunc(Number(image?.cols) || 1));
|
|
122
126
|
const originalRows = Math.max(1, Math.trunc(Number(image?.rows) || 1));
|
|
@@ -151,8 +155,14 @@ export async function prepareKittyImages(ansiText, markdownPath, terminalCols =
|
|
|
151
155
|
if (!(await file.exists())) throw new Error("missing image");
|
|
152
156
|
data = Buffer.from(await file.arrayBuffer());
|
|
153
157
|
}
|
|
154
|
-
|
|
158
|
+
let size = imageSize(data);
|
|
155
159
|
if (!size?.width || !size?.height) throw new Error("unsupported image");
|
|
160
|
+
if (options.kittyMode === "compat" && size.mime !== "image/png") {
|
|
161
|
+
data = await convertToKittyPng(data);
|
|
162
|
+
size = imageSize(data);
|
|
163
|
+
if (size?.mime !== "image/png" || !size.width || !size.height)
|
|
164
|
+
throw new Error("PNG conversion failed");
|
|
165
|
+
}
|
|
156
166
|
const estimatedCols = Math.max(1, Math.round(size.width / 8));
|
|
157
167
|
const cols = Math.min(maxCols, estimatedCols);
|
|
158
168
|
const rows = Math.max(1, Math.round((cols * size.height) / size.width / 2));
|
package/src/cui/rpc.mjs
CHANGED
|
@@ -451,15 +451,18 @@ function safeFrontValue(value)
|
|
|
451
451
|
};
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
export async function evalFront(mod, text)
|
|
454
|
+
export async function evalFront(mod, text, scope = {})
|
|
455
455
|
{
|
|
456
456
|
try{
|
|
457
457
|
|
|
458
458
|
text = text.replace(/^javascript:/, "");
|
|
459
459
|
|
|
460
|
-
const
|
|
460
|
+
const entryMap = new Map(Object.entries(mod).filter(([name]) => name !== "$"));
|
|
461
461
|
if (typeof globalThis.$ === "function")
|
|
462
|
-
|
|
462
|
+
entryMap.set("$", globalThis.$);
|
|
463
|
+
for (const [name, value] of Object.entries(scope))
|
|
464
|
+
entryMap.set(name, value);
|
|
465
|
+
const entries = [...entryMap];
|
|
463
466
|
const names = entries.map(([name]) => name);
|
|
464
467
|
const values = entries.map(([, value]) => safeFrontValue(value));
|
|
465
468
|
|
package/src/cui/server.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function main()
|
|
|
50
50
|
|
|
51
51
|
const basePath = "/" + crypto.randomUUID()
|
|
52
52
|
const pathname = basePath + "/"
|
|
53
|
-
const
|
|
53
|
+
const serverOptions = {
|
|
54
54
|
port: Number(process.env.PORT || 3000),
|
|
55
55
|
development: {
|
|
56
56
|
hmr: true,
|
|
@@ -101,7 +101,18 @@ const server = Bun.serve({
|
|
|
101
101
|
fetch() {
|
|
102
102
|
return new Response("Not Found", { status: 404 });
|
|
103
103
|
},
|
|
104
|
-
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let server;
|
|
107
|
+
try {
|
|
108
|
+
server = Bun.serve(serverOptions);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const addressInUse = error?.code === "EADDRINUSE"
|
|
111
|
+
|| error?.cause?.code === "EADDRINUSE"
|
|
112
|
+
|| /EADDRINUSE|address already in use/i.test(String(error?.message || error));
|
|
113
|
+
if (serverOptions.port !== 3000 || !addressInUse) throw error;
|
|
114
|
+
server = Bun.serve({ ...serverOptions, port: 0 });
|
|
115
|
+
}
|
|
105
116
|
|
|
106
117
|
console.error(mda("- Bun RPC server listening on"));
|
|
107
118
|
console.log(`http://localhost:${server.port}${pathname}`);
|