@cuppacue/cli 0.1.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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/ai/__tests__/generate.test.d.ts +2 -0
  4. package/dist/ai/__tests__/generate.test.d.ts.map +1 -0
  5. package/dist/ai/__tests__/generate.test.js +129 -0
  6. package/dist/ai/__tests__/generate.test.js.map +1 -0
  7. package/dist/ai/__tests__/images.test.d.ts +2 -0
  8. package/dist/ai/__tests__/images.test.d.ts.map +1 -0
  9. package/dist/ai/__tests__/images.test.js +186 -0
  10. package/dist/ai/__tests__/images.test.js.map +1 -0
  11. package/dist/ai/__tests__/prompt.test.d.ts +2 -0
  12. package/dist/ai/__tests__/prompt.test.d.ts.map +1 -0
  13. package/dist/ai/__tests__/prompt.test.js +98 -0
  14. package/dist/ai/__tests__/prompt.test.js.map +1 -0
  15. package/dist/ai/__tests__/refine.test.d.ts +2 -0
  16. package/dist/ai/__tests__/refine.test.d.ts.map +1 -0
  17. package/dist/ai/__tests__/refine.test.js +87 -0
  18. package/dist/ai/__tests__/refine.test.js.map +1 -0
  19. package/dist/ai/client.d.ts +4 -0
  20. package/dist/ai/client.d.ts.map +1 -0
  21. package/dist/ai/client.js +36 -0
  22. package/dist/ai/client.js.map +1 -0
  23. package/dist/ai/example.d.ts +6 -0
  24. package/dist/ai/example.d.ts.map +1 -0
  25. package/dist/ai/example.js +284 -0
  26. package/dist/ai/example.js.map +1 -0
  27. package/dist/ai/generate.d.ts +15 -0
  28. package/dist/ai/generate.d.ts.map +1 -0
  29. package/dist/ai/generate.js +120 -0
  30. package/dist/ai/generate.js.map +1 -0
  31. package/dist/ai/images.d.ts +8 -0
  32. package/dist/ai/images.d.ts.map +1 -0
  33. package/dist/ai/images.js +71 -0
  34. package/dist/ai/images.js.map +1 -0
  35. package/dist/ai/index.d.ts +7 -0
  36. package/dist/ai/index.d.ts.map +1 -0
  37. package/dist/ai/index.js +7 -0
  38. package/dist/ai/index.js.map +1 -0
  39. package/dist/ai/prompt.d.ts +10 -0
  40. package/dist/ai/prompt.d.ts.map +1 -0
  41. package/dist/ai/prompt.js +119 -0
  42. package/dist/ai/prompt.js.map +1 -0
  43. package/dist/ai/refine.d.ts +6 -0
  44. package/dist/ai/refine.d.ts.map +1 -0
  45. package/dist/ai/refine.js +22 -0
  46. package/dist/ai/refine.js.map +1 -0
  47. package/dist/ai/schema.d.ts +5 -0
  48. package/dist/ai/schema.d.ts.map +1 -0
  49. package/dist/ai/schema.js +292 -0
  50. package/dist/ai/schema.js.map +1 -0
  51. package/dist/commands/__tests__/backstage.test.d.ts +2 -0
  52. package/dist/commands/__tests__/backstage.test.d.ts.map +1 -0
  53. package/dist/commands/__tests__/backstage.test.js +45 -0
  54. package/dist/commands/__tests__/backstage.test.js.map +1 -0
  55. package/dist/commands/__tests__/export.test.d.ts +2 -0
  56. package/dist/commands/__tests__/export.test.d.ts.map +1 -0
  57. package/dist/commands/__tests__/export.test.js +50 -0
  58. package/dist/commands/__tests__/export.test.js.map +1 -0
  59. package/dist/commands/ai.d.ts +2 -0
  60. package/dist/commands/ai.d.ts.map +1 -0
  61. package/dist/commands/ai.js +113 -0
  62. package/dist/commands/ai.js.map +1 -0
  63. package/dist/commands/build.d.ts +2 -0
  64. package/dist/commands/build.d.ts.map +1 -0
  65. package/dist/commands/build.js +121 -0
  66. package/dist/commands/build.js.map +1 -0
  67. package/dist/commands/export-pdf.d.ts +9 -0
  68. package/dist/commands/export-pdf.d.ts.map +1 -0
  69. package/dist/commands/export-pdf.js +109 -0
  70. package/dist/commands/export-pdf.js.map +1 -0
  71. package/dist/commands/export.d.ts +2 -0
  72. package/dist/commands/export.d.ts.map +1 -0
  73. package/dist/commands/export.js +147 -0
  74. package/dist/commands/export.js.map +1 -0
  75. package/dist/commands/init.d.ts +2 -0
  76. package/dist/commands/init.d.ts.map +1 -0
  77. package/dist/commands/init.js +54 -0
  78. package/dist/commands/init.js.map +1 -0
  79. package/dist/commands/preview.d.ts +2 -0
  80. package/dist/commands/preview.d.ts.map +1 -0
  81. package/dist/commands/preview.js +78 -0
  82. package/dist/commands/preview.js.map +1 -0
  83. package/dist/commands/serve.d.ts +7 -0
  84. package/dist/commands/serve.d.ts.map +1 -0
  85. package/dist/commands/serve.js +773 -0
  86. package/dist/commands/serve.js.map +1 -0
  87. package/dist/commands/validate.d.ts +2 -0
  88. package/dist/commands/validate.d.ts.map +1 -0
  89. package/dist/commands/validate.js +39 -0
  90. package/dist/commands/validate.js.map +1 -0
  91. package/dist/github/__tests__/fetcher.test.d.ts +2 -0
  92. package/dist/github/__tests__/fetcher.test.d.ts.map +1 -0
  93. package/dist/github/__tests__/fetcher.test.js +102 -0
  94. package/dist/github/__tests__/fetcher.test.js.map +1 -0
  95. package/dist/github/__tests__/rewrite-images.test.d.ts +2 -0
  96. package/dist/github/__tests__/rewrite-images.test.d.ts.map +1 -0
  97. package/dist/github/__tests__/rewrite-images.test.js +79 -0
  98. package/dist/github/__tests__/rewrite-images.test.js.map +1 -0
  99. package/dist/github/__tests__/url-parser.test.d.ts +2 -0
  100. package/dist/github/__tests__/url-parser.test.d.ts.map +1 -0
  101. package/dist/github/__tests__/url-parser.test.js +96 -0
  102. package/dist/github/__tests__/url-parser.test.js.map +1 -0
  103. package/dist/github/fetcher.d.ts +10 -0
  104. package/dist/github/fetcher.d.ts.map +1 -0
  105. package/dist/github/fetcher.js +53 -0
  106. package/dist/github/fetcher.js.map +1 -0
  107. package/dist/github/index.d.ts +5 -0
  108. package/dist/github/index.d.ts.map +1 -0
  109. package/dist/github/index.js +4 -0
  110. package/dist/github/index.js.map +1 -0
  111. package/dist/github/rewrite-images.d.ts +7 -0
  112. package/dist/github/rewrite-images.d.ts.map +1 -0
  113. package/dist/github/rewrite-images.js +26 -0
  114. package/dist/github/rewrite-images.js.map +1 -0
  115. package/dist/github/url-parser.d.ts +12 -0
  116. package/dist/github/url-parser.d.ts.map +1 -0
  117. package/dist/github/url-parser.js +60 -0
  118. package/dist/github/url-parser.js.map +1 -0
  119. package/dist/index.d.ts +3 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +95 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/markdown/__tests__/auto-slide.test.d.ts +2 -0
  124. package/dist/markdown/__tests__/auto-slide.test.d.ts.map +1 -0
  125. package/dist/markdown/__tests__/auto-slide.test.js +325 -0
  126. package/dist/markdown/__tests__/auto-slide.test.js.map +1 -0
  127. package/dist/markdown/__tests__/compiler.test.d.ts +2 -0
  128. package/dist/markdown/__tests__/compiler.test.d.ts.map +1 -0
  129. package/dist/markdown/__tests__/compiler.test.js +142 -0
  130. package/dist/markdown/__tests__/compiler.test.js.map +1 -0
  131. package/dist/markdown/__tests__/custom-parsers.test.d.ts +2 -0
  132. package/dist/markdown/__tests__/custom-parsers.test.d.ts.map +1 -0
  133. package/dist/markdown/__tests__/custom-parsers.test.js +182 -0
  134. package/dist/markdown/__tests__/custom-parsers.test.js.map +1 -0
  135. package/dist/markdown/__tests__/frontmatter.test.d.ts +2 -0
  136. package/dist/markdown/__tests__/frontmatter.test.d.ts.map +1 -0
  137. package/dist/markdown/__tests__/frontmatter.test.js +162 -0
  138. package/dist/markdown/__tests__/frontmatter.test.js.map +1 -0
  139. package/dist/markdown/__tests__/layout.test.d.ts +2 -0
  140. package/dist/markdown/__tests__/layout.test.d.ts.map +1 -0
  141. package/dist/markdown/__tests__/layout.test.js +85 -0
  142. package/dist/markdown/__tests__/layout.test.js.map +1 -0
  143. package/dist/markdown/__tests__/parser.test.d.ts +2 -0
  144. package/dist/markdown/__tests__/parser.test.d.ts.map +1 -0
  145. package/dist/markdown/__tests__/parser.test.js +646 -0
  146. package/dist/markdown/__tests__/parser.test.js.map +1 -0
  147. package/dist/markdown/__tests__/timesheet-gen.test.d.ts +2 -0
  148. package/dist/markdown/__tests__/timesheet-gen.test.d.ts.map +1 -0
  149. package/dist/markdown/__tests__/timesheet-gen.test.js +90 -0
  150. package/dist/markdown/__tests__/timesheet-gen.test.js.map +1 -0
  151. package/dist/markdown/auto-slide.d.ts +16 -0
  152. package/dist/markdown/auto-slide.d.ts.map +1 -0
  153. package/dist/markdown/auto-slide.js +144 -0
  154. package/dist/markdown/auto-slide.js.map +1 -0
  155. package/dist/markdown/compiler.d.ts +12 -0
  156. package/dist/markdown/compiler.d.ts.map +1 -0
  157. package/dist/markdown/compiler.js +47 -0
  158. package/dist/markdown/compiler.js.map +1 -0
  159. package/dist/markdown/custom-parsers.d.ts +69 -0
  160. package/dist/markdown/custom-parsers.d.ts.map +1 -0
  161. package/dist/markdown/custom-parsers.js +227 -0
  162. package/dist/markdown/custom-parsers.js.map +1 -0
  163. package/dist/markdown/frontmatter.d.ts +33 -0
  164. package/dist/markdown/frontmatter.d.ts.map +1 -0
  165. package/dist/markdown/frontmatter.js +83 -0
  166. package/dist/markdown/frontmatter.js.map +1 -0
  167. package/dist/markdown/id.d.ts +4 -0
  168. package/dist/markdown/id.d.ts.map +1 -0
  169. package/dist/markdown/id.js +15 -0
  170. package/dist/markdown/id.js.map +1 -0
  171. package/dist/markdown/index.d.ts +8 -0
  172. package/dist/markdown/index.d.ts.map +1 -0
  173. package/dist/markdown/index.js +6 -0
  174. package/dist/markdown/index.js.map +1 -0
  175. package/dist/markdown/parser.d.ts +35 -0
  176. package/dist/markdown/parser.d.ts.map +1 -0
  177. package/dist/markdown/parser.js +605 -0
  178. package/dist/markdown/parser.js.map +1 -0
  179. package/dist/markdown/timesheet-gen.d.ts +6 -0
  180. package/dist/markdown/timesheet-gen.d.ts.map +1 -0
  181. package/dist/markdown/timesheet-gen.js +223 -0
  182. package/dist/markdown/timesheet-gen.js.map +1 -0
  183. package/dist/player/CuePlayer.d.ts +58 -0
  184. package/dist/player/CuePlayer.d.ts.map +1 -0
  185. package/dist/player/NavBar.d.ts +31 -0
  186. package/dist/player/NavBar.d.ts.map +1 -0
  187. package/dist/player/PresenterView.d.ts +25 -0
  188. package/dist/player/PresenterView.d.ts.map +1 -0
  189. package/dist/player/SlideOverview.d.ts +16 -0
  190. package/dist/player/SlideOverview.d.ts.map +1 -0
  191. package/dist/player/__mocks__/chart.d.ts +29 -0
  192. package/dist/player/__mocks__/chart.d.ts.map +1 -0
  193. package/dist/player/__mocks__/mermaid.d.ts +8 -0
  194. package/dist/player/__mocks__/mermaid.d.ts.map +1 -0
  195. package/dist/player/__tests__/animator.test.d.ts +2 -0
  196. package/dist/player/__tests__/animator.test.d.ts.map +1 -0
  197. package/dist/player/__tests__/layout.test.d.ts +2 -0
  198. package/dist/player/__tests__/layout.test.d.ts.map +1 -0
  199. package/dist/player/__tests__/overview.test.d.ts +2 -0
  200. package/dist/player/__tests__/overview.test.d.ts.map +1 -0
  201. package/dist/player/__tests__/timeline.test.d.ts +2 -0
  202. package/dist/player/__tests__/timeline.test.d.ts.map +1 -0
  203. package/dist/player/animator.d.ts +4 -0
  204. package/dist/player/animator.d.ts.map +1 -0
  205. package/dist/player/cuppacue-player.css +1 -0
  206. package/dist/player/cuppacue-player.js +3247 -0
  207. package/dist/player/index.d.ts +14 -0
  208. package/dist/player/index.d.ts.map +1 -0
  209. package/dist/player/loader.d.ts +3 -0
  210. package/dist/player/loader.d.ts.map +1 -0
  211. package/dist/player/main.d.ts +2 -0
  212. package/dist/player/main.d.ts.map +1 -0
  213. package/dist/player/navigator.d.ts +27 -0
  214. package/dist/player/navigator.d.ts.map +1 -0
  215. package/dist/player/renderer.d.ts +9 -0
  216. package/dist/player/renderer.d.ts.map +1 -0
  217. package/dist/player/renderers/cards.d.ts +3 -0
  218. package/dist/player/renderers/cards.d.ts.map +1 -0
  219. package/dist/player/renderers/chart.d.ts +3 -0
  220. package/dist/player/renderers/chart.d.ts.map +1 -0
  221. package/dist/player/renderers/icons.d.ts +6 -0
  222. package/dist/player/renderers/icons.d.ts.map +1 -0
  223. package/dist/player/renderers/mermaid.d.ts +3 -0
  224. package/dist/player/renderers/mermaid.d.ts.map +1 -0
  225. package/dist/player/renderers/stats.d.ts +3 -0
  226. package/dist/player/renderers/stats.d.ts.map +1 -0
  227. package/dist/player/renderers/terminal.d.ts +3 -0
  228. package/dist/player/renderers/terminal.d.ts.map +1 -0
  229. package/dist/player/themes/dark.d.ts +2 -0
  230. package/dist/player/themes/dark.d.ts.map +1 -0
  231. package/dist/player/themes/light.d.ts +2 -0
  232. package/dist/player/themes/light.d.ts.map +1 -0
  233. package/dist/player/timeline.d.ts +43 -0
  234. package/dist/player/timeline.d.ts.map +1 -0
  235. package/dist/serve.d.ts +2 -0
  236. package/dist/serve.d.ts.map +1 -0
  237. package/dist/serve.js +171 -0
  238. package/dist/serve.js.map +1 -0
  239. package/dist/templates/discover.d.ts +20 -0
  240. package/dist/templates/discover.d.ts.map +1 -0
  241. package/dist/templates/discover.js +86 -0
  242. package/dist/templates/discover.js.map +1 -0
  243. package/dist/templates/loader.d.ts +17 -0
  244. package/dist/templates/loader.d.ts.map +1 -0
  245. package/dist/templates/loader.js +40 -0
  246. package/dist/templates/loader.js.map +1 -0
  247. package/package.json +62 -0
@@ -0,0 +1,646 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { parseMarkdown } from "../parser.js";
3
+ import { resetCounters } from "../id.js";
4
+ describe("parseMarkdown", () => {
5
+ beforeEach(() => {
6
+ resetCounters();
7
+ });
8
+ it("splits scenes on ---", () => {
9
+ const md = `# Scene One
10
+
11
+ ---
12
+
13
+ # Scene Two
14
+
15
+ ---
16
+
17
+ # Scene Three`;
18
+ const { content } = parseMarkdown(md);
19
+ expect(content.scenes).toHaveLength(3);
20
+ });
21
+ it("maps H1 to title element", () => {
22
+ const { content } = parseMarkdown("# Hello World");
23
+ const el = content.scenes[0].elements[0];
24
+ expect(el.type).toBe("text");
25
+ expect(el).toHaveProperty("role", "title");
26
+ expect(el).toHaveProperty("content", "Hello World");
27
+ });
28
+ it("maps H2 to heading element", () => {
29
+ const { content } = parseMarkdown("## Section Title");
30
+ const el = content.scenes[0].elements[0];
31
+ expect(el.type).toBe("text");
32
+ expect(el).toHaveProperty("role", "heading");
33
+ expect(el).toHaveProperty("content", "Section Title");
34
+ });
35
+ it("maps H3 to subtitle element", () => {
36
+ const { content } = parseMarkdown("### A Subtitle");
37
+ const el = content.scenes[0].elements[0];
38
+ expect(el.type).toBe("text");
39
+ expect(el).toHaveProperty("role", "subtitle");
40
+ });
41
+ it("maps code fences to CodeElement", () => {
42
+ const { content } = parseMarkdown("```typescript\nconst x = 1;\n```");
43
+ const el = content.scenes[0].elements[0];
44
+ expect(el.type).toBe("code");
45
+ expect(el).toHaveProperty("language", "typescript");
46
+ expect(el).toHaveProperty("content", "const x = 1;");
47
+ });
48
+ it("maps blockquote to caption", () => {
49
+ const { content } = parseMarkdown("> A caption");
50
+ const el = content.scenes[0].elements[0];
51
+ expect(el.type).toBe("text");
52
+ expect(el).toHaveProperty("role", "caption");
53
+ });
54
+ it("maps unordered list to ListElement", () => {
55
+ const { content } = parseMarkdown("- One\n- Two\n- Three");
56
+ const el = content.scenes[0].elements[0];
57
+ expect(el.type).toBe("list");
58
+ expect(el).toHaveProperty("items", ["One", "Two", "Three"]);
59
+ expect(el).toHaveProperty("ordered", false);
60
+ });
61
+ it("maps ordered list", () => {
62
+ const { content } = parseMarkdown("1. First\n2. Second");
63
+ const el = content.scenes[0].elements[0];
64
+ expect(el.type).toBe("list");
65
+ expect(el).toHaveProperty("ordered", true);
66
+ });
67
+ it("generates unique IDs across elements", () => {
68
+ const { content } = parseMarkdown("# Title\n\n### Subtitle\n\nParagraph");
69
+ const ids = content.scenes[0].elements.map((el) => el.id);
70
+ const unique = new Set(ids);
71
+ expect(unique.size).toBe(ids.length);
72
+ });
73
+ it("maps image in paragraph to ImageElement", () => {
74
+ const { content } = parseMarkdown("![Alt text](image.png)");
75
+ const el = content.scenes[0].elements[0];
76
+ expect(el.type).toBe("image");
77
+ expect(el).toHaveProperty("src", "image.png");
78
+ expect(el).toHaveProperty("alt", "Alt text");
79
+ });
80
+ it("assigns layout to each scene", () => {
81
+ const { content } = parseMarkdown("# Title\n\n---\n\n## Heading\n\n- Item 1\n- Item 2");
82
+ expect(content.scenes[0].layout).toBe("intro");
83
+ expect(content.scenes[1].layout).toBe("header-body");
84
+ });
85
+ it("does not set position fields on elements", () => {
86
+ const { content } = parseMarkdown("# Title\n\n```ts\nconst x = 1;\n```");
87
+ for (const el of content.scenes[0].elements) {
88
+ expect(el).not.toHaveProperty("position");
89
+ }
90
+ });
91
+ // ── Speaker Notes ───────────────────────────────────────
92
+ it("extracts notes from HTML comment", () => {
93
+ const { content } = parseMarkdown("# Title\n\n<!-- notes: Remember to explain this -->");
94
+ expect(content.scenes[0].notes).toBe("Remember to explain this");
95
+ });
96
+ it("extracts multi-line notes", () => {
97
+ const { content } = parseMarkdown("# Title\n\n<!-- notes: Line one\nLine two\nLine three -->");
98
+ expect(content.scenes[0].notes).toBe("Line one\nLine two\nLine three");
99
+ });
100
+ it("does not render notes comment as element", () => {
101
+ const { content } = parseMarkdown("# Title\n\n<!-- notes: secret -->");
102
+ expect(content.scenes[0].elements).toHaveLength(1);
103
+ expect(content.scenes[0].elements[0].type).toBe("text");
104
+ });
105
+ it("scene without notes has no notes field", () => {
106
+ const { content } = parseMarkdown("# Title");
107
+ expect(content.scenes[0]).not.toHaveProperty("notes");
108
+ });
109
+ // ── Inline Formatting ────────────────────────────────────
110
+ it("preserves bold formatting in paragraph", () => {
111
+ const { content } = parseMarkdown("This is **bold** text");
112
+ const el = content.scenes[0].elements[0];
113
+ expect(el).toHaveProperty("content");
114
+ expect(el.content).toContain("<strong>bold</strong>");
115
+ });
116
+ it("preserves italic formatting", () => {
117
+ const { content } = parseMarkdown("This is *italic* text");
118
+ const el = content.scenes[0].elements[0];
119
+ expect(el.content).toContain("<em>italic</em>");
120
+ });
121
+ it("preserves inline code", () => {
122
+ const { content } = parseMarkdown("Use `const x = 1` here");
123
+ const el = content.scenes[0].elements[0];
124
+ expect(el.content).toContain("<code>const x = 1</code>");
125
+ });
126
+ it("preserves strikethrough", () => {
127
+ const { content } = parseMarkdown("This is ~~wrong~~ correct");
128
+ const el = content.scenes[0].elements[0];
129
+ expect(el.content).toContain("<del>wrong</del>");
130
+ });
131
+ it("preserves markdown links", () => {
132
+ const { content } = parseMarkdown("Visit [Google](https://google.com)");
133
+ const el = content.scenes[0].elements[0];
134
+ expect(el.content).toContain('<a href="https://google.com"');
135
+ expect(el.content).toContain("Google</a>");
136
+ });
137
+ it("preserves inline formatting in headings", () => {
138
+ const { content } = parseMarkdown("# Hello **World**");
139
+ const el = content.scenes[0].elements[0];
140
+ expect(el.content).toContain("<strong>World</strong>");
141
+ });
142
+ it("preserves inline formatting in list items", () => {
143
+ const { content } = parseMarkdown("- **Bold** item\n- *Italic* item");
144
+ const el = content.scenes[0].elements[0];
145
+ expect(el.type).toBe("list");
146
+ expect(el.items[0]).toContain("<strong>Bold</strong>");
147
+ expect(el.items[1]).toContain("<em>Italic</em>");
148
+ });
149
+ it("escapes HTML in text", () => {
150
+ const { content } = parseMarkdown("Use <script>alert('xss')</script> carefully");
151
+ const el = content.scenes[0].elements[0];
152
+ expect(el.content).not.toContain("<script>");
153
+ expect(el.content).toContain("&lt;script&gt;");
154
+ });
155
+ it("plain text without formatting is unchanged", () => {
156
+ const { content } = parseMarkdown("Plain text paragraph");
157
+ const el = content.scenes[0].elements[0];
158
+ expect(el.content).toBe("Plain text paragraph");
159
+ });
160
+ // ── Tables ───────────────────────────────────────────────
161
+ it("maps markdown table to TableElement", () => {
162
+ const md = "| Name | Age |\n| --- | --- |\n| Alice | 30 |\n| Bob | 25 |";
163
+ const { content } = parseMarkdown(md);
164
+ const el = content.scenes[0].elements[0];
165
+ expect(el.type).toBe("table");
166
+ expect(el).toHaveProperty("headers", ["Name", "Age"]);
167
+ expect(el).toHaveProperty("rows", [["Alice", "30"], ["Bob", "25"]]);
168
+ });
169
+ it("preserves table column alignments", () => {
170
+ const md = "| Left | Center | Right |\n| :--- | :---: | ---: |\n| a | b | c |";
171
+ const { content } = parseMarkdown(md);
172
+ const el = content.scenes[0].elements[0];
173
+ expect(el.alignments).toEqual(["left", "center", "right"]);
174
+ });
175
+ it("supports inline formatting in table cells", () => {
176
+ const md = "| Feature | Status |\n| --- | --- |\n| **Bold** | *Done* |";
177
+ const { content } = parseMarkdown(md);
178
+ const el = content.scenes[0].elements[0];
179
+ expect(el.rows[0][0]).toContain("<strong>Bold</strong>");
180
+ expect(el.rows[0][1]).toContain("<em>Done</em>");
181
+ });
182
+ it("infers header-body layout for heading + table", () => {
183
+ const md = "## Comparison\n\n| A | B |\n| --- | --- |\n| 1 | 2 |";
184
+ const { content } = parseMarkdown(md);
185
+ expect(content.scenes[0].layout).toBe("header-body");
186
+ });
187
+ // ── Custom Element Types ────────────────────────────────────
188
+ it("maps ```mermaid to MermaidElement", () => {
189
+ const md = "```mermaid\nflowchart LR\n A --> B\n```";
190
+ const { content } = parseMarkdown(md);
191
+ const el = content.scenes[0].elements[0];
192
+ expect(el.type).toBe("mermaid");
193
+ expect(el).toHaveProperty("definition", "flowchart LR\n A --> B");
194
+ });
195
+ it("maps ```chart:bar to ChartElement", () => {
196
+ const md = "```chart:bar\n| X | Y |\n| A | 10 |\n```";
197
+ const { content } = parseMarkdown(md);
198
+ const el = content.scenes[0].elements[0];
199
+ expect(el.type).toBe("chart");
200
+ expect(el).toHaveProperty("chartType", "bar");
201
+ });
202
+ it("maps ```chart:pie to ChartElement", () => {
203
+ const md = "```chart:pie\n| Cat | Count |\n| Dogs | 60 |\n| Cats | 40 |\n```";
204
+ const { content } = parseMarkdown(md);
205
+ const el = content.scenes[0].elements[0];
206
+ expect(el.type).toBe("chart");
207
+ expect(el).toHaveProperty("chartType", "pie");
208
+ });
209
+ it("maps ```cards to CardsElement", () => {
210
+ const md = "```cards\n- title: Fast\n icon: zap\n desc: Quick builds\n```";
211
+ const { content } = parseMarkdown(md);
212
+ const el = content.scenes[0].elements[0];
213
+ expect(el.type).toBe("cards");
214
+ expect(el).toHaveProperty("cards");
215
+ expect(el.cards).toHaveLength(1);
216
+ });
217
+ it("maps ```stats to StatsElement", () => {
218
+ const md = "```stats\n- 100 | Users\n- 50 | Projects\n```";
219
+ const { content } = parseMarkdown(md);
220
+ const el = content.scenes[0].elements[0];
221
+ expect(el.type).toBe("stats");
222
+ expect(el).toHaveProperty("stats");
223
+ expect(el.stats).toHaveLength(2);
224
+ });
225
+ it("maps ```terminal to TerminalElement", () => {
226
+ const md = "```terminal\n$ echo hello\nhello\n```";
227
+ const { content } = parseMarkdown(md);
228
+ const el = content.scenes[0].elements[0];
229
+ expect(el.type).toBe("terminal");
230
+ expect(el).toHaveProperty("lines");
231
+ expect(el.lines).toHaveLength(2);
232
+ });
233
+ it("infers header-body layout for heading + custom element", () => {
234
+ const md = "## Metrics\n\n```stats\n- 100 | Users\n```";
235
+ const { content } = parseMarkdown(md);
236
+ expect(content.scenes[0].layout).toBe("header-body");
237
+ });
238
+ it("infers full layout for standalone custom element", () => {
239
+ const md = "```chart:bar\n| X | Y |\n| A | 10 |\n```";
240
+ const { content } = parseMarkdown(md);
241
+ expect(content.scenes[0].layout).toBe("full");
242
+ });
243
+ it("regular code blocks are not affected by custom parsing", () => {
244
+ const md = "```typescript\nconst x = 1;\n```";
245
+ const { content } = parseMarkdown(md);
246
+ const el = content.scenes[0].elements[0];
247
+ expect(el.type).toBe("code");
248
+ expect(el).toHaveProperty("language", "typescript");
249
+ });
250
+ it("tracks local image resources", () => {
251
+ const md = "![arch](./images/arch.png)";
252
+ const { localResources } = parseMarkdown(md);
253
+ expect(localResources).toContain("./images/arch.png");
254
+ });
255
+ it("does not track external image URLs as resources", () => {
256
+ const md = "![logo](https://example.com/logo.png)";
257
+ const { localResources } = parseMarkdown(md);
258
+ expect(localResources).toHaveLength(0);
259
+ });
260
+ // ── Per-Slide Frontmatter ─────────────────────────────────
261
+ it("applies layout override from per-slide frontmatter", () => {
262
+ const md = `# Slide 1
263
+
264
+ ---
265
+ layout: split
266
+ ---
267
+
268
+ ## Slide 2
269
+
270
+ Some body text`;
271
+ const { content } = parseMarkdown(md);
272
+ expect(content.scenes).toHaveLength(2);
273
+ expect(content.scenes[1].layout).toBe("split");
274
+ });
275
+ it("applies background from per-slide frontmatter", () => {
276
+ const md = `# Slide 1
277
+
278
+ ---
279
+ background: "#1a1a2e"
280
+ ---
281
+
282
+ ## Slide 2`;
283
+ const { content } = parseMarkdown(md);
284
+ expect(content.scenes[1].background).toBe("#1a1a2e");
285
+ });
286
+ it("applies transition from per-slide frontmatter", () => {
287
+ const md = `# Slide 1
288
+
289
+ ---
290
+ transition: fade
291
+ ---
292
+
293
+ ## Slide 2`;
294
+ const { content } = parseMarkdown(md);
295
+ expect(content.scenes[1].transition).toEqual({ type: "fade" });
296
+ });
297
+ it("applies class from per-slide frontmatter", () => {
298
+ const md = `# Slide 1
299
+
300
+ ---
301
+ class: emphasis
302
+ ---
303
+
304
+ ## Slide 2`;
305
+ const { content } = parseMarkdown(md);
306
+ expect(content.scenes[1].class).toBe("emphasis");
307
+ });
308
+ it("injects image element from frontmatter image field", () => {
309
+ const md = `# Slide 1
310
+
311
+ ---
312
+ layout: image-right
313
+ image: ./images/photo.jpg
314
+ ---
315
+
316
+ ## Slide 2`;
317
+ const { content } = parseMarkdown(md);
318
+ expect(content.scenes[1].layout).toBe("image-right");
319
+ const imgEl = content.scenes[1].elements.find(e => e.type === "image");
320
+ expect(imgEl).toBeDefined();
321
+ expect(imgEl).toHaveProperty("src", "./images/photo.jpg");
322
+ });
323
+ it("tracks frontmatter image as local resource", () => {
324
+ const md = `---
325
+ layout: intro
326
+ image: ./images/logo.png
327
+ ---
328
+
329
+ # Title`;
330
+ const { localResources } = parseMarkdown(md);
331
+ expect(localResources).toContain("./images/logo.png");
332
+ });
333
+ it("slides without frontmatter still work normally", () => {
334
+ const md = `# Slide 1
335
+
336
+ ---
337
+
338
+ ## Slide 2
339
+
340
+ - Item A
341
+ - Item B`;
342
+ const { content } = parseMarkdown(md);
343
+ expect(content.scenes).toHaveLength(2);
344
+ expect(content.scenes[0].layout).toBe("intro");
345
+ expect(content.scenes[1].layout).toBe("header-body");
346
+ });
347
+ it("handles multiple slides with mixed frontmatter", () => {
348
+ const md = `# Title
349
+
350
+ ---
351
+ layout: section
352
+ ---
353
+
354
+ ## Part One
355
+
356
+ ---
357
+
358
+ ## Part Two
359
+
360
+ - Detail`;
361
+ const { content } = parseMarkdown(md);
362
+ expect(content.scenes).toHaveLength(3);
363
+ expect(content.scenes[0].layout).toBe("intro");
364
+ expect(content.scenes[1].layout).toBe("section");
365
+ expect(content.scenes[2].layout).toBe("header-body");
366
+ });
367
+ it("does not treat regular text with colons as frontmatter", () => {
368
+ const md = `# Slide 1
369
+
370
+ ---
371
+
372
+ Author: John Doe wrote this
373
+
374
+ ---
375
+
376
+ ## Slide 3`;
377
+ const { content } = parseMarkdown(md);
378
+ // "Author:" is not a known FM key, so it's treated as slide content
379
+ expect(content.scenes).toHaveLength(3);
380
+ expect(content.scenes[1].elements.length).toBeGreaterThan(0);
381
+ });
382
+ // ── Background Image Resources ──────────────────────────────
383
+ it("tracks background image path as local resource", () => {
384
+ const md = `# Slide 1
385
+
386
+ ---
387
+ background: ./images/hero.jpg
388
+ ---
389
+
390
+ ## Slide 2`;
391
+ const { localResources } = parseMarkdown(md);
392
+ expect(localResources).toContain("./images/hero.jpg");
393
+ });
394
+ it("does not track background color as resource", () => {
395
+ const md = `# Slide 1
396
+
397
+ ---
398
+ background: "#1a1a2e"
399
+ ---
400
+
401
+ ## Slide 2`;
402
+ const { localResources } = parseMarkdown(md);
403
+ expect(localResources).toHaveLength(0);
404
+ });
405
+ it("does not track external background URL as resource", () => {
406
+ const md = `# Slide 1
407
+
408
+ ---
409
+ background: https://example.com/hero.jpg
410
+ ---
411
+
412
+ ## Slide 2`;
413
+ const { localResources } = parseMarkdown(md);
414
+ expect(localResources).toHaveLength(0);
415
+ });
416
+ // ── Footer Directive ────────────────────────────────────
417
+ it("extracts footer from HTML comment directive", () => {
418
+ const { content } = parseMarkdown("# Title\n\n<!-- footer: Q4 2025 Report -->");
419
+ const footer = content.scenes[0].elements.find((e) => e.type === "text" && e.role === "footer");
420
+ expect(footer).toBeDefined();
421
+ expect(footer).toHaveProperty("content", "Q4 2025 Report");
422
+ });
423
+ it("footer element does not affect layout inference", () => {
424
+ const { content } = parseMarkdown("# Title\n\n<!-- footer: Some text -->");
425
+ // Title-only slide should still be intro, not header-body
426
+ expect(content.scenes[0].layout).toBe("intro");
427
+ });
428
+ // ── AccentBar Frontmatter ──────────────────────────────
429
+ it("applies accentBar from per-slide frontmatter", () => {
430
+ const md = `# Slide 1
431
+
432
+ ---
433
+ accentBar: "#38bdf8"
434
+ ---
435
+
436
+ ## Slide 2`;
437
+ const { content } = parseMarkdown(md);
438
+ expect(content.scenes[1].accentBar).toBeDefined();
439
+ expect(content.scenes[1].accentBar.color).toBe("#38bdf8");
440
+ });
441
+ it("parses accentBar with position", () => {
442
+ const md = `# Slide 1
443
+
444
+ ---
445
+ accentBar: "#38bdf8 bottom"
446
+ ---
447
+
448
+ ## Slide 2`;
449
+ const { content } = parseMarkdown(md);
450
+ expect(content.scenes[1].accentBar).toBeDefined();
451
+ expect(content.scenes[1].accentBar.color).toBe("#38bdf8");
452
+ expect(content.scenes[1].accentBar.position).toBe("bottom");
453
+ });
454
+ // ── Scene Templates ───────────────────────────────────────
455
+ it("auto-assigns intro template to first scene", () => {
456
+ const md = `# Title
457
+
458
+ ---
459
+
460
+ ## Content
461
+
462
+ - Item 1
463
+
464
+ ---
465
+
466
+ ## Thank You`;
467
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
468
+ expect(content.scenes[0].sceneTemplate).toBe("tech-talk/intro");
469
+ });
470
+ it("auto-assigns closing template to last scene", () => {
471
+ const md = `# Title
472
+
473
+ ---
474
+
475
+ ## Content
476
+
477
+ - Item 1
478
+
479
+ ---
480
+
481
+ ## Thank You`;
482
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
483
+ expect(content.scenes[2].sceneTemplate).toBe("tech-talk/closing");
484
+ });
485
+ it("auto-assigns section template to heading-only scenes", () => {
486
+ const md = `# Title
487
+
488
+ ---
489
+
490
+ ## Section Divider
491
+
492
+ ---
493
+
494
+ ## Content
495
+
496
+ - Item 1
497
+
498
+ ---
499
+
500
+ ## Thank You`;
501
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
502
+ expect(content.scenes[1].sceneTemplate).toBe("tech-talk/section");
503
+ });
504
+ it("auto-assigns content template to list scenes", () => {
505
+ const md = `# Title
506
+
507
+ ---
508
+
509
+ ## Content
510
+
511
+ - Item 1
512
+ - Item 2
513
+
514
+ ---
515
+
516
+ ## Thank You`;
517
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
518
+ expect(content.scenes[1].sceneTemplate).toBe("tech-talk/content");
519
+ });
520
+ it("auto-assigns code template to scenes with code elements", () => {
521
+ const md = `# Title
522
+
523
+ ---
524
+
525
+ ## Example
526
+
527
+ \`\`\`typescript
528
+ const x = 1;
529
+ \`\`\`
530
+
531
+ ---
532
+
533
+ ## Thank You`;
534
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
535
+ expect(content.scenes[1].sceneTemplate).toBe("tech-talk/code");
536
+ });
537
+ it("code template takes priority over content when both code and list present", () => {
538
+ const md = `# Title
539
+
540
+ ---
541
+
542
+ ## Mixed
543
+
544
+ - Point one
545
+
546
+ \`\`\`typescript
547
+ const x = 1;
548
+ \`\`\`
549
+
550
+ ---
551
+
552
+ ## Thank You`;
553
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
554
+ expect(content.scenes[1].sceneTemplate).toBe("tech-talk/code");
555
+ });
556
+ it("per-slide template override is not overwritten by auto-assignment", () => {
557
+ const md = `---
558
+ template: custom/intro
559
+ badge: "MY TALK"
560
+ ---
561
+
562
+ # Title
563
+
564
+ ---
565
+
566
+ ## Thank You`;
567
+ const { content } = parseMarkdown(md, { presentationTemplate: "tech-talk" });
568
+ expect(content.scenes[0].sceneTemplate).toBe("custom/intro");
569
+ expect(content.scenes[0].templateData).toEqual({ badge: "MY TALK" });
570
+ });
571
+ it("collects templateData from frontmatter", () => {
572
+ const md = `---
573
+ template: tech-talk/intro
574
+ badge: "TECH TALK"
575
+ footerKeywords: "Fast · Reliable · Open Source"
576
+ ---
577
+
578
+ # Title`;
579
+ const { content } = parseMarkdown(md);
580
+ expect(content.scenes[0].sceneTemplate).toBe("tech-talk/intro");
581
+ expect(content.scenes[0].templateData).toEqual({
582
+ badge: "TECH TALK",
583
+ footerKeywords: "Fast · Reliable · Open Source",
584
+ });
585
+ });
586
+ it("non-templated presentations are unchanged", () => {
587
+ const md = `# Title
588
+
589
+ ---
590
+
591
+ ## Content
592
+
593
+ - Item`;
594
+ const { content } = parseMarkdown(md);
595
+ expect(content.scenes[0].sceneTemplate).toBeUndefined();
596
+ expect(content.scenes[1].sceneTemplate).toBeUndefined();
597
+ });
598
+ // ── Code Highlight Lines ─────────────────────────────────
599
+ it("parses highlight lines from code fence info string", () => {
600
+ const md = "```typescript {1-3,5}\nconst a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nconst e = 5;\n```";
601
+ const { content } = parseMarkdown(md);
602
+ const el = content.scenes[0].elements[0];
603
+ expect(el.type).toBe("code");
604
+ expect(el).toHaveProperty("highlightLines", [1, 2, 3, 5]);
605
+ expect(el).toHaveProperty("language", "typescript");
606
+ });
607
+ it("parses single line highlight", () => {
608
+ const md = "```python {3}\nprint('a')\nprint('b')\nprint('c')\n```";
609
+ const { content } = parseMarkdown(md);
610
+ const el = content.scenes[0].elements[0];
611
+ expect(el).toHaveProperty("highlightLines", [3]);
612
+ expect(el).toHaveProperty("language", "python");
613
+ });
614
+ it("code without highlight lines has no highlightLines field", () => {
615
+ const md = "```typescript\nconst x = 1;\n```";
616
+ const { content } = parseMarkdown(md);
617
+ const el = content.scenes[0].elements[0];
618
+ expect(el).not.toHaveProperty("highlightLines");
619
+ });
620
+ it("parses multiple ranges in highlight lines", () => {
621
+ const md = "```js {1-2,4-6,8}\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n```";
622
+ const { content } = parseMarkdown(md);
623
+ const el = content.scenes[0].elements[0];
624
+ expect(el).toHaveProperty("highlightLines", [1, 2, 4, 5, 6, 8]);
625
+ });
626
+ it("does not split on --- inside code fences", () => {
627
+ const md = `# Slide 1
628
+
629
+ ---
630
+
631
+ ## Slide 2
632
+
633
+ \`\`\`markdown
634
+ ---
635
+ layout: split
636
+ ---
637
+ \`\`\``;
638
+ const { content } = parseMarkdown(md);
639
+ expect(content.scenes).toHaveLength(2);
640
+ // The code block should contain the --- lines
641
+ const codeEl = content.scenes[1].elements.find(e => e.type === "code");
642
+ expect(codeEl).toBeDefined();
643
+ expect(codeEl.content).toContain("---");
644
+ });
645
+ });
646
+ //# sourceMappingURL=parser.test.js.map