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/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: false,
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 and write generated files in the current directory
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
- Outputs & overwrites ./testapp.md, opens it in the TUI, and writes 5 generated files
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 (isEditLockedBuffer(buf)) {
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
- const readme = readInternalAssetText("README.md") ?? await Bun.file(join(REPO_ROOT, "README.md")).text();
7350
- process.stdout.write(Bun.markdown.ansi(readme, { hyperlinks: true }));
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 bundledTestappSource());
7554
+ process.stdout.write(await bundledMarkdownSource("testapp.md"));
7360
7555
  }
7361
7556
 
7362
- async function bundledTestappSource() {
7363
- return readInternalAssetText("testapp.md") ?? await Bun.file(join(REPO_ROOT, "testapp.md")).text();
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
- if (flags.demo) {
7422
- const demoPath = resolve("testapp.md");
7423
- await Bun.write(demoPath, await bundledTestappSource());
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);