jsmdcui 0.4.0 → 0.6.3
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 +225 -0
- package/README.md +267 -25
- package/image-processor.md +360 -0
- package/image-processor.zh-TW.md +360 -0
- package/package.json +1 -1
- package/runmd.mjs +147 -10
- package/runtime/help/help.md +267 -25
- package/runtime/syntax/markdown.yaml +12 -0
- package/select.md +61 -0
- package/single-exe/packAssets.sh +1 -1
- package/src/config/defaults.js +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/rpc.mjs +207 -2
- package/src/cui/task-checkbox.mjs +16 -0
- package/src/index.js +256 -22
- package/src/plugins/js-bridge.js +283 -0
- package/testapp.md +23 -0
- package/tests/demo.test.js +110 -1
- package/tests/id-collision.test.js +146 -0
- package/tests/wui.test.js +30 -0
package/src/index.js
CHANGED
|
@@ -98,6 +98,8 @@ import { mkdir } from "node:fs/promises";
|
|
|
98
98
|
import { dirname, basename, join, resolve, sep } from "node:path";
|
|
99
99
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
100
100
|
import process from "node:process";
|
|
101
|
+
import { toggleTaskCheckboxBeforeColumn } from "./cui/task-checkbox.mjs";
|
|
102
|
+
import { checkMarkdownIdCollisions, formatMarkdownIdCheckAnsi } from "./cui/id-collision.mjs";
|
|
101
103
|
import { Config } from "./config/config.js";
|
|
102
104
|
import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
|
|
103
105
|
import { cleanConfig } from "./config/clean.js";
|
|
@@ -195,7 +197,7 @@ const DEFAULT_SETTINGS = {
|
|
|
195
197
|
backup: true,
|
|
196
198
|
backupdir: "",
|
|
197
199
|
permbackup: false,
|
|
198
|
-
softwrap:
|
|
200
|
+
softwrap: true,
|
|
199
201
|
wordwrap: false,
|
|
200
202
|
pageoverlap: 2,
|
|
201
203
|
scrollmargin: 3,
|
|
@@ -538,6 +540,70 @@ function mdcuiCellPayload(buf, y, x, trigger = "unknown") {
|
|
|
538
540
|
};
|
|
539
541
|
}
|
|
540
542
|
|
|
543
|
+
function resizeMdcuiTextBlock(buf, y, x) {
|
|
544
|
+
if (!buf || !isMdcuiEncoding(buf.encoding) || x !== 0) return null;
|
|
545
|
+
const line = String(buf.lines?.[y] ?? "");
|
|
546
|
+
const top = line.match(/^(\s*)(┌─|╭─|\+-)\s*text(?:[#.][A-Za-z_][\w:.-]*)*\s*$/);
|
|
547
|
+
const bottom = line.match(/^(\s*)(└─|╰─|\+-)\s*$/);
|
|
548
|
+
if (!top && !bottom) return null;
|
|
549
|
+
|
|
550
|
+
let insertAt = -1;
|
|
551
|
+
let removeAt = -1;
|
|
552
|
+
let bodyLine = "";
|
|
553
|
+
|
|
554
|
+
if (bottom) {
|
|
555
|
+
for (let row = y - 1; row >= 0; row--) {
|
|
556
|
+
const header = String(buf.lines[row] ?? "").match(/^(\s*)(┌─|╭─|\+-)\s*text(?:[#.][A-Za-z_][\w:.-]*)*\s*$/);
|
|
557
|
+
if (!header || header[1] !== bottom[1]) continue;
|
|
558
|
+
const marker = header[2] === "+-" ? "|" : "│";
|
|
559
|
+
insertAt = y;
|
|
560
|
+
bodyLine = header[1] + marker + " ";
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
} else if (top) {
|
|
564
|
+
const marker = top[2] === "+-" ? "|" : "│";
|
|
565
|
+
for (let row = y + 1; row < buf.lines.length; row++) {
|
|
566
|
+
const rest = String(buf.lines[row] ?? "").slice(top[1].length);
|
|
567
|
+
if (/^(?:└─|╰─|\+-)\s*$/.test(rest)) {
|
|
568
|
+
const candidate = row - 1;
|
|
569
|
+
if (candidate > y && String(buf.lines[candidate] ?? "") === top[1] + marker + " ")
|
|
570
|
+
removeAt = candidate;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (insertAt < 0 && removeAt < 0) return "unchanged";
|
|
577
|
+
buf.pushUndo?.(true);
|
|
578
|
+
|
|
579
|
+
const row = insertAt >= 0 ? insertAt : removeAt;
|
|
580
|
+
const deleteCount = removeAt >= 0 ? 1 : 0;
|
|
581
|
+
const replacement = insertAt >= 0 ? [bodyLine] : [];
|
|
582
|
+
buf.lines.splice(row, deleteCount, ...replacement);
|
|
583
|
+
|
|
584
|
+
if (Array.isArray(buf._ansiStyleLines)) {
|
|
585
|
+
const template = buf._ansiStyleLines[Math.max(0, row - 1)] ?? null;
|
|
586
|
+
buf._ansiStyleLines.splice(row, deleteCount, ...replacement.map(() => template));
|
|
587
|
+
}
|
|
588
|
+
if (typeof buf._mdcuiAnsiText === "string") {
|
|
589
|
+
const ansiLines = buf._mdcuiAnsiText.split("\n");
|
|
590
|
+
ansiLines.splice(row, deleteCount, ...replacement);
|
|
591
|
+
buf._mdcuiAnsiText = ansiLines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (insertAt >= 0) {
|
|
595
|
+
if (buf.cursor.y >= insertAt) buf.cursor.y++;
|
|
596
|
+
} else if (buf.cursor.y > removeAt) {
|
|
597
|
+
buf.cursor.y--;
|
|
598
|
+
} else if (buf.cursor.y === removeAt) {
|
|
599
|
+
buf.cursor.y = Math.max(0, removeAt - 1);
|
|
600
|
+
}
|
|
601
|
+
buf.invalidateHighlightFrom?.(row, { force: true });
|
|
602
|
+
buf.modified = true;
|
|
603
|
+
buf.ensureCursor?.();
|
|
604
|
+
return insertAt >= 0 ? "added" : "removed";
|
|
605
|
+
}
|
|
606
|
+
|
|
541
607
|
function detectFileFormat(text, fallback = DEFAULT_SETTINGS.fileformat) {
|
|
542
608
|
if (text.length === 0) return fallback === "dos" ? "dos" : "unix";
|
|
543
609
|
const newlineIdx = text.indexOf("\n");
|
|
@@ -557,6 +623,24 @@ function isReadonlyBuffer(buf) {
|
|
|
557
623
|
return Boolean(buf?.readonly || buf?.Settings?.readonly || buf?.Type?.Readonly);
|
|
558
624
|
}
|
|
559
625
|
|
|
626
|
+
function mdcuiEditablePrefixLength(buf, y = buf?.cursor?.y ?? 0) {
|
|
627
|
+
if (!isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding)) return 0;
|
|
628
|
+
const line = String(buf?.lines?.[y] ?? "");
|
|
629
|
+
return /^(?:│|\|) /.test(line) ? 2 : 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function canEditMdcuiAtCursor(buf) {
|
|
633
|
+
const prefixLength = mdcuiEditablePrefixLength(buf);
|
|
634
|
+
return prefixLength > 0 && (buf?.cursor?.x ?? 0) >= prefixLength;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function canEditMdcuiSelection(buf, selection) {
|
|
638
|
+
if (!selection) return true;
|
|
639
|
+
const { first, last } = selectionBounds(selection);
|
|
640
|
+
const prefixLength = mdcuiEditablePrefixLength(buf, first.y);
|
|
641
|
+
return first.y === last.y && prefixLength > 0 && first.x >= prefixLength;
|
|
642
|
+
}
|
|
643
|
+
|
|
560
644
|
function isEditLockedBuffer(buf) {
|
|
561
645
|
return isMdcuiEncoding(buf?.encoding ?? buf?.Settings?.encoding);
|
|
562
646
|
}
|
|
@@ -865,11 +949,16 @@ function parseArgs(argv) {
|
|
|
865
949
|
options: false,
|
|
866
950
|
help: false,
|
|
867
951
|
clean: false,
|
|
952
|
+
check: false,
|
|
868
953
|
cat: false,
|
|
869
954
|
docs: false,
|
|
955
|
+
exportReadme: false,
|
|
870
956
|
changelog: false,
|
|
871
957
|
testapp: false,
|
|
872
958
|
demo: false,
|
|
959
|
+
demoSelect: false,
|
|
960
|
+
demoImgtool: false,
|
|
961
|
+
demoImgtoolZh: false,
|
|
873
962
|
allowUrl: false,
|
|
874
963
|
buildExe: false,
|
|
875
964
|
buildFor: "",
|
|
@@ -889,6 +978,7 @@ function parseArgs(argv) {
|
|
|
889
978
|
else if (arg === "-options") flags.options = true;
|
|
890
979
|
else if (arg === "-help" || arg === "--help" || arg === "-h") flags.help = true;
|
|
891
980
|
else if (arg === "-clean") flags.clean = true;
|
|
981
|
+
else if (arg === "--check") flags.check = true;
|
|
892
982
|
else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
|
|
893
983
|
else if (arg === "--xxd" || arg === "--hexdump") {
|
|
894
984
|
flags.cat = true;
|
|
@@ -907,9 +997,13 @@ function parseArgs(argv) {
|
|
|
907
997
|
flags.settings.set("encoding", "utf-8");
|
|
908
998
|
}
|
|
909
999
|
else if (arg === "--docs" || arg === "--readme") flags.docs = true;
|
|
1000
|
+
else if (arg === "--export-readme") flags.exportReadme = true;
|
|
910
1001
|
else if (arg === "--changelog") flags.changelog = true;
|
|
911
1002
|
else if (arg === "--testapp.md") flags.testapp = true;
|
|
912
1003
|
else if (arg === "--demo") flags.demo = true;
|
|
1004
|
+
else if (arg === "--demo-select") flags.demoSelect = true;
|
|
1005
|
+
else if (arg === "--demo-imgtool") flags.demoImgtool = true;
|
|
1006
|
+
else if (arg === "--demo-imgtool-zh") flags.demoImgtoolZh = true;
|
|
913
1007
|
else if (arg === "--allow-url") flags.allowUrl = true;
|
|
914
1008
|
else if (arg === "--build-exe") flags.buildExe = true;
|
|
915
1009
|
else if (arg === "--build-for") flags.buildFor = argv[++i] ?? "";
|
|
@@ -942,9 +1036,13 @@ function usage() {
|
|
|
942
1036
|
${pkg.name} --wui [FILE.md]
|
|
943
1037
|
|
|
944
1038
|
Modes:
|
|
1039
|
+
--check FILE.md
|
|
1040
|
+
Check heading and fenced-block IDs for collisions, print details, and exit
|
|
1041
|
+
Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
|
|
945
1042
|
--wui [FILE.md]
|
|
946
1043
|
Generate or overwrite Markdown UI files beside FILE.md and start the server
|
|
947
|
-
Without FILE.md, use testapp.md
|
|
1044
|
+
Without FILE.md, use the existing ./testapp.md without overwriting it
|
|
1045
|
+
If ./testapp.md is missing, write the bundled demo there first
|
|
948
1046
|
--cat, --ccat, --bat, --glow
|
|
949
1047
|
Render file(s) and write to stdout, then exit (.md uses mdcui/createTui)
|
|
950
1048
|
A local .md file also writes or overwrites five generated files beside it
|
|
@@ -978,6 +1076,8 @@ Information:
|
|
|
978
1076
|
Show version+backend info & exit
|
|
979
1077
|
--docs, --readme
|
|
980
1078
|
Show ${pkg.name}'s README.md & exit
|
|
1079
|
+
--export-readme
|
|
1080
|
+
Write or overwrite ./README.md with the bundled README.md & exit
|
|
981
1081
|
--changelog
|
|
982
1082
|
Show CHANGELOG.md & exit
|
|
983
1083
|
-profile, --profile
|
|
@@ -987,7 +1087,17 @@ Demo:
|
|
|
987
1087
|
--testapp.md
|
|
988
1088
|
Write the bundled testapp.md to stdout & exit
|
|
989
1089
|
--demo
|
|
990
|
-
|
|
1090
|
+
Use the existing ./testapp.md without overwriting it, or write the bundled demo if missing
|
|
1091
|
+
Open it in the TUI and write 5 generated files beside it
|
|
1092
|
+
--demo-select
|
|
1093
|
+
Use the existing ./select.md without overwriting it, or write the bundled demo if missing
|
|
1094
|
+
Open it in the TUI and write 5 generated files beside it
|
|
1095
|
+
--demo-imgtool
|
|
1096
|
+
Use the existing ./image-processor.md without overwriting it, or write the bundled demo if missing
|
|
1097
|
+
Open the Bun.Image processor in the TUI and write 5 generated files beside it
|
|
1098
|
+
--demo-imgtool-zh
|
|
1099
|
+
Use the existing ./image-processor.zh-TW.md without overwriting it, or write the bundled demo if missing
|
|
1100
|
+
Open the Traditional Chinese Bun.Image processor in the TUI and write 5 generated files beside it
|
|
991
1101
|
|
|
992
1102
|
Remote Markdown:
|
|
993
1103
|
--allow-url
|
|
@@ -1198,7 +1308,8 @@ class BufferModel {
|
|
|
1198
1308
|
}
|
|
1199
1309
|
|
|
1200
1310
|
insert(text) {
|
|
1201
|
-
if (this.isEditLocked()) return false;
|
|
1311
|
+
if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
|
|
1312
|
+
if (isMdcuiEncoding(this.encoding) && /[\r\n]/.test(String(text))) return false;
|
|
1202
1313
|
for (const ch of text) {
|
|
1203
1314
|
if (ch === "\r" || ch === "\n") this.newline(false);
|
|
1204
1315
|
else if (ch >= " " || ch === "\t") this.insertChar(ch);
|
|
@@ -1207,7 +1318,7 @@ class BufferModel {
|
|
|
1207
1318
|
}
|
|
1208
1319
|
|
|
1209
1320
|
insertChar(ch) {
|
|
1210
|
-
if (this.isEditLocked()) return false;
|
|
1321
|
+
if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
|
|
1211
1322
|
if (ch === "\t" && DEFAULT_SETTINGS.tabstospaces) ch = " ".repeat(DEFAULT_SETTINGS.tabsize);
|
|
1212
1323
|
const line = this.line();
|
|
1213
1324
|
this.lines[this.cursor.y] = line.slice(0, this.cursor.x) + ch + line.slice(this.cursor.x);
|
|
@@ -1218,6 +1329,7 @@ class BufferModel {
|
|
|
1218
1329
|
}
|
|
1219
1330
|
|
|
1220
1331
|
newline(autoindent = true) {
|
|
1332
|
+
if (isMdcuiEncoding(this.encoding)) return false;
|
|
1221
1333
|
if (this.isEditLocked()) return false;
|
|
1222
1334
|
const line = this.line();
|
|
1223
1335
|
const left = line.slice(0, this.cursor.x);
|
|
@@ -1236,7 +1348,9 @@ class BufferModel {
|
|
|
1236
1348
|
}
|
|
1237
1349
|
|
|
1238
1350
|
backspace() {
|
|
1239
|
-
if (this.isEditLocked()) return false;
|
|
1351
|
+
if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
|
|
1352
|
+
const mdcuiPrefixLength = mdcuiEditablePrefixLength(this);
|
|
1353
|
+
if (mdcuiPrefixLength > 0 && this.cursor.x <= mdcuiPrefixLength) return false;
|
|
1240
1354
|
if (this.cursor.x > 0) {
|
|
1241
1355
|
const line = this.line();
|
|
1242
1356
|
let start = this.cursor.x - 1;
|
|
@@ -1263,7 +1377,7 @@ class BufferModel {
|
|
|
1263
1377
|
}
|
|
1264
1378
|
|
|
1265
1379
|
deleteForward() {
|
|
1266
|
-
if (this.isEditLocked()) return false;
|
|
1380
|
+
if (this.isEditLocked() && !canEditMdcuiAtCursor(this)) return false;
|
|
1267
1381
|
const line = this.line();
|
|
1268
1382
|
if (this.cursor.x < line.length) {
|
|
1269
1383
|
const cp = line.codePointAt(this.cursor.x);
|
|
@@ -1272,6 +1386,8 @@ class BufferModel {
|
|
|
1272
1386
|
this.invalidateHighlightFrom(this.cursor.y);
|
|
1273
1387
|
this.modified = true;
|
|
1274
1388
|
return true;
|
|
1389
|
+
} else if (isMdcuiEncoding(this.encoding)) {
|
|
1390
|
+
return false;
|
|
1275
1391
|
} else if (this.cursor.y < this.lines.length - 1) {
|
|
1276
1392
|
this.lines[this.cursor.y] += this.lines[this.cursor.y + 1];
|
|
1277
1393
|
this.lines.splice(this.cursor.y + 1, 1);
|
|
@@ -1401,8 +1517,8 @@ class BufferModel {
|
|
|
1401
1517
|
this.cursor.x = x;
|
|
1402
1518
|
}
|
|
1403
1519
|
|
|
1404
|
-
pushUndo() {
|
|
1405
|
-
if (this.isEditLocked()) return;
|
|
1520
|
+
pushUndo(force = false) {
|
|
1521
|
+
if (!force && this.isEditLocked() && !canEditMdcuiAtCursor(this)) return;
|
|
1406
1522
|
this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
|
|
1407
1523
|
this._undoSerial = (this._undoSerial ?? 0) + 1;
|
|
1408
1524
|
if (this.undoStack.length > 500) this.undoStack.shift();
|
|
@@ -1410,7 +1526,7 @@ class BufferModel {
|
|
|
1410
1526
|
}
|
|
1411
1527
|
|
|
1412
1528
|
undo() {
|
|
1413
|
-
if (this.isEditLocked()) return false;
|
|
1529
|
+
if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
|
|
1414
1530
|
if (!this.undoStack.length) return false;
|
|
1415
1531
|
this.redoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
|
|
1416
1532
|
const s = this.undoStack.pop();
|
|
@@ -1423,7 +1539,7 @@ class BufferModel {
|
|
|
1423
1539
|
}
|
|
1424
1540
|
|
|
1425
1541
|
redo() {
|
|
1426
|
-
if (this.isEditLocked()) return false;
|
|
1542
|
+
if (this.isEditLocked() && !isMdcuiEncoding(this.encoding)) return false;
|
|
1427
1543
|
if (!this.redoStack.length) return false;
|
|
1428
1544
|
this.undoStack.push({ lines: this.lines.slice(), cursor: { ...this.cursor }, serial: this._undoSerial });
|
|
1429
1545
|
const s = this.redoStack.pop();
|
|
@@ -2302,6 +2418,7 @@ class App {
|
|
|
2302
2418
|
this._messageRowY = null;
|
|
2303
2419
|
this._messageRowClickZone = null;
|
|
2304
2420
|
this._resizeRenderSeq = 0;
|
|
2421
|
+
this._mdcuiExitNotified = new WeakSet();
|
|
2305
2422
|
}
|
|
2306
2423
|
|
|
2307
2424
|
get tab() { return this.tabs[this.activeTabIdx]; }
|
|
@@ -2445,6 +2562,8 @@ class App {
|
|
|
2445
2562
|
}
|
|
2446
2563
|
|
|
2447
2564
|
async stop(code = 0) {
|
|
2565
|
+
for (const buf of this.buffers)
|
|
2566
|
+
await this.notifyMdcuiExit(buf, "exit");
|
|
2448
2567
|
this.running = false;
|
|
2449
2568
|
if (this._backupTimer) { clearInterval(this._backupTimer); this._backupTimer = null; }
|
|
2450
2569
|
await Promise.allSettled(this.buffers.map((buf) => buf._backupWritePromise).filter(Boolean));
|
|
@@ -3488,7 +3607,15 @@ class App {
|
|
|
3488
3607
|
}
|
|
3489
3608
|
|
|
3490
3609
|
if (event.type === "paste") {
|
|
3491
|
-
if (
|
|
3610
|
+
if (
|
|
3611
|
+
(isEditLockedBuffer(buf) && !isMdcuiEncoding(buf?.encoding))
|
|
3612
|
+
|| (isMdcuiEncoding(buf?.encoding) && (
|
|
3613
|
+
!canEditMdcuiAtCursor(buf)
|
|
3614
|
+
||
|
|
3615
|
+
/[\r\n]/.test(event.text)
|
|
3616
|
+
|| !canEditMdcuiSelection(buf, this.pane?.selection)
|
|
3617
|
+
))
|
|
3618
|
+
) {
|
|
3492
3619
|
this.message = "Buffer is read-only";
|
|
3493
3620
|
this.render();
|
|
3494
3621
|
return;
|
|
@@ -3866,13 +3993,17 @@ class App {
|
|
|
3866
3993
|
break;
|
|
3867
3994
|
default:
|
|
3868
3995
|
if (text >= " " || text.includes("\n")) {
|
|
3869
|
-
if (isMdcuiEncoding(buf?.encoding) && text === " ") {
|
|
3996
|
+
if (isMdcuiEncoding(buf?.encoding) && text === " " && !canEditMdcuiAtCursor(buf)) {
|
|
3870
3997
|
await this.handleMdcuiCellCallback(buf, buf.cursor.y, buf.cursor.x, "space");
|
|
3871
3998
|
break;
|
|
3872
3999
|
}
|
|
3873
4000
|
if (!this._undoInsertChain || this.pane?.selection) {
|
|
3874
4001
|
buf.pushUndo();
|
|
3875
4002
|
}
|
|
4003
|
+
if (isMdcuiEncoding(buf?.encoding) && !canEditMdcuiSelection(buf, this.pane?.selection)) {
|
|
4004
|
+
this.message = "Protected mdcui prefix";
|
|
4005
|
+
break;
|
|
4006
|
+
}
|
|
3876
4007
|
this._undoInsertChain = true;
|
|
3877
4008
|
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
3878
4009
|
await this.insertTextWithHooks(text);
|
|
@@ -3885,6 +4016,12 @@ class App {
|
|
|
3885
4016
|
async handleMdcuiCellCallback(buf, y = buf?.cursor?.y ?? 0, x = buf?.cursor?.x ?? 0, trigger = "unknown") {
|
|
3886
4017
|
const payload = mdcuiCellPayload(buf, y, x, trigger);
|
|
3887
4018
|
if (!payload) return false;
|
|
4019
|
+
const resizeResult = resizeMdcuiTextBlock(buf, y, x);
|
|
4020
|
+
if (resizeResult) {
|
|
4021
|
+
if (resizeResult === "added") this.message = "Added text row";
|
|
4022
|
+
else if (resizeResult === "removed") this.message = "Removed empty text row";
|
|
4023
|
+
return true;
|
|
4024
|
+
}
|
|
3888
4025
|
const callback = this.context?.mdcuiCallback ?? globalThis.mdcuiCallback;
|
|
3889
4026
|
if (typeof callback === "function") {
|
|
3890
4027
|
try {
|
|
@@ -3953,6 +4090,12 @@ class App {
|
|
|
3953
4090
|
return true;
|
|
3954
4091
|
}
|
|
3955
4092
|
}
|
|
4093
|
+
|
|
4094
|
+
const checkboxResult = toggleTaskCheckboxBeforeColumn(payload.line, x);
|
|
4095
|
+
if (checkboxResult.toggled) {
|
|
4096
|
+
buf.lines[y] = checkboxResult.line;
|
|
4097
|
+
buf.modified = true;
|
|
4098
|
+
}
|
|
3956
4099
|
|
|
3957
4100
|
this.message = `event ${payload.trigger}:`+
|
|
3958
4101
|
`(${payload.row},${payload.col})`+
|
|
@@ -4751,6 +4894,11 @@ class App {
|
|
|
4751
4894
|
|
|
4752
4895
|
async quit() {
|
|
4753
4896
|
const buffer = this.buffer;
|
|
4897
|
+
if (isMdcuiEncoding(buffer?.encoding)) {
|
|
4898
|
+
await this.notifyMdcuiExit(buffer, "quit");
|
|
4899
|
+
await this._doCloseCurrentPane();
|
|
4900
|
+
return;
|
|
4901
|
+
}
|
|
4754
4902
|
if (buffer.modified) {
|
|
4755
4903
|
if (!this.prompt) {
|
|
4756
4904
|
this.openYNPrompt(
|
|
@@ -4771,6 +4919,7 @@ class App {
|
|
|
4771
4919
|
// Close only the current pane; if it's the last pane in the tab, close the tab.
|
|
4772
4920
|
async _doCloseCurrentPane() {
|
|
4773
4921
|
if (this.tab.panes().length > 1) {
|
|
4922
|
+
await this.notifyMdcuiExit(this.buffer, "close-pane");
|
|
4774
4923
|
this.closePane(this.pane);
|
|
4775
4924
|
this.render();
|
|
4776
4925
|
} else {
|
|
@@ -4788,6 +4937,32 @@ class App {
|
|
|
4788
4937
|
if (!this.prompt && !this.buffer.modified) await this.closeCurrentTab({ force: true });
|
|
4789
4938
|
}
|
|
4790
4939
|
|
|
4940
|
+
async notifyMdcuiExit(buffer, reason = "exit") {
|
|
4941
|
+
if (
|
|
4942
|
+
!buffer
|
|
4943
|
+
|| !isMdcuiEncoding(buffer.encoding)
|
|
4944
|
+
|| this._mdcuiExitNotified.has(buffer)
|
|
4945
|
+
) return;
|
|
4946
|
+
this._mdcuiExitNotified.add(buffer);
|
|
4947
|
+
|
|
4948
|
+
const frontPath = buffer.path && !isHttpUrl(buffer.path)
|
|
4949
|
+
? `${buffer.path}.front.js`
|
|
4950
|
+
: "";
|
|
4951
|
+
if (!frontPath || !existsSync(frontPath)) return;
|
|
4952
|
+
|
|
4953
|
+
try {
|
|
4954
|
+
const frontMod = await import(localModuleUrl(frontPath));
|
|
4955
|
+
if (typeof frontMod.onMdcuiExit !== "function") return;
|
|
4956
|
+
await frontMod.onMdcuiExit({
|
|
4957
|
+
reason,
|
|
4958
|
+
path: buffer.path,
|
|
4959
|
+
$: globalThis.$,
|
|
4960
|
+
});
|
|
4961
|
+
} catch (error) {
|
|
4962
|
+
console.error(`[mdcui] onMdcuiExit: ${error?.message || error}`);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4791
4966
|
async toggleHelp() {
|
|
4792
4967
|
const cur = this.pane;
|
|
4793
4968
|
if (cur?.isHelp) {
|
|
@@ -4886,11 +5061,14 @@ class App {
|
|
|
4886
5061
|
async closeCurrentTab({ force = false } = {}) {
|
|
4887
5062
|
if (!force && this.buffer?.modified) return await this.quit();
|
|
4888
5063
|
if (this.tabs.length <= 1) {
|
|
5064
|
+
await this.notifyMdcuiExit(this.buffer, "close-tab");
|
|
4889
5065
|
await this.stop(0);
|
|
4890
5066
|
return;
|
|
4891
5067
|
}
|
|
4892
5068
|
const closingBuffers = [...new Set(this.tab.panes().flatMap((pane) => [pane.buffer, pane.prevBuffer]).filter(Boolean))];
|
|
4893
5069
|
const closing = this.buffer;
|
|
5070
|
+
for (const buffer of closingBuffers)
|
|
5071
|
+
await this.notifyMdcuiExit(buffer, "close-tab");
|
|
4894
5072
|
this.tabs.splice(this.activeTabIdx, 1);
|
|
4895
5073
|
this.activeTabIdx = Math.min(this.activeTabIdx, this.tabs.length - 1);
|
|
4896
5074
|
this.message = "";
|
|
@@ -5674,6 +5852,14 @@ class App {
|
|
|
5674
5852
|
this._freshClip = false;
|
|
5675
5853
|
const pasted = this.clipboard.read();
|
|
5676
5854
|
if (pasted) {
|
|
5855
|
+
if (isMdcuiEncoding(buf?.encoding) && (
|
|
5856
|
+
/[\r\n]/.test(pasted)
|
|
5857
|
+
|| !canEditMdcuiAtCursor(buf)
|
|
5858
|
+
|| !canEditMdcuiSelection(buf, this.pane?.selection)
|
|
5859
|
+
)) {
|
|
5860
|
+
this.message = "Protected mdcui content";
|
|
5861
|
+
break;
|
|
5862
|
+
}
|
|
5677
5863
|
buf.pushUndo();
|
|
5678
5864
|
if (this.pane?.selection) deleteSelection(buf, this.pane);
|
|
5679
5865
|
buf.insert(pasted);
|
|
@@ -7346,8 +7532,17 @@ async function loadBuffers(files, command) {
|
|
|
7346
7532
|
}
|
|
7347
7533
|
|
|
7348
7534
|
async function printReadmeDocs() {
|
|
7349
|
-
|
|
7350
|
-
|
|
7535
|
+
process.stdout.write(Bun.markdown.ansi(await bundledReadmeSource(), { hyperlinks: true }));
|
|
7536
|
+
}
|
|
7537
|
+
|
|
7538
|
+
async function bundledReadmeSource() {
|
|
7539
|
+
return readInternalAssetText("README.md") ?? await Bun.file(join(REPO_ROOT, "README.md")).text();
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7542
|
+
async function exportReadme() {
|
|
7543
|
+
const readmePath = resolve("README.md");
|
|
7544
|
+
await Bun.write(readmePath, await bundledReadmeSource());
|
|
7545
|
+
console.log(`Wrote ${readmePath}`);
|
|
7351
7546
|
}
|
|
7352
7547
|
|
|
7353
7548
|
async function printChangelogDocs() {
|
|
@@ -7356,11 +7551,11 @@ async function printChangelogDocs() {
|
|
|
7356
7551
|
}
|
|
7357
7552
|
|
|
7358
7553
|
async function printTestappSource() {
|
|
7359
|
-
process.stdout.write(await
|
|
7554
|
+
process.stdout.write(await bundledMarkdownSource("testapp.md"));
|
|
7360
7555
|
}
|
|
7361
7556
|
|
|
7362
|
-
async function
|
|
7363
|
-
return readInternalAssetText(
|
|
7557
|
+
async function bundledMarkdownSource(filename) {
|
|
7558
|
+
return readInternalAssetText(filename) ?? await Bun.file(join(REPO_ROOT, filename)).text();
|
|
7364
7559
|
}
|
|
7365
7560
|
|
|
7366
7561
|
async function main() {
|
|
@@ -7406,10 +7601,33 @@ async function main() {
|
|
|
7406
7601
|
console.log("Clipboard:", backends);
|
|
7407
7602
|
return;
|
|
7408
7603
|
}
|
|
7604
|
+
if (flags.check) {
|
|
7605
|
+
if (rawFiles.length !== 1) {
|
|
7606
|
+
console.error("Usage: jsmdcui --check FILE.md");
|
|
7607
|
+
process.exitCode = 2;
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
const checkPath = resolve(rawFiles[0]);
|
|
7611
|
+
try {
|
|
7612
|
+
const file = Bun.file(checkPath);
|
|
7613
|
+
if (!(await file.exists())) throw new Error("file not found");
|
|
7614
|
+
const result = checkMarkdownIdCollisions(await file.text());
|
|
7615
|
+
process.stdout.write(formatMarkdownIdCheckAnsi(checkPath, result));
|
|
7616
|
+
if (result.collisions.length) process.exitCode = 1;
|
|
7617
|
+
} catch (error) {
|
|
7618
|
+
console.error(`Cannot check ${checkPath}: ${error?.message || error}`);
|
|
7619
|
+
process.exitCode = 2;
|
|
7620
|
+
}
|
|
7621
|
+
return;
|
|
7622
|
+
}
|
|
7409
7623
|
if (flags.docs) {
|
|
7410
7624
|
await printReadmeDocs();
|
|
7411
7625
|
return;
|
|
7412
7626
|
}
|
|
7627
|
+
if (flags.exportReadme) {
|
|
7628
|
+
await exportReadme();
|
|
7629
|
+
return;
|
|
7630
|
+
}
|
|
7413
7631
|
if (flags.changelog) {
|
|
7414
7632
|
await printChangelogDocs();
|
|
7415
7633
|
return;
|
|
@@ -7418,9 +7636,20 @@ async function main() {
|
|
|
7418
7636
|
await printTestappSource();
|
|
7419
7637
|
return;
|
|
7420
7638
|
}
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7639
|
+
const demoFilename = flags.demo
|
|
7640
|
+
? "testapp.md"
|
|
7641
|
+
: flags.demoSelect
|
|
7642
|
+
? "select.md"
|
|
7643
|
+
: flags.demoImgtool
|
|
7644
|
+
? "image-processor.md"
|
|
7645
|
+
: flags.demoImgtoolZh
|
|
7646
|
+
? "image-processor.zh-TW.md"
|
|
7647
|
+
: "";
|
|
7648
|
+
if (demoFilename) {
|
|
7649
|
+
const demoPath = resolve(demoFilename);
|
|
7650
|
+
if (!(await Bun.file(demoPath).exists())) {
|
|
7651
|
+
await Bun.write(demoPath, await bundledMarkdownSource(demoFilename));
|
|
7652
|
+
}
|
|
7424
7653
|
rawFiles.splice(0, rawFiles.length, demoPath);
|
|
7425
7654
|
}
|
|
7426
7655
|
if (flags.options) {
|
|
@@ -7902,11 +8131,16 @@ function outdentLine(buf, _ctx) {
|
|
|
7902
8131
|
}
|
|
7903
8132
|
|
|
7904
8133
|
function deleteSelection(buf, pane) {
|
|
7905
|
-
if (isEditLockedBuffer(buf)) return "";
|
|
7906
8134
|
const selection = pane?.selection;
|
|
7907
8135
|
if (!selection || !buf) return "";
|
|
7908
8136
|
const text = getSelectionText(buf, selection);
|
|
7909
8137
|
const { first, last } = selectionBounds(selection);
|
|
8138
|
+
if (isMdcuiEncoding(buf?.encoding)) {
|
|
8139
|
+
const prefixLength = mdcuiEditablePrefixLength(buf, first.y);
|
|
8140
|
+
if (first.y !== last.y || prefixLength === 0 || first.x < prefixLength) return "";
|
|
8141
|
+
} else if (isEditLockedBuffer(buf)) {
|
|
8142
|
+
return "";
|
|
8143
|
+
}
|
|
7910
8144
|
if (first.y === last.y) {
|
|
7911
8145
|
const line = buf.lines[first.y] ?? "";
|
|
7912
8146
|
buf.lines[first.y] = line.slice(0, first.x) + line.slice(last.x);
|