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 +57 -0
- package/README.md +53 -2
- package/package.json +1 -1
- package/runmd.mjs +70 -7
- package/runtime/help/help.md +53 -2
- package/runtime/syntax/markdown.yaml +12 -0
- package/src/config/defaults.js +1 -1
- package/src/cui/rpc.mjs +137 -2
- package/src/index.js +169 -12
- package/src/plugins/js-bridge.js +168 -0
- package/testapp.md +23 -0
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.
|
|
160
|
-
|
|
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
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("&", "&")
|
|
207
|
+
.replaceAll('"', """)
|
|
208
|
+
.replaceAll("<", "<")
|
|
209
|
+
.replaceAll(">", ">");
|
|
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
|
-
<
|
|
219
|
-
|
|
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)
|
package/runtime/help/help.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.
|
|
160
|
-
|
|
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
|
+
|
package/src/config/defaults.js
CHANGED
|
@@ -38,7 +38,7 @@ export const DEFAULT_COMMON_SETTINGS = {
|
|
|
38
38
|
scrollspeed: 2,
|
|
39
39
|
showchars: "",
|
|
40
40
|
smartpaste: true,
|
|
41
|
-
softwrap:
|
|
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
|
|
103
|
-
|
|
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:
|
|
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 (
|
|
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);
|
package/src/plugins/js-bridge.js
CHANGED
|
@@ -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
|
|