jsmdcui 0.5.0 → 0.7.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/CHANGELOG.md +228 -0
- package/README.md +305 -25
- package/clean.sh +9 -0
- package/demo.resized.jpg +0 -0
- package/demos/image-processor.md +361 -0
- package/demos/image-processor.zh-TW.md +361 -0
- package/demos/select.md +63 -0
- package/demos/todo-zh.md +92 -0
- package/demos/todo.md +93 -0
- package/package.json +1 -1
- package/runmd.mjs +110 -17
- package/runtime/help/help.md +305 -25
- package/single-exe/packAssets.sh +1 -1
- package/src/cui/id-collision.mjs +151 -0
- package/src/cui/kitty-debug.mjs +25 -0
- package/src/cui/kitty-images.mjs +190 -0
- package/src/cui/rpc.mjs +227 -1
- package/src/cui/server.mjs +1 -1
- package/src/cui/task-checkbox.mjs +60 -0
- package/src/index.js +297 -23
- package/src/plugins/js-bridge.js +346 -4
- package/src/screen/screen.js +108 -1
- package/tests/cat-markdown.test.js +6 -5
- package/tests/demo.test.js +203 -4
- package/tests/heading-list-selector.test.js +346 -0
- package/tests/id-collision.test.js +147 -0
- package/tests/kitty-demo.md +184 -0
- package/tests/kitty-images.test.js +147 -0
- package/tests/task-checkbox.test.js +33 -0
- package/tests/wui-responsive-images.test.js +35 -0
- package/tests/wui.test.js +30 -0
- package/tui +4 -2
- package/wui +6 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createTuiSelector } from "../src/plugins/js-bridge.js";
|
|
6
|
+
|
|
7
|
+
const tui = join(import.meta.dir, "..", "tui");
|
|
8
|
+
const bunBin = Bun.which("bun") || process.argv0;
|
|
9
|
+
|
|
10
|
+
async function runCheck(markdown) {
|
|
11
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-check-"));
|
|
12
|
+
const file = join(dir, "app.md");
|
|
13
|
+
await writeFile(file, markdown);
|
|
14
|
+
const result = Bun.spawnSync([bunBin, tui, "--check", file], { stdout: "pipe", stderr: "pipe" });
|
|
15
|
+
await rm(dir, { recursive: true, force: true });
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("--check exits 0 and reports unique IDs", async () => {
|
|
20
|
+
const result = await runCheck("## Input Path\n\n```text#output-path\nvalue\n```\n");
|
|
21
|
+
expect(result.exitCode).toBe(0);
|
|
22
|
+
const raw = result.stdout.toString();
|
|
23
|
+
const output = Bun.stripANSI(raw);
|
|
24
|
+
expect(output).toContain("No ID collisions found");
|
|
25
|
+
expect(output).toContain("PASSED");
|
|
26
|
+
expect(raw).toContain(`${Bun.color("#00d75f", "ansi-16m")}\x1b[1mPASSED\x1b[0m`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("--check reports heading/fenced-block collisions with line details", async () => {
|
|
30
|
+
const result = await runCheck("## Write Status\n\n```text#write-status\nwaiting\n```\n");
|
|
31
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
32
|
+
expect(result.exitCode).toBe(1);
|
|
33
|
+
expect(output).toContain("FAIL — Found 1 colliding ID(s)");
|
|
34
|
+
expect(output).toContain("ID #write-status");
|
|
35
|
+
expect(output).toContain("Declarations: 2");
|
|
36
|
+
expect(output).toContain("Line 1");
|
|
37
|
+
expect(output).toContain("Type: heading");
|
|
38
|
+
expect(output).toContain("Source: ## Write Status");
|
|
39
|
+
expect(output).toContain("Line 3");
|
|
40
|
+
expect(output).toContain("Type: text fenced block");
|
|
41
|
+
expect(output).toContain("Source: ```text#write-status");
|
|
42
|
+
expect(result.stdout.toString()).toContain(`${Bun.color("#ff3030", "ansi-16m")}\x1b[1mFAILED\x1b[0m`);
|
|
43
|
+
expect(output.lastIndexOf("FAILED")).toBeGreaterThan(output.lastIndexOf("Suggested fix"));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("--check reports duplicate fenced-block IDs", async () => {
|
|
47
|
+
const result = await runCheck("```text#myid\na\n```\n\n```textarea#myid\nb\n```\n");
|
|
48
|
+
expect(result.exitCode).toBe(1);
|
|
49
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
50
|
+
expect(output).toContain("ID #myid");
|
|
51
|
+
expect(output).toContain("Declarations: 2");
|
|
52
|
+
expect(output).toContain("FAILED");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("--check includes IDs on arbitrary fenced-block tags", async () => {
|
|
56
|
+
const result = await runCheck("```hello#myid\nyou\n```\n\n# myid\n");
|
|
57
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
58
|
+
expect(result.exitCode).toBe(1);
|
|
59
|
+
expect(output).toContain("ID #myid");
|
|
60
|
+
expect(output).toContain("Declarations: 2");
|
|
61
|
+
expect(output).toContain("Type: hello fenced block");
|
|
62
|
+
expect(output).toContain("Fenced blocks: 1");
|
|
63
|
+
expect(output).toContain("FAILED");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("--check rejects duplicate Markdown heading IDs before Bun adds suffixes", async () => {
|
|
67
|
+
const result = await runCheck("# myid\n\n# myid\n");
|
|
68
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
69
|
+
expect(result.exitCode).toBe(1);
|
|
70
|
+
expect(output).toContain("ID #myid");
|
|
71
|
+
expect(output).toContain("Declarations: 2");
|
|
72
|
+
expect(output).toContain("Line 1");
|
|
73
|
+
expect(output).toContain("Line 3");
|
|
74
|
+
expect(output).toContain("FAILED");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const identityMatrix = [
|
|
78
|
+
{
|
|
79
|
+
name: "tag without class",
|
|
80
|
+
info: "text#myid",
|
|
81
|
+
selectable: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "tag with class",
|
|
85
|
+
info: "text#myid.field",
|
|
86
|
+
selectable: true,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "no tag and no class",
|
|
90
|
+
info: "#myid",
|
|
91
|
+
selectable: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "no tag with class",
|
|
95
|
+
info: "#myid.field",
|
|
96
|
+
selectable: false,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
for (const scenario of identityMatrix) {
|
|
101
|
+
test(`--check identity matrix: ${scenario.name}`, async () => {
|
|
102
|
+
const result = await runCheck(`\`\`\`${scenario.info}\nvalue\n\`\`\`\n\n# myid\n`);
|
|
103
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
104
|
+
expect(result.exitCode).toBe(scenario.selectable ? 1 : 0);
|
|
105
|
+
expect(output).toContain(`Fenced blocks: ${scenario.selectable ? 1 : 0}`);
|
|
106
|
+
if (scenario.selectable) {
|
|
107
|
+
expect(output).toContain("ID #myid");
|
|
108
|
+
expect(output).toContain("Declarations: 2");
|
|
109
|
+
expect(output).toContain("FAILED");
|
|
110
|
+
} else {
|
|
111
|
+
expect(output).toContain("Selectable IDs: 1");
|
|
112
|
+
expect(output).toContain("PASSED");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test("--check ignores tag and class differences when fenced-block IDs collide", async () => {
|
|
118
|
+
const result = await runCheck(
|
|
119
|
+
"```text#myid.left.primary\na\n```\n\n```json#myid.right.secondary\nb\n```\n",
|
|
120
|
+
);
|
|
121
|
+
const output = Bun.stripANSI(result.stdout.toString());
|
|
122
|
+
expect(result.exitCode).toBe(1);
|
|
123
|
+
expect(output).toContain("ID #myid");
|
|
124
|
+
expect(output).toContain("Declarations: 2");
|
|
125
|
+
expect(output).toContain("Type: text fenced block");
|
|
126
|
+
expect(output).toContain("Type: json fenced block");
|
|
127
|
+
expect(output).toContain("FAILED");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("TUI $ selector finds one ID across every tag/class query combination", () => {
|
|
131
|
+
const markdown = "```text#myid.field.primary\nvalue\n```\n";
|
|
132
|
+
const buffer = { lines: markdown.trimEnd().split("\n") };
|
|
133
|
+
const $ = createTuiSelector(() => buffer);
|
|
134
|
+
const selectors = [
|
|
135
|
+
"text#myid.field",
|
|
136
|
+
"text#myid",
|
|
137
|
+
"#myid.field",
|
|
138
|
+
"#myid",
|
|
139
|
+
];
|
|
140
|
+
for (const selector of selectors) expect($(selector).val()).toBe("value");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("--check requires exactly one file", () => {
|
|
144
|
+
const result = Bun.spawnSync([bunBin, tui, "--check"], { stdout: "pipe", stderr: "pipe" });
|
|
145
|
+
expect(result.exitCode).toBe(2);
|
|
146
|
+
expect(result.stderr.toString()).toContain("Usage: jsmdcui --check FILE.md");
|
|
147
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Kitty scrolling test
|
|
2
|
+
|
|
3
|
+
Prelude filler line 01
|
|
4
|
+
Prelude filler line 02
|
|
5
|
+
Prelude filler line 03
|
|
6
|
+
Prelude filler line 04
|
|
7
|
+
Prelude filler line 05
|
|
8
|
+
Prelude filler line 06
|
|
9
|
+
Prelude filler line 07
|
|
10
|
+
Prelude filler line 08
|
|
11
|
+
Prelude filler line 09
|
|
12
|
+
Prelude filler line 10
|
|
13
|
+
Prelude filler line 11
|
|
14
|
+
Prelude filler line 12
|
|
15
|
+
Prelude filler line 13
|
|
16
|
+
Prelude filler line 14
|
|
17
|
+
Prelude filler line 15
|
|
18
|
+
Prelude filler line 16
|
|
19
|
+
Prelude filler line 17
|
|
20
|
+
Prelude filler line 18
|
|
21
|
+
Prelude filler line 19
|
|
22
|
+
Prelude filler line 20
|
|
23
|
+
Prelude filler line 21
|
|
24
|
+
Prelude filler line 22
|
|
25
|
+
Prelude filler line 23
|
|
26
|
+
Prelude filler line 24
|
|
27
|
+
Prelude filler line 25
|
|
28
|
+
Prelude filler line 26
|
|
29
|
+
Prelude filler line 27
|
|
30
|
+
Prelude filler line 28
|
|
31
|
+
Prelude filler line 29
|
|
32
|
+
Prelude filler line 30
|
|
33
|
+
Prelude filler line 31
|
|
34
|
+
Prelude filler line 32
|
|
35
|
+
Prelude filler line 33
|
|
36
|
+
Prelude filler line 34
|
|
37
|
+
Prelude filler line 35
|
|
38
|
+
Prelude filler line 36
|
|
39
|
+
Prelude filler line 37
|
|
40
|
+
Prelude filler line 38
|
|
41
|
+
Prelude filler line 39
|
|
42
|
+
Prelude filler line 40
|
|
43
|
+
Prelude filler line 41
|
|
44
|
+
Prelude filler line 42
|
|
45
|
+
Prelude filler line 43
|
|
46
|
+
Prelude filler line 44
|
|
47
|
+
Prelude filler line 45
|
|
48
|
+
Prelude filler line 46
|
|
49
|
+
Prelude filler line 47
|
|
50
|
+
Prelude filler line 48
|
|
51
|
+
Prelude filler line 49
|
|
52
|
+
Prelude filler line 50
|
|
53
|
+
|
|
54
|
+
## Before image 01
|
|
55
|
+
Content before the image, line 01.
|
|
56
|
+
|
|
57
|
+
## Before image 02
|
|
58
|
+
Content before the image, line 02.
|
|
59
|
+
|
|
60
|
+
## Before image 03
|
|
61
|
+
Content before the image, line 03.
|
|
62
|
+
|
|
63
|
+
## Before image 04
|
|
64
|
+
Content before the image, line 04.
|
|
65
|
+
|
|
66
|
+
## Before image 05
|
|
67
|
+
Content before the image, line 05.
|
|
68
|
+
|
|
69
|
+
## Before image 06
|
|
70
|
+
Content before the image, line 06.
|
|
71
|
+
|
|
72
|
+
## Before image 07
|
|
73
|
+
Content before the image, line 07.
|
|
74
|
+
|
|
75
|
+
## Before image 08
|
|
76
|
+
Content before the image, line 08.
|
|
77
|
+
|
|
78
|
+
## Before image 09
|
|
79
|
+
Content before the image, line 09.
|
|
80
|
+
|
|
81
|
+
## Before image 10
|
|
82
|
+
Content before the image, line 10.
|
|
83
|
+
|
|
84
|
+
## Image anchor
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
|
|
88
|
+
## After image 01
|
|
89
|
+
Content after the image, line 01.
|
|
90
|
+
|
|
91
|
+
## After image 02
|
|
92
|
+
Content after the image, line 02.
|
|
93
|
+
|
|
94
|
+
## After image 03
|
|
95
|
+
Content after the image, line 03.
|
|
96
|
+
|
|
97
|
+
## After image 04
|
|
98
|
+
Content after the image, line 04.
|
|
99
|
+
|
|
100
|
+
## After image 05
|
|
101
|
+
Content after the image, line 05.
|
|
102
|
+
|
|
103
|
+
## After image 06
|
|
104
|
+
Content after the image, line 06.
|
|
105
|
+
|
|
106
|
+
## After image 07
|
|
107
|
+
Content after the image, line 07.
|
|
108
|
+
|
|
109
|
+
## After image 08
|
|
110
|
+
Content after the image, line 08.
|
|
111
|
+
|
|
112
|
+
## After image 09
|
|
113
|
+
Content after the image, line 09.
|
|
114
|
+
|
|
115
|
+
## After image 10
|
|
116
|
+
Content after the image, line 10.
|
|
117
|
+
|
|
118
|
+
## After image 11
|
|
119
|
+
Content after the image, line 11.
|
|
120
|
+
|
|
121
|
+
## After image 12
|
|
122
|
+
Content after the image, line 12.
|
|
123
|
+
|
|
124
|
+
## After image 13
|
|
125
|
+
Content after the image, line 13.
|
|
126
|
+
|
|
127
|
+
## After image 14
|
|
128
|
+
Content after the image, line 14.
|
|
129
|
+
|
|
130
|
+
## After image 15
|
|
131
|
+
Content after the image, line 15.
|
|
132
|
+
|
|
133
|
+

|
|
134
|
+
|
|
135
|
+
Epilogue filler line 01
|
|
136
|
+
Epilogue filler line 02
|
|
137
|
+
Epilogue filler line 03
|
|
138
|
+
Epilogue filler line 04
|
|
139
|
+
Epilogue filler line 05
|
|
140
|
+
Epilogue filler line 06
|
|
141
|
+
Epilogue filler line 07
|
|
142
|
+
Epilogue filler line 08
|
|
143
|
+
Epilogue filler line 09
|
|
144
|
+
Epilogue filler line 10
|
|
145
|
+
Epilogue filler line 11
|
|
146
|
+
Epilogue filler line 12
|
|
147
|
+
Epilogue filler line 13
|
|
148
|
+
Epilogue filler line 14
|
|
149
|
+
Epilogue filler line 15
|
|
150
|
+
Epilogue filler line 16
|
|
151
|
+
Epilogue filler line 17
|
|
152
|
+
Epilogue filler line 18
|
|
153
|
+
Epilogue filler line 19
|
|
154
|
+
Epilogue filler line 20
|
|
155
|
+
Epilogue filler line 21
|
|
156
|
+
Epilogue filler line 22
|
|
157
|
+
Epilogue filler line 23
|
|
158
|
+
Epilogue filler line 24
|
|
159
|
+
Epilogue filler line 25
|
|
160
|
+
Epilogue filler line 26
|
|
161
|
+
Epilogue filler line 27
|
|
162
|
+
Epilogue filler line 28
|
|
163
|
+
Epilogue filler line 29
|
|
164
|
+
Epilogue filler line 30
|
|
165
|
+
Epilogue filler line 31
|
|
166
|
+
Epilogue filler line 32
|
|
167
|
+
Epilogue filler line 33
|
|
168
|
+
Epilogue filler line 34
|
|
169
|
+
Epilogue filler line 35
|
|
170
|
+
Epilogue filler line 36
|
|
171
|
+
Epilogue filler line 37
|
|
172
|
+
Epilogue filler line 38
|
|
173
|
+
Epilogue filler line 39
|
|
174
|
+
Epilogue filler line 40
|
|
175
|
+
Epilogue filler line 41
|
|
176
|
+
Epilogue filler line 42
|
|
177
|
+
Epilogue filler line 43
|
|
178
|
+
Epilogue filler line 44
|
|
179
|
+
Epilogue filler line 45
|
|
180
|
+
Epilogue filler line 46
|
|
181
|
+
Epilogue filler line 47
|
|
182
|
+
Epilogue filler line 48
|
|
183
|
+
Epilogue filler line 49
|
|
184
|
+
Epilogue filler line 50
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { fitKittyImageToWidth, prepareKittyImages } from "../src/cui/kitty-images.mjs";
|
|
6
|
+
import { Screen } from "../src/screen/screen.js";
|
|
7
|
+
|
|
8
|
+
const ONE_PIXEL_PNG = Buffer.from(
|
|
9
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
|
10
|
+
"base64",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
test("Bun-rendered Markdown image links reserve rows and retain Kitty data", async () => {
|
|
14
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-kitty-"));
|
|
15
|
+
const markdownPath = join(dir, "app.md");
|
|
16
|
+
await writeFile(join(dir, "pixel.png"), ONE_PIXEL_PNG);
|
|
17
|
+
const ansi = Bun.markdown.ansi("before\n\n\n\nafter\n", {
|
|
18
|
+
hyperlinks: true,
|
|
19
|
+
columns: 40,
|
|
20
|
+
});
|
|
21
|
+
const result = await prepareKittyImages(ansi, markdownPath, 40);
|
|
22
|
+
|
|
23
|
+
expect(result.images).toHaveLength(1);
|
|
24
|
+
expect(result.images[0]).toMatchObject({ mime: "image/png", cols: 1, rows: 1 });
|
|
25
|
+
expect(result.images[0].data.equals(ONE_PIXEL_PNG)).toBe(true);
|
|
26
|
+
expect(Bun.stripANSI(result.rendered)).toContain("📷 pixel");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("remote Kitty images require allowUrl and use the configured HTTP byte fetcher", async () => {
|
|
30
|
+
const imageUrl = "https://example.test/assets/pixel.png";
|
|
31
|
+
const ansi = Bun.markdown.ansi(``, {
|
|
32
|
+
hyperlinks: true,
|
|
33
|
+
columns: 40,
|
|
34
|
+
});
|
|
35
|
+
const requests = [];
|
|
36
|
+
const fetchHttpBytes = async (url) => {
|
|
37
|
+
requests.push(url);
|
|
38
|
+
return ONE_PIXEL_PNG;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const blocked = await prepareKittyImages(ansi, "/tmp/app.md", 40, { fetchHttpBytes });
|
|
42
|
+
expect(blocked.images).toHaveLength(0);
|
|
43
|
+
expect(requests).toHaveLength(0);
|
|
44
|
+
|
|
45
|
+
const allowed = await prepareKittyImages(ansi, "/tmp/app.md", 40, {
|
|
46
|
+
allowUrl: true,
|
|
47
|
+
fetchHttpBytes,
|
|
48
|
+
});
|
|
49
|
+
expect(requests).toEqual([imageUrl]);
|
|
50
|
+
expect(allowed.images).toHaveLength(1);
|
|
51
|
+
expect(allowed.images[0].path).toBe(imageUrl);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("remote Markdown resolves relative Kitty image URLs against its source URL", async () => {
|
|
55
|
+
const ansi = Bun.markdown.ansi("", {
|
|
56
|
+
hyperlinks: true,
|
|
57
|
+
columns: 40,
|
|
58
|
+
});
|
|
59
|
+
const requests = [];
|
|
60
|
+
const result = await prepareKittyImages(
|
|
61
|
+
ansi,
|
|
62
|
+
"https://example.test/docs/guide/app.md",
|
|
63
|
+
40,
|
|
64
|
+
{
|
|
65
|
+
allowUrl: true,
|
|
66
|
+
fetchHttpBytes: async (url) => {
|
|
67
|
+
requests.push(url);
|
|
68
|
+
return ONE_PIXEL_PNG;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(requests).toEqual(["https://example.test/docs/images/pixel.png?size=1"]);
|
|
74
|
+
expect(result.images).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("Kitty image sizing subtracts the gutter width without depending on remaining rows", () => {
|
|
78
|
+
// A 100-column pane with a 5-column gutter has 95 usable image columns.
|
|
79
|
+
expect(fitKittyImageToWidth({ cols: 100, rows: 40 }, 95)).toEqual({ cols: 95, rows: 38 });
|
|
80
|
+
expect(fitKittyImageToWidth({ cols: 62, rows: 21 }, 95)).toEqual({ cols: 62, rows: 21 });
|
|
81
|
+
expect(fitKittyImageToWidth({ cols: 62, rows: 21 }, 40)).toEqual({ cols: 40, rows: 14 });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("Screen emits chunked Kitty placement at the requested cell and deletes only its IDs", () => {
|
|
85
|
+
const screen = new Screen({ mouse: false, kittyMode: "extended" });
|
|
86
|
+
const writes = [];
|
|
87
|
+
screen.write = (value) => writes.push(String(value));
|
|
88
|
+
screen.setKittyImages([{
|
|
89
|
+
id: 77,
|
|
90
|
+
x: 3,
|
|
91
|
+
y: 4,
|
|
92
|
+
cols: 5,
|
|
93
|
+
rows: 2,
|
|
94
|
+
sourceX: 0,
|
|
95
|
+
sourceY: 0,
|
|
96
|
+
sourceWidth: 1,
|
|
97
|
+
sourceHeight: 1,
|
|
98
|
+
mime: "image/png",
|
|
99
|
+
data: ONE_PIXEL_PNG,
|
|
100
|
+
}]);
|
|
101
|
+
screen.Show();
|
|
102
|
+
const first = writes.join("");
|
|
103
|
+
expect(first).toContain("\x1b[5;4H");
|
|
104
|
+
expect(first).toContain("\x1b_Ga=T,f=100,t=d,i=77,p=77,q=2,x=0,y=0,w=1,h=1,c=5,r=2,C=1,U=image/png,m=0;");
|
|
105
|
+
|
|
106
|
+
writes.length = 0;
|
|
107
|
+
screen.setKittyImages([]);
|
|
108
|
+
screen.Show();
|
|
109
|
+
expect(writes.join("")).toContain("\x1b_Ga=d,d=i,i=77,q=2;");
|
|
110
|
+
expect(writes.join("")).not.toContain("d=a");
|
|
111
|
+
|
|
112
|
+
writes.length = 0;
|
|
113
|
+
screen.setKittyImages([{
|
|
114
|
+
id: 77, x: 3, y: 2, cols: 5, rows: 2,
|
|
115
|
+
sourceX: 0, sourceY: 0, sourceWidth: 1, sourceHeight: 1,
|
|
116
|
+
mime: "image/png", data: ONE_PIXEL_PNG,
|
|
117
|
+
}]);
|
|
118
|
+
screen.Show();
|
|
119
|
+
const replaced = writes.join("");
|
|
120
|
+
expect(replaced).toContain("\x1b_Ga=p,i=77,p=77,q=2,x=0,y=0,w=1,h=1,c=5,r=2,C=1,U=image/png;");
|
|
121
|
+
expect(replaced).not.toContain("a=T");
|
|
122
|
+
expect(replaced).not.toContain(ONE_PIXEL_PNG.toString("base64"));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("Screen gates Kitty output by mode and compat mode omits the MIME U extension", () => {
|
|
126
|
+
const image = {
|
|
127
|
+
id: 88, x: 0, y: 0, cols: 1, rows: 1,
|
|
128
|
+
sourceX: 0, sourceY: 0, sourceWidth: 1, sourceHeight: 1,
|
|
129
|
+
mime: "image/jpeg", data: ONE_PIXEL_PNG,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const off = new Screen({ mouse: false });
|
|
133
|
+
const offWrites = [];
|
|
134
|
+
off.write = (value) => offWrites.push(String(value));
|
|
135
|
+
off.setKittyImages([image]);
|
|
136
|
+
off.Show();
|
|
137
|
+
expect(offWrites.join("")).not.toContain("\x1b_G");
|
|
138
|
+
|
|
139
|
+
const compat = new Screen({ mouse: false, kittyMode: "compat" });
|
|
140
|
+
const compatWrites = [];
|
|
141
|
+
compat.write = (value) => compatWrites.push(String(value));
|
|
142
|
+
compat.setKittyImages([image]);
|
|
143
|
+
compat.Show();
|
|
144
|
+
const output = compatWrites.join("");
|
|
145
|
+
expect(output).toContain("\x1b_Ga=T");
|
|
146
|
+
expect(output).not.toContain("U=image/jpeg");
|
|
147
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
toggleTaskCheckboxBeforeColumn,
|
|
4
|
+
updateAnsiTaskCheckbox,
|
|
5
|
+
} from "../src/cui/task-checkbox.mjs";
|
|
6
|
+
|
|
7
|
+
test("task checkbox toggle reports its position and new checked state", () => {
|
|
8
|
+
expect(toggleTaskCheckboxBeforeColumn(" ☐ todo", 4)).toEqual({
|
|
9
|
+
line: " ☒ todo",
|
|
10
|
+
toggled: true,
|
|
11
|
+
checkboxAt: 2,
|
|
12
|
+
checked: true,
|
|
13
|
+
});
|
|
14
|
+
expect(toggleTaskCheckboxBeforeColumn(" ☒ todo", 4)).toEqual({
|
|
15
|
+
line: " ☐ todo",
|
|
16
|
+
toggled: true,
|
|
17
|
+
checkboxAt: 2,
|
|
18
|
+
checked: false,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("task checkbox toggle updates Bun Markdown ANSI color with the glyph", () => {
|
|
23
|
+
const unchecked = " \x1b[2m☐ \x1b[0mtodo";
|
|
24
|
+
const checked = " \x1b[32m☒ \x1b[0mtodo";
|
|
25
|
+
expect(updateAnsiTaskCheckbox(unchecked, 2, true)).toBe(checked);
|
|
26
|
+
expect(updateAnsiTaskCheckbox(checked, 2, false)).toBe(unchecked);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("ANSI checkbox lookup ignores OSC 8 hyperlink metadata", () => {
|
|
30
|
+
const prefix = "\x1b]8;;https://example.test\x1b\\link\x1b]8;;\x1b\\ ";
|
|
31
|
+
expect(updateAnsiTaskCheckbox(`${prefix}\x1b[2m☐ \x1b[0mtodo`, 5, true))
|
|
32
|
+
.toBe(`${prefix}\x1b[32m☒ \x1b[0mtodo`);
|
|
33
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createWui } from "../runmd.mjs";
|
|
6
|
+
|
|
7
|
+
const temporaryDirectories = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const directory of temporaryDirectories.splice(0)) {
|
|
11
|
+
rmSync(directory, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("createWui constrains images to their content width", async () => {
|
|
16
|
+
const directory = mkdtempSync(join(tmpdir(), "jsmdcui-wui-image-"));
|
|
17
|
+
temporaryDirectories.push(directory);
|
|
18
|
+
const markdownPath = join(directory, "image.md");
|
|
19
|
+
const html = await createWui("", markdownPath);
|
|
20
|
+
|
|
21
|
+
expect(html).toContain("img {\n max-width: 100%;\n height: auto;\n}");
|
|
22
|
+
expect(html.match(/max-width: 100%/g)).toHaveLength(1);
|
|
23
|
+
expect(html.match(/image\.md\.front\.js/g)).toHaveLength(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createWui injects the image rule into an existing document head", async () => {
|
|
27
|
+
const directory = mkdtempSync(join(tmpdir(), "jsmdcui-wui-document-"));
|
|
28
|
+
temporaryDirectories.push(directory);
|
|
29
|
+
const markdownPath = join(directory, "document.md");
|
|
30
|
+
const source = "<!doctype html><html><head><title>x</title></head><body><img src=\"wide.png\"></body></html>";
|
|
31
|
+
const html = await createWui(source, markdownPath);
|
|
32
|
+
|
|
33
|
+
expect(html.indexOf("max-width: 100%")).toBeLessThan(html.indexOf("</head>"));
|
|
34
|
+
expect(html.match(/document\.md\.front\.js/g)).toHaveLength(1);
|
|
35
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readMarkdownInput } from "../runmd.mjs";
|
|
6
|
+
|
|
7
|
+
test("WUI writes the bundled testapp.md when it is missing", async () => {
|
|
8
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-"));
|
|
9
|
+
const mdpath = join(dir, "testapp.md");
|
|
10
|
+
try {
|
|
11
|
+
const source = await readMarkdownInput(mdpath);
|
|
12
|
+
expect(source).toContain("# jsmdcui");
|
|
13
|
+
expect(await readFile(mdpath, "utf8")).toBe(source);
|
|
14
|
+
} finally {
|
|
15
|
+
await rm(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("WUI preserves an existing testapp.md", async () => {
|
|
20
|
+
const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-existing-"));
|
|
21
|
+
const mdpath = join(dir, "testapp.md");
|
|
22
|
+
const existing = "# Keep my WUI demo\n";
|
|
23
|
+
try {
|
|
24
|
+
await writeFile(mdpath, existing);
|
|
25
|
+
expect(await readMarkdownInput(mdpath)).toBe(existing);
|
|
26
|
+
expect(await readFile(mdpath, "utf8")).toBe(existing);
|
|
27
|
+
} finally {
|
|
28
|
+
await rm(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
package/tui
CHANGED
package/wui
CHANGED