jsmdcui 0.4.0 → 0.5.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 CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  All notable user-visible changes to jsmdcui are documented here.
4
4
 
5
+ ## [0.5.0] - 2026-07-16
6
+
7
+ This update adds cross-environment form-like text blocks and a small
8
+ jQuery-style selector API, while keeping the same Markdown source usable in
9
+ both the terminal and browser interfaces.
10
+
11
+ ### Added
12
+
13
+ - Add a minimal global `$` API to terminal frontend evaluation, JavaScript
14
+ plugins, generated browser frontend modules, and `javascript:` links.
15
+ - Support tag, ID, class, and combined selectors such as `$('text')`,
16
+ `$('#answer')`, `$('.field')`, and `$('text#answer.field')`.
17
+ - Add `.val()` and `.val(value)` for reading and replacing block contents.
18
+ Getters always return a string and use an empty string for missing elements,
19
+ invalid selectors, or lookup errors. Setters support chaining and resize
20
+ terminal blocks to fit multiline values.
21
+ - Recognize both raw Markdown fenced blocks and rendered Bun ANSI blocks,
22
+ including ASCII and Unicode frame characters.
23
+ - Add editable terminal `text` blocks. Content after the protected `│ ` or
24
+ `| ` prefix can be edited, while frame characters, line joins, inserted
25
+ newlines, and multiline pastes remain protected.
26
+ - Add interactive terminal text-block resizing. Activating the lower-left
27
+ frame corner adds an empty content row; activating the upper-left corner
28
+ removes only a trailing empty row and never deletes non-empty content.
29
+ - Add the optional frontend lifecycle callback
30
+ `onMdcuiExit({ reason, path, $ })`. The terminal awaits it before closing an
31
+ mdcui buffer and invokes it at most once per buffer.
32
+ - Add Markdown syntax completions for fenced block language identifiers.
33
+
34
+ ### Changed
35
+
36
+ - Convert `text` and `textarea` fenced blocks into native browser
37
+ `<textarea>` elements while preserving their ID, classes, original language
38
+ metadata, and selector compatibility.
39
+ - Prefer native `document.querySelector()` in the browser `$` implementation,
40
+ then fall back to mdcui metadata and the original `pre > code` structure.
41
+ - Automatically wrap and resize generated browser textareas on page load,
42
+ input, `.val(value)`, and window resize. Initial `rows` and `cols` are
43
+ derived from the Markdown content.
44
+ - Generate complete HTML5 documents for WUI output, including `<!doctype
45
+ html>`, UTF-8 metadata, a responsive viewport, a document title, and
46
+ `lang="zh-TW"`.
47
+ - Enable `softwrap` by default.
48
+ - Close modified mdcui buffers without displaying a save prompt. Applications
49
+ can use `onMdcuiExit` to collect edited field values or call backend RPC
50
+ functions before closing.
51
+ - Update the bundled example with editable text fields, selector API examples,
52
+ answer validation, and manual text-box resizing instructions.
53
+
54
+ ### Fixed
55
+
56
+ - Keep terminal text-block frame prefixes intact during editing and prevent
57
+ Delete at the end of a content row from merging it with the next row.
58
+ - Preserve browser textarea IDs and classes during Markdown-to-HTML
59
+ conversion, including compatibility metadata for original `text` selectors.
60
+ - Keep README and built-in help lifecycle documentation synchronized.
61
+
5
62
  ## [0.4.0] - 2026-07-16
6
63
 
7
64
  This release separates executable Markdown UI views from ordinary editable
package/README.md CHANGED
@@ -100,6 +100,11 @@ Create `app.md`:
100
100
 
101
101
  - [Say hello](javascript:sayHello())
102
102
  - [Get server time](javascript:showServerTime())
103
+ - [Update text box](javascript:updateText())
104
+
105
+ ```text#myid.myclass
106
+ Editable in both TUI and WUI
107
+ ```
103
108
 
104
109
  ```js front
105
110
  export function sayHello() {
@@ -111,6 +116,10 @@ export async function showServerTime() {
111
116
  const time = await rpc.getServerTime();
112
117
  alert(time);
113
118
  }
119
+
120
+ export function updateText() {
121
+ $('#myid').val($('.myclass').val() + ' ✓');
122
+ }
114
123
  ```
115
124
 
116
125
  ```js back
@@ -127,12 +136,37 @@ bun src/index.js app.md
127
136
  bun src/index.js --wui app.md
128
137
  ```
129
138
 
139
+ ### Text blocks
140
+
141
+ Both `text` and `textarea` fenced blocks define editable text fields:
142
+
143
+ ````md
144
+ ```text#message.note
145
+ Initial value
146
+ ```
147
+ ````
148
+
149
+ The same Markdown works in both interfaces. In the browser WUI it becomes a
150
+ native `<textarea>` with the declared ID and classes. Long text wraps
151
+ automatically, and the field height is recalculated when the user types, the
152
+ window is resized, or frontend code calls `.val(value)`.
153
+
154
+ In the terminal TUI, only content after the protected `│ ` or `| ` prefix can
155
+ be edited. The frame prefix cannot be deleted, Enter cannot insert a newline,
156
+ Delete at the end of a row cannot join the next row, and multiline paste is
157
+ blocked. Activate the lower-left frame corner to add a row. Activate the
158
+ upper-left frame corner to remove the trailing row only when it is empty;
159
+ non-empty content is never removed.
160
+
130
161
  The three UI building blocks are:
131
162
 
132
163
  - Regular Markdown provides headings, text, lists, task checkboxes, code, and
133
164
  links.
134
165
  - A `js front` block contains UI code. Exported functions can use
135
166
  `alert`, `confirm`, `prompt`, and the generated `rpc` client.
167
+ - A front module may export `async function onMdcuiExit({ reason, path, $ })`.
168
+ The terminal UI awaits it before closing an `mdcui` buffer. Modified
169
+ `mdcui` buffers close without a save prompt.
136
170
  - A `js back` block exports trusted backend functions. In the browser WUI,
137
171
  `rpc` publishes only exported functions whose exported names do not start
138
172
  with `_`. Call a published function from the front end with
@@ -149,6 +183,22 @@ Use a `javascript:` Markdown link to run front-end code:
149
183
  [Button label](javascript:exportedFunction())
150
184
  ```
151
185
 
186
+ Use `onMdcuiExit` when a terminal Markdown app needs to submit or otherwise
187
+ process edited fields before it closes:
188
+
189
+ ```js
190
+ export async function onMdcuiExit({ reason, path, $ }) {
191
+ await rpc.saveDraft({
192
+ reason,
193
+ path,
194
+ message: $('#message').val(),
195
+ });
196
+ }
197
+ ```
198
+
199
+ The callback is optional, may be asynchronous, and is called at most once for
200
+ each mdcui buffer.
201
+
152
202
  The front and back code blocks are extracted and are not shown in the rendered
153
203
  UI. In the terminal, `rpc` calls the generated backend module directly. In the
154
204
  browser, it sends the call to the relative `rpc` endpoint under the printed
@@ -156,8 +206,9 @@ UUID URL (`/<uuid>/rpc`).
156
206
 
157
207
  ## Terminal interaction
158
208
 
159
- Markdown files automatically use `mdcui` mode. The rendered buffer is
160
- read-only, but navigation, selection, search, and copy remain available.
209
+ Markdown files automatically use `mdcui` mode. Most rendered content remains
210
+ protected, while `text` block content rows can be edited. Navigation,
211
+ selection, search, and copy remain available.
161
212
 
162
213
  | Input | Result |
163
214
  | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsmdcui",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Markdown as a Common UI for Terminals and Web Browsers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/runmd.mjs CHANGED
@@ -189,6 +189,54 @@ export function createTui(md,TERMINAL_WIDTH=30) // ANSI Colors
189
189
  )
190
190
  }
191
191
 
192
+ function parseWuiControlIdentity(info)
193
+ {
194
+ const match = String(info ?? "").match(/^([A-Za-z_][\w:-]*)(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/);
195
+ if (!match || !["text", "textarea"].includes(match[1])) return null;
196
+ return {
197
+ tag: match[1],
198
+ id: match[2] || "",
199
+ classes: match[3] ? match[3].slice(1).split(".") : [],
200
+ };
201
+ }
202
+
203
+ function escapeHtmlAttribute(value)
204
+ {
205
+ return String(value)
206
+ .replaceAll("&", "&amp;")
207
+ .replaceAll('"', "&quot;")
208
+ .replaceAll("<", "&lt;")
209
+ .replaceAll(">", "&gt;");
210
+ }
211
+
212
+ export function convertWuiTextareas(html)
213
+ {
214
+ return String(html).replace(
215
+ /<pre><code class="language-([^"]+)">([^]*?)<\/code><\/pre>/g,
216
+ (whole, info, content) => {
217
+ const identity = parseWuiControlIdentity(info);
218
+ if (!identity) return whole;
219
+ const value = content.replace(/\n$/, "");
220
+ const contentLines = value.split("\n");
221
+ const cols = Math.max(1, ...contentLines.map(line => [...line].length));
222
+ const attrs = [
223
+ `data-mdcui-tag="${identity.tag}"`,
224
+ `data-mdcui-language="${escapeHtmlAttribute(info)}"`,
225
+ identity.id ? `id="${escapeHtmlAttribute(identity.id)}"` : "",
226
+ `class="${escapeHtmlAttribute([
227
+ `language-${info}`,
228
+ ...identity.classes,
229
+ ].join(" "))}"`,
230
+ `rows="${Math.max(1, contentLines.length)}"`,
231
+ `cols="${cols}"`,
232
+ 'wrap="soft"',
233
+ 'style="box-sizing:border-box;max-width:100%;width:100%;resize:vertical;overflow-y:hidden"',
234
+ ].filter(Boolean).join(" ");
235
+ return `<textarea ${attrs}>${value}</textarea>`;
236
+ },
237
+ );
238
+ }
239
+
192
240
  export async function createWui(md,mdpath) // HTML
193
241
  {
194
242
 
@@ -209,16 +257,31 @@ export async function createWui(md,mdpath) // HTML
209
257
  'class="task-list-item-checkbox" disabled',
210
258
  'class="task-list-item-checkbox"'
211
259
  )
260
+
261
+ md = convertWuiTextareas(md)
212
262
 
213
263
  const mdb = path.basename(mdpath);
214
264
 
215
-
216
- md += `
217
-
218
- <scr`+`ipt type="module" src="./${mdb}.front.js">
219
- </scr`+`ipt>
220
-
221
- `;
265
+ const moduleScript = `<scr`+`ipt type="module" src="./${mdb}.front.js"></scr`+`ipt>`;
266
+ if (!/^\s*<!doctype html>/i.test(md)) {
267
+ md = `<!doctype html>
268
+ <html lang="zh-TW">
269
+ <head>
270
+ <meta charset="utf-8">
271
+ <meta name="viewport" content="width=device-width, initial-scale=1">
272
+ <title>${escapeHtmlAttribute(mdb)}</title>
273
+ </head>
274
+ <body>
275
+ ${md}
276
+ ${moduleScript}
277
+ </body>
278
+ </html>
279
+ `;
280
+ } else if (/<\/body\s*>/i.test(md)) {
281
+ md = md.replace(/<\/body\s*>/i, `${moduleScript}\n</body>`);
282
+ } else {
283
+ md += `\n${moduleScript}\n`;
284
+ }
222
285
 
223
286
 
224
287
  await Bun.write(mdpath+".html",md)
@@ -100,6 +100,11 @@ Create `app.md`:
100
100
 
101
101
  - [Say hello](javascript:sayHello())
102
102
  - [Get server time](javascript:showServerTime())
103
+ - [Update text box](javascript:updateText())
104
+
105
+ ```text#myid.myclass
106
+ Editable in both TUI and WUI
107
+ ```
103
108
 
104
109
  ```js front
105
110
  export function sayHello() {
@@ -111,6 +116,10 @@ export async function showServerTime() {
111
116
  const time = await rpc.getServerTime();
112
117
  alert(time);
113
118
  }
119
+
120
+ export function updateText() {
121
+ $('#myid').val($('.myclass').val() + ' ✓');
122
+ }
114
123
  ```
115
124
 
116
125
  ```js back
@@ -127,12 +136,37 @@ bun src/index.js app.md
127
136
  bun src/index.js --wui app.md
128
137
  ```
129
138
 
139
+ ### Text blocks
140
+
141
+ Both `text` and `textarea` fenced blocks define editable text fields:
142
+
143
+ ````md
144
+ ```text#message.note
145
+ Initial value
146
+ ```
147
+ ````
148
+
149
+ The same Markdown works in both interfaces. In the browser WUI it becomes a
150
+ native `<textarea>` with the declared ID and classes. Long text wraps
151
+ automatically, and the field height is recalculated when the user types, the
152
+ window is resized, or frontend code calls `.val(value)`.
153
+
154
+ In the terminal TUI, only content after the protected `│ ` or `| ` prefix can
155
+ be edited. The frame prefix cannot be deleted, Enter cannot insert a newline,
156
+ Delete at the end of a row cannot join the next row, and multiline paste is
157
+ blocked. Activate the lower-left frame corner to add a row. Activate the
158
+ upper-left frame corner to remove the trailing row only when it is empty;
159
+ non-empty content is never removed.
160
+
130
161
  The three UI building blocks are:
131
162
 
132
163
  - Regular Markdown provides headings, text, lists, task checkboxes, code, and
133
164
  links.
134
165
  - A `js front` block contains UI code. Exported functions can use
135
166
  `alert`, `confirm`, `prompt`, and the generated `rpc` client.
167
+ - A front module may export `async function onMdcuiExit({ reason, path, $ })`.
168
+ The terminal UI awaits it before closing an `mdcui` buffer. Modified
169
+ `mdcui` buffers close without a save prompt.
136
170
  - A `js back` block exports trusted backend functions. In the browser WUI,
137
171
  `rpc` publishes only exported functions whose exported names do not start
138
172
  with `_`. Call a published function from the front end with
@@ -149,6 +183,22 @@ Use a `javascript:` Markdown link to run front-end code:
149
183
  [Button label](javascript:exportedFunction())
150
184
  ```
151
185
 
186
+ Use `onMdcuiExit` when a terminal Markdown app needs to submit or otherwise
187
+ process edited fields before it closes:
188
+
189
+ ```js
190
+ export async function onMdcuiExit({ reason, path, $ }) {
191
+ await rpc.saveDraft({
192
+ reason,
193
+ path,
194
+ message: $('#message').val(),
195
+ });
196
+ }
197
+ ```
198
+
199
+ The callback is optional, may be asynchronous, and is called at most once for
200
+ each mdcui buffer.
201
+
152
202
  The front and back code blocks are extracted and are not shown in the rendered
153
203
  UI. In the terminal, `rpc` calls the generated backend module directly. In the
154
204
  browser, it sends the call to the relative `rpc` endpoint under the printed
@@ -156,8 +206,9 @@ UUID URL (`/<uuid>/rpc`).
156
206
 
157
207
  ## Terminal interaction
158
208
 
159
- Markdown files automatically use `mdcui` mode. The rendered buffer is
160
- read-only, but navigation, selection, search, and copy remain available.
209
+ Markdown files automatically use `mdcui` mode. Most rendered content remains
210
+ protected, while `text` block content rows can be edited. Navigation,
211
+ selection, search, and copy remain available.
161
212
 
162
213
  | Input | Result |
163
214
  | --- | --- |
@@ -47,3 +47,15 @@ rules:
47
47
  start: "`"
48
48
  end: "`"
49
49
  rules: []
50
+
51
+
52
+ # keywords
53
+ # javascript alert confirm prompt rpc
54
+ # front back
55
+ # console log error JSON stringify parse
56
+ # async await Promise
57
+ # textarea text password search
58
+ # email url tel number range color
59
+ # date time datetime-local month week
60
+ # checkbox radio file hidden image button submit reset
61
+
@@ -38,7 +38,7 @@ export const DEFAULT_COMMON_SETTINGS = {
38
38
  scrollspeed: 2,
39
39
  showchars: "",
40
40
  smartpaste: true,
41
- softwrap: false,
41
+ softwrap: true,
42
42
  splitbottom: true,
43
43
  splitright: true,
44
44
  statusformatl: "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
package/src/cui/rpc.mjs CHANGED
@@ -9,6 +9,138 @@ let apilist = new Map()
9
9
 
10
10
  export const jss = JSON.stringify
11
11
 
12
+ function parseDollarIdentity(input, { selector = false } = {})
13
+ {
14
+ const text = String(input ?? "").trim();
15
+ const match = text.match(/^([A-Za-z_][\w:-]*)?(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/);
16
+ if (!match || (!match[1] && !match[2] && !match[3])) return null;
17
+ if (!selector && !match[1]) return null;
18
+ return {
19
+ tag: match[1] || null,
20
+ id: match[2] || null,
21
+ classes: match[3] ? match[3].slice(1).split(".") : [],
22
+ };
23
+ }
24
+
25
+ function matchesDollarIdentity(identity, selector)
26
+ {
27
+ if (selector.tag && identity.tag !== selector.tag) return false;
28
+ if (selector.id && identity.id !== selector.id) return false;
29
+ return selector.classes.every(name => identity.classes.includes(name));
30
+ }
31
+
32
+ function findMarkdownCodeElement(documentObject, selector)
33
+ {
34
+ for (const code of documentObject?.querySelectorAll?.("pre > code") ?? []) {
35
+ for (const className of code.classList ?? []) {
36
+ if (!className.startsWith("language-")) continue;
37
+ const identity = parseDollarIdentity(className.slice("language-".length));
38
+ if (identity && matchesDollarIdentity(identity, selector)) return code;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function findWebDollarElement(documentObject, selectorText, selector)
45
+ {
46
+ try {
47
+ const direct = documentObject?.querySelector?.(String(selectorText));
48
+ if (direct) return direct;
49
+ } catch {}
50
+
51
+ for (const element of documentObject?.querySelectorAll?.("[data-mdcui-tag]") ?? []) {
52
+ const identity = {
53
+ tag: element.getAttribute?.("data-mdcui-tag") || null,
54
+ id: element.id || null,
55
+ classes: [...(element.classList ?? [])],
56
+ };
57
+ if (matchesDollarIdentity(identity, selector)) return element;
58
+ }
59
+
60
+ return findMarkdownCodeElement(documentObject, selector);
61
+ }
62
+
63
+ function webDollarValue(element)
64
+ {
65
+ if (element && "value" in element) return String(element.value ?? "");
66
+ return String(element?.textContent ?? "").replace(/\n$/, "");
67
+ }
68
+
69
+ function resizeWebTextarea(element)
70
+ {
71
+ if (!element || String(element.tagName ?? "").toLowerCase() !== "textarea") return;
72
+ try {
73
+ element.style.height = "auto";
74
+ element.style.height = `${element.scrollHeight}px`;
75
+ const lineHeight = Number.parseFloat(
76
+ element.ownerDocument?.defaultView?.getComputedStyle?.(element)?.lineHeight,
77
+ );
78
+ if (Number.isFinite(lineHeight) && lineHeight > 0)
79
+ element.rows = Math.max(1, Math.ceil(element.scrollHeight / lineHeight));
80
+ } catch {}
81
+ }
82
+
83
+ function installWebTextareaResize(target)
84
+ {
85
+ const documentObject = target?.document;
86
+ if (!documentObject || documentObject.__mdcuiTextareaResizeInstalled) return;
87
+ documentObject.__mdcuiTextareaResizeInstalled = true;
88
+ const resizeAll = () => {
89
+ for (const element of documentObject.querySelectorAll?.("textarea[data-mdcui-tag]") ?? [])
90
+ resizeWebTextarea(element);
91
+ };
92
+ documentObject.addEventListener?.("input", event => {
93
+ if (event.target?.matches?.("textarea[data-mdcui-tag]"))
94
+ resizeWebTextarea(event.target);
95
+ });
96
+ target.addEventListener?.("resize", resizeAll);
97
+ if (documentObject.readyState === "loading")
98
+ documentObject.addEventListener?.("DOMContentLoaded", resizeAll, { once: true });
99
+ else
100
+ queueMicrotask(resizeAll);
101
+ }
102
+
103
+ export function createWebDollar(documentObject = globalThis.document)
104
+ {
105
+ return function $(selectorText) {
106
+ const selector = parseDollarIdentity(selectorText, { selector: true });
107
+ const selection = {
108
+ val(...args) {
109
+ try {
110
+ if (!selector) return args.length > 0 ? selection : "";
111
+ const element = findWebDollarElement(documentObject, selectorText, selector);
112
+ if (!element) return args.length > 0 ? selection : "";
113
+ if (args.length > 0) {
114
+ const value = String(args[0] ?? "");
115
+ if ("value" in element) {
116
+ element.value = value;
117
+ resizeWebTextarea(element);
118
+ }
119
+ else element.textContent = value;
120
+ return selection;
121
+ }
122
+ return webDollarValue(element);
123
+ } catch {
124
+ return args.length > 0 ? selection : "";
125
+ }
126
+ },
127
+ };
128
+ return selection;
129
+ };
130
+ }
131
+
132
+ export function installWebDollar(target = globalThis)
133
+ {
134
+ if (!target?.document) return target?.$;
135
+ const $ = createWebDollar(target.document);
136
+ target.$ = $;
137
+ installWebTextareaResize(target);
138
+ return $;
139
+ }
140
+
141
+ if (typeof globalThis.document !== "undefined")
142
+ installWebDollar(globalThis);
143
+
12
144
 
13
145
  export async function evalBack(backmod, qjson)
14
146
  {
@@ -99,8 +231,11 @@ try{
99
231
 
100
232
  text = text.replace(/^javascript:/, "");
101
233
 
102
- const names = Object.keys(mod);
103
- const values = Object.values(mod).map(safeFrontValue);
234
+ const entries = Object.entries(mod).filter(([name]) => name !== "$");
235
+ if (typeof globalThis.$ === "function")
236
+ entries.push(["$", globalThis.$]);
237
+ const names = entries.map(([name]) => name);
238
+ const values = entries.map(([, value]) => safeFrontValue(value));
104
239
 
105
240
  try {
106
241
  return await new AsyncFunction(...names, `return await (${text})`)(...values);
package/src/index.js CHANGED
@@ -195,7 +195,7 @@ const DEFAULT_SETTINGS = {
195
195
  backup: true,
196
196
  backupdir: "",
197
197
  permbackup: false,
198
- softwrap: false,
198
+ softwrap: true,
199
199
  wordwrap: false,
200
200
  pageoverlap: 2,
201
201
  scrollmargin: 3,
@@ -538,6 +538,70 @@ function mdcuiCellPayload(buf, y, x, trigger = "unknown") {
538
538
  };
539
539
  }
540
540
 
541
+ function resizeMdcuiTextBlock(buf, y, x) {
542
+ if (!buf || !isMdcuiEncoding(buf.encoding) || x !== 0) return null;
543
+ const line = String(buf.lines?.[y] ?? "");
544
+ const top = line.match(/^(\s*)(┌─|╭─|\+-)\s*text(?:[#.][A-Za-z_][\w:.-]*)*\s*$/);
545
+ const bottom = line.match(/^(\s*)(└─|╰─|\+-)\s*$/);
546
+ if (!top && !bottom) return null;
547
+
548
+ let insertAt = -1;
549
+ let removeAt = -1;
550
+ let bodyLine = "";
551
+
552
+ if (bottom) {
553
+ for (let row = y - 1; row >= 0; row--) {
554
+ const header = String(buf.lines[row] ?? "").match(/^(\s*)(┌─|╭─|\+-)\s*text(?:[#.][A-Za-z_][\w:.-]*)*\s*$/);
555
+ if (!header || header[1] !== bottom[1]) continue;
556
+ const marker = header[2] === "+-" ? "|" : "│";
557
+ insertAt = y;
558
+ bodyLine = header[1] + marker + " ";
559
+ break;
560
+ }
561
+ } else if (top) {
562
+ const marker = top[2] === "+-" ? "|" : "│";
563
+ for (let row = y + 1; row < buf.lines.length; row++) {
564
+ const rest = String(buf.lines[row] ?? "").slice(top[1].length);
565
+ if (/^(?:└─|╰─|\+-)\s*$/.test(rest)) {
566
+ const candidate = row - 1;
567
+ if (candidate > y && String(buf.lines[candidate] ?? "") === top[1] + marker + " ")
568
+ removeAt = candidate;
569
+ break;
570
+ }
571
+ }
572
+ }
573
+
574
+ if (insertAt < 0 && removeAt < 0) return "unchanged";
575
+ buf.pushUndo?.(true);
576
+
577
+ const row = insertAt >= 0 ? insertAt : removeAt;
578
+ const deleteCount = removeAt >= 0 ? 1 : 0;
579
+ const replacement = insertAt >= 0 ? [bodyLine] : [];
580
+ buf.lines.splice(row, deleteCount, ...replacement);
581
+
582
+ if (Array.isArray(buf._ansiStyleLines)) {
583
+ const template = buf._ansiStyleLines[Math.max(0, row - 1)] ?? null;
584
+ buf._ansiStyleLines.splice(row, deleteCount, ...replacement.map(() => template));
585
+ }
586
+ if (typeof buf._mdcuiAnsiText === "string") {
587
+ const ansiLines = buf._mdcuiAnsiText.split("\n");
588
+ ansiLines.splice(row, deleteCount, ...replacement);
589
+ buf._mdcuiAnsiText = ansiLines.join("\n");
590
+ }
591
+
592
+ if (insertAt >= 0) {
593
+ if (buf.cursor.y >= insertAt) buf.cursor.y++;
594
+ } else if (buf.cursor.y > removeAt) {
595
+ buf.cursor.y--;
596
+ } else if (buf.cursor.y === removeAt) {
597
+ buf.cursor.y = Math.max(0, removeAt - 1);
598
+ }
599
+ buf.invalidateHighlightFrom?.(row, { force: true });
600
+ buf.modified = true;
601
+ buf.ensureCursor?.();
602
+ return insertAt >= 0 ? "added" : "removed";
603
+ }
604
+
541
605
  function detectFileFormat(text, fallback = DEFAULT_SETTINGS.fileformat) {
542
606
  if (text.length === 0) return fallback === "dos" ? "dos" : "unix";
543
607
  const newlineIdx = text.indexOf("\n");
@@ -557,6 +621,24 @@ function isReadonlyBuffer(buf) {
557
621
  return Boolean(buf?.readonly || buf?.Settings?.readonly || buf?.Type?.Readonly);
558
622
  }
559
623
 
624
+ function mdcuiEditablePrefixLength(buf, y = buf?.cursor?.y ?? 0) {
625
+ if (!isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding)) return 0;
626
+ const line = String(buf?.lines?.[y] ?? "");
627
+ return /^(?:│|\|) /.test(line) ? 2 : 0;
628
+ }
629
+
630
+ function canEditMdcuiAtCursor(buf) {
631
+ const prefixLength = mdcuiEditablePrefixLength(buf);
632
+ return prefixLength > 0 && (buf?.cursor?.x ?? 0) >= prefixLength;
633
+ }
634
+
635
+ function canEditMdcuiSelection(buf, selection) {
636
+ if (!selection) return true;
637
+ const { first, last } = selectionBounds(selection);
638
+ const prefixLength = mdcuiEditablePrefixLength(buf, first.y);
639
+ return first.y === last.y && prefixLength > 0 && first.x >= prefixLength;
640
+ }
641
+
560
642
  function isEditLockedBuffer(buf) {
561
643
  return isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding);
562
644
  }
@@ -1198,7 +1280,8 @@ class BufferModel {
1198
1280
  }
1199
1281
 
1200
1282
  insert(text) {
1201
- if (this.isEditLocked()) return false;
1283
+ if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
1284
+ if (isMdcuiEncoding(this.encoding) && /[\r\n]/.test(String(text))) return false;
1202
1285
  for (const ch of text) {
1203
1286
  if (ch === "\r" || ch === "\n") this.newline(false);
1204
1287
  else if (ch >= " " || ch === "\t") this.insertChar(ch);
@@ -1207,7 +1290,7 @@ class BufferModel {
1207
1290
  }
1208
1291
 
1209
1292
  insertChar(ch) {
1210
- if (this.isEditLocked()) return false;
1293
+ if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
1211
1294
  if (ch === "\t" && DEFAULT_SETTINGS.tabstospaces) ch = " ".repeat(DEFAULT_SETTINGS.tabsize);
1212
1295
  const line = this.line();
1213
1296
  this.lines[this.cursor.y] = line.slice(0, this.cursor.x) + ch + line.slice(this.cursor.x);
@@ -1218,6 +1301,7 @@ class BufferModel {
1218
1301
  }
1219
1302
 
1220
1303
  newline(autoindent = true) {
1304
+ if (isMdcuiEncoding(this.encoding)) return false;
1221
1305
  if (this.isEditLocked()) return false;
1222
1306
  const line = this.line();
1223
1307
  const left = line.slice(0, this.cursor.x);
@@ -1236,7 +1320,9 @@ class BufferModel {
1236
1320
  }
1237
1321
 
1238
1322
  backspace() {
1239
- if (this.isEditLocked()) return false;
1323
+ if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
1324
+ const mdcuiPrefixLength = mdcuiEditablePrefixLength(this);
1325
+ if (mdcuiPrefixLength > 0 && this.cursor.x <= mdcuiPrefixLength) return false;
1240
1326
  if (this.cursor.x > 0) {
1241
1327
  const line = this.line();
1242
1328
  let start = this.cursor.x - 1;
@@ -1263,7 +1349,7 @@ class BufferModel {
1263
1349
  }
1264
1350
 
1265
1351
  deleteForward() {
1266
- if (this.isEditLocked()) return false;
1352
+ if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
1267
1353
  const line = this.line();
1268
1354
  if (this.cursor.x < line.length) {
1269
1355
  const cp = line.codePointAt(this.cursor.x);
@@ -1272,6 +1358,8 @@ class BufferModel {
1272
1358
  this.invalidateHighlightFrom(this.cursor.y);
1273
1359
  this.modified = true;
1274
1360
  return true;
1361
+ } else if (isMdcuiEncoding(this.encoding)) {
1362
+ return false;
1275
1363
  } else if (this.cursor.y < this.lines.length - 1) {
1276
1364
  this.lines[this.cursor.y] += this.lines[this.cursor.y + 1];
1277
1365
  this.lines.splice(this.cursor.y + 1, 1);
@@ -1401,8 +1489,8 @@ class BufferModel {
1401
1489
  this.cursor.x = x;
1402
1490
  }
1403
1491
 
1404
- pushUndo() {
1405
- if (this.isEditLocked()) return;
1492
+ pushUndo(force = false) {
1493
+ if (!force && this.isEditLocked() && !canEditMdcuiAtCursor(this)) return;
1406
1494
  this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1407
1495
  this._undoSerial = (this._undoSerial ?? 0) + 1;
1408
1496
  if (this.undoStack.length > 500) this.undoStack.shift();
@@ -1410,7 +1498,7 @@ class BufferModel {
1410
1498
  }
1411
1499
 
1412
1500
  undo() {
1413
- if (this.isEditLocked()) return false;
1501
+ if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
1414
1502
  if (!this.undoStack.length) return false;
1415
1503
  this.redoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1416
1504
  const s = this.undoStack.pop();
@@ -1423,7 +1511,7 @@ class BufferModel {
1423
1511
  }
1424
1512
 
1425
1513
  redo() {
1426
- if (this.isEditLocked()) return false;
1514
+ if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
1427
1515
  if (!this.redoStack.length) return false;
1428
1516
  this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
1429
1517
  const s = this.redoStack.pop();
@@ -2302,6 +2390,7 @@ class App {
2302
2390
  this._messageRowY = null;
2303
2391
  this._messageRowClickZone = null;
2304
2392
  this._resizeRenderSeq = 0;
2393
+ this._mdcuiExitNotified = new WeakSet();
2305
2394
  }
2306
2395
 
2307
2396
  get tab() { return this.tabs[this.activeTabIdx]; }
@@ -2445,6 +2534,8 @@ class App {
2445
2534
  }
2446
2535
 
2447
2536
  async stop(code = 0) {
2537
+ for (const buf of this.buffers)
2538
+ await this.notifyMdcuiExit(buf, "exit");
2448
2539
  this.running = false;
2449
2540
  if (this._backupTimer) { clearInterval(this._backupTimer); this._backupTimer = null; }
2450
2541
  await Promise.allSettled(this.buffers.map((buf) => buf._backupWritePromise).filter(Boolean));
@@ -3488,7 +3579,15 @@ class App {
3488
3579
  }
3489
3580
 
3490
3581
  if (event.type === "paste") {
3491
- if (isEditLockedBuffer(buf)) {
3582
+ if (
3583
+ (isEditLockedBuffer(buf) && !isMdcuiEncoding(buf?.encoding))
3584
+ || (isMdcuiEncoding(buf?.encoding) && (
3585
+ !canEditMdcuiAtCursor(buf)
3586
+ ||
3587
+ /[\r\n]/.test(event.text)
3588
+ || !canEditMdcuiSelection(buf, this.pane?.selection)
3589
+ ))
3590
+ ) {
3492
3591
  this.message = "Buffer is read-only";
3493
3592
  this.render();
3494
3593
  return;
@@ -3866,13 +3965,17 @@ class App {
3866
3965
  break;
3867
3966
  default:
3868
3967
  if (text >= " " || text.includes("\n")) {
3869
- if (isMdcuiEncoding(buf?.encoding) && text === " ") {
3968
+ if (isMdcuiEncoding(buf?.encoding) && text === " " && !canEditMdcuiAtCursor(buf)) {
3870
3969
  await this.handleMdcuiCellCallback(buf, buf.cursor.y, buf.cursor.x, "space");
3871
3970
  break;
3872
3971
  }
3873
3972
  if (!this._undoInsertChain || this.pane?.selection) {
3874
3973
  buf.pushUndo();
3875
3974
  }
3975
+ if (isMdcuiEncoding(buf?.encoding) && !canEditMdcuiSelection(buf, this.pane?.selection)) {
3976
+ this.message = "Protected mdcui prefix";
3977
+ break;
3978
+ }
3876
3979
  this._undoInsertChain = true;
3877
3980
  if (this.pane?.selection) deleteSelection(buf, this.pane);
3878
3981
  await this.insertTextWithHooks(text);
@@ -3885,6 +3988,12 @@ class App {
3885
3988
  async handleMdcuiCellCallback(buf, y = buf?.cursor?.y ?? 0, x = buf?.cursor?.x ?? 0, trigger = "unknown") {
3886
3989
  const payload = mdcuiCellPayload(buf, y, x, trigger);
3887
3990
  if (!payload) return false;
3991
+ const resizeResult = resizeMdcuiTextBlock(buf, y, x);
3992
+ if (resizeResult) {
3993
+ if (resizeResult === "added") this.message = "Added text row";
3994
+ else if (resizeResult === "removed") this.message = "Removed empty text row";
3995
+ return true;
3996
+ }
3888
3997
  const callback = this.context?.mdcuiCallback ?? globalThis.mdcuiCallback;
3889
3998
  if (typeof callback === "function") {
3890
3999
  try {
@@ -4751,6 +4860,11 @@ class App {
4751
4860
 
4752
4861
  async quit() {
4753
4862
  const buffer = this.buffer;
4863
+ if (isMdcuiEncoding(buffer?.encoding)) {
4864
+ await this.notifyMdcuiExit(buffer, "quit");
4865
+ await this._doCloseCurrentPane();
4866
+ return;
4867
+ }
4754
4868
  if (buffer.modified) {
4755
4869
  if (!this.prompt) {
4756
4870
  this.openYNPrompt(
@@ -4771,6 +4885,7 @@ class App {
4771
4885
  // Close only the current pane; if it's the last pane in the tab, close the tab.
4772
4886
  async _doCloseCurrentPane() {
4773
4887
  if (this.tab.panes().length > 1) {
4888
+ await this.notifyMdcuiExit(this.buffer, "close-pane");
4774
4889
  this.closePane(this.pane);
4775
4890
  this.render();
4776
4891
  } else {
@@ -4788,6 +4903,32 @@ class App {
4788
4903
  if (!this.prompt && !this.buffer.modified) await this.closeCurrentTab({ force: true });
4789
4904
  }
4790
4905
 
4906
+ async notifyMdcuiExit(buffer, reason = "exit") {
4907
+ if (
4908
+ !buffer
4909
+ || !isMdcuiEncoding(buffer.encoding)
4910
+ || this._mdcuiExitNotified.has(buffer)
4911
+ ) return;
4912
+ this._mdcuiExitNotified.add(buffer);
4913
+
4914
+ const frontPath = buffer.path && !isHttpUrl(buffer.path)
4915
+ ? `${buffer.path}.front.js`
4916
+ : "";
4917
+ if (!frontPath || !existsSync(frontPath)) return;
4918
+
4919
+ try {
4920
+ const frontMod = await import(localModuleUrl(frontPath));
4921
+ if (typeof frontMod.onMdcuiExit !== "function") return;
4922
+ await frontMod.onMdcuiExit({
4923
+ reason,
4924
+ path: buffer.path,
4925
+ $: globalThis.$,
4926
+ });
4927
+ } catch (error) {
4928
+ console.error(`[mdcui] onMdcuiExit: ${error?.message || error}`);
4929
+ }
4930
+ }
4931
+
4791
4932
  async toggleHelp() {
4792
4933
  const cur = this.pane;
4793
4934
  if (cur?.isHelp) {
@@ -4886,11 +5027,14 @@ class App {
4886
5027
  async closeCurrentTab({ force = false } = {}) {
4887
5028
  if (!force && this.buffer?.modified) return await this.quit();
4888
5029
  if (this.tabs.length <= 1) {
5030
+ await this.notifyMdcuiExit(this.buffer, "close-tab");
4889
5031
  await this.stop(0);
4890
5032
  return;
4891
5033
  }
4892
5034
  const closingBuffers = [...new Set(this.tab.panes().flatMap((pane) => [pane.buffer, pane.prevBuffer]).filter(Boolean))];
4893
5035
  const closing = this.buffer;
5036
+ for (const buffer of closingBuffers)
5037
+ await this.notifyMdcuiExit(buffer, "close-tab");
4894
5038
  this.tabs.splice(this.activeTabIdx, 1);
4895
5039
  this.activeTabIdx = Math.min(this.activeTabIdx, this.tabs.length - 1);
4896
5040
  this.message = "";
@@ -5674,6 +5818,14 @@ class App {
5674
5818
  this._freshClip = false;
5675
5819
  const pasted = this.clipboard.read();
5676
5820
  if (pasted) {
5821
+ if (isMdcuiEncoding(buf?.encoding) && (
5822
+ /[\r\n]/.test(pasted)
5823
+ || !canEditMdcuiAtCursor(buf)
5824
+ || !canEditMdcuiSelection(buf, this.pane?.selection)
5825
+ )) {
5826
+ this.message = "Protected mdcui content";
5827
+ break;
5828
+ }
5677
5829
  buf.pushUndo();
5678
5830
  if (this.pane?.selection) deleteSelection(buf, this.pane);
5679
5831
  buf.insert(pasted);
@@ -7902,11 +8054,16 @@ function outdentLine(buf, _ctx) {
7902
8054
  }
7903
8055
 
7904
8056
  function deleteSelection(buf, pane) {
7905
- if (isEditLockedBuffer(buf)) return "";
7906
8057
  const selection = pane?.selection;
7907
8058
  if (!selection || !buf) return "";
7908
8059
  const text = getSelectionText(buf, selection);
7909
8060
  const { first, last } = selectionBounds(selection);
8061
+ if (isMdcuiEncoding(buf?.encoding)) {
8062
+ const prefixLength = mdcuiEditablePrefixLength(buf, first.y);
8063
+ if (first.y !== last.y || prefixLength === 0 || first.x < prefixLength) return "";
8064
+ } else if (isEditLockedBuffer(buf)) {
8065
+ return "";
8066
+ }
7910
8067
  if (first.y === last.y) {
7911
8068
  const line = buf.lines[first.y] ?? "";
7912
8069
  buf.lines[first.y] = line.slice(0, first.x) + line.slice(last.x);
@@ -685,11 +685,178 @@ function _deleteSel(buf, pane) {
685
685
  buf.ensureCursor?.();
686
686
  }
687
687
 
688
+ // ── mdcui block selector ────────────────────────────────────────────────────
689
+
690
+ function _parseBlockIdentity(input, { selector = false } = {}) {
691
+ const text = String(input ?? "").trim();
692
+ const match = text.match(/^([A-Za-z_][\w:-]*)?(?:#([A-Za-z_][\w:-]*))?((?:\.[A-Za-z_][\w:-]*)*)$/);
693
+ if (!match || (!match[1] && !match[2] && !match[3])) return null;
694
+ if (!selector && !match[1]) return null;
695
+ return {
696
+ tag: match[1] || null,
697
+ id: match[2] || null,
698
+ classes: match[3] ? match[3].slice(1).split(".") : [],
699
+ };
700
+ }
701
+
702
+ function _blockHeader(line) {
703
+ const text = String(line ?? "");
704
+ const framed = text.match(/^(\s*)(┌─|╭─|\+-)\s*(\S+)\s*$/);
705
+ if (framed) {
706
+ const identity = _parseBlockIdentity(framed[3]);
707
+ return identity
708
+ ? { kind: "framed", indent: framed[1], bodyMarker: framed[2] === "+-" ? "|" : "│", ...identity }
709
+ : null;
710
+ }
711
+
712
+ const fenced = text.match(/^(\s*)(`{3,})\s*(\S+)\s*$/);
713
+ if (fenced) {
714
+ const identity = _parseBlockIdentity(fenced[3]);
715
+ return identity
716
+ ? { kind: "fenced", indent: fenced[1], fenceLength: fenced[2].length, ...identity }
717
+ : null;
718
+ }
719
+
720
+ return null;
721
+ }
722
+
723
+ function _matchesBlock(header, selector) {
724
+ if (selector.tag && header.tag !== selector.tag) return false;
725
+ if (selector.id && header.id !== selector.id) return false;
726
+ return selector.classes.every((name) => header.classes.includes(name));
727
+ }
728
+
729
+ function _findBlock(lines, selector) {
730
+ for (let start = 0; start < lines.length; start++) {
731
+ const header = _blockHeader(lines[start]);
732
+ if (!header || !_matchesBlock(header, selector)) continue;
733
+
734
+ for (let y = start + 1; y < lines.length; y++) {
735
+ const line = String(lines[y] ?? "");
736
+ const rest = line.startsWith(header.indent)
737
+ ? line.slice(header.indent.length)
738
+ : line;
739
+
740
+ if (header.kind === "fenced") {
741
+ const closing = rest.match(/^(`{3,})\s*$/);
742
+ if (closing && closing[1].length >= header.fenceLength)
743
+ return { start, end: y, header };
744
+ continue;
745
+ }
746
+
747
+ if (/^(?:└─|╰─|\+-)\s*$/.test(rest))
748
+ return { start, end: y, header };
749
+ }
750
+ return { start, end: lines.length, header };
751
+ }
752
+ return null;
753
+ }
754
+
755
+ function _blockValue(lines, selector) {
756
+ const block = _findBlock(lines, selector);
757
+ if (!block) return undefined;
758
+ const value = [];
759
+ for (let y = block.start + 1; y < block.end; y++) {
760
+ const line = String(lines[y] ?? "");
761
+ const rest = line.startsWith(block.header.indent)
762
+ ? line.slice(block.header.indent.length)
763
+ : line;
764
+ if (block.header.kind === "fenced") value.push(rest);
765
+ else {
766
+ const body = rest.match(/^(?:│|\|)(?: ?)(.*)$/);
767
+ value.push(body ? body[1] : rest);
768
+ }
769
+ }
770
+ return value.join("\n");
771
+ }
772
+
773
+ function _spliceBufferLines(buffer, start, deleteCount, replacement) {
774
+ const oldCursor = buffer.cursor ? { ...buffer.cursor } : null;
775
+ buffer.lines.splice(start, deleteCount, ...replacement);
776
+
777
+ if (Array.isArray(buffer._ansiStyleLines)) {
778
+ const template = buffer._ansiStyleLines[start] ?? null;
779
+ buffer._ansiStyleLines.splice(
780
+ start,
781
+ deleteCount,
782
+ ...replacement.map(() => template),
783
+ );
784
+ }
785
+
786
+ if (typeof buffer._mdcuiAnsiText === "string") {
787
+ const ansiLines = buffer._mdcuiAnsiText.split("\n");
788
+ ansiLines.splice(start, deleteCount, ...replacement);
789
+ buffer._mdcuiAnsiText = ansiLines.join("\n");
790
+ }
791
+
792
+ buffer.invalidateHighlightFrom?.(start, { force: replacement.length !== deleteCount });
793
+ if (oldCursor) {
794
+ const oldEnd = start + deleteCount;
795
+ if (oldCursor.y >= oldEnd) {
796
+ buffer.cursor.y = oldCursor.y + replacement.length - deleteCount;
797
+ } else if (oldCursor.y >= start) {
798
+ const relativeY = Math.min(oldCursor.y - start, Math.max(0, replacement.length - 1));
799
+ buffer.cursor.y = start + relativeY;
800
+ }
801
+ }
802
+ buffer.modified = true;
803
+ buffer.ensureCursor?.();
804
+ }
805
+
806
+ function _setBlockValue(buffer, selector, value) {
807
+ const lines = buffer.lines;
808
+ const block = _findBlock(lines, selector);
809
+ if (!block) return false;
810
+
811
+ const values = String(value ?? "").replace(/\r\n?/g, "\n").split("\n");
812
+ const contentStart = block.start + 1;
813
+ const capacity = Math.max(0, block.end - contentStart);
814
+
815
+ if (block.header.kind === "fenced") {
816
+ const replacement = values.map((line) => block.header.indent + line);
817
+ _spliceBufferLines(buffer, contentStart, capacity, replacement);
818
+ return true;
819
+ }
820
+
821
+ const rowPrefix = block.header.indent + block.header.bodyMarker + " ";
822
+ const replacement = values.map((line) => rowPrefix + line);
823
+ _spliceBufferLines(buffer, contentStart, capacity, replacement);
824
+ return true;
825
+ }
826
+
827
+ export function createTuiSelector(getBuffer) {
828
+ return function $(selector) {
829
+ const parsedSelector = _parseBlockIdentity(selector, { selector: true });
830
+ const selection = {
831
+ val(...args) {
832
+ try {
833
+ if (!parsedSelector) return args.length > 0 ? selection : "";
834
+ const buffer = getBuffer?.();
835
+ if (!buffer) return args.length > 0 ? selection : "";
836
+ const lines = Array.isArray(buffer.lines)
837
+ ? buffer.lines
838
+ : String(buffer).replace(/\r\n?/g, "\n").split("\n");
839
+ if (args.length > 0) {
840
+ if (Array.isArray(buffer.lines))
841
+ _setBlockValue(buffer, parsedSelector, args[0]);
842
+ return selection;
843
+ }
844
+ return _blockValue(lines, parsedSelector) ?? "";
845
+ } catch {
846
+ return args.length > 0 ? selection : "";
847
+ }
848
+ },
849
+ };
850
+ return selection;
851
+ };
852
+ }
853
+
688
854
  // ── micro global object ───────────────────────────────────────────────────────
689
855
 
690
856
  export function buildMicroGlobal(jsManager) {
691
857
  const getApp = () => jsManager._app;
692
858
  const getCtx = () => jsManager._ctx;
859
+ const $ = createTuiSelector(() => getApp()?.buffer);
693
860
 
694
861
  // Converts cmd args to a safe command string for handleCommand
695
862
  function buildCmdString(name, args) {
@@ -921,6 +1088,7 @@ export function buildMicroGlobal(jsManager) {
921
1088
  };
922
1089
 
923
1090
  globalThis.micro = micro;
1091
+ globalThis.$ = $;
924
1092
  return micro;
925
1093
  }
926
1094
 
package/testapp.md CHANGED
@@ -9,6 +9,20 @@
9
9
  - [Print process.argv](javascript:pav())
10
10
  - [Calculator🧮計算機](javascript:calc())
11
11
  * Use cos sin PI directly
12
+ - [Show ↓ text](javascript:alert($('text').val()))
13
+
14
+ ```text
15
+ Text edit. TUI: Click ↙ to inc lines ↖ to dec
16
+ 可編輯文字框 TUI:按↙擴充行數 ↖減少行數
17
+ ```
18
+ ## Question 問題
19
+ - What is 1+2+3+4+..+..+∞
20
+
21
+ ```text#ans
22
+ -1/12
23
+ ```
24
+ - [Submit 提交](javascript:checkAns())
25
+
12
26
  ## Task list
13
27
  - [X] task1
14
28
  - [ ] task2
@@ -43,6 +57,15 @@ export async function calc()
43
57
 
44
58
  );
45
59
  }
60
+
61
+ export function checkAns()
62
+ {
63
+ if($('#ans').val().trim()=='-1/12')
64
+ $('#ans').val('答對🥳Right!');
65
+ else
66
+ $('#ans').val('答錯😫Wrong!');
67
+ }
68
+
46
69
  ```
47
70
 
48
71