jsmdcui 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -94,17 +94,17 @@ import { accessSync, closeSync, constants, existsSync, openSync, readSync, readd
94
94
  import { mkdir } from "node:fs/promises";
95
95
  import { dirname, basename, join, resolve, sep } from "node:path";
96
96
  import { fileURLToPath, pathToFileURL } from "node:url";
97
- import process from "node:process";
98
97
  import { toggleTaskCheckboxBeforeColumn, updateAnsiTaskCheckbox } from "./cui/task-checkbox.mjs";
99
98
  import { fenceEventMap, inlineFenceEventCode } from "./cui/fence-events.mjs";
100
99
  import { checkMarkdownIdCollisions, formatMarkdownIdCheckAnsi } from "./cui/id-collision.mjs";
101
100
  import { fitKittyImageToWidth, prepareKittyImages } from "./cui/kitty-images.mjs";
101
+ import { kittyModeFromEnvironment } from "./cui/kitty-mode.mjs";
102
102
  import { logKittyPlacement } from "./cui/kitty-debug.mjs";
103
103
  import { Config } from "./config/config.js";
104
104
  import { defaultAllSettings, OPTION_CHOICES, LOCAL_SETTINGS } from "./config/defaults.js";
105
105
  import { cleanConfig } from "./config/clean.js";
106
106
  import { RuntimeRegistry, RTColorscheme, RTHelp } from "./runtime/registry.js";
107
- import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "./runtime/assets.js";
107
+ import { assetPath, buildHtmlBundleImageMap, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "../single-exe/assetsHelper.js";
108
108
  //import { PluginManager } from "./plugins/manager.js";
109
109
  import { JsPluginManager, buildMicroGlobal, buildTuiBlockIndex, findTuiBlockInIndex, insertTuiTextareaNewline, mergeTuiTextareaBackward, mergeTuiTextareaForward, runAction, listActions } from "./plugins/js-bridge.js";
110
110
  import { Colorscheme } from "./config/colorscheme.js";
@@ -124,16 +124,25 @@ import { writeBackup, removeBackup, applyBackup } from "./buffer/backup.js";
124
124
  let kittyImageMode = "off";
125
125
  let allowRemoteKittyImages = false;
126
126
  const remoteMarkdownSources = new Map();
127
+ let bundledImageMapPromise = null;
127
128
  import { isHex3Encoding, isMdcuiEncoding } from "./runtime/encodings.js";
128
129
  import { createInterface } from "node:readline/promises";
129
130
 
130
131
  import pkg from "../package.json" with { type: "json" };
131
132
  import { REPO_ROOT,IS_COMPILED, buildExecutable,buildEarlyExit,stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
133
+ import { expandBuildMdAliases } from "./build-args.js";
132
134
 
133
135
 
134
136
  const __filename = fileURLToPath(import.meta.url);
135
137
  const __dirname = dirname(__filename);
136
138
 
139
+ function getBundledImageMap() {
140
+ if (!IS_COMPILED || !global.MDCUI_MAIN) return null;
141
+ bundledImageMapPromise ??= import(global.MDCUI_MAIN + "-server.js")
142
+ .then((module) => buildHtmlBundleImageMap(module.homepage));
143
+ return bundledImageMapPromise;
144
+ }
145
+
137
146
 
138
147
  if(!globalThis.Bun)
139
148
  {
@@ -379,6 +388,7 @@ async function renderMdcui(markdown, width = process.stdout.columns || 80, mdpat
379
388
  : await prepareKittyImages(tui, resolvedImageBasePath, width, {
380
389
  allowUrl: allowRemoteKittyImages,
381
390
  kittyMode: kittyImageMode,
391
+ getBundledImageMap,
382
392
  });
383
393
  return {
384
394
  rendered: prepared.rendered,
@@ -1118,7 +1128,7 @@ function parseArgs(argv) {
1118
1128
  cdpMaze: false,
1119
1129
  cdpPort: 0,
1120
1130
  cdpAddress: "",
1121
- kittyMode: "off",
1131
+ kittyMode: kittyModeFromEnvironment(process.env.JSMDCUI_KITTY_MODE),
1122
1132
  settings: new Map(),
1123
1133
  };
1124
1134
  const files = [];
@@ -1268,6 +1278,9 @@ Modes:
1268
1278
  Display Markdown images with Kitty graphics and the jsgotty MIME extension
1269
1279
  --kitty-compat
1270
1280
  Display Markdown images with Kitty graphics without the non-standard MIME U field
1281
+
1282
+ JSMDCUI_KITTY_MODE=off|compat|extended
1283
+ Set the Kitty image mode without command-line flags; explicit Kitty flags override it
1271
1284
 
1272
1285
  --edit
1273
1286
  Open files as editable UTF-8 text
@@ -1349,14 +1362,20 @@ Remote Markdown:
1349
1362
  Download HTTP(S) Markdown and, with Kitty mode, its HTTP(S) images; allow its code to run
1350
1363
 
1351
1364
  Experimental single-exe:
1365
+ --build-md-exe <file.md> [BUN_ARGS...]
1366
+ Build with <file.md> embedded as global.MDCUI_MAIN & exit
1367
+ --build-md-for <platform> <file.md> [BUN_ARGS...]
1368
+ Build for <platform> with <file.md> embedded & exit
1369
+
1352
1370
  --build-exe [BUN_ARGS...]
1353
1371
  Build a Bun single-file executable & exit
1354
1372
  --build-for <target> [BUN_ARGS...]
1355
1373
  Build a Bun single-file executable for target & exit
1356
1374
 
1357
1375
  [BUN_ARGS...]
1358
- --define MDCUI_XXX=1
1359
- More info in --readme
1376
+ --define MDCUI_OVERWRITE_DEMO=1
1377
+ --define global.MDCUI_MAIN=myapp.md
1378
+ More info in --readme
1360
1379
  https://bun.com/docs/bundler/executables
1361
1380
  `
1362
1381
  ].join("\n");
@@ -5024,7 +5043,7 @@ class App {
5024
5043
  const name = buf?.name ?? "No name";
5025
5044
  const filename = /^[^\s"'\\]+$/.test(name) ? name : JSON.stringify(name);
5026
5045
  this.openCommandMode(`save ${filename}`);
5027
- break;
5046
+ break; //"
5028
5047
  }
5029
5048
  case "row":
5030
5049
  if (isTerm) {
@@ -8257,6 +8276,8 @@ function stringDefineFromArgv(argv, name) {
8257
8276
  async function main() {
8258
8277
  addCheckpoint("Argument Parsing");
8259
8278
 
8279
+ expandBuildMdAliases(process.argv);
8280
+ stringifyNonPrimitiveDefineValues(process.argv, "process.env.JSMDCUI_KITTY_MODE");
8260
8281
  stringifyNonPrimitiveDefineValues(process.argv, "global.MDCUI_MAIN");
8261
8282
  const argvMdcuiMain = stringDefineFromArgv(
8262
8283
  process.argv,
package/src/lua/engine.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { readInternalAssetBytes } from "../runtime/assets.js";
2
+ import { readInternalAssetBytes } from "../../single-exe/assetsHelper.js";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { REPO_ROOT } from "../../single-exe/compiled.js";
@@ -3,7 +3,7 @@ import { readdir } from "node:fs/promises";
3
3
  import { dirname, join, basename, extname } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { tmpdir } from "node:os";
6
- import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetBytes } from "../runtime/assets.js";
6
+ import { assetPath, hasInternalAssets, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetBytes } from "../../single-exe/assetsHelper.js";
7
7
  import { isMdcuiEncoding } from "../runtime/encodings.js";
8
8
  import { newMessage, newMessageAtLine, MTError, MTWarning, MTInfo } from "../buffer/message.js";
9
9
  import { Loc } from "../buffer/loc.js";
@@ -4,7 +4,7 @@ import { basename, dirname, extname, join, sep } from "node:path";
4
4
  import { fetchHttp, downloadFile } from "../platform/commands.js";
5
5
  import { extractAndStrip } from "../platform/archive.js";
6
6
  import { createLuaEngine } from "../lua/engine.js";
7
- import { assetPath, hasInternalAssets, internalAssetSource, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "../runtime/assets.js";
7
+ import { assetPath, hasInternalAssets, internalAssetSource, listInternalAssetDirs, listInternalAssetPaths, readInternalAssetText } from "../../single-exe/assetsHelper.js";
8
8
  import { execCommand, runBackgroundShell, runCommand } from "../shell/shell.js";
9
9
  import { Loc } from "../buffer/loc.js";
10
10
  import { BTDefault, BTHelp, BTInfo, BTLog, BTRaw, BTScratch, byteOffset, BufferCore } from "../buffer/buffer.js";
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import { basename, extname, join } from "node:path";
4
- import { assetPath, hasInternalAssets, listInternalAssetPaths, readInternalAssetBytes } from "./assets.js";
4
+ import { assetPath, hasInternalAssets, listInternalAssetPaths, readInternalAssetBytes } from "../../single-exe/assetsHelper.js";
5
5
 
6
6
  export const RTColorscheme = 0;
7
7
  export const RTSyntax = 1;
@@ -0,0 +1,116 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ buildHtmlBundleImageMap,
7
+ canonicalHtmlBundleImageHref,
8
+ decodeHtmlAttribute,
9
+ findHtmlBundleAsset,
10
+ findHtmlBundleImageAsset,
11
+ htmlBundleImageAssetPath,
12
+ } from "../single-exe/assetsHelper.js";
13
+
14
+ const homepage = {
15
+ files: [
16
+ {
17
+ input: "../../images/pixel.png",
18
+ path: "/$bunfs/root/pixel-abc123.png",
19
+ loader: "file",
20
+ headers: { "content-type": "image/png" },
21
+ },
22
+ {
23
+ input: "scripts/app.js",
24
+ path: "/$bunfs/root/app-test.js",
25
+ loader: "file",
26
+ headers: { "content-type": "text/javascript" },
27
+ },
28
+ ],
29
+ };
30
+
31
+ test("HTML bundle asset lookup matches the compiled public asset path", () => {
32
+ expect(findHtmlBundleAsset(homepage, "/pixel-abc123.png"))
33
+ .toBe(homepage.files[0]);
34
+ expect(htmlBundleImageAssetPath(homepage, "/pixel-abc123.png?version=2"))
35
+ .toBe("/$bunfs/root/pixel-abc123.png");
36
+ });
37
+
38
+ test("HTML bundle image lookup rejects non-images and unavailable development bundles", () => {
39
+ expect(findHtmlBundleImageAsset(homepage, "/app-test.js")).toBeNull();
40
+ expect(findHtmlBundleAsset({}, "/pixel-abc123.png")).toBeNull();
41
+ expect(findHtmlBundleAsset(null, "/pixel-abc123.png")).toBeNull();
42
+ });
43
+
44
+ test("HTML bundle image map joins preserved Markdown hrefs to compiled bunfs assets", async () => {
45
+ const directory = await mkdtemp(join(tmpdir(), "jsmdcui-html-assets-"));
46
+ const htmlPath = join(directory, "app.html");
47
+ await writeFile(htmlPath, [
48
+ '<img src="/dms-ajghf5bd.jpg" data-mdcui-src="jsmdcui/single-exe/dms.jpg">',
49
+ '<img src="/unicode-xyz.png" data-mdcui-src="%E5%9C%96%E7%89%87/%E6%B8%AC%E8%A9%A6%20%E5%9C%96.png">',
50
+ '<img src="/query-qwe.png" data-mdcui-src="./query.png?x=1&amp;y=2#preview">',
51
+ ].join("\n"));
52
+ const compiledHomepage = {
53
+ index: htmlPath,
54
+ files: [
55
+ {
56
+ input: "dms.jpg",
57
+ path: "/$bunfs/root/dms-ajghf5bd.jpg",
58
+ loader: "file",
59
+ headers: { "content-type": "image/jpeg" },
60
+ },
61
+ {
62
+ input: "../../unicode.png",
63
+ path: "/$bunfs/root/unicode-xyz.png",
64
+ loader: "file",
65
+ headers: { "content-type": "image/png" },
66
+ },
67
+ {
68
+ input: "../query.png",
69
+ path: "/$bunfs/root/query-qwe.png",
70
+ loader: "file",
71
+ headers: { "content-type": "image/png" },
72
+ },
73
+ ],
74
+ };
75
+
76
+ const images = await buildHtmlBundleImageMap(compiledHomepage);
77
+ expect(images.get("jsmdcui/single-exe/dms.jpg"))
78
+ .toBe("/$bunfs/root/dms-ajghf5bd.jpg");
79
+ expect(images.get("圖片/測試 圖.png"))
80
+ .toBe("/$bunfs/root/unicode-xyz.png");
81
+ expect(images.get("./query.png?x=1&y=2#preview"))
82
+ .toBe("/$bunfs/root/query-qwe.png");
83
+ });
84
+
85
+ test("HTML bundle href canonicalization decodes serialization without resolving paths", () => {
86
+ expect(decodeHtmlAttribute("./a.png?x=1&amp;y=2")).toBe("./a.png?x=1&y=2");
87
+ expect(canonicalHtmlBundleImageHref("%E5%9C%96%E7%89%87/a%20b.png"))
88
+ .toBe("圖片/a b.png");
89
+ expect(canonicalHtmlBundleImageHref("./a.png?x=1&amp;y=2", { htmlAttribute: true }))
90
+ .toBe("./a.png?x=1&y=2");
91
+ });
92
+
93
+ test("HTML bundle image map accepts a custom source attribute", async () => {
94
+ const directory = await mkdtemp(join(tmpdir(), "jsmdcui-html-assets-custom-"));
95
+ const htmlPath = join(directory, "custom.html");
96
+ await writeFile(
97
+ htmlPath,
98
+ '<img src="/photo-custom123.jpg" data-original-image="./photos/a.jpg">',
99
+ );
100
+ const customHomepage = {
101
+ index: htmlPath,
102
+ files: [{
103
+ input: "../../photos/a.jpg",
104
+ path: "/$bunfs/root/photo-custom123.jpg",
105
+ loader: "file",
106
+ headers: { "content-type": "image/jpeg" },
107
+ }],
108
+ };
109
+
110
+ const images = await buildHtmlBundleImageMap(
111
+ customHomepage,
112
+ "data-original-image",
113
+ );
114
+ expect(images.get("./photos/a.jpg"))
115
+ .toBe("/$bunfs/root/photo-custom123.jpg");
116
+ });
@@ -1,4 +1,5 @@
1
1
  import { expect, test } from "bun:test";
2
+ import { expandBuildMdAliases } from "../src/build-args.js";
2
3
  import {
3
4
  isCompiledBinary as isSingleExeCompiled,
4
5
  stringifyNonPrimitiveDefineValues,
@@ -10,6 +11,69 @@ test("single-exe recognizes Bun compiled virtual paths", () => {
10
11
  expect(isSingleExeCompiled(["bun", "/project/src/index.js"])).toBe(false);
11
12
  });
12
13
 
14
+ test("--build-md-exe expands before the existing build flow", () => {
15
+ const argv = [
16
+ "bun",
17
+ "src/index.js",
18
+ "--build-md-exe",
19
+ "../中文工具.md",
20
+ "--sourcemap",
21
+ ];
22
+ expect(expandBuildMdAliases(argv)).toBe(true);
23
+ expect(argv).toEqual([
24
+ "bun",
25
+ "src/index.js",
26
+ "--build-exe",
27
+ "--define",
28
+ "global.MDCUI_MAIN=../中文工具.md",
29
+ "--sourcemap",
30
+ ]);
31
+ expect(expandBuildMdAliases(argv)).toBe(false);
32
+ });
33
+
34
+ test("--build-md-exe requires a Markdown path", () => {
35
+ expect(() => expandBuildMdAliases(["bun", "index.js", "--build-md-exe"]))
36
+ .toThrow("Missing Markdown path for --build-md-exe");
37
+ expect(() => expandBuildMdAliases([
38
+ "bun",
39
+ "index.js",
40
+ "--build-md-exe",
41
+ "--sourcemap",
42
+ ])).toThrow("Missing Markdown path for --build-md-exe");
43
+ });
44
+
45
+ test("--build-md-for expands before the existing cross-build flow", () => {
46
+ const argv = [
47
+ "bun",
48
+ "src/index.js",
49
+ "--build-md-for",
50
+ "bun-linux-x64",
51
+ "../中文工具.md",
52
+ "--sourcemap",
53
+ ];
54
+ expect(expandBuildMdAliases(argv)).toBe(true);
55
+ expect(argv).toEqual([
56
+ "bun",
57
+ "src/index.js",
58
+ "--build-for",
59
+ "bun-linux-x64",
60
+ "--define",
61
+ "global.MDCUI_MAIN=../中文工具.md",
62
+ "--sourcemap",
63
+ ]);
64
+ });
65
+
66
+ test("--build-md-for requires a platform and Markdown path", () => {
67
+ expect(() => expandBuildMdAliases(["bun", "index.js", "--build-md-for"]))
68
+ .toThrow("Missing platform for --build-md-for");
69
+ expect(() => expandBuildMdAliases([
70
+ "bun",
71
+ "index.js",
72
+ "--build-md-for",
73
+ "bun-linux-x64",
74
+ ])).toThrow("Missing Markdown path for --build-md-for");
75
+ });
76
+
13
77
  test("non-primitive MDCUI_MAIN define values become strings", () => {
14
78
  const split = [
15
79
  "bun",
@@ -26,6 +26,28 @@ test("Bun-rendered Markdown image links reserve rows and retain Kitty data", asy
26
26
  expect(Bun.stripANSI(result.rendered)).toContain("📷 pixel");
27
27
  });
28
28
 
29
+ test("Kitty images prefer lazily supplied compiled HTML bundle assets", async () => {
30
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-kitty-bundled-"));
31
+ const bundledPath = join(dir, "pixel-bundled.png");
32
+ await writeFile(bundledPath, ONE_PIXEL_PNG);
33
+ const ansi = Bun.markdown.ansi("![pixel](./images/pixel.png?version=2#preview)", {
34
+ hyperlinks: true,
35
+ columns: 40,
36
+ });
37
+ let imageMapRequests = 0;
38
+ const result = await prepareKittyImages(ansi, join(dir, "missing-app.md"), 40, {
39
+ getBundledImageMap: async () => {
40
+ imageMapRequests++;
41
+ return new Map([["./images/pixel.png?version=2#preview", bundledPath]]);
42
+ },
43
+ });
44
+
45
+ expect(imageMapRequests).toBe(1);
46
+ expect(result.images).toHaveLength(1);
47
+ expect(result.images[0].path).toBe(bundledPath);
48
+ expect(result.images[0].data.equals(ONE_PIXEL_PNG)).toBe(true);
49
+ });
50
+
29
51
  test("Kitty compat converts compressed images to standard PNG payloads", async () => {
30
52
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-kitty-compat-"));
31
53
  const markdownPath = join(dir, "app.md");
@@ -0,0 +1,14 @@
1
+ import { expect, test } from "bun:test";
2
+ import { kittyModeFromEnvironment } from "../src/cui/kitty-mode.mjs";
3
+
4
+ test("Kitty mode accepts supported environment values case-insensitively", () => {
5
+ expect(kittyModeFromEnvironment("compat")).toBe("compat");
6
+ expect(kittyModeFromEnvironment(" EXTENDED ")).toBe("extended");
7
+ expect(kittyModeFromEnvironment("off")).toBe("off");
8
+ });
9
+
10
+ test("Kitty mode defaults invalid or missing environment values to off", () => {
11
+ expect(kittyModeFromEnvironment()).toBe("off");
12
+ expect(kittyModeFromEnvironment("yes")).toBe("off");
13
+ expect(kittyModeFromEnvironment("")).toBe("off");
14
+ });
@@ -19,6 +19,7 @@ test("createWui constrains images to their content width", async () => {
19
19
  const html = await createWui("![wide](wide.png)", markdownPath);
20
20
 
21
21
  expect(html).toContain("img {\n max-width: 100%;\n height: auto;\n}");
22
+ expect(html).toContain('src="wide.png" alt="wide" data-mdcui-src="wide.png"');
22
23
  expect(html.match(/max-width: 100%/g)).toHaveLength(1);
23
24
  expect(html.match(/image\.md\.front\.js/g)).toHaveLength(1);
24
25
  });
@@ -31,6 +32,7 @@ test("createWui injects the image rule into an existing document head", async ()
31
32
  const html = await createWui(source, markdownPath);
32
33
 
33
34
  expect(html.indexOf("max-width: 100%")).toBeLessThan(html.indexOf("</head>"));
35
+ expect(html).toContain('src="wide.png" data-mdcui-src="wide.png"');
34
36
  expect(html.match(/document\.md\.front\.js/g)).toHaveLength(1);
35
37
  });
36
38