jsmdcui 0.9.0 → 0.11.1

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
@@ -128,7 +128,7 @@ import { isHex3Encoding, isMdcuiEncoding } from "./runtime/encodings.js";
128
128
  import { createInterface } from "node:readline/promises";
129
129
 
130
130
  import pkg from "../package.json" with { type: "json" };
131
- import { REPO_ROOT,IS_COMPILED, buildExecutable,buildEarlyExit } from "../single-exe/compiled.js";
131
+ import { REPO_ROOT,IS_COMPILED, buildExecutable,buildEarlyExit,stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
132
132
 
133
133
 
134
134
  const __filename = fileURLToPath(import.meta.url);
@@ -181,6 +181,12 @@ const VERSION = pkg.version;
181
181
  const SINGLE_EXE_DIR = resolve(REPO_ROOT, "single-exe");
182
182
  const SINGLE_EXE_ENTRY = resolve(SINGLE_EXE_DIR, "entry.mjs");
183
183
  const DEFAULT_BUILD_OUTFILE = "mdcui";
184
+ const MDCUI_DEFAULT_EDIT_ENABLED = typeof MDCUI_DEFAULT_EDIT !== "undefined";
185
+ const MDCUI_DEFAULT_DEMO_ENABLED = typeof MDCUI_DEFAULT_DEMO !== "undefined";
186
+ const MDCUI_DEFAULT_DEMO_WUI_ENABLED = typeof MDCUI_DEFAULT_DEMO_WUI !== "undefined";
187
+ const MDCUI_OVERWRITE_DEMO_ENABLED = typeof MDCUI_OVERWRITE_DEMO !== "undefined";
188
+ let defaultEdit = false;
189
+ let noRuntimeArgs = false;
184
190
  const decoder = new TextDecoder();
185
191
  let _activeTtyStream = null; // set in App.start() for use by the global error handler
186
192
 
@@ -340,7 +346,7 @@ function normalizeEncodingLabel(encoding = "utf-8") {
340
346
 
341
347
  function encodingForPath(pathOrUrl, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
342
348
  const normalized = normalizeEncodingLabel(encoding);
343
- if (normalized !== "utf-8" || !inferMdcui) return normalized;
349
+ if (normalized !== "utf-8" || !inferMdcui || defaultEdit) return normalized;
344
350
  let pathname = String(pathOrUrl ?? "").replace(/[?#].*$/, "");
345
351
  try {
346
352
  if (isHttpUrl(pathname)) pathname = new URL(pathname).pathname;
@@ -1092,6 +1098,8 @@ function parseArgs(argv) {
1092
1098
  clean: false,
1093
1099
  check: false,
1094
1100
  cat: false,
1101
+ wui: false,
1102
+ printUi: false,
1095
1103
  docs: false,
1096
1104
  exportReadme: false,
1097
1105
  exportCdpMaze: false,
@@ -1099,6 +1107,7 @@ function parseArgs(argv) {
1099
1107
  testapp: false,
1100
1108
  demoList: false,
1101
1109
  demo: null,
1110
+ overwriteDemo: false,
1102
1111
  allowUrl: false,
1103
1112
  buildExe: false,
1104
1113
  buildFor: "",
@@ -1122,6 +1131,8 @@ function parseArgs(argv) {
1122
1131
  else if (arg === "-clean") flags.clean = true;
1123
1132
  else if (arg === "--check") flags.check = true;
1124
1133
  else if (arg === "--cat" || arg === "-cat" || arg === "--ccat" || arg === "-ccat" || arg === "--bat" || arg === "-bat" || arg === "--glow" || arg === "-glow") flags.cat = true;
1134
+ else if (arg === "--wui") flags.wui = true;
1135
+ else if (arg === "--print-ui") flags.printUi = true;
1125
1136
  else if (arg === "--xxd" || arg === "--hexdump") {
1126
1137
  flags.cat = true;
1127
1138
  flags.settings.set("encoding", "hex3");
@@ -1138,12 +1149,16 @@ function parseArgs(argv) {
1138
1149
  else if (arg === "--edit") {
1139
1150
  flags.settings.set("encoding", "utf-8");
1140
1151
  }
1152
+ else if (arg === "--mdcui" || arg === "--tui") {
1153
+ flags.settings.set("encoding", "mdcui");
1154
+ }
1141
1155
  else if (arg === "--docs" || arg === "--readme") flags.docs = true;
1142
1156
  else if (arg === "--export-readme") flags.exportReadme = true;
1143
1157
  else if (arg === "--export-cdp-maze") flags.exportCdpMaze = true;
1144
1158
  else if (arg === "--changelog") flags.changelog = true;
1145
1159
  else if (arg === "--testapp.md") flags.testapp = true;
1146
1160
  else if (arg === "--demo-list") flags.demoList = true;
1161
+ else if (arg === "--overwrite-demo") flags.overwriteDemo = true;
1147
1162
  else if (arg === "--cdp-maze") {
1148
1163
  flags.cdpMaze = true;
1149
1164
  flags.demo = { option: arg, filename: "maze.md", asset: "demos/maze.md" };
@@ -1159,9 +1174,15 @@ function parseArgs(argv) {
1159
1174
  }
1160
1175
  else if (arg.startsWith("--demo-")) {
1161
1176
  const name = arg.slice("--demo-".length);
1162
- flags.demo = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
1177
+ flags.demo = isValidDemoName(name)
1163
1178
  ? { option: arg, filename: `${name}.md`, asset: `demos/${name}.md` }
1164
- : { option: arg, error: "demo filename must contain only letters, numbers, dots, underscores, and hyphens" };
1179
+ : {
1180
+ option: arg,
1181
+ error:
1182
+ "demo filename must start with a Unicode letter or number "
1183
+ + "and contain only Unicode letters, numbers, dots, underscores, "
1184
+ + "or hyphens; whitespace is not allowed",
1185
+ };
1165
1186
  }
1166
1187
  else if (arg === "--allow-url") flags.allowUrl = true;
1167
1188
  else if (arg === "--kitty") flags.kittyMode = "extended";
@@ -1192,20 +1213,70 @@ function parseArgs(argv) {
1192
1213
  return { flags, files };
1193
1214
  }
1194
1215
 
1216
+ function isValidDemoName(name) {
1217
+ return /^[\p{L}\p{N}][\p{L}\p{N}\p{M}._-]*$/u.test(String(name));
1218
+ }
1219
+
1220
+ function demoNameFromMarkdownBasename(filename) {
1221
+ const value = String(filename);
1222
+ if (!value.endsWith(".md")) return "";
1223
+ const name = value.slice(0, -".md".length);
1224
+ return isValidDemoName(name) ? name : "";
1225
+ }
1226
+
1195
1227
  function usage() {
1196
1228
  return [
1197
1229
  `Usage:
1198
- ${pkg.name} [OPTIONS] [FILE.md]
1230
+ ${pkg.name} [OPTIONS] [FILE]
1231
+
1232
+ Execute .md:
1233
+ ${pkg.name} [FILE.md]
1199
1234
  ${pkg.name} --wui [FILE.md]
1235
+ Develop .md:
1236
+ ${pkg.name} --edit [FILE.md]
1237
+ ${pkg.name} --check <FILE.md>
1200
1238
 
1201
1239
  Modes:
1202
- --check FILE.md
1203
- Check heading and fenced-block IDs for collisions, print details, and exit
1204
- Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
1240
+ --tui, --mdcui, -encoding mdcui
1241
+ .md files use this by default
1242
+ Execute a Markdown App
1243
+ Create a Terminal UI
1244
+ Generate & overwrite
1245
+ .front.js, .back.js,
1246
+ .html, -rpc.js, -server.js
1247
+ beside the .md file
1248
+
1205
1249
  --wui [FILE.md]
1206
- Generate or overwrite Markdown UI files beside FILE.md and start the server
1207
- Without FILE.md, use the existing ./testapp.md without overwriting it
1250
+ Execute a Markdown App
1251
+ Create a Web UI
1252
+ Start a web server
1253
+ Generate & overwrite
1254
+ .front.js, .back.js,
1255
+ .html, -rpc.js, -server.js
1256
+ beside the .md file
1257
+
1258
+ Without FILE.md,
1259
+ default to ./testapp.md
1208
1260
  If ./testapp.md is missing, write the bundled demo there first
1261
+ Combine with any --demo option to start that demo as a Web UI
1262
+
1263
+ --print-ui
1264
+ Must be combined with --wui
1265
+ Print the generated TUI, raw ANSI, and HTML before starting the server
1266
+
1267
+ --kitty
1268
+ Display Markdown images with Kitty graphics and the jsgotty MIME extension
1269
+ --kitty-compat
1270
+ Display Markdown images with Kitty graphics without the non-standard MIME U field
1271
+
1272
+ --edit
1273
+ Open files as editable UTF-8 text
1274
+ Override .md mdcui detection
1275
+
1276
+ --check <FILE.md>
1277
+ Check heading and fenced-block IDs for collisions, print details, and exit
1278
+ Exits 0 when IDs are unique, 1 on collisions, and 2 on usage/read errors
1279
+
1209
1280
  --cat, --ccat, --bat, --glow
1210
1281
  Render file(s) and write to stdout, then exit (.md uses mdcui/createTui)
1211
1282
  A local .md file also writes or overwrites five generated files beside it
@@ -1214,15 +1285,6 @@ Modes:
1214
1285
  --hex3, --hex3gz, --hex3zst
1215
1286
  Set -encoding hex3, hex3gz, or hex3zst for this session
1216
1287
  hex3 shows raw bytes; gz/zst variants compress the same hex3 view
1217
- --edit
1218
- Open files as editable UTF-8 text, overriding .md mdcui detection
1219
- -encoding mdcui
1220
- Render Markdown through runmd.mjs#createTui; .md files use this automatically
1221
- Writes .front.js, .back.js, .html, -rpc.js, and -server.js beside the .md file
1222
- --kitty
1223
- Display Markdown images with Kitty graphics and the jsgotty MIME extension
1224
- --kitty-compat
1225
- Display Markdown images with Kitty graphics without the non-standard MIME U field
1226
1288
 
1227
1289
  Settings:
1228
1290
  -SETTING VALUE
@@ -1230,54 +1292,73 @@ Settings:
1230
1292
  -options
1231
1293
  List all setting names and defaults, then exit.
1232
1294
 
1233
- CDP:
1295
+ CDP(Chrome DevTools Protocol):
1234
1296
  --cdp-maze
1235
1297
  Open the maze demo, start CDP on localhost:9222, and solve it automatically
1298
+ --export-cdp-maze
1299
+ Write or overwrite ./cdp-maze.js with the bundled CDP maze solver & exit
1300
+
1236
1301
  --remote-debugging-port=PORT
1237
1302
  Start CDP (Chrome DevTools Protocol) server on PORT at launch
1238
1303
  --remote-debugging-address=ADDRESS
1239
1304
  Bind CDP server to ADDRESS (default: 127.0.0.1); use 0.0.0.0 for all interfaces
1240
1305
 
1241
1306
  Information:
1242
- -help, -h, --help
1307
+ --help, -h, -help
1243
1308
  Show this help & exit
1244
- -version, -V, --version
1309
+ --version, -V, -version
1245
1310
  Show version+backend info & exit
1246
- --docs, --readme
1311
+
1312
+ --readme, --docs
1247
1313
  Show ${pkg.name}'s README.md & exit
1248
1314
  --export-readme
1249
1315
  Write or overwrite ./README.md with the bundled README.md & exit
1250
- --export-cdp-maze
1251
- Write or overwrite ./cdp-maze.js with the bundled CDP maze solver & exit
1316
+
1252
1317
  --changelog
1253
1318
  Show CHANGELOG.md & exit
1254
- -profile, --profile
1319
+ --profile, -profile
1255
1320
  Print startup performance profile and exit
1256
1321
 
1257
1322
  Demo:
1258
1323
  --testapp.md
1259
- Write the bundled testapp.md to stdout & exit
1324
+ Print the bundled testapp.md to stdout & exit
1260
1325
  --demo-list
1261
- List the bundled demos and their command-line options, then exit
1326
+ List the bundled demos & exit
1327
+
1262
1328
  --demo
1263
- Use the existing ./testapp.md without overwriting it, or write the bundled demo if missing
1329
+ Execute an existing ./testapp.md
1330
+ Or write the bundled demo if missing
1264
1331
  Open it in the TUI and write 5 generated files beside it
1332
+
1265
1333
  --demo-<filename>
1266
- Load demos/<filename>.md, preserving an existing ./<filename>.md or writing the bundled copy
1334
+ Execute an existing <filename>.md
1335
+ Or write the bundled demos/<filename>.md if missing
1267
1336
  Open it in the TUI and write 5 generated files beside it
1268
1337
  For example: --demo-select, --demo-todo, or --demo-todo-zh
1338
+
1269
1339
  --demo-imgtool
1270
1340
  Alias for --demo-image-processor
1271
1341
  --demo-imgtool-zh
1272
1342
  Alias for --demo-image-processor.zh-TW
1273
1343
 
1344
+ --overwrite-demo
1345
+ Overwrite an existing local demo with the bundled copy; combine with any --demo option
1346
+
1274
1347
  Remote Markdown:
1275
1348
  --allow-url
1276
1349
  Download HTTP(S) Markdown and, with Kitty mode, its HTTP(S) images; allow its code to run
1277
1350
 
1278
- Experimental:
1279
- --build-exe Build a Bun single-file executable and exit
1280
- --build-for <target> Build a Bun single-file executable for target`
1351
+ Experimental single-exe:
1352
+ --build-exe [BUN_ARGS...]
1353
+ Build a Bun single-file executable & exit
1354
+ --build-for <target> [BUN_ARGS...]
1355
+ Build a Bun single-file executable for target & exit
1356
+
1357
+ [BUN_ARGS...]
1358
+ --define MDCUI_XXX=1
1359
+ More info in --readme
1360
+ https://bun.com/docs/bundler/executables
1361
+ `
1281
1362
  ].join("\n");
1282
1363
  }
1283
1364
 
@@ -1337,6 +1418,7 @@ class BufferModel {
1337
1418
  this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(sourceText ?? tuiSourceText ?? text) : new Map();
1338
1419
  this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
1339
1420
  this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
1421
+ this._useBundledMdcuiModules = false;
1340
1422
  this.cursor = { x: 0, y: 0 };
1341
1423
  this.scroll = { x: 0, y: 0, row: 0 };
1342
1424
  this._modified = false;
@@ -4477,7 +4559,10 @@ class App {
4477
4559
  ) return false;
4478
4560
 
4479
4561
  const frontPath = `${buf.path}.front.js`;
4480
- if (!existsSync(frontPath)) return false;
4562
+ const useBundledMdcuiModules = Boolean(
4563
+ global.MDCUI_MAIN && buf._useBundledMdcuiModules,
4564
+ );
4565
+ if (!useBundledMdcuiModules && !existsSync(frontPath)) return false;
4481
4566
  try {
4482
4567
  const selection = globalThis.$?.(`${declaration.tag}#${id}`);
4483
4568
  const target = {
@@ -4492,8 +4577,12 @@ class App {
4492
4577
  });
4493
4578
  const event = tuiKeyEventForFront(inputEvent, target, eventName);
4494
4579
  const [{ evalFront }, frontMod] = await Promise.all([
4495
- import("./cui/rpc.mjs"),
4496
- import(localModuleUrl(frontPath)),
4580
+ useBundledMdcuiModules
4581
+ ? import(global.MDCUI_MAIN + "-rpc.js")
4582
+ : import("./cui/rpc.mjs"),
4583
+ useBundledMdcuiModules
4584
+ ? import(global.MDCUI_MAIN + ".front.js")
4585
+ : import(localModuleUrl(frontPath)),
4497
4586
  ]);
4498
4587
  const result = await evalFront(frontMod, code, { event });
4499
4588
  if (result && typeof result === "object" && result.ok === false) {
@@ -4528,9 +4617,16 @@ class App {
4528
4617
  { // defaultCallback
4529
4618
  if (payload.link && /^javascript:/i.test(payload.link) && buf?.path && !isHttpUrl(buf.path)) {
4530
4619
  try {
4620
+ const useBundledMdcuiModules = Boolean(
4621
+ global.MDCUI_MAIN && buf._useBundledMdcuiModules,
4622
+ );
4531
4623
  const [{ evalFront }, frontMod] = await Promise.all([
4532
- import("./cui/rpc.mjs"),
4533
- import(localModuleUrl(`${buf.path}.front.js`)),
4624
+ useBundledMdcuiModules
4625
+ ? import(global.MDCUI_MAIN + "-rpc.js")
4626
+ : import("./cui/rpc.mjs"),
4627
+ useBundledMdcuiModules
4628
+ ? import(global.MDCUI_MAIN + ".front.js")
4629
+ : import(localModuleUrl(`${buf.path}.front.js`)),
4534
4630
  ]);
4535
4631
  const result = await evalFront(frontMod, payload.link);
4536
4632
  if (result && typeof result === "object" && result.ok === false) {
@@ -5418,10 +5514,18 @@ class App {
5418
5514
  const frontPath = buffer.path && !isHttpUrl(buffer.path)
5419
5515
  ? `${buffer.path}.front.js`
5420
5516
  : "";
5421
- if (!frontPath || !existsSync(frontPath)) return;
5517
+ const useBundledMdcuiModules = Boolean(
5518
+ global.MDCUI_MAIN && buffer._useBundledMdcuiModules,
5519
+ );
5520
+ if (
5521
+ !useBundledMdcuiModules
5522
+ && (!frontPath || !existsSync(frontPath))
5523
+ ) return;
5422
5524
 
5423
5525
  try {
5424
- const frontMod = await import(localModuleUrl(frontPath));
5526
+ const frontMod = useBundledMdcuiModules
5527
+ ? await import(global.MDCUI_MAIN + ".front.js")
5528
+ : await import(localModuleUrl(frontPath));
5425
5529
  if (typeof frontMod.onMdcuiExit !== "function") return;
5426
5530
  await frontMod.onMdcuiExit({
5427
5531
  reason,
@@ -6958,6 +7062,11 @@ function makeBufferAdapter(buffer) {
6958
7062
  get Name() { return buffer.Name ?? buffer.name ?? ""; },
6959
7063
  get Modified() { return buffer.modified ?? false; },
6960
7064
  get Settings() { return buffer.Settings; },
7065
+ get MdcuiModuleSource() {
7066
+ return global.MDCUI_MAIN && buffer._useBundledMdcuiModules
7067
+ ? "embedded"
7068
+ : "external";
7069
+ },
6961
7070
  Line: (...args) => buffer.Line(Number(lastArg(args))),
6962
7071
  Insert: (...args) => insertAtLoc(buffer, decodeLoc(args.at(-2)), String(args.at(-1))),
6963
7072
  Replace: (...args) => replaceAtLocs(buffer, decodeLoc(args.at(-3)), decodeLoc(args.at(-2)), String(args.at(-1))),
@@ -8073,19 +8182,182 @@ function printDemoList() {
8073
8182
  console.log(" --demo-imgtool-zh --demo-image-processor.zh-TW");
8074
8183
  }
8075
8184
 
8185
+ async function prepareDemo(flags, rawFiles) {
8186
+ if (!flags.demo) return { ok: true };
8187
+ if (flags.demo.error) {
8188
+ console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
8189
+ process.exitCode = 2;
8190
+ return { ok: false };
8191
+ }
8192
+ let demoSource;
8193
+ try {
8194
+ demoSource = await bundledMarkdownSource(flags.demo.asset);
8195
+ } catch {
8196
+ console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
8197
+ process.exitCode = 2;
8198
+ return { ok: false };
8199
+ }
8200
+
8201
+ const demoPath = resolve(flags.demo.filename);
8202
+ const demoFile = Bun.file(demoPath);
8203
+ const demoExists = await demoFile.exists();
8204
+ const writeBundledContent = flags.overwriteDemo || !demoExists;
8205
+ const bundledByteLength = Buffer.byteLength(demoSource);
8206
+ const demoByteLength = writeBundledContent
8207
+ ? bundledByteLength
8208
+ : demoFile.size;
8209
+ const configuredMainBase = String(
8210
+ global.MDCUI_MAIN_BASE
8211
+ || (global.MDCUI_MAIN ? basename(String(global.MDCUI_MAIN)) : ""),
8212
+ );
8213
+ const isConfiguredMainDemo = Boolean(
8214
+ global.MDCUI_MAIN
8215
+ && configuredMainBase
8216
+ && basename(demoPath) === configuredMainBase,
8217
+ );
8218
+
8219
+ if (writeBundledContent) {
8220
+ await Bun.write(demoPath, demoSource);
8221
+ } else if (isConfiguredMainDemo && demoByteLength !== bundledByteLength) {
8222
+ console.error(
8223
+ `[mdcui] ${demoPath} differs from the bundled demo `
8224
+ + `(${demoByteLength} bytes vs ${bundledByteLength} bytes). `
8225
+ + "The edited demo may not work correctly if it imports companion modules "
8226
+ + "instead of keeping all application code in the Markdown file.",
8227
+ );
8228
+ }
8229
+
8230
+ rawFiles.splice(0, rawFiles.length, demoPath);
8231
+ return {
8232
+ ok: true,
8233
+ path: demoPath,
8234
+ useBundledModules: isConfiguredMainDemo
8235
+ && demoByteLength === bundledByteLength,
8236
+ };
8237
+ }
8238
+
8239
+ function stringDefineFromArgv(argv, name) {
8240
+ const prefix = `${name}=`;
8241
+ for (let i = 0; i < argv.length; i++) {
8242
+ let definition = "";
8243
+ if (argv[i] === "--define") definition = argv[i + 1] ?? "";
8244
+ else if (argv[i].startsWith("--define="))
8245
+ definition = argv[i].slice("--define=".length);
8246
+ if (!definition.startsWith(prefix)) continue;
8247
+ try {
8248
+ const value = JSON.parse(definition.slice(prefix.length));
8249
+ return typeof value === "string" ? value : "";
8250
+ } catch {
8251
+ return "";
8252
+ }
8253
+ }
8254
+ return "";
8255
+ }
8256
+
8076
8257
  async function main() {
8077
- if (process.argv[2] === "--wui") {
8078
- process.argv.splice(2, 1);
8258
+ addCheckpoint("Argument Parsing");
8259
+
8260
+ stringifyNonPrimitiveDefineValues(process.argv, "global.MDCUI_MAIN");
8261
+ const argvMdcuiMain = stringDefineFromArgv(
8262
+ process.argv,
8263
+ "global.MDCUI_MAIN",
8264
+ );
8265
+ let sourceMdcuiMain = "";
8266
+ if (IS_COMPILED) {
8267
+ sourceMdcuiMain = "";
8268
+ } else {
8269
+ sourceMdcuiMain = String(argvMdcuiMain || global.MDCUI_MAIN || "");
8270
+ }
8271
+
8272
+ if (sourceMdcuiMain) {
8273
+ const mdcuiMain = resolve(sourceMdcuiMain);
8274
+ const mdcuiMainBase = basename(mdcuiMain);
8275
+ const mdcuiMainDemoName = demoNameFromMarkdownBasename(mdcuiMainBase);
8276
+ if (!mdcuiMainDemoName) {
8277
+ throw new Error(
8278
+ "MDCUI_MAIN basename must end with lowercase .md, start with a "
8279
+ + "Unicode letter or number, and contain no whitespace; only Unicode "
8280
+ + "letters, numbers, dots, underscores, and hyphens are allowed",
8281
+ );
8282
+ }
8079
8283
  const runmd = await import("../runmd.mjs");
8080
- await runmd.main();
8081
- return;
8284
+ const mdcuiMainFile = Bun.file(mdcuiMain);
8285
+ if (!(await mdcuiMainFile.exists())) {
8286
+ throw new Error(`MDCUI_MAIN file not found: ${mdcuiMain}`);
8287
+ }
8288
+ let markdown = await mdcuiMainFile.text();
8289
+ const demoPath = join(REPO_ROOT, "demos", mdcuiMainBase);
8290
+ if (resolve(mdcuiMain) !== resolve(demoPath)) {
8291
+ await Bun.write(demoPath, markdown);
8292
+ }
8293
+ markdown = await runmd.extractJs(markdown, mdcuiMain, {
8294
+ bundling: true,
8295
+ });
8296
+ await runmd.createWui(markdown, mdcuiMain, {
8297
+ bundling: true,
8298
+ });
8299
+
8300
+ if (
8301
+ process.argv.includes("--build-for")
8302
+ || process.argv.includes("--build-exe")
8303
+ ) {
8304
+ if (argvMdcuiMain) {
8305
+ const normalizedDefine =
8306
+ `global.MDCUI_MAIN=${JSON.stringify(mdcuiMain)}`;
8307
+ for (let i = 0; i < process.argv.length; i++) {
8308
+ if (
8309
+ process.argv[i] === "--define"
8310
+ && process.argv[i + 1]?.startsWith("global.MDCUI_MAIN=")
8311
+ ) {
8312
+ process.argv[i + 1] = normalizedDefine;
8313
+ } else if (
8314
+ process.argv[i].startsWith("--define=global.MDCUI_MAIN=")
8315
+ ) {
8316
+ process.argv[i] = `--define=${normalizedDefine}`;
8317
+ }
8318
+ }
8319
+ } else {
8320
+ process.argv.push(
8321
+ "--define",
8322
+ `global.MDCUI_MAIN=${JSON.stringify(mdcuiMain)}`,
8323
+ );
8324
+ }
8325
+ process.argv.push(
8326
+ "--define",
8327
+ `global.MDCUI_MAIN_BASE=${JSON.stringify(mdcuiMainBase)}`,
8328
+ );
8329
+ }
8082
8330
  }
8083
8331
 
8084
- addCheckpoint("Argument Parsing");
8085
-
8086
8332
  await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
8333
+ defaultEdit = await Bun.file(join(REPO_ROOT, "src", "MDCUI_DEFAULT_EDIT")).exists();
8087
8334
 
8088
- const { flags, files: rawFiles } = parseArgs(process.argv.slice(2));
8335
+ const runtimeArgs = process.argv.slice(2);
8336
+ noRuntimeArgs = runtimeArgs.length === 0;
8337
+ if (MDCUI_DEFAULT_EDIT_ENABLED) runtimeArgs.unshift("--edit");
8338
+ if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_ENABLED) runtimeArgs.push("--demo");
8339
+ if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_WUI_ENABLED) runtimeArgs.push("--wui");
8340
+ if (
8341
+ noRuntimeArgs
8342
+ && (global.MDCUI_MAIN || global.MDCUI_MAIN_BASE)
8343
+ ) {
8344
+ const mdcuiMainBase = String(
8345
+ global.MDCUI_MAIN_BASE || basename(String(global.MDCUI_MAIN)),
8346
+ );
8347
+ const demoName = demoNameFromMarkdownBasename(mdcuiMainBase);
8348
+ if (!demoName) {
8349
+ console.error(
8350
+ "Invalid MDCUI_MAIN_BASE: expected a lowercase .md basename with "
8351
+ + "Unicode letters or numbers and no whitespace",
8352
+ );
8353
+ process.exitCode = 2;
8354
+ return;
8355
+ }
8356
+ runtimeArgs.push(`--demo-${demoName}`);
8357
+ }
8358
+ if (MDCUI_OVERWRITE_DEMO_ENABLED) runtimeArgs.push("--overwrite-demo");
8359
+ const { flags, files: rawFiles } = parseArgs(runtimeArgs);
8360
+ let demoPreparation = null;
8089
8361
  kittyImageMode = flags.kittyMode;
8090
8362
  allowRemoteKittyImages = flags.allowUrl;
8091
8363
 
@@ -8096,7 +8368,7 @@ async function main() {
8096
8368
  if (flags.version) {
8097
8369
  const ttsCmd = detectTtsCmd();
8098
8370
  console.log(pkg.name+":",pkg.description)
8099
- console.log(" Rewritten by: Dr. John (醫者小智)")
8371
+ console.log(" Made by: Dr. John (醫者小智)")
8100
8372
  console.log("")
8101
8373
  console.log("Version:", VERSION);
8102
8374
  console.log("Runtime:", `Bun ${Bun.version}`);
@@ -8104,6 +8376,19 @@ async function main() {
8104
8376
  console.log("Http client:",detectHttpBackend());
8105
8377
  console.log("TTS:", ttsCmd ? ttsCmd.cmd[0] : "not found");
8106
8378
  console.log({SUPPORTED_ENCODING_LABELS})
8379
+ console.log("Distribution settings:");
8380
+ console.log(" MDCUI_DEFAULT_EDIT:", (MDCUI_DEFAULT_EDIT_ENABLED || defaultEdit) ? "enabled" : "disabled");
8381
+ console.log(" MDCUI_DEFAULT_DEMO:", MDCUI_DEFAULT_DEMO_ENABLED ? "enabled" : "disabled");
8382
+ console.log(" MDCUI_DEFAULT_DEMO_WUI:", MDCUI_DEFAULT_DEMO_WUI_ENABLED ? "enabled" : "disabled");
8383
+ console.log(" MDCUI_OVERWRITE_DEMO:", MDCUI_OVERWRITE_DEMO_ENABLED ? "enabled" : "disabled");
8384
+ console.log(
8385
+ " global.MDCUI_MAIN:",
8386
+ String(global.MDCUI_MAIN || "") || "not set",
8387
+ );
8388
+ console.log(
8389
+ " global.MDCUI_MAIN_BASE:",
8390
+ String(global.MDCUI_MAIN_BASE || "") || "not set",
8391
+ );
8107
8392
  const clipboard = new ClipboardManager();
8108
8393
  let osc52Available = false;
8109
8394
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -8118,6 +8403,18 @@ async function main() {
8118
8403
  console.log("Clipboard:", backends);
8119
8404
  return;
8120
8405
  }
8406
+ if (flags.wui) {
8407
+ demoPreparation = await prepareDemo(flags, rawFiles);
8408
+ if (!demoPreparation.ok) return;
8409
+ const runmd = await import("../runmd.mjs");
8410
+ await runmd.main(30, {
8411
+ mdpath: rawFiles[0] ?? null,
8412
+ overwriteDemo: flags.overwriteDemo && rawFiles.length === 0,
8413
+ printUi: flags.printUi,
8414
+ useBundledMdcuiServer: demoPreparation.useBundledModules,
8415
+ });
8416
+ return;
8417
+ }
8121
8418
  if (flags.check) {
8122
8419
  if (rawFiles.length !== 1) {
8123
8420
  console.error("Usage: jsmdcui --check FILE.md");
@@ -8162,24 +8459,8 @@ async function main() {
8162
8459
  return;
8163
8460
  }
8164
8461
  if (flags.demo) {
8165
- if (flags.demo.error) {
8166
- console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
8167
- process.exitCode = 2;
8168
- return;
8169
- }
8170
- let demoSource;
8171
- try {
8172
- demoSource = await bundledMarkdownSource(flags.demo.asset);
8173
- } catch {
8174
- console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
8175
- process.exitCode = 2;
8176
- return;
8177
- }
8178
- const demoPath = resolve(flags.demo.filename);
8179
- if (!(await Bun.file(demoPath).exists())) {
8180
- await Bun.write(demoPath, demoSource);
8181
- }
8182
- rawFiles.splice(0, rawFiles.length, demoPath);
8462
+ demoPreparation = await prepareDemo(flags, rawFiles);
8463
+ if (!demoPreparation.ok) return;
8183
8464
  }
8184
8465
  if (flags.options) {
8185
8466
  for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
@@ -8192,6 +8473,7 @@ async function main() {
8192
8473
  const config = await new Config({ configDir: flags.configDir }).init();
8193
8474
  config.applyCliSettings(flags.settings);
8194
8475
  const encodingExplicit = flags.settings.has("encoding") || Object.hasOwn(config.parsedSettings, "encoding");
8476
+ const inputFiletype = flags.settings.has("filetype") ? String(flags.settings.get("filetype")) : null;
8195
8477
  syncEditorSettings(config);
8196
8478
 
8197
8479
  addCheckpoint("Runtime Registry Init");
@@ -8203,7 +8485,14 @@ async function main() {
8203
8485
  const syntaxDefinitions = await loadSyntaxDefinitions(runtime);
8204
8486
 
8205
8487
  if (flags.cat) {
8206
- await catFiles(rawFiles, colorscheme, syntaxDefinitions, config.getGlobalOption("encoding"), !encodingExplicit);
8488
+ await catFiles(
8489
+ rawFiles,
8490
+ colorscheme,
8491
+ syntaxDefinitions,
8492
+ config.getGlobalOption("encoding"),
8493
+ !encodingExplicit,
8494
+ inputFiletype,
8495
+ );
8207
8496
  return;
8208
8497
  }
8209
8498
 
@@ -8268,6 +8557,7 @@ async function main() {
8268
8557
  runtime,
8269
8558
  jsPlugins,
8270
8559
  encodingExplicit,
8560
+ inputFiletype,
8271
8561
  allowUrl: flags.allowUrl,
8272
8562
  kittyMode: flags.kittyMode,
8273
8563
  };
@@ -8318,6 +8608,16 @@ async function main() {
8318
8608
  const buffers = await loadBuffers(files.map((file) =>
8319
8609
  isHttpUrl(file) ? file : resolve(file)
8320
8610
  ), command);
8611
+ if (demoPreparation?.useBundledModules) {
8612
+ const bundledDemoBuffer = buffers.find((buffer) =>
8613
+ buffer.path
8614
+ && !isHttpUrl(buffer.path)
8615
+ && resolve(buffer.path) === demoPreparation.path
8616
+ );
8617
+ if (bundledDemoBuffer) {
8618
+ bundledDemoBuffer._useBundledMdcuiModules = true;
8619
+ }
8620
+ }
8321
8621
 
8322
8622
  let historyPromise = Promise.resolve();
8323
8623
  if (config.getGlobalOption("savehistory") !== false) {
@@ -8725,9 +9025,12 @@ function getSelectionText(buf, selection) {
8725
9025
 
8726
9026
  function attachSyntax(buffer, context, path, text) {
8727
9027
  buffer._syntaxContext = context;
8728
- const def = detectBufferSyntax(context.syntaxDefinitions, path, text);
9028
+ const explicitFiletype = context.inputFiletype;
9029
+ const def = explicitFiletype == null
9030
+ ? detectBufferSyntax(context.syntaxDefinitions, path, text)
9031
+ : context.syntaxDefinitions?.find((candidate) => candidate.filetype === explicitFiletype) ?? null;
8729
9032
  buffer.syntaxDefinition = def;
8730
- buffer.filetype = def?.filetype ?? "unknown";
9033
+ buffer.filetype = explicitFiletype == null ? (def?.filetype ?? "unknown") : String(explicitFiletype);
8731
9034
  buffer.Settings.filetype = buffer.filetype;
8732
9035
  buffer.highlighter = def ? new Highlighter(def, context.syntaxDefinitions ?? []) : null;
8733
9036
  buffer._highlightCache = null;
@@ -8805,7 +9108,7 @@ function syncEditorSettings(config) {
8805
9108
  }
8806
9109
  }
8807
9110
 
8808
- async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true) {
9111
+ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAULT_SETTINGS.encoding, inferMdcui = true, filetype = null) {
8809
9112
  const targets = files.length > 0 ? files.map((f) => ({ path: f, stdin: false })) : [{ path: null, stdin: true }];
8810
9113
  for (const { path: filePath, stdin } of targets) {
8811
9114
  let content;
@@ -8836,7 +9139,7 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
8836
9139
  process.stdout.write(ansiContent ?? content);
8837
9140
  if (!(ansiContent ?? content).endsWith("\n")) process.stdout.write("\n");
8838
9141
  continue;
8839
- } else if (effectivePath && /\.md$/i.test(effectivePath)) {
9142
+ } else if (filetype == null && effectivePath && /\.md$/i.test(effectivePath)) {
8840
9143
  process.stdout.write(
8841
9144
  Bun.markdown.ansi(content,{
8842
9145
  hyperlinks:true
@@ -8845,11 +9148,13 @@ async function catFiles(files, colorscheme, syntaxDefinitions, encoding = DEFAUL
8845
9148
  continue;
8846
9149
  }
8847
9150
  const lines = normalizeBufferText(content).split("\n");
8848
- const def = detectSyntax(syntaxDefinitions, {
8849
- path: effectivePath ?? "",
8850
- firstLine: lines[0] ?? "",
8851
- lines: lines.slice(0, 50),
8852
- });
9151
+ const def = filetype == null
9152
+ ? detectSyntax(syntaxDefinitions, {
9153
+ path: effectivePath ?? "",
9154
+ firstLine: lines[0] ?? "",
9155
+ lines: lines.slice(0, 50),
9156
+ })
9157
+ : syntaxDefinitions.find((candidate) => candidate.filetype === filetype) ?? null;
8853
9158
  const highlighter = def ? new Highlighter(def, syntaxDefinitions) : null;
8854
9159
  if (!highlighter) {
8855
9160
  process.stdout.write(content);