jsmdcui 0.6.3 → 0.8.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +216 -33
  3. package/clean.sh +9 -0
  4. package/demo.resized.jpg +0 -0
  5. package/{image-processor.md → demos/image-processor.md} +1 -0
  6. package/{image-processor.zh-TW.md → demos/image-processor.zh-TW.md} +1 -0
  7. package/demos/maze.md +144 -0
  8. package/{select.md → demos/select.md} +2 -0
  9. package/demos/todo-zh.md +190 -0
  10. package/demos/todo.md +191 -0
  11. package/edit +9 -0
  12. package/package.json +1 -1
  13. package/runmd.mjs +77 -16
  14. package/runtime/help/help.md +216 -33
  15. package/runtime/jsplugins/cdp/cdp.js +5 -2
  16. package/runtime/jsplugins/chapter/chapter.js +4 -2
  17. package/runtime/jsplugins/example/example.js +10 -7
  18. package/runtime/syntax/markdown.yaml +1 -0
  19. package/single-exe/packAssets.sh +1 -1
  20. package/src/cui/fence-events.mjs +106 -0
  21. package/src/cui/id-collision.mjs +10 -28
  22. package/src/cui/kitty-debug.mjs +25 -0
  23. package/src/cui/kitty-images.mjs +200 -0
  24. package/src/cui/rpc.mjs +169 -10
  25. package/src/cui/server.mjs +14 -3
  26. package/src/cui/task-checkbox.mjs +44 -0
  27. package/src/index.js +615 -101
  28. package/src/platform/terminal.js +5 -0
  29. package/src/plugins/js-bridge.js +354 -21
  30. package/src/screen/screen.js +108 -1
  31. package/tests/cat-markdown.test.js +6 -5
  32. package/tests/demo.test.js +99 -9
  33. package/tests/fence-events.test.js +405 -0
  34. package/tests/heading-list-selector.test.js +346 -0
  35. package/tests/id-collision.test.js +14 -2
  36. package/tests/js-plugin-prompts.test.js +46 -0
  37. package/tests/key-event.md +10 -0
  38. package/tests/kitty-demo.md +184 -0
  39. package/tests/kitty-images.test.js +169 -0
  40. package/tests/platform-terminal.test.js +8 -0
  41. package/tests/task-checkbox.test.js +33 -0
  42. package/tests/textarea.md +8 -0
  43. package/tests/wui-responsive-images.test.js +35 -0
  44. package/tests/wui.test.js +16 -1
  45. package/tui +4 -2
  46. package/wui +6 -2
@@ -0,0 +1,346 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createWebDollar } from "../src/cui/rpc.mjs";
3
+ import { createTuiSelector } from "../src/plugins/js-bridge.js";
4
+
5
+ function tuiSelector(markdown) {
6
+ const ansi = String(Bun.markdown.ansi(markdown, { hyperlinks: true, columns: 80 }));
7
+ const buffer = {
8
+ lines: Bun.stripANSI(ansi).split("\n"),
9
+ _mdcuiTuiSourceText: markdown,
10
+ _mdcuiAnsiText: ansi,
11
+ cursor: { x: 0, y: 0 },
12
+ ensureCursor() {},
13
+ invalidateHighlightFrom() {},
14
+ };
15
+ return { $: createTuiSelector(() => buffer), buffer };
16
+ }
17
+
18
+ const markdown = `## Features
19
+
20
+ - [x] Search
21
+ - [ ] Notifications
22
+ - [x] Nested notification option
23
+ - [x] Offline
24
+
25
+ Paragraph between lists.
26
+
27
+ - [x] Later list item
28
+
29
+ ## Next
30
+ `;
31
+
32
+ describe("TUI heading task-list Array methods", () => {
33
+ test("push and unshift accept multiple items and return the new direct length", () => {
34
+ const { $ } = tuiSelector(markdown);
35
+ const features = $("#features");
36
+
37
+ expect(features.push("Export", { value: "Sync", checked: true })).toBe(5);
38
+ expect(features.val()).toEqual(["Search", "Offline", "Sync"]);
39
+ expect(features.unshift("First", { label: "Pinned", checked: true })).toBe(7);
40
+ expect(features.val()).toEqual(["Pinned", "Search", "Offline", "Sync"]);
41
+ });
42
+
43
+ test("pop and shift return removed labels and remove nested children with the parent", () => {
44
+ const { $, buffer } = tuiSelector(markdown);
45
+ const features = $("#features");
46
+
47
+ expect(features.shift()).toBe("Search");
48
+ expect(buffer.lines.some((line) => line.includes("Search"))).toBe(false);
49
+ expect(features.shift()).toBe("Notifications");
50
+ expect(buffer.lines.some((line) => line.includes("Nested notification option"))).toBe(false);
51
+ expect(features.pop()).toBe("Offline");
52
+ expect(features.pop()).toBeUndefined();
53
+ });
54
+
55
+ test("an emptied list keeps its insertion point for later pushes", () => {
56
+ const { $ } = tuiSelector("## Features\n\n- [ ] Only\n\n## Next\n");
57
+ const features = $("#features");
58
+
59
+ expect(features.pop()).toBe("Only");
60
+ expect(features.pop()).toBeUndefined();
61
+ expect(features.push({ value: "Again", checked: true })).toBe(1);
62
+ expect(features.val()).toEqual(["Again"]);
63
+ });
64
+
65
+ test("val and mutations stop at the first rendered task list", () => {
66
+ const { $ } = tuiSelector(markdown);
67
+ const features = $("#features");
68
+
69
+ expect(features.val()).toEqual(["Search", "Offline"]);
70
+ expect(features.pop()).toBe("Offline");
71
+ expect(features.val()).toEqual(["Search"]);
72
+ });
73
+
74
+ test("a loose task list remains one list across indented item content", () => {
75
+ const { $ } = tuiSelector(`## Features
76
+
77
+ - [x] First
78
+
79
+ More detail
80
+
81
+ - [x] Second
82
+
83
+ Paragraph after the list.
84
+
85
+ - [x] Later list
86
+ `);
87
+ const features = $("#features");
88
+
89
+ expect(features.val()).toEqual(["First", "Second"]);
90
+ expect(features.pop()).toBe("Second");
91
+ expect(features.pop()).toBe("First");
92
+ expect(features.pop()).toBeUndefined();
93
+ });
94
+
95
+ test("splice follows Array indexing, insertion, deletion, and return semantics", () => {
96
+ const { $, buffer } = tuiSelector(markdown);
97
+ const features = $("#features");
98
+
99
+ expect(features.splice()).toEqual([]);
100
+ expect(features.splice(1, 1, { value: "Replacement", checked: true }, "Extra"))
101
+ .toEqual(["Notifications"]);
102
+ expect(buffer.lines.some((line) => line.includes("Nested notification option"))).toBe(false);
103
+ expect(features.val()).toEqual(["Search", "Replacement", "Offline"]);
104
+ expect(features.splice(-1)).toEqual(["Offline"]);
105
+ expect(features.splice(1, undefined, "Inserted")).toEqual([]);
106
+ expect(features.splice(0, Infinity)).toEqual(["Search", "Inserted", "Replacement", "Extra"]);
107
+ expect(features.val()).toEqual([]);
108
+ expect(features.splice(0, 0, { label: "Again", checked: true })).toEqual([]);
109
+ expect(features.val()).toEqual(["Again"]);
110
+ });
111
+
112
+ test("slice returns read-only item snapshots with checked state", () => {
113
+ const { $ } = tuiSelector(markdown);
114
+ const features = $("#features");
115
+
116
+ const result = features.slice(0, 2);
117
+ expect(result).toEqual([
118
+ { value: "Search", checked: true },
119
+ { value: "Notifications", checked: false },
120
+ ]);
121
+ result[0].value = "Changed snapshot";
122
+ result[0].checked = false;
123
+ expect(features.slice(-1)).toEqual([{ value: "Offline", checked: true }]);
124
+ expect(features.val()).toEqual(["Search", "Offline"]);
125
+ });
126
+
127
+ test("a heading without a task list cannot create one implicitly", () => {
128
+ const { $ } = tuiSelector("## Empty\n\nParagraph.\n");
129
+ expect($("#empty").push("No target")).toBe(0);
130
+ expect($("#empty").unshift("No target")).toBe(0);
131
+ expect($("#empty").pop()).toBeUndefined();
132
+ expect($("#empty").shift()).toBeUndefined();
133
+ });
134
+ });
135
+
136
+ class TestText {
137
+ constructor(text, ownerDocument) {
138
+ this.textContent = String(text);
139
+ this.ownerDocument = ownerDocument;
140
+ this.parentElement = null;
141
+ }
142
+ }
143
+
144
+ class TestElement {
145
+ constructor(tagName, ownerDocument) {
146
+ this.tagName = tagName.toUpperCase();
147
+ this.ownerDocument = ownerDocument;
148
+ this.parentElement = null;
149
+ this.childNodes = [];
150
+ this.classList = new Set();
151
+ this.id = "";
152
+ }
153
+
154
+ get children() {
155
+ return this.childNodes.filter((node) => node instanceof TestElement);
156
+ }
157
+
158
+ get firstChild() {
159
+ return this.childNodes[0] ?? null;
160
+ }
161
+
162
+ get nextElementSibling() {
163
+ const siblings = this.parentElement?.children ?? [];
164
+ const index = siblings.indexOf(this);
165
+ return index >= 0 ? siblings[index + 1] ?? null : null;
166
+ }
167
+
168
+ get textContent() {
169
+ return this.childNodes.map((node) => node.textContent).join("");
170
+ }
171
+
172
+ append(...nodes) {
173
+ for (const node of nodes) {
174
+ node.parentElement = this;
175
+ this.childNodes.push(node);
176
+ }
177
+ }
178
+
179
+ insertBefore(node, before) {
180
+ node.parentElement = this;
181
+ const index = before == null ? -1 : this.childNodes.indexOf(before);
182
+ if (index < 0) this.childNodes.push(node);
183
+ else this.childNodes.splice(index, 0, node);
184
+ }
185
+
186
+ remove() {
187
+ const siblings = this.parentElement?.childNodes;
188
+ const index = siblings?.indexOf(this) ?? -1;
189
+ if (index >= 0) siblings.splice(index, 1);
190
+ this.parentElement = null;
191
+ }
192
+
193
+ matches(selector) {
194
+ if (selector === "ul, ol") return this.tagName === "UL" || this.tagName === "OL";
195
+ if (selector === "li.task-list-item")
196
+ return this.tagName === "LI" && this.classList.has("task-list-item");
197
+ return false;
198
+ }
199
+
200
+ closest(selector) {
201
+ for (let node = this; node; node = node.parentElement) {
202
+ if (selector === "label" && node.tagName === "LABEL") return node;
203
+ if (selector === "li.task-list-item" && node.matches(selector)) return node;
204
+ }
205
+ return null;
206
+ }
207
+
208
+ querySelector(selector) {
209
+ return this.querySelectorAll(selector)[0] ?? null;
210
+ }
211
+
212
+ querySelectorAll(selector) {
213
+ const matches = [];
214
+ const visit = (node) => {
215
+ for (const child of node.children) {
216
+ if (selector === 'input[type="checkbox"]' &&
217
+ child.tagName === "INPUT" && child.type === "checkbox") matches.push(child);
218
+ else if (selector === "ul, ol" && child.matches(selector)) matches.push(child);
219
+ else if (selector === "li.task-list-item" && child.matches(selector)) matches.push(child);
220
+ visit(child);
221
+ }
222
+ };
223
+ visit(this);
224
+ return matches;
225
+ }
226
+ }
227
+
228
+ class TestDocument {
229
+ constructor() {
230
+ this.root = new TestElement("main", this);
231
+ }
232
+
233
+ createElement(tagName) {
234
+ return new TestElement(tagName, this);
235
+ }
236
+
237
+ createTextNode(text) {
238
+ return new TestText(text, this);
239
+ }
240
+
241
+ querySelector(selector) {
242
+ if (!selector.startsWith("#")) return null;
243
+ const id = selector.slice(1);
244
+ return this.querySelectorAll("heading").find((element) => element.id === id) ?? null;
245
+ }
246
+
247
+ querySelectorAll(selector) {
248
+ if (selector === "[data-mdcui-tag]" || selector === "pre > code") return [];
249
+ if (selector === "heading") {
250
+ const headings = [];
251
+ const visit = (node) => {
252
+ for (const child of node.children) {
253
+ if (/^H[1-6]$/.test(child.tagName)) headings.push(child);
254
+ visit(child);
255
+ }
256
+ };
257
+ visit(this.root);
258
+ return headings;
259
+ }
260
+ return [];
261
+ }
262
+ }
263
+
264
+ function appendWebItem(documentObject, list, value, checked = false) {
265
+ const item = documentObject.createElement("li");
266
+ item.classList.add("task-list-item");
267
+ const label = documentObject.createElement("label");
268
+ const checkbox = documentObject.createElement("input");
269
+ checkbox.type = "checkbox";
270
+ checkbox.checked = checked;
271
+ label.append(checkbox, documentObject.createTextNode(value));
272
+ item.append(label);
273
+ list.append(item);
274
+ return item;
275
+ }
276
+
277
+ function webSelector() {
278
+ const documentObject = new TestDocument();
279
+ const section = documentObject.createElement("section");
280
+ const heading = documentObject.createElement("h2");
281
+ heading.id = "features";
282
+ const list = documentObject.createElement("ul");
283
+ appendWebItem(documentObject, list, "Search", true);
284
+ appendWebItem(documentObject, list, "Notifications");
285
+ appendWebItem(documentObject, list, "Offline", true);
286
+ section.append(heading, list);
287
+ documentObject.root.append(section);
288
+ return { $: createWebDollar(documentObject), list };
289
+ }
290
+
291
+ describe("WUI heading task-list Array methods", () => {
292
+ test("uses the first list and follows Array return values", () => {
293
+ const { $ } = webSelector();
294
+ const features = $("#features");
295
+
296
+ expect(features.push("Export", { value: "Sync", checked: true })).toBe(5);
297
+ expect(features.val()).toEqual(["Search", "Offline", "Sync"]);
298
+ expect(features.unshift("First", "Second")).toBe(7);
299
+ expect(features.shift()).toBe("First");
300
+ expect(features.pop()).toBe("Sync");
301
+ expect(features.val()).toEqual(["Search", "Offline"]);
302
+ });
303
+
304
+ test("an empty first list remains the target", () => {
305
+ const { $, list } = webSelector();
306
+ const features = $("#features");
307
+
308
+ expect(features.shift()).toBe("Search");
309
+ expect(features.shift()).toBe("Notifications");
310
+ expect(features.shift()).toBe("Offline");
311
+ expect(features.shift()).toBeUndefined();
312
+ expect(features.val()).toEqual([]);
313
+ expect(features.push({ label: "Again", checked: true })).toBe(1);
314
+ expect(list.children).toHaveLength(1);
315
+ expect(features.val()).toEqual(["Again"]);
316
+ });
317
+
318
+ test("splice follows Array indexing and returns removed labels", () => {
319
+ const { $ } = webSelector();
320
+ const features = $("#features");
321
+
322
+ expect(features.splice()).toEqual([]);
323
+ expect(features.splice(1, 1, { value: "Replacement", checked: true }, "Extra"))
324
+ .toEqual(["Notifications"]);
325
+ expect(features.val()).toEqual(["Search", "Replacement", "Offline"]);
326
+ expect(features.splice(-1)).toEqual(["Offline"]);
327
+ expect(features.splice(0, Infinity)).toEqual(["Search", "Replacement", "Extra"]);
328
+ expect(features.splice(0, 0, { value: "Again", checked: true })).toEqual([]);
329
+ expect(features.val()).toEqual(["Again"]);
330
+ });
331
+
332
+ test("slice returns read-only item snapshots with checked state", () => {
333
+ const { $ } = webSelector();
334
+ const features = $("#features");
335
+
336
+ const result = features.slice(0, 2);
337
+ expect(result).toEqual([
338
+ { value: "Search", checked: true },
339
+ { value: "Notifications", checked: false },
340
+ ]);
341
+ result[0].value = "Changed snapshot";
342
+ result[0].checked = false;
343
+ expect(features.slice(-1)).toEqual([{ value: "Offline", checked: true }]);
344
+ expect(features.val()).toEqual(["Search", "Offline"]);
345
+ });
346
+ });
@@ -5,12 +5,13 @@ import { join } from "node:path";
5
5
  import { createTuiSelector } from "../src/plugins/js-bridge.js";
6
6
 
7
7
  const tui = join(import.meta.dir, "..", "tui");
8
+ const bunBin = Bun.which("bun") || process.argv0;
8
9
 
9
10
  async function runCheck(markdown) {
10
11
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-check-"));
11
12
  const file = join(dir, "app.md");
12
13
  await writeFile(file, markdown);
13
- const result = Bun.spawnSync([tui, "--check", file], { stdout: "pipe", stderr: "pipe" });
14
+ const result = Bun.spawnSync([bunBin, tui, "--check", file], { stdout: "pipe", stderr: "pipe" });
14
15
  await rm(dir, { recursive: true, force: true });
15
16
  return result;
16
17
  }
@@ -42,6 +43,17 @@ test("--check reports heading/fenced-block collisions with line details", async
42
43
  expect(output.lastIndexOf("FAILED")).toBeGreaterThan(output.lastIndexOf("Suggested fix"));
43
44
  });
44
45
 
46
+ test("--check keeps fenced IDs that are followed by inline event attributes", async () => {
47
+ const result = await runCheck(
48
+ '## Write Status\n\n```text#write-status @keydown="refresh(); validate(event)"\nwaiting\n```\n',
49
+ );
50
+ const output = Bun.stripANSI(result.stdout.toString());
51
+ expect(result.exitCode).toBe(1);
52
+ expect(output).toContain("ID #write-status");
53
+ expect(output).toContain("Type: text fenced block");
54
+ expect(output).toContain('Source: ```text#write-status @keydown="refresh(); validate(event)"');
55
+ });
56
+
45
57
  test("--check reports duplicate fenced-block IDs", async () => {
46
58
  const result = await runCheck("```text#myid\na\n```\n\n```textarea#myid\nb\n```\n");
47
59
  expect(result.exitCode).toBe(1);
@@ -140,7 +152,7 @@ test("TUI $ selector finds one ID across every tag/class query combination", ()
140
152
  });
141
153
 
142
154
  test("--check requires exactly one file", () => {
143
- const result = Bun.spawnSync([tui, "--check"], { stdout: "pipe", stderr: "pipe" });
155
+ const result = Bun.spawnSync([bunBin, tui, "--check"], { stdout: "pipe", stderr: "pipe" });
144
156
  expect(result.exitCode).toBe(2);
145
157
  expect(result.stderr.toString()).toContain("Usage: jsmdcui --check FILE.md");
146
158
  });
@@ -0,0 +1,46 @@
1
+ import { expect, test } from "bun:test";
2
+ import { buildMicroGlobal } from "../src/plugins/js-bridge.js";
3
+
4
+ test("micro prompt APIs delegate and return synchronously", () => {
5
+ const previousMicro = globalThis.micro;
6
+ const previousSelector = globalThis.$;
7
+ const calls = [];
8
+ const app = {
9
+ protectedAlert(message) {
10
+ calls.push(["alert", message]);
11
+ return undefined;
12
+ },
13
+ protectedConfirm(message) {
14
+ calls.push(["confirm", message]);
15
+ return true;
16
+ },
17
+ protectedPrompt(message, defaultValue) {
18
+ calls.push(["prompt", message, defaultValue]);
19
+ return "answer";
20
+ },
21
+ };
22
+
23
+ try {
24
+ const micro = buildMicroGlobal({ _app: app, _ctx: null, on() {} });
25
+ const alertResult = micro.alert("hello");
26
+ const confirmResult = micro.confirm("continue?");
27
+ const promptResult = micro.prompt("name", "Ada");
28
+
29
+ expect(alertResult).toBeUndefined();
30
+ expect(confirmResult).toBe(true);
31
+ expect(promptResult).toBe("answer");
32
+ expect(alertResult?.then).toBeUndefined();
33
+ expect(confirmResult?.then).toBeUndefined();
34
+ expect(promptResult?.then).toBeUndefined();
35
+ expect(calls).toEqual([
36
+ ["alert", "hello"],
37
+ ["confirm", "continue?"],
38
+ ["prompt", "name", "Ada"],
39
+ ]);
40
+ } finally {
41
+ if (previousMicro === undefined) delete globalThis.micro;
42
+ else globalThis.micro = previousMicro;
43
+ if (previousSelector === undefined) delete globalThis.$;
44
+ else globalThis.$ = previousSelector;
45
+ }
46
+ });
@@ -0,0 +1,10 @@
1
+ ```text#myid @keydown.prevent="handle(event)"
2
+ hello
3
+ ```
4
+
5
+ ```js front
6
+ export function handle(e)
7
+ {
8
+ alert(JSON.stringify(e,null,1))
9
+ }
10
+ ```
@@ -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
+ ![img](../demo.resized.jpg)
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
+ ![img](../demo.jpg)
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