jsmdcui 0.10.1 → 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/CHANGELOG.md +56 -0
- package/README.md +69 -29
- package/package.json +1 -1
- package/runmd.mjs +51 -7
- package/runtime/help/help.md +69 -29
- package/runtime/jsplugins/example/example.js +3 -1
- package/single-exe/README.md +82 -25
- package/single-exe/README.md-rpc.js +623 -0
- package/single-exe/README.md-server.js +127 -0
- package/single-exe/README.md.back.js +2 -0
- package/single-exe/README.md.front.js +27 -0
- package/single-exe/README.md.html +325 -0
- package/single-exe/compiled.js +52 -0
- package/src/index.js +227 -20
- package/src/plugins/js-bridge.js +5 -0
- package/tests/compiled-runtime.test.js +68 -1
- package/tests/demo.test.js +136 -1
- package/tests/js-plugin-prompts.test.js +30 -0
- package/tests/wui-responsive-images.test.js +21 -2
- package/tests/wui.test.js +31 -0
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);
|
|
@@ -186,6 +186,7 @@ const MDCUI_DEFAULT_DEMO_ENABLED = typeof MDCUI_DEFAULT_DEMO !== "undefined";
|
|
|
186
186
|
const MDCUI_DEFAULT_DEMO_WUI_ENABLED = typeof MDCUI_DEFAULT_DEMO_WUI !== "undefined";
|
|
187
187
|
const MDCUI_OVERWRITE_DEMO_ENABLED = typeof MDCUI_OVERWRITE_DEMO !== "undefined";
|
|
188
188
|
let defaultEdit = false;
|
|
189
|
+
let noRuntimeArgs = false;
|
|
189
190
|
const decoder = new TextDecoder();
|
|
190
191
|
let _activeTtyStream = null; // set in App.start() for use by the global error handler
|
|
191
192
|
|
|
@@ -1173,9 +1174,15 @@ function parseArgs(argv) {
|
|
|
1173
1174
|
}
|
|
1174
1175
|
else if (arg.startsWith("--demo-")) {
|
|
1175
1176
|
const name = arg.slice("--demo-".length);
|
|
1176
|
-
flags.demo =
|
|
1177
|
+
flags.demo = isValidDemoName(name)
|
|
1177
1178
|
? { option: arg, filename: `${name}.md`, asset: `demos/${name}.md` }
|
|
1178
|
-
: {
|
|
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
|
+
};
|
|
1179
1186
|
}
|
|
1180
1187
|
else if (arg === "--allow-url") flags.allowUrl = true;
|
|
1181
1188
|
else if (arg === "--kitty") flags.kittyMode = "extended";
|
|
@@ -1206,6 +1213,17 @@ function parseArgs(argv) {
|
|
|
1206
1213
|
return { flags, files };
|
|
1207
1214
|
}
|
|
1208
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
|
+
|
|
1209
1227
|
function usage() {
|
|
1210
1228
|
return [
|
|
1211
1229
|
`Usage:
|
|
@@ -1337,7 +1355,7 @@ Experimental single-exe:
|
|
|
1337
1355
|
Build a Bun single-file executable for target & exit
|
|
1338
1356
|
|
|
1339
1357
|
[BUN_ARGS...]
|
|
1340
|
-
--define MDCUI_XXX=
|
|
1358
|
+
--define MDCUI_XXX=1
|
|
1341
1359
|
More info in --readme
|
|
1342
1360
|
https://bun.com/docs/bundler/executables
|
|
1343
1361
|
`
|
|
@@ -1400,6 +1418,7 @@ class BufferModel {
|
|
|
1400
1418
|
this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(sourceText ?? tuiSourceText ?? text) : new Map();
|
|
1401
1419
|
this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
|
|
1402
1420
|
this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
|
|
1421
|
+
this._useBundledMdcuiModules = false;
|
|
1403
1422
|
this.cursor = { x: 0, y: 0 };
|
|
1404
1423
|
this.scroll = { x: 0, y: 0, row: 0 };
|
|
1405
1424
|
this._modified = false;
|
|
@@ -4540,7 +4559,10 @@ class App {
|
|
|
4540
4559
|
) return false;
|
|
4541
4560
|
|
|
4542
4561
|
const frontPath = `${buf.path}.front.js`;
|
|
4543
|
-
|
|
4562
|
+
const useBundledMdcuiModules = Boolean(
|
|
4563
|
+
global.MDCUI_MAIN && buf._useBundledMdcuiModules,
|
|
4564
|
+
);
|
|
4565
|
+
if (!useBundledMdcuiModules && !existsSync(frontPath)) return false;
|
|
4544
4566
|
try {
|
|
4545
4567
|
const selection = globalThis.$?.(`${declaration.tag}#${id}`);
|
|
4546
4568
|
const target = {
|
|
@@ -4555,8 +4577,12 @@ class App {
|
|
|
4555
4577
|
});
|
|
4556
4578
|
const event = tuiKeyEventForFront(inputEvent, target, eventName);
|
|
4557
4579
|
const [{ evalFront }, frontMod] = await Promise.all([
|
|
4558
|
-
|
|
4559
|
-
|
|
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)),
|
|
4560
4586
|
]);
|
|
4561
4587
|
const result = await evalFront(frontMod, code, { event });
|
|
4562
4588
|
if (result && typeof result === "object" && result.ok === false) {
|
|
@@ -4591,9 +4617,16 @@ class App {
|
|
|
4591
4617
|
{ // defaultCallback
|
|
4592
4618
|
if (payload.link && /^javascript:/i.test(payload.link) && buf?.path && !isHttpUrl(buf.path)) {
|
|
4593
4619
|
try {
|
|
4620
|
+
const useBundledMdcuiModules = Boolean(
|
|
4621
|
+
global.MDCUI_MAIN && buf._useBundledMdcuiModules,
|
|
4622
|
+
);
|
|
4594
4623
|
const [{ evalFront }, frontMod] = await Promise.all([
|
|
4595
|
-
|
|
4596
|
-
|
|
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`)),
|
|
4597
4630
|
]);
|
|
4598
4631
|
const result = await evalFront(frontMod, payload.link);
|
|
4599
4632
|
if (result && typeof result === "object" && result.ok === false) {
|
|
@@ -5481,10 +5514,18 @@ class App {
|
|
|
5481
5514
|
const frontPath = buffer.path && !isHttpUrl(buffer.path)
|
|
5482
5515
|
? `${buffer.path}.front.js`
|
|
5483
5516
|
: "";
|
|
5484
|
-
|
|
5517
|
+
const useBundledMdcuiModules = Boolean(
|
|
5518
|
+
global.MDCUI_MAIN && buffer._useBundledMdcuiModules,
|
|
5519
|
+
);
|
|
5520
|
+
if (
|
|
5521
|
+
!useBundledMdcuiModules
|
|
5522
|
+
&& (!frontPath || !existsSync(frontPath))
|
|
5523
|
+
) return;
|
|
5485
5524
|
|
|
5486
5525
|
try {
|
|
5487
|
-
const frontMod =
|
|
5526
|
+
const frontMod = useBundledMdcuiModules
|
|
5527
|
+
? await import(global.MDCUI_MAIN + ".front.js")
|
|
5528
|
+
: await import(localModuleUrl(frontPath));
|
|
5488
5529
|
if (typeof frontMod.onMdcuiExit !== "function") return;
|
|
5489
5530
|
await frontMod.onMdcuiExit({
|
|
5490
5531
|
reason,
|
|
@@ -7021,6 +7062,11 @@ function makeBufferAdapter(buffer) {
|
|
|
7021
7062
|
get Name() { return buffer.Name ?? buffer.name ?? ""; },
|
|
7022
7063
|
get Modified() { return buffer.modified ?? false; },
|
|
7023
7064
|
get Settings() { return buffer.Settings; },
|
|
7065
|
+
get MdcuiModuleSource() {
|
|
7066
|
+
return global.MDCUI_MAIN && buffer._useBundledMdcuiModules
|
|
7067
|
+
? "embedded"
|
|
7068
|
+
: "external";
|
|
7069
|
+
},
|
|
7024
7070
|
Line: (...args) => buffer.Line(Number(lastArg(args))),
|
|
7025
7071
|
Insert: (...args) => insertAtLoc(buffer, decodeLoc(args.at(-2)), String(args.at(-1))),
|
|
7026
7072
|
Replace: (...args) => replaceAtLocs(buffer, decodeLoc(args.at(-3)), decodeLoc(args.at(-2)), String(args.at(-1))),
|
|
@@ -8137,11 +8183,11 @@ function printDemoList() {
|
|
|
8137
8183
|
}
|
|
8138
8184
|
|
|
8139
8185
|
async function prepareDemo(flags, rawFiles) {
|
|
8140
|
-
if (!flags.demo) return true;
|
|
8186
|
+
if (!flags.demo) return { ok: true };
|
|
8141
8187
|
if (flags.demo.error) {
|
|
8142
8188
|
console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
|
|
8143
8189
|
process.exitCode = 2;
|
|
8144
|
-
return false;
|
|
8190
|
+
return { ok: false };
|
|
8145
8191
|
}
|
|
8146
8192
|
let demoSource;
|
|
8147
8193
|
try {
|
|
@@ -8149,29 +8195,169 @@ async function prepareDemo(flags, rawFiles) {
|
|
|
8149
8195
|
} catch {
|
|
8150
8196
|
console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
|
|
8151
8197
|
process.exitCode = 2;
|
|
8152
|
-
return false;
|
|
8198
|
+
return { ok: false };
|
|
8153
8199
|
}
|
|
8200
|
+
|
|
8154
8201
|
const demoPath = resolve(flags.demo.filename);
|
|
8155
|
-
|
|
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) {
|
|
8156
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
|
+
);
|
|
8157
8228
|
}
|
|
8229
|
+
|
|
8158
8230
|
rawFiles.splice(0, rawFiles.length, demoPath);
|
|
8159
|
-
return
|
|
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 "";
|
|
8160
8255
|
}
|
|
8161
8256
|
|
|
8162
8257
|
async function main() {
|
|
8163
8258
|
addCheckpoint("Argument Parsing");
|
|
8164
|
-
|
|
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
|
+
}
|
|
8283
|
+
const runmd = await import("../runmd.mjs");
|
|
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
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
|
|
8165
8332
|
await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
|
|
8166
8333
|
defaultEdit = await Bun.file(join(REPO_ROOT, "src", "MDCUI_DEFAULT_EDIT")).exists();
|
|
8167
8334
|
|
|
8168
8335
|
const runtimeArgs = process.argv.slice(2);
|
|
8169
|
-
|
|
8336
|
+
noRuntimeArgs = runtimeArgs.length === 0;
|
|
8170
8337
|
if (MDCUI_DEFAULT_EDIT_ENABLED) runtimeArgs.unshift("--edit");
|
|
8171
8338
|
if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_ENABLED) runtimeArgs.push("--demo");
|
|
8172
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
|
+
}
|
|
8173
8358
|
if (MDCUI_OVERWRITE_DEMO_ENABLED) runtimeArgs.push("--overwrite-demo");
|
|
8174
8359
|
const { flags, files: rawFiles } = parseArgs(runtimeArgs);
|
|
8360
|
+
let demoPreparation = null;
|
|
8175
8361
|
kittyImageMode = flags.kittyMode;
|
|
8176
8362
|
allowRemoteKittyImages = flags.allowUrl;
|
|
8177
8363
|
|
|
@@ -8195,6 +8381,14 @@ async function main() {
|
|
|
8195
8381
|
console.log(" MDCUI_DEFAULT_DEMO:", MDCUI_DEFAULT_DEMO_ENABLED ? "enabled" : "disabled");
|
|
8196
8382
|
console.log(" MDCUI_DEFAULT_DEMO_WUI:", MDCUI_DEFAULT_DEMO_WUI_ENABLED ? "enabled" : "disabled");
|
|
8197
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
|
+
);
|
|
8198
8392
|
const clipboard = new ClipboardManager();
|
|
8199
8393
|
let osc52Available = false;
|
|
8200
8394
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -8210,12 +8404,14 @@ async function main() {
|
|
|
8210
8404
|
return;
|
|
8211
8405
|
}
|
|
8212
8406
|
if (flags.wui) {
|
|
8213
|
-
|
|
8407
|
+
demoPreparation = await prepareDemo(flags, rawFiles);
|
|
8408
|
+
if (!demoPreparation.ok) return;
|
|
8214
8409
|
const runmd = await import("../runmd.mjs");
|
|
8215
8410
|
await runmd.main(30, {
|
|
8216
8411
|
mdpath: rawFiles[0] ?? null,
|
|
8217
8412
|
overwriteDemo: flags.overwriteDemo && rawFiles.length === 0,
|
|
8218
8413
|
printUi: flags.printUi,
|
|
8414
|
+
useBundledMdcuiServer: demoPreparation.useBundledModules,
|
|
8219
8415
|
});
|
|
8220
8416
|
return;
|
|
8221
8417
|
}
|
|
@@ -8263,7 +8459,8 @@ async function main() {
|
|
|
8263
8459
|
return;
|
|
8264
8460
|
}
|
|
8265
8461
|
if (flags.demo) {
|
|
8266
|
-
|
|
8462
|
+
demoPreparation = await prepareDemo(flags, rawFiles);
|
|
8463
|
+
if (!demoPreparation.ok) return;
|
|
8267
8464
|
}
|
|
8268
8465
|
if (flags.options) {
|
|
8269
8466
|
for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
|
|
@@ -8411,6 +8608,16 @@ async function main() {
|
|
|
8411
8608
|
const buffers = await loadBuffers(files.map((file) =>
|
|
8412
8609
|
isHttpUrl(file) ? file : resolve(file)
|
|
8413
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
|
+
}
|
|
8414
8621
|
|
|
8415
8622
|
let historyPromise = Promise.resolve();
|
|
8416
8623
|
if (config.getGlobalOption("savehistory") !== false) {
|
package/src/plugins/js-bridge.js
CHANGED
|
@@ -1622,6 +1622,11 @@ function _makeBufAPI(buffer) {
|
|
|
1622
1622
|
get Type() { return buffer.Type; },
|
|
1623
1623
|
get Settings() { return buffer.Settings; },
|
|
1624
1624
|
get Modified() { return buffer.modified; },
|
|
1625
|
+
get MdcuiModuleSource() {
|
|
1626
|
+
return global.MDCUI_MAIN && buffer._useBundledMdcuiModules
|
|
1627
|
+
? "embedded"
|
|
1628
|
+
: "external";
|
|
1629
|
+
},
|
|
1625
1630
|
|
|
1626
1631
|
Line: (n) => buffer.Line(n),
|
|
1627
1632
|
LinesNum: () => buffer.LinesNum(),
|
|
@@ -1,8 +1,75 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
isCompiledBinary as isSingleExeCompiled,
|
|
4
|
+
stringifyNonPrimitiveDefineValues,
|
|
5
|
+
} from "../single-exe/compiled.js";
|
|
3
6
|
|
|
4
7
|
test("single-exe recognizes Bun compiled virtual paths", () => {
|
|
5
8
|
expect(isSingleExeCompiled(["bun", "/$bunfs/root/app.js"])).toBe(true);
|
|
6
9
|
expect(isSingleExeCompiled(["bun", "B:/~BUN/root/app.js"])).toBe(true);
|
|
7
10
|
expect(isSingleExeCompiled(["bun", "/project/src/index.js"])).toBe(false);
|
|
8
11
|
});
|
|
12
|
+
|
|
13
|
+
test("non-primitive MDCUI_MAIN define values become strings", () => {
|
|
14
|
+
const split = [
|
|
15
|
+
"bun",
|
|
16
|
+
"src/index.js",
|
|
17
|
+
"--build-exe",
|
|
18
|
+
"--define",
|
|
19
|
+
"global.MDCUI_MAIN=../中文=工具.md",
|
|
20
|
+
];
|
|
21
|
+
expect(
|
|
22
|
+
stringifyNonPrimitiveDefineValues(split, "global.MDCUI_MAIN"),
|
|
23
|
+
).toBe(split);
|
|
24
|
+
expect(split.at(-1)).toBe(
|
|
25
|
+
'global.MDCUI_MAIN="../中文=工具.md"',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const inline = [
|
|
29
|
+
"--define=global.MDCUI_MAIN=../m.md",
|
|
30
|
+
"--define",
|
|
31
|
+
"OTHER=../other.md",
|
|
32
|
+
"--define",
|
|
33
|
+
"global.MDCUI_MAIN_BASE=m.md",
|
|
34
|
+
];
|
|
35
|
+
stringifyNonPrimitiveDefineValues(inline, "global.MDCUI_MAIN");
|
|
36
|
+
expect(inline).toEqual([
|
|
37
|
+
'--define=global.MDCUI_MAIN="../m.md"',
|
|
38
|
+
"--define",
|
|
39
|
+
"OTHER=../other.md",
|
|
40
|
+
"--define",
|
|
41
|
+
"global.MDCUI_MAIN_BASE=m.md",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
for (const source of ["", "[]", "{}", "makePath()"]) {
|
|
45
|
+
const argv = ["--define", `global.MDCUI_MAIN=${source}`];
|
|
46
|
+
stringifyNonPrimitiveDefineValues(argv, "global.MDCUI_MAIN");
|
|
47
|
+
expect(argv[1]).toBe(
|
|
48
|
+
`global.MDCUI_MAIN=${JSON.stringify(source)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("primitive MDCUI_MAIN define values keep their type", () => {
|
|
54
|
+
const cases = new Map([
|
|
55
|
+
['"../m.md"', '"../m.md"'],
|
|
56
|
+
["'../m.md'", '"../m.md"'],
|
|
57
|
+
["true", "true"],
|
|
58
|
+
["false", "false"],
|
|
59
|
+
["null", "null"],
|
|
60
|
+
["undefined", "undefined"],
|
|
61
|
+
["0", "0"],
|
|
62
|
+
["-1.5", "-1.5"],
|
|
63
|
+
["NaN", "NaN"],
|
|
64
|
+
["Infinity", "Infinity"],
|
|
65
|
+
["0xff", "0xff"],
|
|
66
|
+
["0b10", "0b10"],
|
|
67
|
+
["1_000", "1_000"],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
for (const [source, normalized] of cases) {
|
|
71
|
+
const argv = ["--define", `global.MDCUI_MAIN=${source}`];
|
|
72
|
+
stringifyNonPrimitiveDefineValues(argv, "global.MDCUI_MAIN");
|
|
73
|
+
expect(argv[1]).toBe(`global.MDCUI_MAIN=${normalized}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
package/tests/demo.test.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readMarkdownInput } from "../runmd.mjs";
|
|
6
6
|
|
|
7
7
|
const tui = join(import.meta.dir, "..", "tui");
|
|
8
|
+
const indexEntry = join(import.meta.dir, "..", "src", "index.js");
|
|
9
|
+
const demosDirectory = join(import.meta.dir, "..", "demos");
|
|
8
10
|
const bunBin = Bun.which("bun") || process.argv0;
|
|
9
11
|
|
|
10
12
|
test("--help describes the non-overwriting demo behavior", () => {
|
|
@@ -40,6 +42,139 @@ test("--demo-list lists root and automatically discovered demos", () => {
|
|
|
40
42
|
expect(output).toContain("--demo-imgtool --demo-image-processor");
|
|
41
43
|
});
|
|
42
44
|
|
|
45
|
+
test("--version lists every MDCUI build define", () => {
|
|
46
|
+
const result = Bun.spawnSync([bunBin, tui, "--version"], {
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.exitCode).toBe(0);
|
|
52
|
+
const output = result.stdout.toString();
|
|
53
|
+
expect(output).toContain("MDCUI_DEFAULT_EDIT:");
|
|
54
|
+
expect(output).toContain("MDCUI_DEFAULT_DEMO:");
|
|
55
|
+
expect(output).toContain("MDCUI_DEFAULT_DEMO_WUI:");
|
|
56
|
+
expect(output).toContain("MDCUI_OVERWRITE_DEMO:");
|
|
57
|
+
expect(output).toContain("global.MDCUI_MAIN:");
|
|
58
|
+
expect(output).toContain("global.MDCUI_MAIN_BASE:");
|
|
59
|
+
|
|
60
|
+
const presenceResult = Bun.spawnSync(
|
|
61
|
+
[
|
|
62
|
+
bunBin,
|
|
63
|
+
"--define",
|
|
64
|
+
"MDCUI_DEFAULT_DEMO_WUI=0",
|
|
65
|
+
indexEntry,
|
|
66
|
+
"--version",
|
|
67
|
+
],
|
|
68
|
+
{
|
|
69
|
+
stdout: "pipe",
|
|
70
|
+
stderr: "pipe",
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
expect(presenceResult.exitCode).toBe(0);
|
|
74
|
+
expect(presenceResult.stdout.toString()).toContain(
|
|
75
|
+
"MDCUI_DEFAULT_DEMO_WUI: enabled",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("--demo names allow Chinese but reject whitespace", () => {
|
|
80
|
+
const unicodeResult = Bun.spawnSync(
|
|
81
|
+
[bunBin, tui, "--demo-不存在的程式", "-cat", "-encoding", "utf8"],
|
|
82
|
+
{
|
|
83
|
+
stdout: "pipe",
|
|
84
|
+
stderr: "pipe",
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
expect(unicodeResult.exitCode).toBe(2);
|
|
88
|
+
expect(unicodeResult.stderr.toString()).toContain(
|
|
89
|
+
"Unknown demo --demo-不存在的程式",
|
|
90
|
+
);
|
|
91
|
+
expect(unicodeResult.stderr.toString()).not.toContain("Invalid demo option");
|
|
92
|
+
|
|
93
|
+
const whitespaceResult = Bun.spawnSync(
|
|
94
|
+
[bunBin, tui, "--demo-中文 程式", "-cat", "-encoding", "utf8"],
|
|
95
|
+
{
|
|
96
|
+
stdout: "pipe",
|
|
97
|
+
stderr: "pipe",
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
expect(whitespaceResult.exitCode).toBe(2);
|
|
101
|
+
expect(whitespaceResult.stderr.toString()).toContain("Invalid demo option");
|
|
102
|
+
expect(whitespaceResult.stderr.toString()).toContain(
|
|
103
|
+
"whitespace is not allowed",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("Unicode MDCUI_MAIN overwrite starts the embedded WUI server", async () => {
|
|
108
|
+
const directory = await mkdtemp(join(tmpdir(), "jsmdcui-main-unicode-"));
|
|
109
|
+
const sourceDirectory = join(directory, "source");
|
|
110
|
+
const workDirectory = join(directory, "work");
|
|
111
|
+
const demoName = `中文工具-${crypto.randomUUID()}`;
|
|
112
|
+
const markdownName = `${demoName}.md`;
|
|
113
|
+
const sourcePath = join(sourceDirectory, markdownName);
|
|
114
|
+
const workPath = join(workDirectory, markdownName);
|
|
115
|
+
const bundledDemoPath = join(demosDirectory, markdownName);
|
|
116
|
+
const preloadPath = join(directory, "mock-serve.mjs");
|
|
117
|
+
const markdown = `# 中文工具
|
|
118
|
+
|
|
119
|
+
\`\`\`js front
|
|
120
|
+
export async function run() {
|
|
121
|
+
return await rpc.answer();
|
|
122
|
+
}
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
\`\`\`js back
|
|
126
|
+
import { helper } from "./helper.js";
|
|
127
|
+
export function answer() {
|
|
128
|
+
return helper();
|
|
129
|
+
}
|
|
130
|
+
\`\`\`
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await mkdir(sourceDirectory);
|
|
135
|
+
await mkdir(workDirectory);
|
|
136
|
+
await writeFile(sourcePath, markdown);
|
|
137
|
+
await writeFile(
|
|
138
|
+
join(sourceDirectory, "helper.js"),
|
|
139
|
+
'export function helper() { return "ok"; }\n',
|
|
140
|
+
);
|
|
141
|
+
await writeFile(workPath, "# stale file with a different byte length\n");
|
|
142
|
+
await writeFile(
|
|
143
|
+
preloadPath,
|
|
144
|
+
`Bun.serve = () => ({ port: 34567, stop() {} });\n`,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const result = Bun.spawnSync(
|
|
148
|
+
[
|
|
149
|
+
bunBin,
|
|
150
|
+
"--preload",
|
|
151
|
+
preloadPath,
|
|
152
|
+
"--define",
|
|
153
|
+
`global.MDCUI_MAIN=${JSON.stringify(sourcePath)}`,
|
|
154
|
+
indexEntry,
|
|
155
|
+
"--wui",
|
|
156
|
+
`--demo-${demoName}`,
|
|
157
|
+
"--overwrite-demo",
|
|
158
|
+
],
|
|
159
|
+
{
|
|
160
|
+
cwd: workDirectory,
|
|
161
|
+
stdout: "pipe",
|
|
162
|
+
stderr: "pipe",
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(result.exitCode).toBe(0);
|
|
167
|
+
expect(await readFile(workPath, "utf8")).toBe(markdown);
|
|
168
|
+
const stderr = Bun.stripANSI(result.stderr.toString());
|
|
169
|
+
expect(stderr).toContain("[mdcui] Starting embedded WUI server:");
|
|
170
|
+
expect(stderr).toContain(sourcePath);
|
|
171
|
+
expect(stderr).not.toContain("Cannot find module");
|
|
172
|
+
} finally {
|
|
173
|
+
await rm(bundledDemoPath, { force: true });
|
|
174
|
+
await rm(directory, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
43
178
|
test("--export-cdp-maze writes and overwrites the bundled solver", async () => {
|
|
44
179
|
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-export-cdp-maze-"));
|
|
45
180
|
const outputPath = join(dir, "cdp-maze.js");
|
|
@@ -44,3 +44,33 @@ test("micro prompt APIs delegate and return synchronously", () => {
|
|
|
44
44
|
else globalThis.$ = previousSelector;
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
test("current buffer exposes its actual MDCUI module source", () => {
|
|
49
|
+
const previousMicro = globalThis.micro;
|
|
50
|
+
const previousSelector = globalThis.$;
|
|
51
|
+
const previousMain = global.MDCUI_MAIN;
|
|
52
|
+
const buffer = {
|
|
53
|
+
path: "/tmp/app.md",
|
|
54
|
+
_useBundledMdcuiModules: true,
|
|
55
|
+
};
|
|
56
|
+
const app = { buffer };
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
global.MDCUI_MAIN = "/build/app.md";
|
|
60
|
+
const micro = buildMicroGlobal({ _app: app, _ctx: null, on() {} });
|
|
61
|
+
|
|
62
|
+
expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("embedded");
|
|
63
|
+
buffer._useBundledMdcuiModules = false;
|
|
64
|
+
expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("external");
|
|
65
|
+
delete global.MDCUI_MAIN;
|
|
66
|
+
buffer._useBundledMdcuiModules = true;
|
|
67
|
+
expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("external");
|
|
68
|
+
} finally {
|
|
69
|
+
if (previousMain === undefined) delete global.MDCUI_MAIN;
|
|
70
|
+
else global.MDCUI_MAIN = previousMain;
|
|
71
|
+
if (previousMicro === undefined) delete globalThis.micro;
|
|
72
|
+
else globalThis.micro = previousMicro;
|
|
73
|
+
if (previousSelector === undefined) delete globalThis.$;
|
|
74
|
+
else globalThis.$ = previousSelector;
|
|
75
|
+
}
|
|
76
|
+
});
|