@pyreon/document 0.10.0 → 0.11.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 (76) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/confluence-Bd3ua1Ut.js.map +1 -1
  3. package/lib/csv-COrS4qdy.js.map +1 -1
  4. package/lib/discord-BLUnkEh9.js.map +1 -1
  5. package/lib/{dist-BsqdI2nY.js → dist-CYL41kqQ.js} +2 -2
  6. package/lib/dist-CYL41kqQ.js.map +1 -0
  7. package/lib/{docx-BEBOihjl.js → docx-uNAel545.js} +7 -2
  8. package/lib/docx-uNAel545.js.map +1 -0
  9. package/lib/email-D0bbfWq4.js.map +1 -1
  10. package/lib/{exceljs-BoIDUUaw.js → exceljs-BYETsesT.js} +314 -314
  11. package/lib/exceljs-BYETsesT.js.map +1 -0
  12. package/lib/google-chat-CkKCBUWC.js.map +1 -1
  13. package/lib/html-B5biprN2.js.map +1 -1
  14. package/lib/index.js +17 -8
  15. package/lib/index.js.map +1 -1
  16. package/lib/markdown-CdtlFGC0.js.map +1 -1
  17. package/lib/notion-iG2C5bEY.js.map +1 -1
  18. package/lib/{pdf-DIUQUEdj.js → pdf-IuBgTb3T.js} +9 -3
  19. package/lib/pdf-IuBgTb3T.js.map +1 -0
  20. package/lib/{pdfmake-DnmLxK4Q.js → pdfmake-CKMX5URW.js} +2 -4
  21. package/lib/pdfmake-CKMX5URW.js.map +1 -0
  22. package/lib/{pptx-Dd33oL3_.js → pptx-DXiMiYFM.js} +7 -2
  23. package/lib/pptx-DXiMiYFM.js.map +1 -0
  24. package/lib/{pptxgen.es-COcgXsyx.js → pptxgen.es-FsqHs8mD.js} +3 -6
  25. package/lib/pptxgen.es-FsqHs8mD.js.map +1 -0
  26. package/lib/sanitize-O_3j1mNJ.js.map +1 -1
  27. package/lib/slack-BI3EQwYm.js.map +1 -1
  28. package/lib/svg-BKxumy-p.js.map +1 -1
  29. package/lib/teams-Cwz9lce0.js.map +1 -1
  30. package/lib/telegram-gYFqyMXb.js.map +1 -1
  31. package/lib/text-l1XNXBOC.js.map +1 -1
  32. package/lib/types/index.d.ts +43 -39
  33. package/lib/types/index.d.ts.map +1 -1
  34. package/lib/{vfs_fonts-Df1kkZ4Y.js → vfs_fonts-Cap07Jg3.js} +2 -2
  35. package/lib/vfs_fonts-Cap07Jg3.js.map +1 -0
  36. package/lib/whatsapp-CjSGoOKx.js.map +1 -1
  37. package/lib/{xlsx-Bb5TWyXQ.js → xlsx-Cvu4LBNy.js} +8 -2
  38. package/lib/xlsx-Cvu4LBNy.js.map +1 -0
  39. package/package.json +19 -7
  40. package/src/builder.ts +53 -44
  41. package/src/download.ts +32 -36
  42. package/src/env.d.ts +3 -17
  43. package/src/index.ts +6 -8
  44. package/src/nodes.ts +45 -45
  45. package/src/render.ts +45 -118
  46. package/src/renderers/confluence.ts +64 -80
  47. package/src/renderers/csv.ts +11 -18
  48. package/src/renderers/discord.ts +38 -50
  49. package/src/renderers/docx.ts +78 -120
  50. package/src/renderers/email.ts +73 -92
  51. package/src/renderers/google-chat.ts +35 -47
  52. package/src/renderers/html.ts +78 -101
  53. package/src/renderers/markdown.ts +43 -53
  54. package/src/renderers/notion.ts +63 -85
  55. package/src/renderers/pdf.ts +92 -115
  56. package/src/renderers/pptx.ts +60 -66
  57. package/src/renderers/slack.ts +49 -61
  58. package/src/renderers/svg.ts +49 -63
  59. package/src/renderers/teams.ts +68 -80
  60. package/src/renderers/telegram.ts +40 -54
  61. package/src/renderers/text.ts +44 -66
  62. package/src/renderers/whatsapp.ts +34 -48
  63. package/src/renderers/xlsx.ts +47 -61
  64. package/src/sanitize.ts +21 -25
  65. package/src/tests/document.test.ts +1337 -1385
  66. package/src/tests/stress.test.ts +111 -111
  67. package/src/types.ts +66 -65
  68. package/lib/dist-BsqdI2nY.js.map +0 -1
  69. package/lib/docx-BEBOihjl.js.map +0 -1
  70. package/lib/exceljs-BoIDUUaw.js.map +0 -1
  71. package/lib/pdf-DIUQUEdj.js.map +0 -1
  72. package/lib/pdfmake-DnmLxK4Q.js.map +0 -1
  73. package/lib/pptx-Dd33oL3_.js.map +0 -1
  74. package/lib/pptxgen.es-COcgXsyx.js.map +0 -1
  75. package/lib/vfs_fonts-Df1kkZ4Y.js.map +0 -1
  76. package/lib/xlsx-Bb5TWyXQ.js.map +0 -1
@@ -1,5 +1,6 @@
1
- import { afterEach, describe, expect, it } from 'vitest'
1
+ import { afterEach, describe, expect, it } from "vitest"
2
2
  import {
3
+ _resetRenderers,
3
4
  Button,
4
5
  Code,
5
6
  Column,
@@ -16,15 +17,14 @@ import {
16
17
  PageBreak,
17
18
  Quote,
18
19
  Row,
20
+ registerRenderer,
21
+ render,
19
22
  Section,
20
23
  Spacer,
21
24
  Table,
22
25
  Text,
23
- _resetRenderers,
24
- registerRenderer,
25
- render,
26
26
  unregisterRenderer,
27
- } from '../index'
27
+ } from "../index"
28
28
 
29
29
  afterEach(() => {
30
30
  _resetRenderers()
@@ -32,869 +32,844 @@ afterEach(() => {
32
32
 
33
33
  // ─── Node Construction ──────────────────────────────────────────────────────
34
34
 
35
- describe('node construction', () => {
36
- it('Document creates a document node', () => {
37
- const doc = Document({ title: 'Test', children: 'hello' })
38
- expect(doc.type).toBe('document')
39
- expect(doc.props.title).toBe('Test')
40
- expect(doc.children).toEqual(['hello'])
35
+ describe("node construction", () => {
36
+ it("Document creates a document node", () => {
37
+ const doc = Document({ title: "Test", children: "hello" })
38
+ expect(doc.type).toBe("document")
39
+ expect(doc.props.title).toBe("Test")
40
+ expect(doc.children).toEqual(["hello"])
41
41
  })
42
42
 
43
- it('Page creates a page node', () => {
44
- const page = Page({ size: 'A4', margin: 40, children: 'content' })
45
- expect(page.type).toBe('page')
46
- expect(page.props.size).toBe('A4')
43
+ it("Page creates a page node", () => {
44
+ const page = Page({ size: "A4", margin: 40, children: "content" })
45
+ expect(page.type).toBe("page")
46
+ expect(page.props.size).toBe("A4")
47
47
  expect(page.props.margin).toBe(40)
48
48
  })
49
49
 
50
- it('Section with direction', () => {
51
- const section = Section({ direction: 'row', gap: 20, children: 'a' })
52
- expect(section.type).toBe('section')
53
- expect(section.props.direction).toBe('row')
50
+ it("Section with direction", () => {
51
+ const section = Section({ direction: "row", gap: 20, children: "a" })
52
+ expect(section.type).toBe("section")
53
+ expect(section.props.direction).toBe("row")
54
54
  expect(section.props.gap).toBe(20)
55
55
  })
56
56
 
57
- it('Row and Column', () => {
57
+ it("Row and Column", () => {
58
58
  const row = Row({
59
59
  gap: 10,
60
- children: [Column({ width: '50%', children: 'left' })],
60
+ children: [Column({ width: "50%", children: "left" })],
61
61
  })
62
- expect(row.type).toBe('row')
62
+ expect(row.type).toBe("row")
63
63
  expect(row.children).toHaveLength(1)
64
64
  const col = row.children[0]!
65
- expect(typeof col).not.toBe('string')
66
- if (typeof col !== 'string') {
67
- expect(col.type).toBe('column')
68
- expect(col.props.width).toBe('50%')
65
+ expect(typeof col).not.toBe("string")
66
+ if (typeof col !== "string") {
67
+ expect(col.type).toBe("column")
68
+ expect(col.props.width).toBe("50%")
69
69
  }
70
70
  })
71
71
 
72
- it('Heading defaults to level 1', () => {
73
- const h = Heading({ children: 'Title' })
74
- expect(h.type).toBe('heading')
72
+ it("Heading defaults to level 1", () => {
73
+ const h = Heading({ children: "Title" })
74
+ expect(h.type).toBe("heading")
75
75
  expect(h.props.level).toBe(1)
76
76
  })
77
77
 
78
- it('Heading with custom level', () => {
79
- const h = Heading({ level: 3, children: 'Subtitle' })
78
+ it("Heading with custom level", () => {
79
+ const h = Heading({ level: 3, children: "Subtitle" })
80
80
  expect(h.props.level).toBe(3)
81
81
  })
82
82
 
83
- it('Text with formatting', () => {
83
+ it("Text with formatting", () => {
84
84
  const t = Text({
85
85
  bold: true,
86
86
  italic: true,
87
87
  size: 14,
88
- color: '#333',
89
- children: 'hello',
88
+ color: "#333",
89
+ children: "hello",
90
90
  })
91
- expect(t.type).toBe('text')
91
+ expect(t.type).toBe("text")
92
92
  expect(t.props.bold).toBe(true)
93
93
  expect(t.props.italic).toBe(true)
94
94
  expect(t.props.size).toBe(14)
95
95
  })
96
96
 
97
- it('Link', () => {
98
- const l = Link({ href: 'https://example.com', children: 'click' })
99
- expect(l.type).toBe('link')
100
- expect(l.props.href).toBe('https://example.com')
97
+ it("Link", () => {
98
+ const l = Link({ href: "https://example.com", children: "click" })
99
+ expect(l.type).toBe("link")
100
+ expect(l.props.href).toBe("https://example.com")
101
101
  })
102
102
 
103
- it('Image with all props', () => {
103
+ it("Image with all props", () => {
104
104
  const img = Image({
105
- src: '/logo.png',
105
+ src: "/logo.png",
106
106
  width: 100,
107
107
  height: 50,
108
- alt: 'Logo',
109
- caption: 'Company logo',
108
+ alt: "Logo",
109
+ caption: "Company logo",
110
110
  })
111
- expect(img.type).toBe('image')
112
- expect(img.props.src).toBe('/logo.png')
111
+ expect(img.type).toBe("image")
112
+ expect(img.props.src).toBe("/logo.png")
113
113
  expect(img.props.width).toBe(100)
114
- expect(img.props.caption).toBe('Company logo')
114
+ expect(img.props.caption).toBe("Company logo")
115
115
  expect(img.children).toEqual([])
116
116
  })
117
117
 
118
- it('Table with columns and rows', () => {
118
+ it("Table with columns and rows", () => {
119
119
  const t = Table({
120
- columns: ['Name', { header: 'Price', align: 'right' }],
121
- rows: [['Widget', '$10']],
120
+ columns: ["Name", { header: "Price", align: "right" }],
121
+ rows: [["Widget", "$10"]],
122
122
  striped: true,
123
123
  })
124
- expect(t.type).toBe('table')
124
+ expect(t.type).toBe("table")
125
125
  expect(t.props.columns).toHaveLength(2)
126
126
  expect(t.props.rows).toHaveLength(1)
127
127
  expect(t.props.striped).toBe(true)
128
128
  })
129
129
 
130
- it('List with items', () => {
130
+ it("List with items", () => {
131
131
  const l = List({
132
132
  ordered: true,
133
- children: [ListItem({ children: 'one' }), ListItem({ children: 'two' })],
133
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
134
134
  })
135
- expect(l.type).toBe('list')
135
+ expect(l.type).toBe("list")
136
136
  expect(l.props.ordered).toBe(true)
137
137
  expect(l.children).toHaveLength(2)
138
138
  })
139
139
 
140
- it('Code', () => {
141
- const c = Code({ language: 'typescript', children: 'const x = 1' })
142
- expect(c.type).toBe('code')
143
- expect(c.props.language).toBe('typescript')
140
+ it("Code", () => {
141
+ const c = Code({ language: "typescript", children: "const x = 1" })
142
+ expect(c.type).toBe("code")
143
+ expect(c.props.language).toBe("typescript")
144
144
  })
145
145
 
146
- it('Divider', () => {
147
- const d = Divider({ color: '#ccc', thickness: 2 })
148
- expect(d.type).toBe('divider')
149
- expect(d.props.color).toBe('#ccc')
146
+ it("Divider", () => {
147
+ const d = Divider({ color: "#ccc", thickness: 2 })
148
+ expect(d.type).toBe("divider")
149
+ expect(d.props.color).toBe("#ccc")
150
150
  })
151
151
 
152
- it('Divider with defaults', () => {
152
+ it("Divider with defaults", () => {
153
153
  const d = Divider()
154
- expect(d.type).toBe('divider')
154
+ expect(d.type).toBe("divider")
155
155
  })
156
156
 
157
- it('Spacer', () => {
157
+ it("Spacer", () => {
158
158
  const s = Spacer({ height: 30 })
159
- expect(s.type).toBe('spacer')
159
+ expect(s.type).toBe("spacer")
160
160
  expect(s.props.height).toBe(30)
161
161
  })
162
162
 
163
- it('Button', () => {
164
- const b = Button({ href: '/pay', background: '#4f46e5', children: 'Pay' })
165
- expect(b.type).toBe('button')
166
- expect(b.props.href).toBe('/pay')
167
- expect(b.props.background).toBe('#4f46e5')
163
+ it("Button", () => {
164
+ const b = Button({ href: "/pay", background: "#4f46e5", children: "Pay" })
165
+ expect(b.type).toBe("button")
166
+ expect(b.props.href).toBe("/pay")
167
+ expect(b.props.background).toBe("#4f46e5")
168
168
  })
169
169
 
170
- it('Quote', () => {
171
- const q = Quote({ borderColor: '#blue', children: 'wise words' })
172
- expect(q.type).toBe('quote')
173
- expect(q.props.borderColor).toBe('#blue')
170
+ it("Quote", () => {
171
+ const q = Quote({ borderColor: "#blue", children: "wise words" })
172
+ expect(q.type).toBe("quote")
173
+ expect(q.props.borderColor).toBe("#blue")
174
174
  })
175
175
 
176
- it('isDocNode returns true for nodes', () => {
177
- expect(isDocNode(Heading({ children: 'hi' }))).toBe(true)
176
+ it("isDocNode returns true for nodes", () => {
177
+ expect(isDocNode(Heading({ children: "hi" }))).toBe(true)
178
178
  })
179
179
 
180
- it('isDocNode returns false for non-nodes', () => {
181
- expect(isDocNode('string')).toBe(false)
180
+ it("isDocNode returns false for non-nodes", () => {
181
+ expect(isDocNode("string")).toBe(false)
182
182
  expect(isDocNode(null)).toBe(false)
183
183
  expect(isDocNode(42)).toBe(false)
184
184
  expect(isDocNode({})).toBe(false)
185
185
  })
186
186
 
187
- it('normalizes nested children', () => {
187
+ it("normalizes nested children", () => {
188
188
  const doc = Document({
189
- children: [
190
- Heading({ children: 'A' }),
191
- [Text({ children: 'B' }), Text({ children: 'C' })],
192
- ],
189
+ children: [Heading({ children: "A" }), [Text({ children: "B" }), Text({ children: "C" })]],
193
190
  })
194
191
  expect(doc.children).toHaveLength(3)
195
192
  })
196
193
 
197
- it('handles null/undefined/false children', () => {
198
- const doc = Document({ children: [null, undefined, false, 'text'] })
199
- expect(doc.children).toEqual(['text'])
194
+ it("handles null/undefined/false children", () => {
195
+ const doc = Document({ children: [null, undefined, false, "text"] })
196
+ expect(doc.children).toEqual(["text"])
200
197
  })
201
198
 
202
- it('converts numbers to strings in children', () => {
199
+ it("converts numbers to strings in children", () => {
203
200
  const t = Text({ children: 42 as unknown as string })
204
- expect(t.children).toEqual(['42'])
201
+ expect(t.children).toEqual(["42"])
205
202
  })
206
203
  })
207
204
 
208
205
  // ─── HTML Renderer ──────────────────────────────────────────────────────────
209
206
 
210
- describe('HTML renderer', () => {
211
- it('renders a simple document', async () => {
207
+ describe("HTML renderer", () => {
208
+ it("renders a simple document", async () => {
212
209
  const doc = Document({
213
- title: 'Test',
214
- children: Page({ children: Heading({ children: 'Hello' }) }),
210
+ title: "Test",
211
+ children: Page({ children: Heading({ children: "Hello" }) }),
215
212
  })
216
- const html = (await render(doc, 'html')) as string
217
- expect(html).toContain('<!DOCTYPE html>')
218
- expect(html).toContain('<title>Test</title>')
219
- expect(html).toContain('<h1')
220
- expect(html).toContain('Hello')
213
+ const html = (await render(doc, "html")) as string
214
+ expect(html).toContain("<!DOCTYPE html>")
215
+ expect(html).toContain("<title>Test</title>")
216
+ expect(html).toContain("<h1")
217
+ expect(html).toContain("Hello")
221
218
  })
222
219
 
223
- it('renders text with formatting', async () => {
220
+ it("renders text with formatting", async () => {
224
221
  const doc = Document({
225
222
  children: Text({
226
223
  bold: true,
227
- color: '#f00',
224
+ color: "#f00",
228
225
  size: 20,
229
- align: 'center',
230
- children: 'Bold Red',
226
+ align: "center",
227
+ children: "Bold Red",
231
228
  }),
232
229
  })
233
- const html = (await render(doc, 'html')) as string
234
- expect(html).toContain('font-weight:bold')
235
- expect(html).toContain('color:#f00')
236
- expect(html).toContain('font-size:20px')
237
- expect(html).toContain('text-align:center')
230
+ const html = (await render(doc, "html")) as string
231
+ expect(html).toContain("font-weight:bold")
232
+ expect(html).toContain("color:#f00")
233
+ expect(html).toContain("font-size:20px")
234
+ expect(html).toContain("text-align:center")
238
235
  })
239
236
 
240
- it('renders a table', async () => {
237
+ it("renders a table", async () => {
241
238
  const doc = Document({
242
239
  children: Table({
243
- columns: ['Name', { header: 'Price', align: 'right' }],
240
+ columns: ["Name", { header: "Price", align: "right" }],
244
241
  rows: [
245
- ['Widget', '$10'],
246
- ['Gadget', '$20'],
242
+ ["Widget", "$10"],
243
+ ["Gadget", "$20"],
247
244
  ],
248
245
  striped: true,
249
- headerStyle: { background: '#000', color: '#fff' },
246
+ headerStyle: { background: "#000", color: "#fff" },
250
247
  }),
251
248
  })
252
- const html = (await render(doc, 'html')) as string
253
- expect(html).toContain('<table')
254
- expect(html).toContain('Widget')
255
- expect(html).toContain('$10')
256
- expect(html).toContain('background:#000')
257
- expect(html).toContain('color:#fff')
249
+ const html = (await render(doc, "html")) as string
250
+ expect(html).toContain("<table")
251
+ expect(html).toContain("Widget")
252
+ expect(html).toContain("$10")
253
+ expect(html).toContain("background:#000")
254
+ expect(html).toContain("color:#fff")
258
255
  })
259
256
 
260
- it('renders an image with caption', async () => {
257
+ it("renders an image with caption", async () => {
261
258
  const doc = Document({
262
259
  children: Image({
263
- src: '/img.png',
260
+ src: "/img.png",
264
261
  width: 200,
265
- alt: 'Photo',
266
- caption: 'A photo',
262
+ alt: "Photo",
263
+ caption: "A photo",
267
264
  }),
268
265
  })
269
- const html = (await render(doc, 'html')) as string
270
- expect(html).toContain('<img')
266
+ const html = (await render(doc, "html")) as string
267
+ expect(html).toContain("<img")
271
268
  expect(html).toContain('src="/img.png"')
272
- expect(html).toContain('<figcaption>')
273
- expect(html).toContain('A photo')
269
+ expect(html).toContain("<figcaption>")
270
+ expect(html).toContain("A photo")
274
271
  })
275
272
 
276
- it('renders a link', async () => {
273
+ it("renders a link", async () => {
277
274
  const doc = Document({
278
- children: Link({ href: 'https://example.com', children: 'Click me' }),
275
+ children: Link({ href: "https://example.com", children: "Click me" }),
279
276
  })
280
- const html = (await render(doc, 'html')) as string
277
+ const html = (await render(doc, "html")) as string
281
278
  expect(html).toContain('href="https://example.com"')
282
- expect(html).toContain('Click me')
279
+ expect(html).toContain("Click me")
283
280
  })
284
281
 
285
- it('renders a list', async () => {
282
+ it("renders a list", async () => {
286
283
  const doc = Document({
287
284
  children: List({
288
285
  ordered: true,
289
- children: [
290
- ListItem({ children: 'one' }),
291
- ListItem({ children: 'two' }),
292
- ],
286
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
293
287
  }),
294
288
  })
295
- const html = (await render(doc, 'html')) as string
296
- expect(html).toContain('<ol>')
297
- expect(html).toContain('<li>one</li>')
289
+ const html = (await render(doc, "html")) as string
290
+ expect(html).toContain("<ol>")
291
+ expect(html).toContain("<li>one</li>")
298
292
  })
299
293
 
300
- it('renders code blocks', async () => {
301
- const doc = Document({ children: Code({ children: 'const x = 1' }) })
302
- const html = (await render(doc, 'html')) as string
303
- expect(html).toContain('<pre')
304
- expect(html).toContain('<code>')
305
- expect(html).toContain('const x = 1')
294
+ it("renders code blocks", async () => {
295
+ const doc = Document({ children: Code({ children: "const x = 1" }) })
296
+ const html = (await render(doc, "html")) as string
297
+ expect(html).toContain("<pre")
298
+ expect(html).toContain("<code>")
299
+ expect(html).toContain("const x = 1")
306
300
  })
307
301
 
308
- it('renders divider', async () => {
309
- const doc = Document({ children: Divider({ color: '#ccc', thickness: 2 }) })
310
- const html = (await render(doc, 'html')) as string
311
- expect(html).toContain('<hr')
312
- expect(html).toContain('2px solid #ccc')
302
+ it("renders divider", async () => {
303
+ const doc = Document({ children: Divider({ color: "#ccc", thickness: 2 }) })
304
+ const html = (await render(doc, "html")) as string
305
+ expect(html).toContain("<hr")
306
+ expect(html).toContain("2px solid #ccc")
313
307
  })
314
308
 
315
- it('renders spacer', async () => {
309
+ it("renders spacer", async () => {
316
310
  const doc = Document({ children: Spacer({ height: 30 }) })
317
- const html = (await render(doc, 'html')) as string
318
- expect(html).toContain('height:30px')
311
+ const html = (await render(doc, "html")) as string
312
+ expect(html).toContain("height:30px")
319
313
  })
320
314
 
321
- it('renders button', async () => {
315
+ it("renders button", async () => {
322
316
  const doc = Document({
323
317
  children: Button({
324
- href: '/pay',
325
- background: '#4f46e5',
326
- color: '#fff',
327
- children: 'Pay',
318
+ href: "/pay",
319
+ background: "#4f46e5",
320
+ color: "#fff",
321
+ children: "Pay",
328
322
  }),
329
323
  })
330
- const html = (await render(doc, 'html')) as string
324
+ const html = (await render(doc, "html")) as string
331
325
  expect(html).toContain('href="/pay"')
332
- expect(html).toContain('background:#4f46e5')
333
- expect(html).toContain('Pay')
326
+ expect(html).toContain("background:#4f46e5")
327
+ expect(html).toContain("Pay")
334
328
  })
335
329
 
336
- it('renders blockquote', async () => {
337
- const doc = Document({ children: Quote({ children: 'A wise quote' }) })
338
- const html = (await render(doc, 'html')) as string
339
- expect(html).toContain('<blockquote')
340
- expect(html).toContain('A wise quote')
330
+ it("renders blockquote", async () => {
331
+ const doc = Document({ children: Quote({ children: "A wise quote" }) })
332
+ const html = (await render(doc, "html")) as string
333
+ expect(html).toContain("<blockquote")
334
+ expect(html).toContain("A wise quote")
341
335
  })
342
336
 
343
- it('renders section with row direction', async () => {
337
+ it("renders section with row direction", async () => {
344
338
  const doc = Document({
345
339
  children: Section({
346
- direction: 'row',
340
+ direction: "row",
347
341
  gap: 20,
348
- background: '#f5f5f5',
349
- children: [Text({ children: 'A' }), Text({ children: 'B' })],
342
+ background: "#f5f5f5",
343
+ children: [Text({ children: "A" }), Text({ children: "B" })],
350
344
  }),
351
345
  })
352
- const html = (await render(doc, 'html')) as string
353
- expect(html).toContain('display:flex')
354
- expect(html).toContain('flex-direction:row')
346
+ const html = (await render(doc, "html")) as string
347
+ expect(html).toContain("display:flex")
348
+ expect(html).toContain("flex-direction:row")
355
349
  })
356
350
 
357
- it('renders image with center alignment', async () => {
351
+ it("renders image with center alignment", async () => {
358
352
  const doc = Document({
359
- children: Image({ src: '/img.png', align: 'center' }),
353
+ children: Image({ src: "/img.png", align: "center" }),
360
354
  })
361
- const html = (await render(doc, 'html')) as string
362
- expect(html).toContain('margin:0 auto')
355
+ const html = (await render(doc, "html")) as string
356
+ expect(html).toContain("margin:0 auto")
363
357
  })
364
358
 
365
- it('renders table with bordered option', async () => {
359
+ it("renders table with bordered option", async () => {
366
360
  const doc = Document({
367
- children: Table({ columns: ['A'], rows: [['1']], bordered: true }),
361
+ children: Table({ columns: ["A"], rows: [["1"]], bordered: true }),
368
362
  })
369
- const html = (await render(doc, 'html')) as string
370
- expect(html).toContain('border:1px solid #ddd')
363
+ const html = (await render(doc, "html")) as string
364
+ expect(html).toContain("border:1px solid #ddd")
371
365
  })
372
366
 
373
- it('renders table with caption', async () => {
367
+ it("renders table with caption", async () => {
374
368
  const doc = Document({
375
- children: Table({ columns: ['A'], rows: [['1']], caption: 'My Table' }),
369
+ children: Table({ columns: ["A"], rows: [["1"]], caption: "My Table" }),
376
370
  })
377
- const html = (await render(doc, 'html')) as string
378
- expect(html).toContain('<caption>My Table</caption>')
371
+ const html = (await render(doc, "html")) as string
372
+ expect(html).toContain("<caption>My Table</caption>")
379
373
  })
380
374
 
381
- it('escapes HTML in text', async () => {
375
+ it("escapes HTML in text", async () => {
382
376
  const doc = Document({
383
- children: Text({ children: '<script>alert(1)</script>' }),
377
+ children: Text({ children: "<script>alert(1)</script>" }),
384
378
  })
385
- const html = (await render(doc, 'html')) as string
386
- expect(html).not.toContain('<script>')
387
- expect(html).toContain('&lt;script&gt;')
379
+ const html = (await render(doc, "html")) as string
380
+ expect(html).not.toContain("<script>")
381
+ expect(html).toContain("&lt;script&gt;")
388
382
  })
389
383
 
390
- it('renders text with underline and strikethrough', async () => {
384
+ it("renders text with underline and strikethrough", async () => {
391
385
  const ul = Document({
392
- children: Text({ underline: true, children: 'underlined' }),
386
+ children: Text({ underline: true, children: "underlined" }),
393
387
  })
394
388
  const st = Document({
395
- children: Text({ strikethrough: true, children: 'struck' }),
389
+ children: Text({ strikethrough: true, children: "struck" }),
396
390
  })
397
- expect((await render(ul, 'html')) as string).toContain(
398
- 'text-decoration:underline',
399
- )
400
- expect((await render(st, 'html')) as string).toContain(
401
- 'text-decoration:line-through',
402
- )
391
+ expect((await render(ul, "html")) as string).toContain("text-decoration:underline")
392
+ expect((await render(st, "html")) as string).toContain("text-decoration:line-through")
403
393
  })
404
394
 
405
- it('renders image with right alignment', async () => {
406
- const doc = Document({ children: Image({ src: '/x.png', align: 'right' }) })
407
- const html = (await render(doc, 'html')) as string
408
- expect(html).toContain('margin-left:auto')
395
+ it("renders image with right alignment", async () => {
396
+ const doc = Document({ children: Image({ src: "/x.png", align: "right" }) })
397
+ const html = (await render(doc, "html")) as string
398
+ expect(html).toContain("margin-left:auto")
409
399
  })
410
400
  })
411
401
 
412
402
  // ─── Email Renderer ─────────────────────────────────────────────────────────
413
403
 
414
- describe('email renderer', () => {
415
- it('renders email-safe HTML', async () => {
404
+ describe("email renderer", () => {
405
+ it("renders email-safe HTML", async () => {
416
406
  const doc = Document({
417
- title: 'Welcome',
418
- children: [
419
- Heading({ children: 'Hello!' }),
420
- Text({ children: 'Welcome to our service.' }),
421
- ],
407
+ title: "Welcome",
408
+ children: [Heading({ children: "Hello!" }), Text({ children: "Welcome to our service." })],
422
409
  })
423
- const html = (await render(doc, 'email')) as string
424
- expect(html).toContain('<!DOCTYPE html>')
425
- expect(html).toContain('max-width:600px')
426
- expect(html).toContain('Hello!')
410
+ const html = (await render(doc, "email")) as string
411
+ expect(html).toContain("<!DOCTYPE html>")
412
+ expect(html).toContain("max-width:600px")
413
+ expect(html).toContain("Hello!")
427
414
  // Should have Outlook conditional comments
428
- expect(html).toContain('<!--[if mso]>')
415
+ expect(html).toContain("<!--[if mso]>")
429
416
  })
430
417
 
431
- it('renders bulletproof buttons', async () => {
418
+ it("renders bulletproof buttons", async () => {
432
419
  const doc = Document({
433
420
  children: Button({
434
- href: '/pay',
435
- background: '#4f46e5',
436
- children: 'Pay Now',
421
+ href: "/pay",
422
+ background: "#4f46e5",
423
+ children: "Pay Now",
437
424
  }),
438
425
  })
439
- const html = (await render(doc, 'email')) as string
426
+ const html = (await render(doc, "email")) as string
440
427
  // VML for Outlook
441
- expect(html).toContain('v:roundrect')
428
+ expect(html).toContain("v:roundrect")
442
429
  // CSS for others
443
- expect(html).toContain('background-color:#4f46e5')
444
- expect(html).toContain('Pay Now')
430
+ expect(html).toContain("background-color:#4f46e5")
431
+ expect(html).toContain("Pay Now")
445
432
  })
446
433
 
447
- it('renders table with inline styles', async () => {
434
+ it("renders table with inline styles", async () => {
448
435
  const doc = Document({
449
436
  children: Table({
450
- columns: ['Name', 'Price'],
451
- rows: [['Widget', '$10']],
452
- headerStyle: { background: '#000', color: '#fff' },
437
+ columns: ["Name", "Price"],
438
+ rows: [["Widget", "$10"]],
439
+ headerStyle: { background: "#000", color: "#fff" },
453
440
  }),
454
441
  })
455
- const html = (await render(doc, 'email')) as string
456
- expect(html).toContain('background-color:#000')
457
- expect(html).toContain('color:#fff')
458
- expect(html).toContain('Widget')
442
+ const html = (await render(doc, "email")) as string
443
+ expect(html).toContain("background-color:#000")
444
+ expect(html).toContain("color:#fff")
445
+ expect(html).toContain("Widget")
459
446
  })
460
447
 
461
- it('renders section with row direction using tables', async () => {
448
+ it("renders section with row direction using tables", async () => {
462
449
  const doc = Document({
463
450
  children: Section({
464
- direction: 'row',
465
- children: [Text({ children: 'Left' }), Text({ children: 'Right' })],
451
+ direction: "row",
452
+ children: [Text({ children: "Left" }), Text({ children: "Right" })],
466
453
  }),
467
454
  })
468
- const html = (await render(doc, 'email')) as string
455
+ const html = (await render(doc, "email")) as string
469
456
  // Should use table layout, not flexbox
470
- expect(html).not.toContain('display:flex')
471
- expect(html).toContain('<table')
472
- expect(html).toContain('Left')
473
- expect(html).toContain('Right')
457
+ expect(html).not.toContain("display:flex")
458
+ expect(html).toContain("<table")
459
+ expect(html).toContain("Left")
460
+ expect(html).toContain("Right")
474
461
  })
475
462
 
476
- it('renders divider using table', async () => {
463
+ it("renders divider using table", async () => {
477
464
  const doc = Document({ children: Divider() })
478
- const html = (await render(doc, 'email')) as string
479
- expect(html).toContain('border-top:1px solid')
465
+ const html = (await render(doc, "email")) as string
466
+ expect(html).toContain("border-top:1px solid")
480
467
  })
481
468
 
482
- it('renders quote using table with border-left', async () => {
483
- const doc = Document({ children: Quote({ children: 'A quote' }) })
484
- const html = (await render(doc, 'email')) as string
485
- expect(html).toContain('border-left:4px solid')
469
+ it("renders quote using table with border-left", async () => {
470
+ const doc = Document({ children: Quote({ children: "A quote" }) })
471
+ const html = (await render(doc, "email")) as string
472
+ expect(html).toContain("border-left:4px solid")
486
473
  })
487
474
  })
488
475
 
489
476
  // ─── Markdown Renderer ──────────────────────────────────────────────────────
490
477
 
491
- describe('markdown renderer', () => {
492
- it('renders headings with # prefix', async () => {
478
+ describe("markdown renderer", () => {
479
+ it("renders headings with # prefix", async () => {
493
480
  const doc = Document({
494
481
  children: [
495
- Heading({ level: 1, children: 'Title' }),
496
- Heading({ level: 3, children: 'Subtitle' }),
482
+ Heading({ level: 1, children: "Title" }),
483
+ Heading({ level: 3, children: "Subtitle" }),
497
484
  ],
498
485
  })
499
- const md = (await render(doc, 'md')) as string
500
- expect(md).toContain('# Title')
501
- expect(md).toContain('### Subtitle')
486
+ const md = (await render(doc, "md")) as string
487
+ expect(md).toContain("# Title")
488
+ expect(md).toContain("### Subtitle")
502
489
  })
503
490
 
504
- it('renders bold and italic text', async () => {
491
+ it("renders bold and italic text", async () => {
505
492
  const doc = Document({
506
493
  children: [
507
- Text({ bold: true, children: 'bold' }),
508
- Text({ italic: true, children: 'italic' }),
509
- Text({ strikethrough: true, children: 'struck' }),
494
+ Text({ bold: true, children: "bold" }),
495
+ Text({ italic: true, children: "italic" }),
496
+ Text({ strikethrough: true, children: "struck" }),
510
497
  ],
511
498
  })
512
- const md = (await render(doc, 'md')) as string
513
- expect(md).toContain('**bold**')
514
- expect(md).toContain('*italic*')
515
- expect(md).toContain('~~struck~~')
499
+ const md = (await render(doc, "md")) as string
500
+ expect(md).toContain("**bold**")
501
+ expect(md).toContain("*italic*")
502
+ expect(md).toContain("~~struck~~")
516
503
  })
517
504
 
518
- it('renders tables as pipe tables', async () => {
505
+ it("renders tables as pipe tables", async () => {
519
506
  const doc = Document({
520
507
  children: Table({
521
- columns: ['Name', { header: 'Price', align: 'right' }],
522
- rows: [['Widget', '$10']],
508
+ columns: ["Name", { header: "Price", align: "right" }],
509
+ rows: [["Widget", "$10"]],
523
510
  }),
524
511
  })
525
- const md = (await render(doc, 'md')) as string
526
- expect(md).toContain('| Name | Price |')
527
- expect(md).toContain('| --- | ---: |')
528
- expect(md).toContain('| Widget | $10 |')
512
+ const md = (await render(doc, "md")) as string
513
+ expect(md).toContain("| Name | Price |")
514
+ expect(md).toContain("| --- | ---: |")
515
+ expect(md).toContain("| Widget | $10 |")
529
516
  })
530
517
 
531
- it('renders links in markdown format', async () => {
518
+ it("renders links in markdown format", async () => {
532
519
  const doc = Document({
533
- children: Link({ href: 'https://example.com', children: 'click' }),
520
+ children: Link({ href: "https://example.com", children: "click" }),
534
521
  })
535
- const md = (await render(doc, 'md')) as string
536
- expect(md).toContain('[click](https://example.com)')
522
+ const md = (await render(doc, "md")) as string
523
+ expect(md).toContain("[click](https://example.com)")
537
524
  })
538
525
 
539
- it('renders images', async () => {
526
+ it("renders images", async () => {
540
527
  const doc = Document({
541
- children: Image({ src: '/img.png', alt: 'Photo', caption: 'A photo' }),
528
+ children: Image({ src: "/img.png", alt: "Photo", caption: "A photo" }),
542
529
  })
543
- const md = (await render(doc, 'md')) as string
544
- expect(md).toContain('![Photo](/img.png)')
545
- expect(md).toContain('*A photo*')
530
+ const md = (await render(doc, "md")) as string
531
+ expect(md).toContain("![Photo](/img.png)")
532
+ expect(md).toContain("*A photo*")
546
533
  })
547
534
 
548
- it('renders code blocks with language', async () => {
535
+ it("renders code blocks with language", async () => {
549
536
  const doc = Document({
550
- children: Code({ language: 'typescript', children: 'const x = 1' }),
537
+ children: Code({ language: "typescript", children: "const x = 1" }),
551
538
  })
552
- const md = (await render(doc, 'md')) as string
553
- expect(md).toContain('```typescript')
554
- expect(md).toContain('const x = 1')
555
- expect(md).toContain('```')
539
+ const md = (await render(doc, "md")) as string
540
+ expect(md).toContain("```typescript")
541
+ expect(md).toContain("const x = 1")
542
+ expect(md).toContain("```")
556
543
  })
557
544
 
558
- it('renders ordered and unordered lists', async () => {
545
+ it("renders ordered and unordered lists", async () => {
559
546
  const ol = Document({
560
547
  children: List({
561
548
  ordered: true,
562
- children: [
563
- ListItem({ children: 'first' }),
564
- ListItem({ children: 'second' }),
565
- ],
549
+ children: [ListItem({ children: "first" }), ListItem({ children: "second" })],
566
550
  }),
567
551
  })
568
552
  const ul = Document({
569
553
  children: List({
570
- children: [ListItem({ children: 'a' }), ListItem({ children: 'b' })],
554
+ children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
571
555
  }),
572
556
  })
573
- const orderedMd = (await render(ol, 'md')) as string
574
- const unorderedMd = (await render(ul, 'md')) as string
575
- expect(orderedMd).toContain('1. first')
576
- expect(orderedMd).toContain('2. second')
577
- expect(unorderedMd).toContain('- a')
578
- expect(unorderedMd).toContain('- b')
557
+ const orderedMd = (await render(ol, "md")) as string
558
+ const unorderedMd = (await render(ul, "md")) as string
559
+ expect(orderedMd).toContain("1. first")
560
+ expect(orderedMd).toContain("2. second")
561
+ expect(unorderedMd).toContain("- a")
562
+ expect(unorderedMd).toContain("- b")
579
563
  })
580
564
 
581
- it('renders divider as ---', async () => {
565
+ it("renders divider as ---", async () => {
582
566
  const doc = Document({ children: Divider() })
583
- const md = (await render(doc, 'md')) as string
584
- expect(md).toContain('---')
567
+ const md = (await render(doc, "md")) as string
568
+ expect(md).toContain("---")
585
569
  })
586
570
 
587
- it('renders button as link', async () => {
571
+ it("renders button as link", async () => {
588
572
  const doc = Document({
589
- children: Button({ href: '/pay', children: 'Pay' }),
573
+ children: Button({ href: "/pay", children: "Pay" }),
590
574
  })
591
- const md = (await render(doc, 'md')) as string
592
- expect(md).toContain('[Pay](/pay)')
575
+ const md = (await render(doc, "md")) as string
576
+ expect(md).toContain("[Pay](/pay)")
593
577
  })
594
578
 
595
- it('renders quote with >', async () => {
596
- const doc = Document({ children: Quote({ children: 'wise' }) })
597
- const md = (await render(doc, 'md')) as string
598
- expect(md).toContain('> wise')
579
+ it("renders quote with >", async () => {
580
+ const doc = Document({ children: Quote({ children: "wise" }) })
581
+ const md = (await render(doc, "md")) as string
582
+ expect(md).toContain("> wise")
599
583
  })
600
584
 
601
- it('renders table with caption', async () => {
585
+ it("renders table with caption", async () => {
602
586
  const doc = Document({
603
- children: Table({ columns: ['A'], rows: [['1']], caption: 'My Table' }),
587
+ children: Table({ columns: ["A"], rows: [["1"]], caption: "My Table" }),
604
588
  })
605
- const md = (await render(doc, 'md')) as string
606
- expect(md).toContain('*My Table*')
589
+ const md = (await render(doc, "md")) as string
590
+ expect(md).toContain("*My Table*")
607
591
  })
608
592
  })
609
593
 
610
594
  // ─── Text Renderer ──────────────────────────────────────────────────────────
611
595
 
612
- describe('text renderer', () => {
613
- it('renders headings with underlines', async () => {
596
+ describe("text renderer", () => {
597
+ it("renders headings with underlines", async () => {
614
598
  const doc = Document({
615
- children: [
616
- Heading({ level: 1, children: 'Title' }),
617
- Heading({ level: 2, children: 'Sub' }),
618
- ],
599
+ children: [Heading({ level: 1, children: "Title" }), Heading({ level: 2, children: "Sub" })],
619
600
  })
620
- const text = (await render(doc, 'text')) as string
621
- expect(text).toContain('TITLE')
622
- expect(text).toContain('=====')
623
- expect(text).toContain('Sub')
624
- expect(text).toContain('---')
601
+ const text = (await render(doc, "text")) as string
602
+ expect(text).toContain("TITLE")
603
+ expect(text).toContain("=====")
604
+ expect(text).toContain("Sub")
605
+ expect(text).toContain("---")
625
606
  })
626
607
 
627
- it('renders aligned table columns', async () => {
608
+ it("renders aligned table columns", async () => {
628
609
  const doc = Document({
629
610
  children: Table({
630
611
  columns: [
631
- { header: 'Name', align: 'left' },
632
- { header: 'Price', align: 'right' },
612
+ { header: "Name", align: "left" },
613
+ { header: "Price", align: "right" },
633
614
  ],
634
- rows: [['Widget', '$10']],
615
+ rows: [["Widget", "$10"]],
635
616
  }),
636
617
  })
637
- const text = (await render(doc, 'text')) as string
638
- expect(text).toContain('Name')
639
- expect(text).toContain('Price')
640
- expect(text).toContain('Widget')
618
+ const text = (await render(doc, "text")) as string
619
+ expect(text).toContain("Name")
620
+ expect(text).toContain("Price")
621
+ expect(text).toContain("Widget")
641
622
  })
642
623
 
643
- it('renders button as link reference', async () => {
624
+ it("renders button as link reference", async () => {
644
625
  const doc = Document({
645
- children: Button({ href: '/pay', children: 'Pay' }),
626
+ children: Button({ href: "/pay", children: "Pay" }),
646
627
  })
647
- const text = (await render(doc, 'text')) as string
648
- expect(text).toContain('[Pay]')
649
- expect(text).toContain('/pay')
628
+ const text = (await render(doc, "text")) as string
629
+ expect(text).toContain("[Pay]")
630
+ expect(text).toContain("/pay")
650
631
  })
651
632
 
652
- it('renders image as placeholder', async () => {
633
+ it("renders image as placeholder", async () => {
653
634
  const doc = Document({
654
- children: Image({ src: '/x.png', alt: 'Photo', caption: 'Nice' }),
635
+ children: Image({ src: "/x.png", alt: "Photo", caption: "Nice" }),
655
636
  })
656
- const text = (await render(doc, 'text')) as string
657
- expect(text).toContain('[Photo — Nice]')
637
+ const text = (await render(doc, "text")) as string
638
+ expect(text).toContain("[Photo — Nice]")
658
639
  })
659
640
  })
660
641
 
661
642
  // ─── CSV Renderer ───────────────────────────────────────────────────────────
662
643
 
663
- describe('CSV renderer', () => {
664
- it('extracts tables as CSV', async () => {
644
+ describe("CSV renderer", () => {
645
+ it("extracts tables as CSV", async () => {
665
646
  const doc = Document({
666
647
  children: Table({
667
- columns: ['Name', 'Price'],
648
+ columns: ["Name", "Price"],
668
649
  rows: [
669
- ['Widget', '$10'],
670
- ['Gadget', '$20'],
650
+ ["Widget", "$10"],
651
+ ["Gadget", "$20"],
671
652
  ],
672
653
  }),
673
654
  })
674
- const csv = (await render(doc, 'csv')) as string
675
- expect(csv).toContain('Name,Price')
676
- expect(csv).toContain('Widget,$10')
677
- expect(csv).toContain('Gadget,$20')
655
+ const csv = (await render(doc, "csv")) as string
656
+ expect(csv).toContain("Name,Price")
657
+ expect(csv).toContain("Widget,$10")
658
+ expect(csv).toContain("Gadget,$20")
678
659
  })
679
660
 
680
- it('escapes commas and quotes', async () => {
661
+ it("escapes commas and quotes", async () => {
681
662
  const doc = Document({
682
663
  children: Table({
683
- columns: ['Name'],
684
- rows: [['Widget, Inc.'], ['He said "hello"']],
664
+ columns: ["Name"],
665
+ rows: [["Widget, Inc."], ['He said "hello"']],
685
666
  }),
686
667
  })
687
- const csv = (await render(doc, 'csv')) as string
668
+ const csv = (await render(doc, "csv")) as string
688
669
  expect(csv).toContain('"Widget, Inc."')
689
670
  expect(csv).toContain('"He said ""hello"""')
690
671
  })
691
672
 
692
- it('returns message when no tables', async () => {
693
- const doc = Document({ children: Text({ children: 'no tables here' }) })
694
- const csv = (await render(doc, 'csv')) as string
695
- expect(csv).toContain('No tables found')
673
+ it("returns message when no tables", async () => {
674
+ const doc = Document({ children: Text({ children: "no tables here" }) })
675
+ const csv = (await render(doc, "csv")) as string
676
+ expect(csv).toContain("No tables found")
696
677
  })
697
678
 
698
- it('handles multiple tables', async () => {
679
+ it("handles multiple tables", async () => {
699
680
  const doc = Document({
700
681
  children: [
701
- Table({ columns: ['A'], rows: [['1']] }),
702
- Table({ columns: ['B'], rows: [['2']] }),
682
+ Table({ columns: ["A"], rows: [["1"]] }),
683
+ Table({ columns: ["B"], rows: [["2"]] }),
703
684
  ],
704
685
  })
705
- const csv = (await render(doc, 'csv')) as string
706
- expect(csv).toContain('A')
707
- expect(csv).toContain('B')
686
+ const csv = (await render(doc, "csv")) as string
687
+ expect(csv).toContain("A")
688
+ expect(csv).toContain("B")
708
689
  })
709
690
 
710
- it('adds caption as comment', async () => {
691
+ it("adds caption as comment", async () => {
711
692
  const doc = Document({
712
- children: Table({ columns: ['A'], rows: [['1']], caption: 'My Data' }),
693
+ children: Table({ columns: ["A"], rows: [["1"]], caption: "My Data" }),
713
694
  })
714
- const csv = (await render(doc, 'csv')) as string
715
- expect(csv).toContain('# My Data')
695
+ const csv = (await render(doc, "csv")) as string
696
+ expect(csv).toContain("# My Data")
716
697
  })
717
698
  })
718
699
 
719
700
  // ─── Builder Pattern ────────────────────────────────────────────────────────
720
701
 
721
- describe('createDocument builder', () => {
722
- it('builds a document with heading and text', async () => {
723
- const doc = createDocument({ title: 'Test' })
724
- .heading('Title')
725
- .text('Hello world')
702
+ describe("createDocument builder", () => {
703
+ it("builds a document with heading and text", async () => {
704
+ const doc = createDocument({ title: "Test" }).heading("Title").text("Hello world")
726
705
 
727
706
  const node = doc.build()
728
- expect(node.type).toBe('document')
729
- expect(node.props.title).toBe('Test')
707
+ expect(node.type).toBe("document")
708
+ expect(node.props.title).toBe("Test")
730
709
  })
731
710
 
732
- it('renders to HTML', async () => {
711
+ it("renders to HTML", async () => {
733
712
  const doc = createDocument()
734
- .heading('Report')
735
- .text('Summary text')
713
+ .heading("Report")
714
+ .text("Summary text")
736
715
  .table({
737
- columns: ['Name', 'Value'],
738
- rows: [['A', '1']],
716
+ columns: ["Name", "Value"],
717
+ rows: [["A", "1"]],
739
718
  })
740
719
 
741
720
  const html = await doc.toHtml()
742
- expect(html).toContain('Report')
743
- expect(html).toContain('Summary text')
744
- expect(html).toContain('<table')
721
+ expect(html).toContain("Report")
722
+ expect(html).toContain("Summary text")
723
+ expect(html).toContain("<table")
745
724
  })
746
725
 
747
- it('renders to markdown', async () => {
726
+ it("renders to markdown", async () => {
748
727
  const doc = createDocument()
749
- .heading('Title')
750
- .text('Body', { bold: true })
751
- .list(['item 1', 'item 2'])
728
+ .heading("Title")
729
+ .text("Body", { bold: true })
730
+ .list(["item 1", "item 2"])
752
731
 
753
732
  const md = await doc.toMarkdown()
754
- expect(md).toContain('# Title')
755
- expect(md).toContain('**Body**')
756
- expect(md).toContain('- item 1')
733
+ expect(md).toContain("# Title")
734
+ expect(md).toContain("**Body**")
735
+ expect(md).toContain("- item 1")
757
736
  })
758
737
 
759
- it('renders to text', async () => {
760
- const doc = createDocument().heading('Title').text('Body')
738
+ it("renders to text", async () => {
739
+ const doc = createDocument().heading("Title").text("Body")
761
740
 
762
741
  const text = await doc.toText()
763
- expect(text).toContain('TITLE')
764
- expect(text).toContain('Body')
742
+ expect(text).toContain("TITLE")
743
+ expect(text).toContain("Body")
765
744
  })
766
745
 
767
- it('renders to CSV', async () => {
768
- const doc = createDocument().table({ columns: ['X'], rows: [['1'], ['2']] })
746
+ it("renders to CSV", async () => {
747
+ const doc = createDocument().table({ columns: ["X"], rows: [["1"], ["2"]] })
769
748
 
770
749
  const csv = await doc.toCsv()
771
- expect(csv).toContain('X')
772
- expect(csv).toContain('1')
750
+ expect(csv).toContain("X")
751
+ expect(csv).toContain("1")
773
752
  })
774
753
 
775
- it('supports all builder methods', () => {
754
+ it("supports all builder methods", () => {
776
755
  const doc = createDocument()
777
- .heading('H')
778
- .text('T')
779
- .paragraph('P')
780
- .image('/img.png')
781
- .table({ columns: ['A'], rows: [['1']] })
782
- .list(['a', 'b'])
783
- .code('x = 1', { language: 'python' })
756
+ .heading("H")
757
+ .text("T")
758
+ .paragraph("P")
759
+ .image("/img.png")
760
+ .table({ columns: ["A"], rows: [["1"]] })
761
+ .list(["a", "b"])
762
+ .code("x = 1", { language: "python" })
784
763
  .divider()
785
764
  .spacer(20)
786
- .quote('Q')
787
- .button('Click', { href: '/go' })
788
- .link('Link', { href: '/link' })
765
+ .quote("Q")
766
+ .button("Click", { href: "/go" })
767
+ .link("Link", { href: "/link" })
789
768
 
790
769
  const node = doc.build()
791
- expect(node.type).toBe('document')
770
+ expect(node.type).toBe("document")
792
771
  })
793
772
 
794
- it('chart without instance shows placeholder', async () => {
773
+ it("chart without instance shows placeholder", async () => {
795
774
  const doc = createDocument().chart(null)
796
775
 
797
776
  const html = await doc.toHtml()
798
- expect(html).toContain('[Chart]')
777
+ expect(html).toContain("[Chart]")
799
778
  })
800
779
 
801
- it('flow without instance shows placeholder', async () => {
780
+ it("flow without instance shows placeholder", async () => {
802
781
  const doc = createDocument().flow(null)
803
782
 
804
783
  const html = await doc.toHtml()
805
- expect(html).toContain('[Flow Diagram]')
784
+ expect(html).toContain("[Flow Diagram]")
806
785
  })
807
786
 
808
- it('chart with getDataURL captures image', async () => {
787
+ it("chart with getDataURL captures image", async () => {
809
788
  const mockChart = {
810
- getDataURL: () => 'data:image/png;base64,abc123',
789
+ getDataURL: () => "data:image/png;base64,abc123",
811
790
  }
812
791
  const doc = createDocument().chart(mockChart, { width: 400 })
813
792
  const html = await doc.toHtml()
814
- expect(html).toContain('data:image/png;base64,abc123')
793
+ expect(html).toContain("data:image/png;base64,abc123")
815
794
  })
816
795
 
817
- it('flow with toSVG captures image', async () => {
796
+ it("flow with toSVG captures image", async () => {
818
797
  const mockFlow = {
819
- toSVG: () => '<svg><rect/></svg>',
798
+ toSVG: () => "<svg><rect/></svg>",
820
799
  }
821
800
  const doc = createDocument().flow(mockFlow, { width: 500 })
822
801
  const html = await doc.toHtml()
823
- expect(html).toContain('data:image/svg+xml')
802
+ expect(html).toContain("data:image/svg+xml")
824
803
  })
825
804
  })
826
805
 
827
806
  // ─── Custom Renderers ───────────────────────────────────────────────────────
828
807
 
829
- describe('custom renderers', () => {
830
- it('registerRenderer adds a custom format', async () => {
831
- registerRenderer('custom', {
808
+ describe("custom renderers", () => {
809
+ it("registerRenderer adds a custom format", async () => {
810
+ registerRenderer("custom", {
832
811
  async render(node) {
833
812
  return `CUSTOM:${node.type}`
834
813
  },
835
814
  })
836
815
 
837
- const doc = Document({ children: 'hello' })
838
- const result = await render(doc, 'custom')
839
- expect(result).toBe('CUSTOM:document')
816
+ const doc = Document({ children: "hello" })
817
+ const result = await render(doc, "custom")
818
+ expect(result).toBe("CUSTOM:document")
840
819
  })
841
820
 
842
- it('unregisterRenderer removes a format', () => {
843
- registerRenderer('temp', {
821
+ it("unregisterRenderer removes a format", () => {
822
+ registerRenderer("temp", {
844
823
  async render() {
845
- return 'x'
824
+ return "x"
846
825
  },
847
826
  })
848
- unregisterRenderer('temp')
849
- expect(render(Document({ children: 'x' }), 'temp')).rejects.toThrow(
850
- 'No renderer registered',
851
- )
827
+ unregisterRenderer("temp")
828
+ expect(render(Document({ children: "x" }), "temp")).rejects.toThrow("No renderer registered")
852
829
  })
853
830
 
854
- it('throws for unknown format', () => {
855
- expect(render(Document({ children: 'x' }), 'unknown')).rejects.toThrow(
856
- 'No renderer registered',
857
- )
831
+ it("throws for unknown format", () => {
832
+ expect(render(Document({ children: "x" }), "unknown")).rejects.toThrow("No renderer registered")
858
833
  })
859
834
 
860
- it('lazy renderer is cached after first use', async () => {
835
+ it("lazy renderer is cached after first use", async () => {
861
836
  let loadCount = 0
862
- registerRenderer('lazy', async () => {
837
+ registerRenderer("lazy", async () => {
863
838
  loadCount++
864
839
  return {
865
840
  async render() {
866
- return 'lazy-result'
841
+ return "lazy-result"
867
842
  },
868
843
  }
869
844
  })
870
845
 
871
- await render(Document({ children: 'x' }), 'lazy')
872
- await render(Document({ children: 'x' }), 'lazy')
846
+ await render(Document({ children: "x" }), "lazy")
847
+ await render(Document({ children: "x" }), "lazy")
873
848
  expect(loadCount).toBe(1)
874
849
  })
875
850
  })
876
851
 
877
852
  // ─── Real-World Document ────────────────────────────────────────────────────
878
853
 
879
- describe('real-world document', () => {
854
+ describe("real-world document", () => {
880
855
  function createInvoice() {
881
856
  return Document({
882
- title: 'Invoice #1234',
883
- author: 'Acme Corp',
857
+ title: "Invoice #1234",
858
+ author: "Acme Corp",
884
859
  children: Page({
885
- size: 'A4',
860
+ size: "A4",
886
861
  margin: 40,
887
862
  children: [
888
863
  Row({
889
864
  gap: 20,
890
865
  children: [
891
866
  Column({
892
- children: Image({ src: '/logo.png', width: 80, alt: 'Logo' }),
867
+ children: Image({ src: "/logo.png", width: 80, alt: "Logo" }),
893
868
  }),
894
869
  Column({
895
870
  children: [
896
- Heading({ children: 'Invoice #1234' }),
897
- Text({ color: '#666', children: 'March 23, 2026' }),
871
+ Heading({ children: "Invoice #1234" }),
872
+ Text({ color: "#666", children: "March 23, 2026" }),
898
873
  ],
899
874
  }),
900
875
  ],
@@ -902,557 +877,544 @@ describe('real-world document', () => {
902
877
  Spacer({ height: 30 }),
903
878
  Table({
904
879
  columns: [
905
- { header: 'Item', width: '50%' },
906
- { header: 'Qty', width: '15%', align: 'center' },
907
- { header: 'Price', width: '15%', align: 'right' },
908
- { header: 'Total', width: '20%', align: 'right' },
880
+ { header: "Item", width: "50%" },
881
+ { header: "Qty", width: "15%", align: "center" },
882
+ { header: "Price", width: "15%", align: "right" },
883
+ { header: "Total", width: "20%", align: "right" },
909
884
  ],
910
885
  rows: [
911
- ['Widget Pro', '2', '$50', '$100'],
912
- ['Gadget Plus', '1', '$75', '$75'],
913
- ['Service Fee', '1', '$25', '$25'],
886
+ ["Widget Pro", "2", "$50", "$100"],
887
+ ["Gadget Plus", "1", "$75", "$75"],
888
+ ["Service Fee", "1", "$25", "$25"],
914
889
  ],
915
890
  striped: true,
916
- headerStyle: { background: '#1a1a2e', color: '#fff' },
891
+ headerStyle: { background: "#1a1a2e", color: "#fff" },
917
892
  }),
918
893
  Spacer({ height: 20 }),
919
894
  Text({
920
895
  bold: true,
921
- align: 'right',
896
+ align: "right",
922
897
  size: 18,
923
- children: 'Total: $200',
898
+ children: "Total: $200",
924
899
  }),
925
900
  Divider(),
926
901
  Text({
927
- color: '#999',
902
+ color: "#999",
928
903
  size: 12,
929
- children: 'Thank you for your business!',
904
+ children: "Thank you for your business!",
930
905
  }),
931
906
  Button({
932
- href: 'https://acme.com/pay/1234',
933
- background: '#4f46e5',
934
- align: 'center',
935
- children: 'Pay Now',
907
+ href: "https://acme.com/pay/1234",
908
+ background: "#4f46e5",
909
+ align: "center",
910
+ children: "Pay Now",
936
911
  }),
937
912
  ],
938
913
  }),
939
914
  })
940
915
  }
941
916
 
942
- it('renders as HTML', async () => {
943
- const html = (await render(createInvoice(), 'html')) as string
944
- expect(html).toContain('Invoice #1234')
945
- expect(html).toContain('Widget Pro')
946
- expect(html).toContain('Total: $200')
947
- expect(html).toContain('Pay Now')
917
+ it("renders as HTML", async () => {
918
+ const html = (await render(createInvoice(), "html")) as string
919
+ expect(html).toContain("Invoice #1234")
920
+ expect(html).toContain("Widget Pro")
921
+ expect(html).toContain("Total: $200")
922
+ expect(html).toContain("Pay Now")
948
923
  })
949
924
 
950
- it('renders as email', async () => {
951
- const html = (await render(createInvoice(), 'email')) as string
952
- expect(html).toContain('Invoice #1234')
953
- expect(html).toContain('max-width:600px')
954
- expect(html).toContain('v:roundrect') // Outlook button
925
+ it("renders as email", async () => {
926
+ const html = (await render(createInvoice(), "email")) as string
927
+ expect(html).toContain("Invoice #1234")
928
+ expect(html).toContain("max-width:600px")
929
+ expect(html).toContain("v:roundrect") // Outlook button
955
930
  })
956
931
 
957
- it('renders as markdown', async () => {
958
- const md = (await render(createInvoice(), 'md')) as string
959
- expect(md).toContain('# Invoice #1234')
960
- expect(md).toContain('| Widget Pro')
961
- expect(md).toContain('**Total: $200**')
932
+ it("renders as markdown", async () => {
933
+ const md = (await render(createInvoice(), "md")) as string
934
+ expect(md).toContain("# Invoice #1234")
935
+ expect(md).toContain("| Widget Pro")
936
+ expect(md).toContain("**Total: $200**")
962
937
  })
963
938
 
964
- it('renders as text', async () => {
965
- const text = (await render(createInvoice(), 'text')) as string
966
- expect(text).toContain('INVOICE #1234')
967
- expect(text).toContain('Widget Pro')
968
- expect(text).toContain('Total: $200')
939
+ it("renders as text", async () => {
940
+ const text = (await render(createInvoice(), "text")) as string
941
+ expect(text).toContain("INVOICE #1234")
942
+ expect(text).toContain("Widget Pro")
943
+ expect(text).toContain("Total: $200")
969
944
  })
970
945
 
971
- it('renders as CSV', async () => {
972
- const csv = (await render(createInvoice(), 'csv')) as string
973
- expect(csv).toContain('Item,Qty,Price,Total')
974
- expect(csv).toContain('Widget Pro,2,$50,$100')
946
+ it("renders as CSV", async () => {
947
+ const csv = (await render(createInvoice(), "csv")) as string
948
+ expect(csv).toContain("Item,Qty,Price,Total")
949
+ expect(csv).toContain("Widget Pro,2,$50,$100")
975
950
  })
976
951
  })
977
952
 
978
953
  // ─── Text Renderer — additional coverage ────────────────────────────────────
979
954
 
980
- describe('text renderer — additional', () => {
981
- it('renders link with URL', async () => {
955
+ describe("text renderer — additional", () => {
956
+ it("renders link with URL", async () => {
982
957
  const doc = Document({
983
- children: Link({ href: 'https://x.com', children: 'Link' }),
958
+ children: Link({ href: "https://x.com", children: "Link" }),
984
959
  })
985
- const text = (await render(doc, 'text')) as string
986
- expect(text).toContain('Link (https://x.com)')
960
+ const text = (await render(doc, "text")) as string
961
+ expect(text).toContain("Link (https://x.com)")
987
962
  })
988
963
 
989
- it('renders table with caption', async () => {
964
+ it("renders table with caption", async () => {
990
965
  const doc = Document({
991
- children: Table({ columns: ['A'], rows: [['1']], caption: 'Data' }),
966
+ children: Table({ columns: ["A"], rows: [["1"]], caption: "Data" }),
992
967
  })
993
- const text = (await render(doc, 'text')) as string
994
- expect(text).toContain('Data')
968
+ const text = (await render(doc, "text")) as string
969
+ expect(text).toContain("Data")
995
970
  })
996
971
 
997
- it('renders ordered list', async () => {
972
+ it("renders ordered list", async () => {
998
973
  const doc = Document({
999
974
  children: List({
1000
975
  ordered: true,
1001
- children: [
1002
- ListItem({ children: 'one' }),
1003
- ListItem({ children: 'two' }),
1004
- ],
976
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
1005
977
  }),
1006
978
  })
1007
- const text = (await render(doc, 'text')) as string
1008
- expect(text).toContain('1. one')
1009
- expect(text).toContain('2. two')
979
+ const text = (await render(doc, "text")) as string
980
+ expect(text).toContain("1. one")
981
+ expect(text).toContain("2. two")
1010
982
  })
1011
983
 
1012
- it('renders unordered list', async () => {
984
+ it("renders unordered list", async () => {
1013
985
  const doc = Document({
1014
986
  children: List({
1015
- children: [ListItem({ children: 'a' }), ListItem({ children: 'b' })],
987
+ children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
1016
988
  }),
1017
989
  })
1018
- const text = (await render(doc, 'text')) as string
1019
- expect(text).toContain('* a')
1020
- expect(text).toContain('* b')
990
+ const text = (await render(doc, "text")) as string
991
+ expect(text).toContain("* a")
992
+ expect(text).toContain("* b")
1021
993
  })
1022
994
 
1023
- it('renders code block', async () => {
1024
- const doc = Document({ children: Code({ children: 'x = 1' }) })
1025
- const text = (await render(doc, 'text')) as string
1026
- expect(text).toContain('x = 1')
995
+ it("renders code block", async () => {
996
+ const doc = Document({ children: Code({ children: "x = 1" }) })
997
+ const text = (await render(doc, "text")) as string
998
+ expect(text).toContain("x = 1")
1027
999
  })
1028
1000
 
1029
- it('renders divider', async () => {
1001
+ it("renders divider", async () => {
1030
1002
  const doc = Document({ children: Divider() })
1031
- const text = (await render(doc, 'text')) as string
1032
- expect(text).toContain('')
1003
+ const text = (await render(doc, "text")) as string
1004
+ expect(text).toContain("")
1033
1005
  })
1034
1006
 
1035
- it('renders spacer as newline', async () => {
1007
+ it("renders spacer as newline", async () => {
1036
1008
  const doc = Document({
1037
- children: [
1038
- Text({ children: 'A' }),
1039
- Spacer({ height: 20 }),
1040
- Text({ children: 'B' }),
1041
- ],
1009
+ children: [Text({ children: "A" }), Spacer({ height: 20 }), Text({ children: "B" })],
1042
1010
  })
1043
- const text = (await render(doc, 'text')) as string
1044
- expect(text).toContain('A')
1045
- expect(text).toContain('B')
1011
+ const text = (await render(doc, "text")) as string
1012
+ expect(text).toContain("A")
1013
+ expect(text).toContain("B")
1046
1014
  })
1047
1015
 
1048
- it('renders quote with indentation', async () => {
1049
- const doc = Document({ children: Quote({ children: 'wise' }) })
1050
- const text = (await render(doc, 'text')) as string
1016
+ it("renders quote with indentation", async () => {
1017
+ const doc = Document({ children: Quote({ children: "wise" }) })
1018
+ const text = (await render(doc, "text")) as string
1051
1019
  expect(text).toContain('"wise"')
1052
1020
  })
1053
1021
 
1054
- it('renders section/row/column', async () => {
1022
+ it("renders section/row/column", async () => {
1055
1023
  const doc = Document({
1056
1024
  children: Section({
1057
1025
  children: Row({
1058
- children: Column({ children: Text({ children: 'nested' }) }),
1026
+ children: Column({ children: Text({ children: "nested" }) }),
1059
1027
  }),
1060
1028
  }),
1061
1029
  })
1062
- const text = (await render(doc, 'text')) as string
1063
- expect(text).toContain('nested')
1030
+ const text = (await render(doc, "text")) as string
1031
+ expect(text).toContain("nested")
1064
1032
  })
1065
1033
 
1066
- it('renders heading level 3+', async () => {
1067
- const doc = Document({ children: Heading({ level: 4, children: 'Sub' }) })
1068
- const text = (await render(doc, 'text')) as string
1069
- expect(text).toContain('Sub')
1034
+ it("renders heading level 3+", async () => {
1035
+ const doc = Document({ children: Heading({ level: 4, children: "Sub" }) })
1036
+ const text = (await render(doc, "text")) as string
1037
+ expect(text).toContain("Sub")
1070
1038
  // Level 3+ should not have underline
1071
- expect(text).not.toContain('===')
1072
- expect(text).not.toContain('---')
1039
+ expect(text).not.toContain("===")
1040
+ expect(text).not.toContain("---")
1073
1041
  })
1074
1042
 
1075
- it('renders table with center aligned column', async () => {
1043
+ it("renders table with center aligned column", async () => {
1076
1044
  const doc = Document({
1077
1045
  children: Table({
1078
- columns: [{ header: 'Name', align: 'center' }],
1079
- rows: [['X']],
1046
+ columns: [{ header: "Name", align: "center" }],
1047
+ rows: [["X"]],
1080
1048
  }),
1081
1049
  })
1082
- const text = (await render(doc, 'text')) as string
1083
- expect(text).toContain('Name')
1084
- expect(text).toContain('X')
1050
+ const text = (await render(doc, "text")) as string
1051
+ expect(text).toContain("Name")
1052
+ expect(text).toContain("X")
1085
1053
  })
1086
1054
  })
1087
1055
 
1088
1056
  // ─── Builder — additional coverage ───────────────────────────────────────────
1089
1057
 
1090
- describe('builder — additional', () => {
1091
- it('pageBreak wraps content', () => {
1092
- const doc = createDocument().heading('Page 1').pageBreak().heading('Page 2')
1058
+ describe("builder — additional", () => {
1059
+ it("pageBreak wraps content", () => {
1060
+ const doc = createDocument().heading("Page 1").pageBreak().heading("Page 2")
1093
1061
  const node = doc.build()
1094
- expect(node.type).toBe('document')
1062
+ expect(node.type).toBe("document")
1095
1063
  })
1096
1064
 
1097
- it('toEmail renders', async () => {
1098
- const html = await createDocument().heading('Hi').toEmail()
1099
- expect(html).toContain('Hi')
1100
- expect(html).toContain('max-width:600px')
1065
+ it("toEmail renders", async () => {
1066
+ const html = await createDocument().heading("Hi").toEmail()
1067
+ expect(html).toContain("Hi")
1068
+ expect(html).toContain("max-width:600px")
1101
1069
  })
1102
1070
 
1103
- it('toCsv renders', async () => {
1071
+ it("toCsv renders", async () => {
1104
1072
  const csv = await createDocument()
1105
- .table({ columns: ['X'], rows: [['1']] })
1073
+ .table({ columns: ["X"], rows: [["1"]] })
1106
1074
  .toCsv()
1107
- expect(csv).toContain('X')
1075
+ expect(csv).toContain("X")
1108
1076
  })
1109
1077
 
1110
- it('toText renders', async () => {
1111
- const text = await createDocument().heading('Hi').toText()
1112
- expect(text).toContain('HI')
1078
+ it("toText renders", async () => {
1079
+ const text = await createDocument().heading("Hi").toText()
1080
+ expect(text).toContain("HI")
1113
1081
  })
1114
1082
  })
1115
1083
 
1116
1084
  // ─── Markdown — additional branch coverage ──────────────────────────────────
1117
1085
 
1118
- describe('markdown — additional branches', () => {
1119
- it('renders table with center aligned column', async () => {
1086
+ describe("markdown — additional branches", () => {
1087
+ it("renders table with center aligned column", async () => {
1120
1088
  const doc = Document({
1121
1089
  children: Table({
1122
- columns: [{ header: 'X', align: 'center' }],
1123
- rows: [['1']],
1090
+ columns: [{ header: "X", align: "center" }],
1091
+ rows: [["1"]],
1124
1092
  }),
1125
1093
  })
1126
- const md = (await render(doc, 'md')) as string
1127
- expect(md).toContain(':---:')
1094
+ const md = (await render(doc, "md")) as string
1095
+ expect(md).toContain(":---:")
1128
1096
  })
1129
1097
 
1130
- it('renders table with left aligned column (default)', async () => {
1098
+ it("renders table with left aligned column (default)", async () => {
1131
1099
  const doc = Document({
1132
1100
  children: Table({
1133
- columns: [{ header: 'X', align: 'left' }],
1134
- rows: [['1']],
1101
+ columns: [{ header: "X", align: "left" }],
1102
+ rows: [["1"]],
1135
1103
  }),
1136
1104
  })
1137
- const md = (await render(doc, 'md')) as string
1138
- expect(md).toContain('| --- |')
1105
+ const md = (await render(doc, "md")) as string
1106
+ expect(md).toContain("| --- |")
1139
1107
  })
1140
1108
 
1141
- it('renders empty table gracefully', async () => {
1109
+ it("renders empty table gracefully", async () => {
1142
1110
  const doc = Document({
1143
1111
  children: Table({ columns: [], rows: [] }),
1144
1112
  })
1145
- const md = (await render(doc, 'md')) as string
1113
+ const md = (await render(doc, "md")) as string
1146
1114
  expect(md).toBeDefined()
1147
1115
  })
1148
1116
 
1149
- it('renders image without caption', async () => {
1150
- const doc = Document({ children: Image({ src: '/x.png', alt: 'X' }) })
1151
- const md = (await render(doc, 'md')) as string
1152
- expect(md).toContain('![X](/x.png)')
1153
- expect(md).not.toContain('*')
1117
+ it("renders image without caption", async () => {
1118
+ const doc = Document({ children: Image({ src: "/x.png", alt: "X" }) })
1119
+ const md = (await render(doc, "md")) as string
1120
+ expect(md).toContain("![X](/x.png)")
1121
+ expect(md).not.toContain("*")
1154
1122
  })
1155
1123
 
1156
- it('renders image without alt', async () => {
1157
- const doc = Document({ children: Image({ src: '/x.png' }) })
1158
- const md = (await render(doc, 'md')) as string
1159
- expect(md).toContain('![](/x.png)')
1124
+ it("renders image without alt", async () => {
1125
+ const doc = Document({ children: Image({ src: "/x.png" }) })
1126
+ const md = (await render(doc, "md")) as string
1127
+ expect(md).toContain("![](/x.png)")
1160
1128
  })
1161
1129
 
1162
- it('renders code without language', async () => {
1163
- const doc = Document({ children: Code({ children: 'x = 1' }) })
1164
- const md = (await render(doc, 'md')) as string
1165
- expect(md).toContain('```\nx = 1\n```')
1130
+ it("renders code without language", async () => {
1131
+ const doc = Document({ children: Code({ children: "x = 1" }) })
1132
+ const md = (await render(doc, "md")) as string
1133
+ expect(md).toContain("```\nx = 1\n```")
1166
1134
  })
1167
1135
 
1168
- it('renders spacer as newline', async () => {
1136
+ it("renders spacer as newline", async () => {
1169
1137
  const doc = Document({ children: Spacer({ height: 20 }) })
1170
- const md = (await render(doc, 'md')) as string
1138
+ const md = (await render(doc, "md")) as string
1171
1139
  expect(md).toBeDefined()
1172
1140
  })
1173
1141
  })
1174
1142
 
1175
1143
  // ─── Email — additional branch coverage ─────────────────────────────────────
1176
1144
 
1177
- describe('email — additional branches', () => {
1178
- it('renders section with background and padding', async () => {
1145
+ describe("email — additional branches", () => {
1146
+ it("renders section with background and padding", async () => {
1179
1147
  const doc = Document({
1180
1148
  children: Section({
1181
- background: '#f00',
1149
+ background: "#f00",
1182
1150
  padding: [10, 20],
1183
1151
  borderRadius: 8,
1184
- children: Text({ children: 'hi' }),
1152
+ children: Text({ children: "hi" }),
1185
1153
  }),
1186
1154
  })
1187
- const html = (await render(doc, 'email')) as string
1188
- expect(html).toContain('background-color:#f00')
1189
- expect(html).toContain('border-radius:8px')
1155
+ const html = (await render(doc, "email")) as string
1156
+ expect(html).toContain("background-color:#f00")
1157
+ expect(html).toContain("border-radius:8px")
1190
1158
  })
1191
1159
 
1192
- it('renders image with right alignment', async () => {
1193
- const doc = Document({ children: Image({ src: '/x.png', align: 'right' }) })
1194
- const html = (await render(doc, 'email')) as string
1195
- expect(html).toContain('text-align:right')
1160
+ it("renders image with right alignment", async () => {
1161
+ const doc = Document({ children: Image({ src: "/x.png", align: "right" }) })
1162
+ const html = (await render(doc, "email")) as string
1163
+ expect(html).toContain("text-align:right")
1196
1164
  })
1197
1165
 
1198
- it('renders striped table', async () => {
1166
+ it("renders striped table", async () => {
1199
1167
  const doc = Document({
1200
1168
  children: Table({
1201
- columns: ['A'],
1202
- rows: [['1'], ['2'], ['3']],
1169
+ columns: ["A"],
1170
+ rows: [["1"], ["2"], ["3"]],
1203
1171
  striped: true,
1204
1172
  }),
1205
1173
  })
1206
- const html = (await render(doc, 'email')) as string
1207
- expect(html).toContain('background-color:#f9f9f9')
1174
+ const html = (await render(doc, "email")) as string
1175
+ expect(html).toContain("background-color:#f9f9f9")
1208
1176
  })
1209
1177
 
1210
- it('renders heading level 2', async () => {
1211
- const doc = Document({ children: Heading({ level: 2, children: 'Sub' }) })
1212
- const html = (await render(doc, 'email')) as string
1213
- expect(html).toContain('<h2')
1214
- expect(html).toContain('font-size:24px')
1178
+ it("renders heading level 2", async () => {
1179
+ const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
1180
+ const html = (await render(doc, "email")) as string
1181
+ expect(html).toContain("<h2")
1182
+ expect(html).toContain("font-size:24px")
1215
1183
  })
1216
1184
 
1217
- it('renders text with all formatting options', async () => {
1185
+ it("renders text with all formatting options", async () => {
1218
1186
  const doc = Document({
1219
1187
  children: Text({
1220
1188
  size: 16,
1221
1189
  bold: true,
1222
1190
  italic: true,
1223
1191
  underline: true,
1224
- align: 'center',
1192
+ align: "center",
1225
1193
  lineHeight: 2,
1226
- children: 'styled',
1194
+ children: "styled",
1227
1195
  }),
1228
1196
  })
1229
- const html = (await render(doc, 'email')) as string
1230
- expect(html).toContain('font-size:16px')
1231
- expect(html).toContain('font-weight:bold')
1232
- expect(html).toContain('font-style:italic')
1233
- expect(html).toContain('text-decoration:underline')
1234
- expect(html).toContain('text-align:center')
1197
+ const html = (await render(doc, "email")) as string
1198
+ expect(html).toContain("font-size:16px")
1199
+ expect(html).toContain("font-weight:bold")
1200
+ expect(html).toContain("font-style:italic")
1201
+ expect(html).toContain("text-decoration:underline")
1202
+ expect(html).toContain("text-align:center")
1235
1203
  })
1236
1204
 
1237
- it('renders text with strikethrough', async () => {
1205
+ it("renders text with strikethrough", async () => {
1238
1206
  const doc = Document({
1239
- children: Text({ strikethrough: true, children: 'old' }),
1207
+ children: Text({ strikethrough: true, children: "old" }),
1240
1208
  })
1241
- const html = (await render(doc, 'email')) as string
1242
- expect(html).toContain('text-decoration:line-through')
1209
+ const html = (await render(doc, "email")) as string
1210
+ expect(html).toContain("text-decoration:line-through")
1243
1211
  })
1244
1212
 
1245
- it('renders button with custom alignment', async () => {
1213
+ it("renders button with custom alignment", async () => {
1246
1214
  const doc = Document({
1247
- children: Button({ href: '/x', align: 'center', children: 'Go' }),
1215
+ children: Button({ href: "/x", align: "center", children: "Go" }),
1248
1216
  })
1249
- const html = (await render(doc, 'email')) as string
1250
- expect(html).toContain('text-align:center')
1217
+ const html = (await render(doc, "email")) as string
1218
+ expect(html).toContain("text-align:center")
1251
1219
  })
1252
1220
 
1253
- it('renders section column direction (default)', async () => {
1221
+ it("renders section column direction (default)", async () => {
1254
1222
  const doc = Document({
1255
- children: Section({ children: Text({ children: 'content' }) }),
1223
+ children: Section({ children: Text({ children: "content" }) }),
1256
1224
  })
1257
- const html = (await render(doc, 'email')) as string
1258
- expect(html).toContain('content')
1225
+ const html = (await render(doc, "email")) as string
1226
+ expect(html).toContain("content")
1259
1227
  })
1260
1228
 
1261
- it('renders section with gap in row', async () => {
1229
+ it("renders section with gap in row", async () => {
1262
1230
  const doc = Document({
1263
1231
  children: Section({
1264
- direction: 'row',
1232
+ direction: "row",
1265
1233
  gap: 16,
1266
- children: [Text({ children: 'a' }), Text({ children: 'b' })],
1234
+ children: [Text({ children: "a" }), Text({ children: "b" })],
1267
1235
  }),
1268
1236
  })
1269
- const html = (await render(doc, 'email')) as string
1270
- expect(html).toContain('padding:0 8px')
1237
+ const html = (await render(doc, "email")) as string
1238
+ expect(html).toContain("padding:0 8px")
1271
1239
  })
1272
1240
  })
1273
1241
 
1274
1242
  // ─── HTML — additional branch coverage ──────────────────────────────────────
1275
1243
 
1276
- describe('html — additional branches', () => {
1277
- it('renders section column direction (default)', async () => {
1244
+ describe("html — additional branches", () => {
1245
+ it("renders section column direction (default)", async () => {
1278
1246
  const doc = Document({
1279
- children: Section({ children: Text({ children: 'x' }) }),
1247
+ children: Section({ children: Text({ children: "x" }) }),
1280
1248
  })
1281
- const html = (await render(doc, 'html')) as string
1282
- expect(html).not.toContain('display:flex')
1249
+ const html = (await render(doc, "html")) as string
1250
+ expect(html).not.toContain("display:flex")
1283
1251
  })
1284
1252
 
1285
- it('renders page with margin as array', async () => {
1253
+ it("renders page with margin as array", async () => {
1286
1254
  const doc = Document({
1287
- children: Page({ margin: [10, 20], children: Text({ children: 'hi' }) }),
1255
+ children: Page({ margin: [10, 20], children: Text({ children: "hi" }) }),
1288
1256
  })
1289
- const html = (await render(doc, 'html')) as string
1290
- expect(html).toContain('10px 20px')
1257
+ const html = (await render(doc, "html")) as string
1258
+ expect(html).toContain("10px 20px")
1291
1259
  })
1292
1260
 
1293
- it('renders page with 4-value margin', async () => {
1261
+ it("renders page with 4-value margin", async () => {
1294
1262
  const doc = Document({
1295
1263
  children: Page({
1296
1264
  margin: [10, 20, 30, 40],
1297
- children: Text({ children: 'hi' }),
1265
+ children: Text({ children: "hi" }),
1298
1266
  }),
1299
1267
  })
1300
- const html = (await render(doc, 'html')) as string
1301
- expect(html).toContain('10px 20px 30px 40px')
1268
+ const html = (await render(doc, "html")) as string
1269
+ expect(html).toContain("10px 20px 30px 40px")
1302
1270
  })
1303
1271
 
1304
- it('renders text with lineHeight', async () => {
1272
+ it("renders text with lineHeight", async () => {
1305
1273
  const doc = Document({
1306
- children: Text({ lineHeight: 1.8, children: 'text' }),
1274
+ children: Text({ lineHeight: 1.8, children: "text" }),
1307
1275
  })
1308
- const html = (await render(doc, 'html')) as string
1309
- expect(html).toContain('line-height:1.8')
1276
+ const html = (await render(doc, "html")) as string
1277
+ expect(html).toContain("line-height:1.8")
1310
1278
  })
1311
1279
  })
1312
1280
 
1313
1281
  // ─── CSV — additional branch coverage ───────────────────────────────────────
1314
1282
 
1315
- describe('csv — additional branches', () => {
1316
- it('finds tables nested in pages', async () => {
1283
+ describe("csv — additional branches", () => {
1284
+ it("finds tables nested in pages", async () => {
1317
1285
  const doc = Document({
1318
1286
  children: Page({
1319
1287
  children: Section({
1320
- children: Table({ columns: ['Nested'], rows: [['val']] }),
1288
+ children: Table({ columns: ["Nested"], rows: [["val"]] }),
1321
1289
  }),
1322
1290
  }),
1323
1291
  })
1324
- const csv = (await render(doc, 'csv')) as string
1325
- expect(csv).toContain('Nested')
1326
- expect(csv).toContain('val')
1292
+ const csv = (await render(doc, "csv")) as string
1293
+ expect(csv).toContain("Nested")
1294
+ expect(csv).toContain("val")
1327
1295
  })
1328
1296
  })
1329
1297
 
1330
1298
  // ─── Render Dispatcher — additional coverage ────────────────────────────────
1331
1299
 
1332
- describe('render dispatcher — additional', () => {
1333
- it('error message includes available formats', async () => {
1300
+ describe("render dispatcher — additional", () => {
1301
+ it("error message includes available formats", async () => {
1334
1302
  try {
1335
- await render(Document({ children: 'x' }), 'nonexistent')
1303
+ await render(Document({ children: "x" }), "nonexistent")
1336
1304
  } catch (e) {
1337
- expect((e as Error).message).toContain('No renderer registered')
1338
- expect((e as Error).message).toContain('Available:')
1339
- expect((e as Error).message).toContain('html')
1305
+ expect((e as Error).message).toContain("No renderer registered")
1306
+ expect((e as Error).message).toContain("Available:")
1307
+ expect((e as Error).message).toContain("html")
1340
1308
  }
1341
1309
  })
1342
1310
  })
1343
1311
 
1344
1312
  // ─── Email Renderer — additional coverage ───────────────────────────────────
1345
1313
 
1346
- describe('email renderer — additional', () => {
1347
- it('renders image with caption', async () => {
1314
+ describe("email renderer — additional", () => {
1315
+ it("renders image with caption", async () => {
1348
1316
  const doc = Document({
1349
- children: Image({ src: '/x.png', alt: 'Photo', caption: 'Nice' }),
1317
+ children: Image({ src: "/x.png", alt: "Photo", caption: "Nice" }),
1350
1318
  })
1351
- const html = (await render(doc, 'email')) as string
1352
- expect(html).toContain('Nice')
1319
+ const html = (await render(doc, "email")) as string
1320
+ expect(html).toContain("Nice")
1353
1321
  })
1354
1322
 
1355
- it('renders image with center alignment', async () => {
1323
+ it("renders image with center alignment", async () => {
1356
1324
  const doc = Document({
1357
- children: Image({ src: '/x.png', align: 'center' }),
1325
+ children: Image({ src: "/x.png", align: "center" }),
1358
1326
  })
1359
- const html = (await render(doc, 'email')) as string
1360
- expect(html).toContain('text-align:center')
1327
+ const html = (await render(doc, "email")) as string
1328
+ expect(html).toContain("text-align:center")
1361
1329
  })
1362
1330
 
1363
- it('renders code block', async () => {
1364
- const doc = Document({ children: Code({ children: 'const x = 1' }) })
1365
- const html = (await render(doc, 'email')) as string
1366
- expect(html).toContain('Courier New')
1367
- expect(html).toContain('const x = 1')
1331
+ it("renders code block", async () => {
1332
+ const doc = Document({ children: Code({ children: "const x = 1" }) })
1333
+ const html = (await render(doc, "email")) as string
1334
+ expect(html).toContain("Courier New")
1335
+ expect(html).toContain("const x = 1")
1368
1336
  })
1369
1337
 
1370
- it('renders spacer with line-height trick', async () => {
1338
+ it("renders spacer with line-height trick", async () => {
1371
1339
  const doc = Document({ children: Spacer({ height: 20 }) })
1372
- const html = (await render(doc, 'email')) as string
1373
- expect(html).toContain('height:20px')
1374
- expect(html).toContain('line-height:20px')
1340
+ const html = (await render(doc, "email")) as string
1341
+ expect(html).toContain("height:20px")
1342
+ expect(html).toContain("line-height:20px")
1375
1343
  })
1376
1344
 
1377
- it('renders list', async () => {
1345
+ it("renders list", async () => {
1378
1346
  const doc = Document({
1379
1347
  children: List({
1380
- children: [
1381
- ListItem({ children: 'one' }),
1382
- ListItem({ children: 'two' }),
1383
- ],
1348
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
1384
1349
  }),
1385
1350
  })
1386
- const html = (await render(doc, 'email')) as string
1387
- expect(html).toContain('<ul')
1388
- expect(html).toContain('<li')
1351
+ const html = (await render(doc, "email")) as string
1352
+ expect(html).toContain("<ul")
1353
+ expect(html).toContain("<li")
1389
1354
  })
1390
1355
 
1391
- it('renders table caption', async () => {
1356
+ it("renders table caption", async () => {
1392
1357
  const doc = Document({
1393
- children: Table({ columns: ['A'], rows: [['1']], caption: 'Data' }),
1358
+ children: Table({ columns: ["A"], rows: [["1"]], caption: "Data" }),
1394
1359
  })
1395
- const html = (await render(doc, 'email')) as string
1396
- expect(html).toContain('Data')
1360
+ const html = (await render(doc, "email")) as string
1361
+ expect(html).toContain("Data")
1397
1362
  })
1398
1363
 
1399
- it('renders link with target _blank', async () => {
1364
+ it("renders link with target _blank", async () => {
1400
1365
  const doc = Document({
1401
- children: Link({ href: 'https://x.com', children: 'X' }),
1366
+ children: Link({ href: "https://x.com", children: "X" }),
1402
1367
  })
1403
- const html = (await render(doc, 'email')) as string
1368
+ const html = (await render(doc, "email")) as string
1404
1369
  expect(html).toContain('target="_blank"')
1405
1370
  })
1406
1371
 
1407
- it('renders row layout using tables', async () => {
1372
+ it("renders row layout using tables", async () => {
1408
1373
  const doc = Document({
1409
1374
  children: Row({
1410
1375
  gap: 10,
1411
- children: [Text({ children: 'L' }), Text({ children: 'R' })],
1376
+ children: [Text({ children: "L" }), Text({ children: "R" })],
1412
1377
  }),
1413
1378
  })
1414
- const html = (await render(doc, 'email')) as string
1415
- expect(html).toContain('<table')
1379
+ const html = (await render(doc, "email")) as string
1380
+ expect(html).toContain("<table")
1416
1381
  expect(html).toContain('valign="top"')
1417
1382
  })
1418
1383
  })
1419
1384
 
1420
1385
  // ─── DOCX Renderer (integration) ────────────────────────────────────────────
1421
1386
 
1422
- describe('DOCX renderer', () => {
1423
- it('renders a document with heading, text, table, list, code, divider to a valid Uint8Array', async () => {
1387
+ describe("DOCX renderer", () => {
1388
+ it("renders a document with heading, text, table, list, code, divider to a valid Uint8Array", async () => {
1424
1389
  const doc = Document({
1425
- title: 'DOCX Test',
1426
- author: 'Test Suite',
1390
+ title: "DOCX Test",
1391
+ author: "Test Suite",
1427
1392
  children: Page({
1428
- size: 'A4',
1393
+ size: "A4",
1429
1394
  margin: 40,
1430
1395
  children: [
1431
- Heading({ children: 'DOCX Integration Test' }),
1432
- Text({ children: 'A test paragraph.', bold: true }),
1396
+ Heading({ children: "DOCX Integration Test" }),
1397
+ Text({ children: "A test paragraph.", bold: true }),
1433
1398
  Table({
1434
- columns: ['Name', 'Value'],
1399
+ columns: ["Name", "Value"],
1435
1400
  rows: [
1436
- ['Alpha', '100'],
1437
- ['Beta', '200'],
1401
+ ["Alpha", "100"],
1402
+ ["Beta", "200"],
1438
1403
  ],
1439
1404
  striped: true,
1440
- headerStyle: { background: '#333333', color: '#ffffff' },
1405
+ headerStyle: { background: "#333333", color: "#ffffff" },
1441
1406
  }),
1442
1407
  List({
1443
1408
  ordered: true,
1444
- children: [
1445
- ListItem({ children: 'First' }),
1446
- ListItem({ children: 'Second' }),
1447
- ],
1409
+ children: [ListItem({ children: "First" }), ListItem({ children: "Second" })],
1448
1410
  }),
1449
- Code({ children: 'const x = 42' }),
1411
+ Code({ children: "const x = 42" }),
1450
1412
  Divider(),
1451
1413
  ],
1452
1414
  }),
1453
1415
  })
1454
1416
 
1455
- const result = await render(doc, 'docx')
1417
+ const result = await render(doc, "docx")
1456
1418
  expect(result).toBeInstanceOf(Uint8Array)
1457
1419
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1458
1420
  // DOCX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
@@ -1460,81 +1422,76 @@ describe('DOCX renderer', () => {
1460
1422
  expect((result as Uint8Array)[1]).toBe(0x4b)
1461
1423
  }, 15000)
1462
1424
 
1463
- it('embeds base64 images via ImageRun', async () => {
1425
+ it("embeds base64 images via ImageRun", async () => {
1464
1426
  // 1x1 red pixel PNG as base64
1465
1427
  const redPixel =
1466
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
1428
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
1467
1429
 
1468
1430
  const doc = Document({
1469
1431
  children: Page({
1470
- children: [
1471
- Image({ src: redPixel, width: 50, height: 50, caption: 'Red pixel' }),
1472
- ],
1432
+ children: [Image({ src: redPixel, width: 50, height: 50, caption: "Red pixel" })],
1473
1433
  }),
1474
1434
  })
1475
1435
 
1476
- const result = await render(doc, 'docx')
1436
+ const result = await render(doc, "docx")
1477
1437
  expect(result).toBeInstanceOf(Uint8Array)
1478
1438
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1479
1439
  }, 15000)
1480
1440
 
1481
- it('renders external URL images as placeholders', async () => {
1441
+ it("renders external URL images as placeholders", async () => {
1482
1442
  const doc = Document({
1483
1443
  children: Image({
1484
- src: 'https://example.com/logo.png',
1485
- alt: 'Logo',
1486
- caption: 'Company',
1444
+ src: "https://example.com/logo.png",
1445
+ alt: "Logo",
1446
+ caption: "Company",
1487
1447
  }),
1488
1448
  })
1489
1449
 
1490
- const result = await render(doc, 'docx')
1450
+ const result = await render(doc, "docx")
1491
1451
  expect(result).toBeInstanceOf(Uint8Array)
1492
1452
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1493
1453
  }, 15000)
1494
1454
 
1495
- it('renders page with header and footer', async () => {
1455
+ it("renders page with header and footer", async () => {
1496
1456
  const doc = Document({
1497
1457
  children: Page({
1498
- header: Text({ children: 'My Header' }),
1499
- footer: Text({ children: 'Page Footer' }),
1500
- children: [
1501
- Heading({ children: 'Content' }),
1502
- Text({ children: 'Body text.' }),
1503
- ],
1458
+ header: Text({ children: "My Header" }),
1459
+ footer: Text({ children: "Page Footer" }),
1460
+ children: [Heading({ children: "Content" }), Text({ children: "Body text." })],
1504
1461
  }),
1505
1462
  })
1506
1463
 
1507
- const result = await render(doc, 'docx')
1464
+ const result = await render(doc, "docx")
1508
1465
  expect(result).toBeInstanceOf(Uint8Array)
1509
1466
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1510
1467
  }, 15000)
1511
1468
 
1512
- it('renders table with bordered option and column widths', async () => {
1469
+ it("renders table with bordered option and column widths", async () => {
1513
1470
  const doc = Document({
1514
1471
  children: Table({
1515
1472
  columns: [
1516
- { header: 'Name', width: '60%' },
1517
- { header: 'Price', width: '40%', align: 'right' },
1473
+ { header: "Name", width: "60%" },
1474
+ { header: "Price", width: "40%", align: "right" },
1518
1475
  ],
1519
- rows: [['Widget', '$10']],
1476
+ rows: [["Widget", "$10"]],
1520
1477
  bordered: true,
1521
1478
  }),
1522
1479
  })
1523
1480
 
1524
- const result = await render(doc, 'docx')
1481
+ const result = await render(doc, "docx")
1525
1482
  expect(result).toBeInstanceOf(Uint8Array)
1526
1483
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1527
1484
  }, 15000)
1528
1485
 
1529
- it('renders nested lists', async () => {
1486
+ it("renders nested lists", async () => {
1530
1487
  const doc = Document({
1531
1488
  children: List({
1532
1489
  children: [
1533
1490
  ListItem({
1534
1491
  children: [
1535
- 'Parent item',
1492
+ "Parent item",
1536
1493
  List({
1537
- children: [ListItem({ children: 'Child item' })],
1494
+ children: [ListItem({ children: "Child item" })],
1538
1495
  }),
1539
1496
  ],
1540
1497
  }),
@@ -1542,7 +1499,7 @@ describe('DOCX renderer', () => {
1542
1499
  }),
1543
1500
  })
1544
1501
 
1545
- const result = await render(doc, 'docx')
1502
+ const result = await render(doc, "docx")
1546
1503
  expect(result).toBeInstanceOf(Uint8Array)
1547
1504
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1548
1505
  }, 15000)
@@ -1550,28 +1507,28 @@ describe('DOCX renderer', () => {
1550
1507
 
1551
1508
  // ─── XLSX Renderer (integration) ────────────────────────────────────────────
1552
1509
 
1553
- describe('XLSX renderer', () => {
1554
- it('renders a document with tables to a valid Uint8Array', async () => {
1510
+ describe("XLSX renderer", () => {
1511
+ it("renders a document with tables to a valid Uint8Array", async () => {
1555
1512
  const doc = Document({
1556
- title: 'XLSX Test',
1557
- author: 'Test Suite',
1513
+ title: "XLSX Test",
1514
+ author: "Test Suite",
1558
1515
  children: Page({
1559
1516
  children: [
1560
- Heading({ children: 'Sales Data' }),
1517
+ Heading({ children: "Sales Data" }),
1561
1518
  Table({
1562
- columns: ['Product', 'Revenue', 'Margin'],
1519
+ columns: ["Product", "Revenue", "Margin"],
1563
1520
  rows: [
1564
- ['Widget', '$1,234.56', '15%'],
1565
- ['Gadget', '$2,500.00', '22.5%'],
1521
+ ["Widget", "$1,234.56", "15%"],
1522
+ ["Gadget", "$2,500.00", "22.5%"],
1566
1523
  ],
1567
1524
  striped: true,
1568
- headerStyle: { background: '#1a1a2e', color: '#ffffff' },
1525
+ headerStyle: { background: "#1a1a2e", color: "#ffffff" },
1569
1526
  }),
1570
1527
  ],
1571
1528
  }),
1572
1529
  })
1573
1530
 
1574
- const result = await render(doc, 'xlsx')
1531
+ const result = await render(doc, "xlsx")
1575
1532
  expect(result).toBeInstanceOf(Uint8Array)
1576
1533
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1577
1534
  // XLSX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
@@ -1579,75 +1536,75 @@ describe('XLSX renderer', () => {
1579
1536
  expect((result as Uint8Array)[1]).toBe(0x4b)
1580
1537
  }, 15000)
1581
1538
 
1582
- it('parses currency values as numbers', async () => {
1539
+ it("parses currency values as numbers", async () => {
1583
1540
  const doc = Document({
1584
1541
  children: Table({
1585
- columns: ['Amount'],
1586
- rows: [['$1,234.56'], ['$500'], ['-$100.50']],
1542
+ columns: ["Amount"],
1543
+ rows: [["$1,234.56"], ["$500"], ["-$100.50"]],
1587
1544
  }),
1588
1545
  })
1589
1546
 
1590
- const result = await render(doc, 'xlsx')
1547
+ const result = await render(doc, "xlsx")
1591
1548
  expect(result).toBeInstanceOf(Uint8Array)
1592
1549
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1593
1550
  }, 15000)
1594
1551
 
1595
- it('parses percentage values', async () => {
1552
+ it("parses percentage values", async () => {
1596
1553
  const doc = Document({
1597
1554
  children: Table({
1598
- columns: ['Rate'],
1599
- rows: [['45%'], ['12.5%'], ['-3%']],
1555
+ columns: ["Rate"],
1556
+ rows: [["45%"], ["12.5%"], ["-3%"]],
1600
1557
  }),
1601
1558
  })
1602
1559
 
1603
- const result = await render(doc, 'xlsx')
1560
+ const result = await render(doc, "xlsx")
1604
1561
  expect(result).toBeInstanceOf(Uint8Array)
1605
1562
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1606
1563
  }, 15000)
1607
1564
 
1608
- it('renders multiple tables on the same sheet with spacing', async () => {
1565
+ it("renders multiple tables on the same sheet with spacing", async () => {
1609
1566
  const doc = Document({
1610
1567
  children: [
1611
- Heading({ children: 'Report' }),
1568
+ Heading({ children: "Report" }),
1612
1569
  Table({
1613
- columns: ['A', 'B'],
1570
+ columns: ["A", "B"],
1614
1571
  rows: [
1615
- ['1', '2'],
1616
- ['3', '4'],
1572
+ ["1", "2"],
1573
+ ["3", "4"],
1617
1574
  ],
1618
- caption: 'First Table',
1575
+ caption: "First Table",
1619
1576
  }),
1620
1577
  Table({
1621
- columns: ['X', 'Y'],
1622
- rows: [['a', 'b']],
1623
- caption: 'Second Table',
1578
+ columns: ["X", "Y"],
1579
+ rows: [["a", "b"]],
1580
+ caption: "Second Table",
1624
1581
  }),
1625
1582
  ],
1626
1583
  })
1627
1584
 
1628
- const result = await render(doc, 'xlsx')
1585
+ const result = await render(doc, "xlsx")
1629
1586
  expect(result).toBeInstanceOf(Uint8Array)
1630
1587
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1631
1588
  }, 15000)
1632
1589
 
1633
- it('renders bordered tables', async () => {
1590
+ it("renders bordered tables", async () => {
1634
1591
  const doc = Document({
1635
1592
  children: Table({
1636
- columns: ['Name', 'Value'],
1637
- rows: [['Alpha', '100']],
1593
+ columns: ["Name", "Value"],
1594
+ rows: [["Alpha", "100"]],
1638
1595
  bordered: true,
1639
1596
  }),
1640
1597
  })
1641
1598
 
1642
- const result = await render(doc, 'xlsx')
1599
+ const result = await render(doc, "xlsx")
1643
1600
  expect(result).toBeInstanceOf(Uint8Array)
1644
1601
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1645
1602
  }, 15000)
1646
1603
 
1647
- it('renders empty document with default sheet', async () => {
1648
- const doc = Document({ children: Text({ children: 'no tables' }) })
1604
+ it("renders empty document with default sheet", async () => {
1605
+ const doc = Document({ children: Text({ children: "no tables" }) })
1649
1606
 
1650
- const result = await render(doc, 'xlsx')
1607
+ const result = await render(doc, "xlsx")
1651
1608
  expect(result).toBeInstanceOf(Uint8Array)
1652
1609
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1653
1610
  }, 15000)
@@ -1655,82 +1612,76 @@ describe('XLSX renderer', () => {
1655
1612
 
1656
1613
  // ─── PDF Renderer (integration) ─────────────────────────────────────────────
1657
1614
 
1658
- describe('PDF renderer', () => {
1659
- it('renders a document with heading, text, table, and data: image to a valid Uint8Array', async () => {
1615
+ describe("PDF renderer", () => {
1616
+ it("renders a document with heading, text, table, and data: image to a valid Uint8Array", async () => {
1660
1617
  // 1x1 red pixel PNG as base64
1661
1618
  const redPixel =
1662
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
1619
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
1663
1620
 
1664
1621
  const doc = Document({
1665
- title: 'PDF Test',
1666
- author: 'Test Suite',
1622
+ title: "PDF Test",
1623
+ author: "Test Suite",
1667
1624
  children: Page({
1668
- size: 'A4',
1625
+ size: "A4",
1669
1626
  margin: 40,
1670
1627
  children: [
1671
- Heading({ children: 'PDF Integration Test' }),
1672
- Text({ children: 'This is a test paragraph.', bold: true }),
1628
+ Heading({ children: "PDF Integration Test" }),
1629
+ Text({ children: "This is a test paragraph.", bold: true }),
1673
1630
  Table({
1674
- columns: ['Name', 'Value'],
1631
+ columns: ["Name", "Value"],
1675
1632
  rows: [
1676
- ['Alpha', '100'],
1677
- ['Beta', '200'],
1633
+ ["Alpha", "100"],
1634
+ ["Beta", "200"],
1678
1635
  ],
1679
1636
  striped: true,
1680
- headerStyle: { background: '#333333', color: '#ffffff' },
1637
+ headerStyle: { background: "#333333", color: "#ffffff" },
1681
1638
  }),
1682
1639
  Image({ src: redPixel, width: 50, height: 50 }),
1683
1640
  List({
1684
1641
  ordered: true,
1685
- children: [
1686
- ListItem({ children: 'First' }),
1687
- ListItem({ children: 'Second' }),
1688
- ],
1642
+ children: [ListItem({ children: "First" }), ListItem({ children: "Second" })],
1689
1643
  }),
1690
1644
  Divider(),
1691
- Quote({ children: 'A wise quote.' }),
1645
+ Quote({ children: "A wise quote." }),
1692
1646
  ],
1693
1647
  }),
1694
1648
  })
1695
1649
 
1696
- const result = await render(doc, 'pdf')
1650
+ const result = await render(doc, "pdf")
1697
1651
  expect(result).toBeInstanceOf(Uint8Array)
1698
1652
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1699
1653
  // PDF files start with %PDF
1700
1654
  const header = String.fromCharCode(...(result as Uint8Array).slice(0, 5))
1701
- expect(header).toBe('%PDF-')
1655
+ expect(header).toBe("%PDF-")
1702
1656
  }, 15000)
1703
1657
 
1704
- it('renders images with HTTP URLs as placeholder text', async () => {
1658
+ it("renders images with HTTP URLs as placeholder text", async () => {
1705
1659
  const doc = Document({
1706
- title: 'HTTP Image Test',
1660
+ title: "HTTP Image Test",
1707
1661
  children: Page({
1708
1662
  children: [
1709
- Heading({ children: 'Test' }),
1710
- Image({ src: 'https://example.com/image.png', width: 100 }),
1663
+ Heading({ children: "Test" }),
1664
+ Image({ src: "https://example.com/image.png", width: 100 }),
1711
1665
  ],
1712
1666
  }),
1713
1667
  })
1714
1668
 
1715
- const result = await render(doc, 'pdf')
1669
+ const result = await render(doc, "pdf")
1716
1670
  expect(result).toBeInstanceOf(Uint8Array)
1717
1671
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1718
1672
  }, 15000)
1719
1673
 
1720
- it('renders page with header and footer', async () => {
1674
+ it("renders page with header and footer", async () => {
1721
1675
  const doc = Document({
1722
- title: 'Header/Footer Test',
1676
+ title: "Header/Footer Test",
1723
1677
  children: Page({
1724
- header: Text({ children: 'Page Header', bold: true }),
1725
- footer: Text({ children: 'Page Footer', size: 10 }),
1726
- children: [
1727
- Heading({ children: 'Content' }),
1728
- Text({ children: 'Body text.' }),
1729
- ],
1678
+ header: Text({ children: "Page Header", bold: true }),
1679
+ footer: Text({ children: "Page Footer", size: 10 }),
1680
+ children: [Heading({ children: "Content" }), Text({ children: "Body text." })],
1730
1681
  }),
1731
1682
  })
1732
1683
 
1733
- const result = await render(doc, 'pdf')
1684
+ const result = await render(doc, "pdf")
1734
1685
  expect(result).toBeInstanceOf(Uint8Array)
1735
1686
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1736
1687
  }, 15000)
@@ -1738,34 +1689,31 @@ describe('PDF renderer', () => {
1738
1689
 
1739
1690
  // ─── PPTX Renderer (integration) ────────────────────────────────────────────
1740
1691
 
1741
- describe('PPTX renderer', () => {
1742
- it('renders a document with pages, headings, text, and tables to a valid Uint8Array', async () => {
1692
+ describe("PPTX renderer", () => {
1693
+ it("renders a document with pages, headings, text, and tables to a valid Uint8Array", async () => {
1743
1694
  const doc = Document({
1744
- title: 'PPTX Test',
1745
- author: 'Test Suite',
1695
+ title: "PPTX Test",
1696
+ author: "Test Suite",
1746
1697
  children: [
1747
1698
  Page({
1748
1699
  children: [
1749
- Heading({ children: 'Slide 1 Title' }),
1750
- Text({ children: 'Introduction text.', bold: true }),
1700
+ Heading({ children: "Slide 1 Title" }),
1701
+ Text({ children: "Introduction text.", bold: true }),
1751
1702
  List({
1752
- children: [
1753
- ListItem({ children: 'Point A' }),
1754
- ListItem({ children: 'Point B' }),
1755
- ],
1703
+ children: [ListItem({ children: "Point A" }), ListItem({ children: "Point B" })],
1756
1704
  }),
1757
1705
  ],
1758
1706
  }),
1759
1707
  Page({
1760
1708
  children: [
1761
- Heading({ level: 2, children: 'Slide 2 Data' }),
1709
+ Heading({ level: 2, children: "Slide 2 Data" }),
1762
1710
  Table({
1763
- columns: ['Metric', 'Value'],
1711
+ columns: ["Metric", "Value"],
1764
1712
  rows: [
1765
- ['Revenue', '$1M'],
1766
- ['Profit', '$300K'],
1713
+ ["Revenue", "$1M"],
1714
+ ["Profit", "$300K"],
1767
1715
  ],
1768
- headerStyle: { background: '#1a1a2e', color: '#ffffff' },
1716
+ headerStyle: { background: "#1a1a2e", color: "#ffffff" },
1769
1717
  striped: true,
1770
1718
  }),
1771
1719
  ],
@@ -1773,7 +1721,7 @@ describe('PPTX renderer', () => {
1773
1721
  ],
1774
1722
  })
1775
1723
 
1776
- const result = await render(doc, 'pptx')
1724
+ const result = await render(doc, "pptx")
1777
1725
  expect(result).toBeInstanceOf(Uint8Array)
1778
1726
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1779
1727
  // PPTX files are ZIP archives — first two bytes are PK (0x50, 0x4B)
@@ -1781,44 +1729,44 @@ describe('PPTX renderer', () => {
1781
1729
  expect((result as Uint8Array)[1]).toBe(0x4b)
1782
1730
  }, 15000)
1783
1731
 
1784
- it('renders a document without explicit pages as a single slide', async () => {
1732
+ it("renders a document without explicit pages as a single slide", async () => {
1785
1733
  const doc = Document({
1786
- title: 'Single Slide',
1734
+ title: "Single Slide",
1787
1735
  children: [
1788
- Heading({ children: 'Auto Slide' }),
1789
- Text({ children: 'No explicit page wrapper.' }),
1736
+ Heading({ children: "Auto Slide" }),
1737
+ Text({ children: "No explicit page wrapper." }),
1790
1738
  ],
1791
1739
  })
1792
1740
 
1793
- const result = await render(doc, 'pptx')
1741
+ const result = await render(doc, "pptx")
1794
1742
  expect(result).toBeInstanceOf(Uint8Array)
1795
1743
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1796
1744
  }, 15000)
1797
1745
 
1798
- it('renders all node types without errors', async () => {
1746
+ it("renders all node types without errors", async () => {
1799
1747
  // 1x1 red pixel PNG as base64
1800
1748
  const redPixel =
1801
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
1749
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
1802
1750
 
1803
1751
  const doc = Document({
1804
1752
  children: Page({
1805
1753
  children: [
1806
- Heading({ children: 'Title' }),
1807
- Text({ children: 'Body', bold: true, italic: true }),
1754
+ Heading({ children: "Title" }),
1755
+ Text({ children: "Body", bold: true, italic: true }),
1808
1756
  Image({ src: redPixel, width: 50, height: 50 }),
1809
- Code({ children: 'const x = 1' }),
1810
- Quote({ children: 'A quote' }),
1811
- Link({ href: 'https://example.com', children: 'Link text' }),
1812
- Button({ href: '/action', background: '#4f46e5', children: 'Click' }),
1757
+ Code({ children: "const x = 1" }),
1758
+ Quote({ children: "A quote" }),
1759
+ Link({ href: "https://example.com", children: "Link text" }),
1760
+ Button({ href: "/action", background: "#4f46e5", children: "Click" }),
1813
1761
  Divider(),
1814
1762
  Spacer({ height: 20 }),
1815
- List({ ordered: true, children: [ListItem({ children: 'one' })] }),
1816
- Section({ children: Text({ children: 'nested' }) }),
1763
+ List({ ordered: true, children: [ListItem({ children: "one" })] }),
1764
+ Section({ children: Text({ children: "nested" }) }),
1817
1765
  ],
1818
1766
  }),
1819
1767
  })
1820
1768
 
1821
- const result = await render(doc, 'pptx')
1769
+ const result = await render(doc, "pptx")
1822
1770
  expect(result).toBeInstanceOf(Uint8Array)
1823
1771
  expect((result as Uint8Array).length).toBeGreaterThan(0)
1824
1772
  }, 15000)
@@ -1826,227 +1774,224 @@ describe('PPTX renderer', () => {
1826
1774
 
1827
1775
  // ─── Slack Renderer ─────────────────────────────────────────────────────────
1828
1776
 
1829
- describe('Slack renderer', () => {
1830
- it('renders heading as header block', async () => {
1831
- const doc = Document({ children: Heading({ children: 'Hello' }) })
1832
- const json = (await render(doc, 'slack')) as string
1777
+ describe("Slack renderer", () => {
1778
+ it("renders heading as header block", async () => {
1779
+ const doc = Document({ children: Heading({ children: "Hello" }) })
1780
+ const json = (await render(doc, "slack")) as string
1833
1781
  const parsed = JSON.parse(json)
1834
1782
  expect(parsed.blocks).toHaveLength(1)
1835
- expect(parsed.blocks[0].type).toBe('header')
1783
+ expect(parsed.blocks[0].type).toBe("header")
1836
1784
  })
1837
1785
 
1838
- it('renders text with bold as mrkdwn', async () => {
1786
+ it("renders text with bold as mrkdwn", async () => {
1839
1787
  const doc = Document({
1840
- children: Text({ bold: true, children: 'Bold text' }),
1788
+ children: Text({ bold: true, children: "Bold text" }),
1841
1789
  })
1842
- const json = (await render(doc, 'slack')) as string
1790
+ const json = (await render(doc, "slack")) as string
1843
1791
  const parsed = JSON.parse(json)
1844
- expect(parsed.blocks[0].text.text).toContain('*Bold text*')
1792
+ expect(parsed.blocks[0].text.text).toContain("*Bold text*")
1845
1793
  })
1846
1794
 
1847
- it('renders button as actions block', async () => {
1795
+ it("renders button as actions block", async () => {
1848
1796
  const doc = Document({
1849
- children: Button({ href: '/go', children: 'Click' }),
1797
+ children: Button({ href: "/go", children: "Click" }),
1850
1798
  })
1851
- const json = (await render(doc, 'slack')) as string
1799
+ const json = (await render(doc, "slack")) as string
1852
1800
  const parsed = JSON.parse(json)
1853
- expect(parsed.blocks[0].type).toBe('actions')
1854
- expect(parsed.blocks[0].elements[0].type).toBe('button')
1801
+ expect(parsed.blocks[0].type).toBe("actions")
1802
+ expect(parsed.blocks[0].elements[0].type).toBe("button")
1855
1803
  })
1856
1804
 
1857
- it('renders table as code block', async () => {
1805
+ it("renders table as code block", async () => {
1858
1806
  const doc = Document({
1859
- children: Table({ columns: ['A', 'B'], rows: [['1', '2']] }),
1807
+ children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
1860
1808
  })
1861
- const json = (await render(doc, 'slack')) as string
1809
+ const json = (await render(doc, "slack")) as string
1862
1810
  const parsed = JSON.parse(json)
1863
- expect(parsed.blocks[0].text.text).toContain('*A*')
1864
- expect(parsed.blocks[0].text.text).toContain('1 | 2')
1811
+ expect(parsed.blocks[0].text.text).toContain("*A*")
1812
+ expect(parsed.blocks[0].text.text).toContain("1 | 2")
1865
1813
  })
1866
1814
 
1867
- it('renders divider', async () => {
1815
+ it("renders divider", async () => {
1868
1816
  const doc = Document({ children: Divider() })
1869
- const json = (await render(doc, 'slack')) as string
1817
+ const json = (await render(doc, "slack")) as string
1870
1818
  const parsed = JSON.parse(json)
1871
- expect(parsed.blocks[0].type).toBe('divider')
1819
+ expect(parsed.blocks[0].type).toBe("divider")
1872
1820
  })
1873
1821
 
1874
- it('renders list as bullet points', async () => {
1822
+ it("renders list as bullet points", async () => {
1875
1823
  const doc = Document({
1876
1824
  children: List({
1877
- children: [
1878
- ListItem({ children: 'one' }),
1879
- ListItem({ children: 'two' }),
1880
- ],
1825
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
1881
1826
  }),
1882
1827
  })
1883
- const json = (await render(doc, 'slack')) as string
1828
+ const json = (await render(doc, "slack")) as string
1884
1829
  const parsed = JSON.parse(json)
1885
- expect(parsed.blocks[0].text.text).toContain('• one')
1886
- expect(parsed.blocks[0].text.text).toContain('• two')
1830
+ expect(parsed.blocks[0].text.text).toContain("• one")
1831
+ expect(parsed.blocks[0].text.text).toContain("• two")
1887
1832
  })
1888
1833
 
1889
- it('renders ordered list', async () => {
1834
+ it("renders ordered list", async () => {
1890
1835
  const doc = Document({
1891
1836
  children: List({
1892
1837
  ordered: true,
1893
- children: [ListItem({ children: 'a' }), ListItem({ children: 'b' })],
1838
+ children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
1894
1839
  }),
1895
1840
  })
1896
- const json = (await render(doc, 'slack')) as string
1841
+ const json = (await render(doc, "slack")) as string
1897
1842
  const parsed = JSON.parse(json)
1898
- expect(parsed.blocks[0].text.text).toContain('1. a')
1899
- expect(parsed.blocks[0].text.text).toContain('2. b')
1843
+ expect(parsed.blocks[0].text.text).toContain("1. a")
1844
+ expect(parsed.blocks[0].text.text).toContain("2. b")
1900
1845
  })
1901
1846
 
1902
- it('renders link in mrkdwn format', async () => {
1847
+ it("renders link in mrkdwn format", async () => {
1903
1848
  const doc = Document({
1904
- children: Link({ href: 'https://x.com', children: 'X' }),
1849
+ children: Link({ href: "https://x.com", children: "X" }),
1905
1850
  })
1906
- const json = (await render(doc, 'slack')) as string
1851
+ const json = (await render(doc, "slack")) as string
1907
1852
  const parsed = JSON.parse(json)
1908
- expect(parsed.blocks[0].text.text).toContain('<https://x.com|X>')
1853
+ expect(parsed.blocks[0].text.text).toContain("<https://x.com|X>")
1909
1854
  })
1910
1855
 
1911
- it('renders code block', async () => {
1856
+ it("renders code block", async () => {
1912
1857
  const doc = Document({
1913
- children: Code({ language: 'js', children: 'const x = 1' }),
1858
+ children: Code({ language: "js", children: "const x = 1" }),
1914
1859
  })
1915
- const json = (await render(doc, 'slack')) as string
1860
+ const json = (await render(doc, "slack")) as string
1916
1861
  const parsed = JSON.parse(json)
1917
- expect(parsed.blocks[0].text.text).toContain('```js')
1918
- expect(parsed.blocks[0].text.text).toContain('const x = 1')
1862
+ expect(parsed.blocks[0].text.text).toContain("```js")
1863
+ expect(parsed.blocks[0].text.text).toContain("const x = 1")
1919
1864
  })
1920
1865
 
1921
- it('renders quote with >', async () => {
1922
- const doc = Document({ children: Quote({ children: 'wise' }) })
1923
- const json = (await render(doc, 'slack')) as string
1866
+ it("renders quote with >", async () => {
1867
+ const doc = Document({ children: Quote({ children: "wise" }) })
1868
+ const json = (await render(doc, "slack")) as string
1924
1869
  const parsed = JSON.parse(json)
1925
- expect(parsed.blocks[0].text.text).toContain('> wise')
1870
+ expect(parsed.blocks[0].text.text).toContain("> wise")
1926
1871
  })
1927
1872
 
1928
- it('renders image with URL', async () => {
1873
+ it("renders image with URL", async () => {
1929
1874
  const doc = Document({
1930
- children: Image({ src: 'https://x.com/img.png', alt: 'Photo' }),
1875
+ children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
1931
1876
  })
1932
- const json = (await render(doc, 'slack')) as string
1877
+ const json = (await render(doc, "slack")) as string
1933
1878
  const parsed = JSON.parse(json)
1934
- expect(parsed.blocks[0].type).toBe('image')
1935
- expect(parsed.blocks[0].image_url).toBe('https://x.com/img.png')
1879
+ expect(parsed.blocks[0].type).toBe("image")
1880
+ expect(parsed.blocks[0].image_url).toBe("https://x.com/img.png")
1936
1881
  })
1937
1882
 
1938
- it('skips non-URL images', async () => {
1883
+ it("skips non-URL images", async () => {
1939
1884
  const doc = Document({
1940
- children: Image({ src: 'data:image/png;base64,abc' }),
1885
+ children: Image({ src: "data:image/png;base64,abc" }),
1941
1886
  })
1942
- const json = (await render(doc, 'slack')) as string
1887
+ const json = (await render(doc, "slack")) as string
1943
1888
  const parsed = JSON.parse(json)
1944
1889
  expect(parsed.blocks).toHaveLength(0)
1945
1890
  })
1946
1891
 
1947
- it('renders page-break as divider', async () => {
1892
+ it("renders page-break as divider", async () => {
1948
1893
  const doc = Document({ children: PageBreak() })
1949
- const json = (await render(doc, 'slack')) as string
1894
+ const json = (await render(doc, "slack")) as string
1950
1895
  const parsed = JSON.parse(json)
1951
- expect(parsed.blocks[0].type).toBe('divider')
1896
+ expect(parsed.blocks[0].type).toBe("divider")
1952
1897
  })
1953
1898
 
1954
- it('renders text with italic and strikethrough', async () => {
1899
+ it("renders text with italic and strikethrough", async () => {
1955
1900
  const doc = Document({
1956
1901
  children: [
1957
- Text({ italic: true, children: 'italic' }),
1958
- Text({ strikethrough: true, children: 'struck' }),
1902
+ Text({ italic: true, children: "italic" }),
1903
+ Text({ strikethrough: true, children: "struck" }),
1959
1904
  ],
1960
1905
  })
1961
- const json = (await render(doc, 'slack')) as string
1906
+ const json = (await render(doc, "slack")) as string
1962
1907
  const parsed = JSON.parse(json)
1963
- expect(parsed.blocks[0].text.text).toContain('_italic_')
1964
- expect(parsed.blocks[1].text.text).toContain('~struck~')
1908
+ expect(parsed.blocks[0].text.text).toContain("_italic_")
1909
+ expect(parsed.blocks[1].text.text).toContain("~struck~")
1965
1910
  })
1966
1911
 
1967
- it('renders image with caption', async () => {
1912
+ it("renders image with caption", async () => {
1968
1913
  const doc = Document({
1969
- children: Image({ src: 'https://x.com/img.png', caption: 'Nice' }),
1914
+ children: Image({ src: "https://x.com/img.png", caption: "Nice" }),
1970
1915
  })
1971
- const json = (await render(doc, 'slack')) as string
1916
+ const json = (await render(doc, "slack")) as string
1972
1917
  const parsed = JSON.parse(json)
1973
- expect(parsed.blocks[0].title.text).toBe('Nice')
1918
+ expect(parsed.blocks[0].title.text).toBe("Nice")
1974
1919
  })
1975
1920
 
1976
- it('renders table with caption', async () => {
1921
+ it("renders table with caption", async () => {
1977
1922
  const doc = Document({
1978
- children: Table({ columns: ['X'], rows: [['1']], caption: 'My Table' }),
1923
+ children: Table({ columns: ["X"], rows: [["1"]], caption: "My Table" }),
1979
1924
  })
1980
- const json = (await render(doc, 'slack')) as string
1925
+ const json = (await render(doc, "slack")) as string
1981
1926
  const parsed = JSON.parse(json)
1982
- expect(parsed.blocks[0].text.text).toContain('_My Table_')
1927
+ expect(parsed.blocks[0].text.text).toContain("_My Table_")
1983
1928
  })
1984
1929
  })
1985
1930
 
1986
1931
  // ─── PageBreak ──────────────────────────────────────────────────────────────
1987
1932
 
1988
- describe('PageBreak', () => {
1989
- it('creates a page-break node', () => {
1933
+ describe("PageBreak", () => {
1934
+ it("creates a page-break node", () => {
1990
1935
  const pb = PageBreak()
1991
- expect(pb.type).toBe('page-break')
1936
+ expect(pb.type).toBe("page-break")
1992
1937
  expect(pb.children).toEqual([])
1993
1938
  })
1994
1939
 
1995
- it('renders as CSS page-break in HTML', async () => {
1940
+ it("renders as CSS page-break in HTML", async () => {
1996
1941
  const doc = Document({ children: PageBreak() })
1997
- const html = (await render(doc, 'html')) as string
1998
- expect(html).toContain('page-break-after:always')
1942
+ const html = (await render(doc, "html")) as string
1943
+ expect(html).toContain("page-break-after:always")
1999
1944
  })
2000
1945
 
2001
- it('renders as separator in email', async () => {
1946
+ it("renders as separator in email", async () => {
2002
1947
  const doc = Document({ children: PageBreak() })
2003
- const html = (await render(doc, 'email')) as string
2004
- expect(html).toContain('border-top:2px solid')
1948
+ const html = (await render(doc, "email")) as string
1949
+ expect(html).toContain("border-top:2px solid")
2005
1950
  })
2006
1951
 
2007
- it('renders as --- in markdown', async () => {
1952
+ it("renders as --- in markdown", async () => {
2008
1953
  const doc = Document({ children: PageBreak() })
2009
- const md = (await render(doc, 'md')) as string
2010
- expect(md).toContain('---')
1954
+ const md = (await render(doc, "md")) as string
1955
+ expect(md).toContain("---")
2011
1956
  })
2012
1957
 
2013
- it('renders as separator in text', async () => {
1958
+ it("renders as separator in text", async () => {
2014
1959
  const doc = Document({ children: PageBreak() })
2015
- const text = (await render(doc, 'text')) as string
2016
- expect(text).toContain('')
1960
+ const text = (await render(doc, "text")) as string
1961
+ expect(text).toContain("")
2017
1962
  })
2018
1963
 
2019
- it('builder pageBreak inserts page-break node', async () => {
2020
- const doc = createDocument().heading('Page 1').pageBreak().heading('Page 2')
1964
+ it("builder pageBreak inserts page-break node", async () => {
1965
+ const doc = createDocument().heading("Page 1").pageBreak().heading("Page 2")
2021
1966
  const html = await doc.toHtml()
2022
- expect(html).toContain('page-break-after:always')
2023
- expect(html).toContain('Page 1')
2024
- expect(html).toContain('Page 2')
1967
+ expect(html).toContain("page-break-after:always")
1968
+ expect(html).toContain("Page 1")
1969
+ expect(html).toContain("Page 2")
2025
1970
  })
2026
1971
  })
2027
1972
 
2028
1973
  // ─── RTL Support ────────────────────────────────────────────────────────────
2029
1974
 
2030
- describe('RTL support', () => {
2031
- it('adds dir=rtl to HTML body', async () => {
2032
- const doc = Document({ children: Text({ children: 'مرحبا' }) })
2033
- const html = (await render(doc, 'html', { direction: 'rtl' })) as string
1975
+ describe("RTL support", () => {
1976
+ it("adds dir=rtl to HTML body", async () => {
1977
+ const doc = Document({ children: Text({ children: "مرحبا" }) })
1978
+ const html = (await render(doc, "html", { direction: "rtl" })) as string
2034
1979
  expect(html).toContain('dir="rtl"')
2035
- expect(html).toContain('direction:rtl')
1980
+ expect(html).toContain("direction:rtl")
2036
1981
  })
2037
1982
 
2038
- it('does not add dir for ltr (default)', async () => {
2039
- const doc = Document({ children: Text({ children: 'Hello' }) })
2040
- const html = (await render(doc, 'html')) as string
1983
+ it("does not add dir for ltr (default)", async () => {
1984
+ const doc = Document({ children: Text({ children: "Hello" }) })
1985
+ const html = (await render(doc, "html")) as string
2041
1986
  expect(html).not.toContain('dir="rtl"')
2042
1987
  })
2043
1988
  })
2044
1989
 
2045
1990
  // ─── keepTogether ───────────────────────────────────────────────────────────
2046
1991
 
2047
- describe('keepTogether', () => {
2048
- it('table accepts keepTogether prop', () => {
2049
- const t = Table({ columns: ['A'], rows: [['1']], keepTogether: true })
1992
+ describe("keepTogether", () => {
1993
+ it("table accepts keepTogether prop", () => {
1994
+ const t = Table({ columns: ["A"], rows: [["1"]], keepTogether: true })
2050
1995
  expect(t.props.keepTogether).toBe(true)
2051
1996
  })
2052
1997
  })
@@ -2055,372 +2000,358 @@ describe('keepTogether', () => {
2055
2000
 
2056
2001
  // ─── SVG Renderer ───────────────────────────────────────────────────────────
2057
2002
 
2058
- describe('SVG renderer', () => {
2059
- it('renders a valid SVG document', async () => {
2060
- const doc = Document({ children: Heading({ children: 'Title' }) })
2061
- const svg = (await render(doc, 'svg')) as string
2003
+ describe("SVG renderer", () => {
2004
+ it("renders a valid SVG document", async () => {
2005
+ const doc = Document({ children: Heading({ children: "Title" }) })
2006
+ const svg = (await render(doc, "svg")) as string
2062
2007
  expect(svg).toContain('<svg xmlns="http://www.w3.org/2000/svg"')
2063
- expect(svg).toContain('</svg>')
2064
- expect(svg).toContain('Title')
2008
+ expect(svg).toContain("</svg>")
2009
+ expect(svg).toContain("Title")
2065
2010
  })
2066
2011
 
2067
- it('renders heading with correct font size', async () => {
2068
- const doc = Document({ children: Heading({ level: 2, children: 'Sub' }) })
2069
- const svg = (await render(doc, 'svg')) as string
2012
+ it("renders heading with correct font size", async () => {
2013
+ const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
2014
+ const svg = (await render(doc, "svg")) as string
2070
2015
  expect(svg).toContain('font-size="24"')
2071
2016
  expect(svg).toContain('font-weight="bold"')
2072
2017
  })
2073
2018
 
2074
- it('renders text with bold and italic', async () => {
2019
+ it("renders text with bold and italic", async () => {
2075
2020
  const doc = Document({
2076
2021
  children: [
2077
- Text({ bold: true, children: 'Bold' }),
2078
- Text({ italic: true, children: 'Italic' }),
2022
+ Text({ bold: true, children: "Bold" }),
2023
+ Text({ italic: true, children: "Italic" }),
2079
2024
  ],
2080
2025
  })
2081
- const svg = (await render(doc, 'svg')) as string
2026
+ const svg = (await render(doc, "svg")) as string
2082
2027
  expect(svg).toContain('font-weight="bold"')
2083
2028
  expect(svg).toContain('font-style="italic"')
2084
2029
  })
2085
2030
 
2086
- it('renders table with header and rows', async () => {
2031
+ it("renders table with header and rows", async () => {
2087
2032
  const doc = Document({
2088
2033
  children: Table({
2089
- columns: ['Name', 'Price'],
2090
- rows: [['Widget', '$10']],
2091
- headerStyle: { background: '#000', color: '#fff' },
2034
+ columns: ["Name", "Price"],
2035
+ rows: [["Widget", "$10"]],
2036
+ headerStyle: { background: "#000", color: "#fff" },
2092
2037
  striped: true,
2093
2038
  }),
2094
2039
  })
2095
- const svg = (await render(doc, 'svg')) as string
2096
- expect(svg).toContain('Name')
2097
- expect(svg).toContain('Widget')
2040
+ const svg = (await render(doc, "svg")) as string
2041
+ expect(svg).toContain("Name")
2042
+ expect(svg).toContain("Widget")
2098
2043
  expect(svg).toContain('fill="#000"')
2099
2044
  })
2100
2045
 
2101
- it('renders image from data URL', async () => {
2046
+ it("renders image from data URL", async () => {
2102
2047
  const doc = Document({
2103
2048
  children: Image({
2104
- src: 'data:image/png;base64,abc',
2049
+ src: "data:image/png;base64,abc",
2105
2050
  width: 200,
2106
2051
  height: 100,
2107
- caption: 'Photo',
2052
+ caption: "Photo",
2108
2053
  }),
2109
2054
  })
2110
- const svg = (await render(doc, 'svg')) as string
2111
- expect(svg).toContain('<image')
2112
- expect(svg).toContain('data:image/png;base64,abc')
2113
- expect(svg).toContain('Photo')
2055
+ const svg = (await render(doc, "svg")) as string
2056
+ expect(svg).toContain("<image")
2057
+ expect(svg).toContain("data:image/png;base64,abc")
2058
+ expect(svg).toContain("Photo")
2114
2059
  })
2115
2060
 
2116
- it('renders image placeholder for local paths', async () => {
2061
+ it("renders image placeholder for local paths", async () => {
2117
2062
  const doc = Document({
2118
- children: Image({ src: '/local.png', alt: 'Local' }),
2063
+ children: Image({ src: "/local.png", alt: "Local" }),
2119
2064
  })
2120
- const svg = (await render(doc, 'svg')) as string
2121
- expect(svg).toContain('Local')
2065
+ const svg = (await render(doc, "svg")) as string
2066
+ expect(svg).toContain("Local")
2122
2067
  expect(svg).toContain('fill="#f0f0f0"')
2123
2068
  })
2124
2069
 
2125
- it('renders list', async () => {
2070
+ it("renders list", async () => {
2126
2071
  const doc = Document({
2127
2072
  children: List({
2128
2073
  ordered: true,
2129
- children: [
2130
- ListItem({ children: 'one' }),
2131
- ListItem({ children: 'two' }),
2132
- ],
2074
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
2133
2075
  }),
2134
2076
  })
2135
- const svg = (await render(doc, 'svg')) as string
2136
- expect(svg).toContain('1. one')
2137
- expect(svg).toContain('2. two')
2077
+ const svg = (await render(doc, "svg")) as string
2078
+ expect(svg).toContain("1. one")
2079
+ expect(svg).toContain("2. two")
2138
2080
  })
2139
2081
 
2140
- it('renders unordered list', async () => {
2082
+ it("renders unordered list", async () => {
2141
2083
  const doc = Document({
2142
2084
  children: List({
2143
- children: [ListItem({ children: 'a' }), ListItem({ children: 'b' })],
2085
+ children: [ListItem({ children: "a" }), ListItem({ children: "b" })],
2144
2086
  }),
2145
2087
  })
2146
- const svg = (await render(doc, 'svg')) as string
2147
- expect(svg).toContain('• a')
2148
- expect(svg).toContain('• b')
2088
+ const svg = (await render(doc, "svg")) as string
2089
+ expect(svg).toContain("• a")
2090
+ expect(svg).toContain("• b")
2149
2091
  })
2150
2092
 
2151
- it('renders code block', async () => {
2152
- const doc = Document({ children: Code({ children: 'const x = 1' }) })
2153
- const svg = (await render(doc, 'svg')) as string
2154
- expect(svg).toContain('const x = 1')
2093
+ it("renders code block", async () => {
2094
+ const doc = Document({ children: Code({ children: "const x = 1" }) })
2095
+ const svg = (await render(doc, "svg")) as string
2096
+ expect(svg).toContain("const x = 1")
2155
2097
  expect(svg).toContain('font-family="monospace"')
2156
2098
  expect(svg).toContain('fill="#f5f5f5"') // background
2157
2099
  })
2158
2100
 
2159
- it('renders divider', async () => {
2160
- const doc = Document({ children: Divider({ color: '#ccc', thickness: 2 }) })
2161
- const svg = (await render(doc, 'svg')) as string
2101
+ it("renders divider", async () => {
2102
+ const doc = Document({ children: Divider({ color: "#ccc", thickness: 2 }) })
2103
+ const svg = (await render(doc, "svg")) as string
2162
2104
  expect(svg).toContain('stroke="#ccc"')
2163
2105
  expect(svg).toContain('stroke-width="2"')
2164
2106
  })
2165
2107
 
2166
- it('renders button', async () => {
2108
+ it("renders button", async () => {
2167
2109
  const doc = Document({
2168
2110
  children: Button({
2169
- href: '/pay',
2170
- background: '#4f46e5',
2171
- children: 'Pay',
2111
+ href: "/pay",
2112
+ background: "#4f46e5",
2113
+ children: "Pay",
2172
2114
  }),
2173
2115
  })
2174
- const svg = (await render(doc, 'svg')) as string
2116
+ const svg = (await render(doc, "svg")) as string
2175
2117
  expect(svg).toContain('fill="#4f46e5"')
2176
- expect(svg).toContain('Pay')
2118
+ expect(svg).toContain("Pay")
2177
2119
  })
2178
2120
 
2179
- it('renders quote', async () => {
2180
- const doc = Document({ children: Quote({ children: 'wise words' }) })
2181
- const svg = (await render(doc, 'svg')) as string
2182
- expect(svg).toContain('wise words')
2121
+ it("renders quote", async () => {
2122
+ const doc = Document({ children: Quote({ children: "wise words" }) })
2123
+ const svg = (await render(doc, "svg")) as string
2124
+ expect(svg).toContain("wise words")
2183
2125
  expect(svg).toContain('font-style="italic"')
2184
2126
  })
2185
2127
 
2186
- it('renders link', async () => {
2128
+ it("renders link", async () => {
2187
2129
  const doc = Document({
2188
- children: Link({ href: 'https://x.com', children: 'X' }),
2130
+ children: Link({ href: "https://x.com", children: "X" }),
2189
2131
  })
2190
- const svg = (await render(doc, 'svg')) as string
2132
+ const svg = (await render(doc, "svg")) as string
2191
2133
  expect(svg).toContain('<a href="https://x.com">')
2192
- expect(svg).toContain('X')
2134
+ expect(svg).toContain("X")
2193
2135
  })
2194
2136
 
2195
- it('renders spacer', async () => {
2137
+ it("renders spacer", async () => {
2196
2138
  const doc = Document({
2197
- children: [
2198
- Text({ children: 'A' }),
2199
- Spacer({ height: 50 }),
2200
- Text({ children: 'B' }),
2201
- ],
2139
+ children: [Text({ children: "A" }), Spacer({ height: 50 }), Text({ children: "B" })],
2202
2140
  })
2203
- const svg = (await render(doc, 'svg')) as string
2204
- expect(svg).toContain('A')
2205
- expect(svg).toContain('B')
2141
+ const svg = (await render(doc, "svg")) as string
2142
+ expect(svg).toContain("A")
2143
+ expect(svg).toContain("B")
2206
2144
  })
2207
2145
 
2208
- it('renders page-break as dashed line', async () => {
2146
+ it("renders page-break as dashed line", async () => {
2209
2147
  const doc = Document({ children: PageBreak() })
2210
- const svg = (await render(doc, 'svg')) as string
2211
- expect(svg).toContain('stroke-dasharray')
2148
+ const svg = (await render(doc, "svg")) as string
2149
+ expect(svg).toContain("stroke-dasharray")
2212
2150
  })
2213
2151
 
2214
- it('supports RTL direction', async () => {
2215
- const doc = Document({ children: Text({ children: 'مرحبا' }) })
2216
- const svg = (await render(doc, 'svg', { direction: 'rtl' })) as string
2152
+ it("supports RTL direction", async () => {
2153
+ const doc = Document({ children: Text({ children: "مرحبا" }) })
2154
+ const svg = (await render(doc, "svg", { direction: "rtl" })) as string
2217
2155
  expect(svg).toContain('direction="rtl"')
2218
2156
  })
2219
2157
 
2220
- it('auto-calculates height from content', async () => {
2158
+ it("auto-calculates height from content", async () => {
2221
2159
  const doc = Document({
2222
- children: [
2223
- Heading({ children: 'A' }),
2224
- Text({ children: 'B' }),
2225
- Text({ children: 'C' }),
2226
- ],
2160
+ children: [Heading({ children: "A" }), Text({ children: "B" }), Text({ children: "C" })],
2227
2161
  })
2228
- const svg = (await render(doc, 'svg')) as string
2162
+ const svg = (await render(doc, "svg")) as string
2229
2163
  const match = svg.match(/height="(\d+)"/)
2230
2164
  expect(match).toBeTruthy()
2231
2165
  expect(Number(match![1])).toBeGreaterThan(80)
2232
2166
  })
2233
2167
 
2234
- it('renders image from HTTP URL', async () => {
2168
+ it("renders image from HTTP URL", async () => {
2235
2169
  const doc = Document({
2236
- children: Image({ src: 'https://x.com/img.png', width: 300 }),
2170
+ children: Image({ src: "https://x.com/img.png", width: 300 }),
2237
2171
  })
2238
- const svg = (await render(doc, 'svg')) as string
2239
- expect(svg).toContain('<image')
2240
- expect(svg).toContain('https://x.com/img.png')
2172
+ const svg = (await render(doc, "svg")) as string
2173
+ expect(svg).toContain("<image")
2174
+ expect(svg).toContain("https://x.com/img.png")
2241
2175
  })
2242
2176
 
2243
- it('builder toSvg works', async () => {
2244
- const svg = await createDocument().heading('Hi').text('World').toSvg()
2245
- expect(svg).toContain('<svg')
2246
- expect(svg).toContain('Hi')
2247
- expect(svg).toContain('World')
2177
+ it("builder toSvg works", async () => {
2178
+ const svg = await createDocument().heading("Hi").text("World").toSvg()
2179
+ expect(svg).toContain("<svg")
2180
+ expect(svg).toContain("Hi")
2181
+ expect(svg).toContain("World")
2248
2182
  })
2249
2183
  })
2250
2184
 
2251
2185
  // ─── Teams Renderer ─────────────────────────────────────────────────────────
2252
2186
 
2253
- describe('Teams renderer', () => {
2254
- it('renders heading as TextBlock', async () => {
2255
- const doc = Document({ children: Heading({ children: 'Hello' }) })
2256
- const json = (await render(doc, 'teams')) as string
2187
+ describe("Teams renderer", () => {
2188
+ it("renders heading as TextBlock", async () => {
2189
+ const doc = Document({ children: Heading({ children: "Hello" }) })
2190
+ const json = (await render(doc, "teams")) as string
2257
2191
  const card = JSON.parse(json)
2258
- expect(card.type).toBe('AdaptiveCard')
2259
- expect(card.body[0].type).toBe('TextBlock')
2260
- expect(card.body[0].text).toBe('Hello')
2261
- expect(card.body[0].weight).toBe('bolder')
2192
+ expect(card.type).toBe("AdaptiveCard")
2193
+ expect(card.body[0].type).toBe("TextBlock")
2194
+ expect(card.body[0].text).toBe("Hello")
2195
+ expect(card.body[0].weight).toBe("bolder")
2262
2196
  })
2263
2197
 
2264
- it('renders bold text', async () => {
2265
- const doc = Document({ children: Text({ bold: true, children: 'Bold' }) })
2266
- const json = (await render(doc, 'teams')) as string
2198
+ it("renders bold text", async () => {
2199
+ const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
2200
+ const json = (await render(doc, "teams")) as string
2267
2201
  const card = JSON.parse(json)
2268
- expect(card.body[0].text).toContain('**Bold**')
2202
+ expect(card.body[0].text).toContain("**Bold**")
2269
2203
  })
2270
2204
 
2271
- it('renders button as Action.OpenUrl', async () => {
2205
+ it("renders button as Action.OpenUrl", async () => {
2272
2206
  const doc = Document({
2273
- children: Button({ href: '/go', children: 'Click' }),
2207
+ children: Button({ href: "/go", children: "Click" }),
2274
2208
  })
2275
- const json = (await render(doc, 'teams')) as string
2209
+ const json = (await render(doc, "teams")) as string
2276
2210
  const card = JSON.parse(json)
2277
- expect(card.body[0].type).toBe('ActionSet')
2278
- expect(card.body[0].actions[0].type).toBe('Action.OpenUrl')
2211
+ expect(card.body[0].type).toBe("ActionSet")
2212
+ expect(card.body[0].actions[0].type).toBe("Action.OpenUrl")
2279
2213
  })
2280
2214
 
2281
- it('renders table as ColumnSet', async () => {
2215
+ it("renders table as ColumnSet", async () => {
2282
2216
  const doc = Document({
2283
- children: Table({ columns: ['A', 'B'], rows: [['1', '2']] }),
2217
+ children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
2284
2218
  })
2285
- const json = (await render(doc, 'teams')) as string
2219
+ const json = (await render(doc, "teams")) as string
2286
2220
  const card = JSON.parse(json)
2287
- expect(card.body[0].type).toBe('ColumnSet')
2221
+ expect(card.body[0].type).toBe("ColumnSet")
2288
2222
  })
2289
2223
 
2290
- it('renders list', async () => {
2224
+ it("renders list", async () => {
2291
2225
  const doc = Document({
2292
2226
  children: List({
2293
- children: [
2294
- ListItem({ children: 'one' }),
2295
- ListItem({ children: 'two' }),
2296
- ],
2227
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
2297
2228
  }),
2298
2229
  })
2299
- const json = (await render(doc, 'teams')) as string
2230
+ const json = (await render(doc, "teams")) as string
2300
2231
  const card = JSON.parse(json)
2301
- expect(card.body[0].text).toContain('• one')
2232
+ expect(card.body[0].text).toContain("• one")
2302
2233
  })
2303
2234
 
2304
- it('renders code as monospace', async () => {
2305
- const doc = Document({ children: Code({ children: 'x = 1' }) })
2306
- const json = (await render(doc, 'teams')) as string
2235
+ it("renders code as monospace", async () => {
2236
+ const doc = Document({ children: Code({ children: "x = 1" }) })
2237
+ const json = (await render(doc, "teams")) as string
2307
2238
  const card = JSON.parse(json)
2308
- expect(card.body[0].fontType).toBe('monospace')
2239
+ expect(card.body[0].fontType).toBe("monospace")
2309
2240
  })
2310
2241
 
2311
- it('renders divider as separator', async () => {
2242
+ it("renders divider as separator", async () => {
2312
2243
  const doc = Document({ children: Divider() })
2313
- const json = (await render(doc, 'teams')) as string
2244
+ const json = (await render(doc, "teams")) as string
2314
2245
  const card = JSON.parse(json)
2315
2246
  expect(card.body[0].separator).toBe(true)
2316
2247
  })
2317
2248
 
2318
- it('renders image with URL', async () => {
2249
+ it("renders image with URL", async () => {
2319
2250
  const doc = Document({
2320
- children: Image({ src: 'https://x.com/img.png', alt: 'Photo' }),
2251
+ children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
2321
2252
  })
2322
- const json = (await render(doc, 'teams')) as string
2253
+ const json = (await render(doc, "teams")) as string
2323
2254
  const card = JSON.parse(json)
2324
- expect(card.body[0].type).toBe('Image')
2255
+ expect(card.body[0].type).toBe("Image")
2325
2256
  })
2326
2257
 
2327
- it('renders quote as Container', async () => {
2328
- const doc = Document({ children: Quote({ children: 'wise' }) })
2329
- const json = (await render(doc, 'teams')) as string
2258
+ it("renders quote as Container", async () => {
2259
+ const doc = Document({ children: Quote({ children: "wise" }) })
2260
+ const json = (await render(doc, "teams")) as string
2330
2261
  const card = JSON.parse(json)
2331
- expect(card.body[0].type).toBe('Container')
2332
- expect(card.body[0].style).toBe('emphasis')
2262
+ expect(card.body[0].type).toBe("Container")
2263
+ expect(card.body[0].style).toBe("emphasis")
2333
2264
  })
2334
2265
 
2335
- it('renders link as markdown', async () => {
2266
+ it("renders link as markdown", async () => {
2336
2267
  const doc = Document({
2337
- children: Link({ href: 'https://x.com', children: 'X' }),
2268
+ children: Link({ href: "https://x.com", children: "X" }),
2338
2269
  })
2339
- const json = (await render(doc, 'teams')) as string
2270
+ const json = (await render(doc, "teams")) as string
2340
2271
  const card = JSON.parse(json)
2341
- expect(card.body[0].text).toContain('[X](https://x.com)')
2272
+ expect(card.body[0].text).toContain("[X](https://x.com)")
2342
2273
  })
2343
2274
 
2344
- it('builder toTeams works', async () => {
2345
- const json = await createDocument().heading('Hi').toTeams()
2275
+ it("builder toTeams works", async () => {
2276
+ const json = await createDocument().heading("Hi").toTeams()
2346
2277
  const card = JSON.parse(json)
2347
- expect(card.type).toBe('AdaptiveCard')
2278
+ expect(card.type).toBe("AdaptiveCard")
2348
2279
  })
2349
2280
  })
2350
2281
 
2351
2282
  // ─── Discord Renderer ───────────────────────────────────────────────────────
2352
2283
 
2353
- describe('Discord renderer', () => {
2354
- it('renders heading as embed title', async () => {
2355
- const doc = Document({ children: Heading({ children: 'Title' }) })
2356
- const json = (await render(doc, 'discord')) as string
2284
+ describe("Discord renderer", () => {
2285
+ it("renders heading as embed title", async () => {
2286
+ const doc = Document({ children: Heading({ children: "Title" }) })
2287
+ const json = (await render(doc, "discord")) as string
2357
2288
  const payload = JSON.parse(json)
2358
- expect(payload.embeds[0].title).toBe('Title')
2289
+ expect(payload.embeds[0].title).toBe("Title")
2359
2290
  })
2360
2291
 
2361
- it('renders text in description', async () => {
2292
+ it("renders text in description", async () => {
2362
2293
  const doc = Document({
2363
- children: [Heading({ children: 'T' }), Text({ children: 'Body' })],
2294
+ children: [Heading({ children: "T" }), Text({ children: "Body" })],
2364
2295
  })
2365
- const json = (await render(doc, 'discord')) as string
2296
+ const json = (await render(doc, "discord")) as string
2366
2297
  const payload = JSON.parse(json)
2367
- expect(payload.embeds[0].description).toContain('Body')
2298
+ expect(payload.embeds[0].description).toContain("Body")
2368
2299
  })
2369
2300
 
2370
- it('renders small table as fields', async () => {
2301
+ it("renders small table as fields", async () => {
2371
2302
  const doc = Document({
2372
2303
  children: Table({
2373
- columns: ['A', 'B'],
2304
+ columns: ["A", "B"],
2374
2305
  rows: [
2375
- ['1', '2'],
2376
- ['3', '4'],
2306
+ ["1", "2"],
2307
+ ["3", "4"],
2377
2308
  ],
2378
2309
  }),
2379
2310
  })
2380
- const json = (await render(doc, 'discord')) as string
2311
+ const json = (await render(doc, "discord")) as string
2381
2312
  const payload = JSON.parse(json)
2382
2313
  expect(payload.embeds[0].fields).toHaveLength(2)
2383
- expect(payload.embeds[0].fields[0].name).toBe('A')
2314
+ expect(payload.embeds[0].fields[0].name).toBe("A")
2384
2315
  expect(payload.embeds[0].fields[0].inline).toBe(true)
2385
2316
  })
2386
2317
 
2387
- it('renders image as embed image', async () => {
2388
- const doc = Document({ children: Image({ src: 'https://x.com/img.png' }) })
2389
- const json = (await render(doc, 'discord')) as string
2318
+ it("renders image as embed image", async () => {
2319
+ const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
2320
+ const json = (await render(doc, "discord")) as string
2390
2321
  const payload = JSON.parse(json)
2391
- expect(payload.embeds[0].image.url).toBe('https://x.com/img.png')
2322
+ expect(payload.embeds[0].image.url).toBe("https://x.com/img.png")
2392
2323
  })
2393
2324
 
2394
- it('renders quote with >', async () => {
2395
- const doc = Document({ children: Quote({ children: 'wise' }) })
2396
- const json = (await render(doc, 'discord')) as string
2325
+ it("renders quote with >", async () => {
2326
+ const doc = Document({ children: Quote({ children: "wise" }) })
2327
+ const json = (await render(doc, "discord")) as string
2397
2328
  const payload = JSON.parse(json)
2398
- expect(payload.embeds[0].description).toContain('> wise')
2329
+ expect(payload.embeds[0].description).toContain("> wise")
2399
2330
  })
2400
2331
 
2401
- it('renders code block', async () => {
2332
+ it("renders code block", async () => {
2402
2333
  const doc = Document({
2403
- children: Code({ language: 'js', children: 'x()' }),
2334
+ children: Code({ language: "js", children: "x()" }),
2404
2335
  })
2405
- const json = (await render(doc, 'discord')) as string
2336
+ const json = (await render(doc, "discord")) as string
2406
2337
  const payload = JSON.parse(json)
2407
- expect(payload.embeds[0].description).toContain('```js')
2338
+ expect(payload.embeds[0].description).toContain("```js")
2408
2339
  })
2409
2340
 
2410
- it('renders list', async () => {
2341
+ it("renders list", async () => {
2411
2342
  const doc = Document({
2412
2343
  children: List({
2413
2344
  ordered: true,
2414
- children: [ListItem({ children: 'a' })],
2345
+ children: [ListItem({ children: "a" })],
2415
2346
  }),
2416
2347
  })
2417
- const json = (await render(doc, 'discord')) as string
2348
+ const json = (await render(doc, "discord")) as string
2418
2349
  const payload = JSON.parse(json)
2419
- expect(payload.embeds[0].description).toContain('1. a')
2350
+ expect(payload.embeds[0].description).toContain("1. a")
2420
2351
  })
2421
2352
 
2422
- it('builder toDiscord works', async () => {
2423
- const json = await createDocument().heading('Hi').text('World').toDiscord()
2353
+ it("builder toDiscord works", async () => {
2354
+ const json = await createDocument().heading("Hi").text("World").toDiscord()
2424
2355
  const payload = JSON.parse(json)
2425
2356
  expect(payload.embeds).toHaveLength(1)
2426
2357
  })
@@ -2428,221 +2359,214 @@ describe('Discord renderer', () => {
2428
2359
 
2429
2360
  // ─── Telegram Renderer ──────────────────────────────────────────────────────
2430
2361
 
2431
- describe('Telegram renderer', () => {
2432
- it('renders heading as bold', async () => {
2433
- const doc = Document({ children: Heading({ children: 'Title' }) })
2434
- const html = (await render(doc, 'telegram')) as string
2435
- expect(html).toContain('<b>Title</b>')
2362
+ describe("Telegram renderer", () => {
2363
+ it("renders heading as bold", async () => {
2364
+ const doc = Document({ children: Heading({ children: "Title" }) })
2365
+ const html = (await render(doc, "telegram")) as string
2366
+ expect(html).toContain("<b>Title</b>")
2436
2367
  })
2437
2368
 
2438
- it('renders text with formatting', async () => {
2369
+ it("renders text with formatting", async () => {
2439
2370
  const doc = Document({
2440
2371
  children: [
2441
- Text({ bold: true, children: 'Bold' }),
2442
- Text({ italic: true, children: 'Italic' }),
2443
- Text({ underline: true, children: 'Under' }),
2444
- Text({ strikethrough: true, children: 'Struck' }),
2372
+ Text({ bold: true, children: "Bold" }),
2373
+ Text({ italic: true, children: "Italic" }),
2374
+ Text({ underline: true, children: "Under" }),
2375
+ Text({ strikethrough: true, children: "Struck" }),
2445
2376
  ],
2446
2377
  })
2447
- const html = (await render(doc, 'telegram')) as string
2448
- expect(html).toContain('<b>Bold</b>')
2449
- expect(html).toContain('<i>Italic</i>')
2450
- expect(html).toContain('<u>Under</u>')
2451
- expect(html).toContain('<s>Struck</s>')
2378
+ const html = (await render(doc, "telegram")) as string
2379
+ expect(html).toContain("<b>Bold</b>")
2380
+ expect(html).toContain("<i>Italic</i>")
2381
+ expect(html).toContain("<u>Under</u>")
2382
+ expect(html).toContain("<s>Struck</s>")
2452
2383
  })
2453
2384
 
2454
- it('renders link as <a>', async () => {
2385
+ it("renders link as <a>", async () => {
2455
2386
  const doc = Document({
2456
- children: Link({ href: 'https://x.com', children: 'X' }),
2387
+ children: Link({ href: "https://x.com", children: "X" }),
2457
2388
  })
2458
- const html = (await render(doc, 'telegram')) as string
2389
+ const html = (await render(doc, "telegram")) as string
2459
2390
  expect(html).toContain('<a href="https://x.com">X</a>')
2460
2391
  })
2461
2392
 
2462
- it('renders table as pre-formatted text', async () => {
2393
+ it("renders table as pre-formatted text", async () => {
2463
2394
  const doc = Document({
2464
- children: Table({ columns: ['A', 'B'], rows: [['1', '2']] }),
2395
+ children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
2465
2396
  })
2466
- const html = (await render(doc, 'telegram')) as string
2467
- expect(html).toContain('<pre>')
2468
- expect(html).toContain('A | B')
2469
- expect(html).toContain('1 | 2')
2397
+ const html = (await render(doc, "telegram")) as string
2398
+ expect(html).toContain("<pre>")
2399
+ expect(html).toContain("A | B")
2400
+ expect(html).toContain("1 | 2")
2470
2401
  })
2471
2402
 
2472
- it('renders code with language', async () => {
2403
+ it("renders code with language", async () => {
2473
2404
  const doc = Document({
2474
- children: Code({ language: 'python', children: 'x = 1' }),
2405
+ children: Code({ language: "python", children: "x = 1" }),
2475
2406
  })
2476
- const html = (await render(doc, 'telegram')) as string
2477
- expect(html).toContain('language-python')
2478
- expect(html).toContain('x = 1')
2407
+ const html = (await render(doc, "telegram")) as string
2408
+ expect(html).toContain("language-python")
2409
+ expect(html).toContain("x = 1")
2479
2410
  })
2480
2411
 
2481
- it('renders code without language', async () => {
2482
- const doc = Document({ children: Code({ children: 'x = 1' }) })
2483
- const html = (await render(doc, 'telegram')) as string
2484
- expect(html).toContain('<pre>x = 1</pre>')
2412
+ it("renders code without language", async () => {
2413
+ const doc = Document({ children: Code({ children: "x = 1" }) })
2414
+ const html = (await render(doc, "telegram")) as string
2415
+ expect(html).toContain("<pre>x = 1</pre>")
2485
2416
  })
2486
2417
 
2487
- it('renders quote as blockquote', async () => {
2488
- const doc = Document({ children: Quote({ children: 'wise' }) })
2489
- const html = (await render(doc, 'telegram')) as string
2490
- expect(html).toContain('<blockquote>wise</blockquote>')
2418
+ it("renders quote as blockquote", async () => {
2419
+ const doc = Document({ children: Quote({ children: "wise" }) })
2420
+ const html = (await render(doc, "telegram")) as string
2421
+ expect(html).toContain("<blockquote>wise</blockquote>")
2491
2422
  })
2492
2423
 
2493
- it('renders list', async () => {
2424
+ it("renders list", async () => {
2494
2425
  const doc = Document({
2495
2426
  children: List({
2496
- children: [
2497
- ListItem({ children: 'one' }),
2498
- ListItem({ children: 'two' }),
2499
- ],
2427
+ children: [ListItem({ children: "one" }), ListItem({ children: "two" })],
2500
2428
  }),
2501
2429
  })
2502
- const html = (await render(doc, 'telegram')) as string
2503
- expect(html).toContain('• one')
2504
- expect(html).toContain('• two')
2430
+ const html = (await render(doc, "telegram")) as string
2431
+ expect(html).toContain("• one")
2432
+ expect(html).toContain("• two")
2505
2433
  })
2506
2434
 
2507
- it('renders button as link', async () => {
2435
+ it("renders button as link", async () => {
2508
2436
  const doc = Document({
2509
- children: Button({ href: '/pay', children: 'Pay' }),
2437
+ children: Button({ href: "/pay", children: "Pay" }),
2510
2438
  })
2511
- const html = (await render(doc, 'telegram')) as string
2439
+ const html = (await render(doc, "telegram")) as string
2512
2440
  expect(html).toContain('<a href="/pay">Pay</a>')
2513
2441
  })
2514
2442
 
2515
- it('skips images (sent separately in Telegram)', async () => {
2516
- const doc = Document({ children: Image({ src: 'https://x.com/img.png' }) })
2517
- const html = (await render(doc, 'telegram')) as string
2518
- expect(html).toBe('')
2443
+ it("skips images (sent separately in Telegram)", async () => {
2444
+ const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
2445
+ const html = (await render(doc, "telegram")) as string
2446
+ expect(html).toBe("")
2519
2447
  })
2520
2448
 
2521
- it('escapes HTML entities', async () => {
2449
+ it("escapes HTML entities", async () => {
2522
2450
  const doc = Document({
2523
- children: Text({ children: '<script>alert(1)</script>' }),
2451
+ children: Text({ children: "<script>alert(1)</script>" }),
2524
2452
  })
2525
- const html = (await render(doc, 'telegram')) as string
2526
- expect(html).not.toContain('<script>')
2527
- expect(html).toContain('&lt;script&gt;')
2453
+ const html = (await render(doc, "telegram")) as string
2454
+ expect(html).not.toContain("<script>")
2455
+ expect(html).toContain("&lt;script&gt;")
2528
2456
  })
2529
2457
 
2530
- it('builder toTelegram works', async () => {
2531
- const html = await createDocument().heading('Hi').text('World').toTelegram()
2532
- expect(html).toContain('<b>Hi</b>')
2533
- expect(html).toContain('World')
2458
+ it("builder toTelegram works", async () => {
2459
+ const html = await createDocument().heading("Hi").text("World").toTelegram()
2460
+ expect(html).toContain("<b>Hi</b>")
2461
+ expect(html).toContain("World")
2534
2462
  })
2535
2463
  })
2536
2464
 
2537
2465
  // ─── Notion Renderer ────────────────────────────────────────────────────────
2538
2466
 
2539
- describe('Notion renderer', () => {
2540
- it('renders heading as heading block', async () => {
2541
- const doc = Document({ children: Heading({ children: 'Title' }) })
2542
- const json = (await render(doc, 'notion')) as string
2467
+ describe("Notion renderer", () => {
2468
+ it("renders heading as heading block", async () => {
2469
+ const doc = Document({ children: Heading({ children: "Title" }) })
2470
+ const json = (await render(doc, "notion")) as string
2543
2471
  const parsed = JSON.parse(json)
2544
- expect(parsed.children[0].type).toBe('heading_1')
2472
+ expect(parsed.children[0].type).toBe("heading_1")
2545
2473
  })
2546
2474
 
2547
- it('renders h2 as heading_2', async () => {
2548
- const doc = Document({ children: Heading({ level: 2, children: 'Sub' }) })
2549
- const json = (await render(doc, 'notion')) as string
2475
+ it("renders h2 as heading_2", async () => {
2476
+ const doc = Document({ children: Heading({ level: 2, children: "Sub" }) })
2477
+ const json = (await render(doc, "notion")) as string
2550
2478
  const parsed = JSON.parse(json)
2551
- expect(parsed.children[0].type).toBe('heading_2')
2479
+ expect(parsed.children[0].type).toBe("heading_2")
2552
2480
  })
2553
2481
 
2554
- it('renders h3+ as heading_3', async () => {
2555
- const doc = Document({ children: Heading({ level: 4, children: 'Sub' }) })
2556
- const json = (await render(doc, 'notion')) as string
2482
+ it("renders h3+ as heading_3", async () => {
2483
+ const doc = Document({ children: Heading({ level: 4, children: "Sub" }) })
2484
+ const json = (await render(doc, "notion")) as string
2557
2485
  const parsed = JSON.parse(json)
2558
- expect(parsed.children[0].type).toBe('heading_3')
2486
+ expect(parsed.children[0].type).toBe("heading_3")
2559
2487
  })
2560
2488
 
2561
- it('renders text as paragraph', async () => {
2562
- const doc = Document({ children: Text({ bold: true, children: 'Bold' }) })
2563
- const json = (await render(doc, 'notion')) as string
2489
+ it("renders text as paragraph", async () => {
2490
+ const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
2491
+ const json = (await render(doc, "notion")) as string
2564
2492
  const parsed = JSON.parse(json)
2565
- expect(parsed.children[0].type).toBe('paragraph')
2566
- expect(parsed.children[0].paragraph.rich_text[0].annotations.bold).toBe(
2567
- true,
2568
- )
2493
+ expect(parsed.children[0].type).toBe("paragraph")
2494
+ expect(parsed.children[0].paragraph.rich_text[0].annotations.bold).toBe(true)
2569
2495
  })
2570
2496
 
2571
- it('renders table with header row', async () => {
2497
+ it("renders table with header row", async () => {
2572
2498
  const doc = Document({
2573
- children: Table({ columns: ['A', 'B'], rows: [['1', '2']] }),
2499
+ children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
2574
2500
  })
2575
- const json = (await render(doc, 'notion')) as string
2501
+ const json = (await render(doc, "notion")) as string
2576
2502
  const parsed = JSON.parse(json)
2577
- expect(parsed.children[0].type).toBe('table')
2503
+ expect(parsed.children[0].type).toBe("table")
2578
2504
  expect(parsed.children[0].table.has_column_header).toBe(true)
2579
2505
  })
2580
2506
 
2581
- it('renders bulleted list', async () => {
2507
+ it("renders bulleted list", async () => {
2582
2508
  const doc = Document({
2583
- children: List({ children: [ListItem({ children: 'a' })] }),
2509
+ children: List({ children: [ListItem({ children: "a" })] }),
2584
2510
  })
2585
- const json = (await render(doc, 'notion')) as string
2511
+ const json = (await render(doc, "notion")) as string
2586
2512
  const parsed = JSON.parse(json)
2587
- expect(parsed.children[0].type).toBe('bulleted_list_item')
2513
+ expect(parsed.children[0].type).toBe("bulleted_list_item")
2588
2514
  })
2589
2515
 
2590
- it('renders numbered list', async () => {
2516
+ it("renders numbered list", async () => {
2591
2517
  const doc = Document({
2592
2518
  children: List({
2593
2519
  ordered: true,
2594
- children: [ListItem({ children: 'a' })],
2520
+ children: [ListItem({ children: "a" })],
2595
2521
  }),
2596
2522
  })
2597
- const json = (await render(doc, 'notion')) as string
2523
+ const json = (await render(doc, "notion")) as string
2598
2524
  const parsed = JSON.parse(json)
2599
- expect(parsed.children[0].type).toBe('numbered_list_item')
2525
+ expect(parsed.children[0].type).toBe("numbered_list_item")
2600
2526
  })
2601
2527
 
2602
- it('renders code block', async () => {
2528
+ it("renders code block", async () => {
2603
2529
  const doc = Document({
2604
- children: Code({ language: 'python', children: 'x = 1' }),
2530
+ children: Code({ language: "python", children: "x = 1" }),
2605
2531
  })
2606
- const json = (await render(doc, 'notion')) as string
2532
+ const json = (await render(doc, "notion")) as string
2607
2533
  const parsed = JSON.parse(json)
2608
- expect(parsed.children[0].type).toBe('code')
2609
- expect(parsed.children[0].code.language).toBe('python')
2534
+ expect(parsed.children[0].type).toBe("code")
2535
+ expect(parsed.children[0].code.language).toBe("python")
2610
2536
  })
2611
2537
 
2612
- it('renders quote', async () => {
2613
- const doc = Document({ children: Quote({ children: 'wise' }) })
2614
- const json = (await render(doc, 'notion')) as string
2538
+ it("renders quote", async () => {
2539
+ const doc = Document({ children: Quote({ children: "wise" }) })
2540
+ const json = (await render(doc, "notion")) as string
2615
2541
  const parsed = JSON.parse(json)
2616
- expect(parsed.children[0].type).toBe('quote')
2542
+ expect(parsed.children[0].type).toBe("quote")
2617
2543
  })
2618
2544
 
2619
- it('renders divider', async () => {
2545
+ it("renders divider", async () => {
2620
2546
  const doc = Document({ children: Divider() })
2621
- const json = (await render(doc, 'notion')) as string
2547
+ const json = (await render(doc, "notion")) as string
2622
2548
  const parsed = JSON.parse(json)
2623
- expect(parsed.children[0].type).toBe('divider')
2549
+ expect(parsed.children[0].type).toBe("divider")
2624
2550
  })
2625
2551
 
2626
- it('renders image with URL', async () => {
2627
- const doc = Document({ children: Image({ src: 'https://x.com/img.png' }) })
2628
- const json = (await render(doc, 'notion')) as string
2552
+ it("renders image with URL", async () => {
2553
+ const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
2554
+ const json = (await render(doc, "notion")) as string
2629
2555
  const parsed = JSON.parse(json)
2630
- expect(parsed.children[0].type).toBe('image')
2556
+ expect(parsed.children[0].type).toBe("image")
2631
2557
  })
2632
2558
 
2633
- it('renders link as paragraph with link', async () => {
2559
+ it("renders link as paragraph with link", async () => {
2634
2560
  const doc = Document({
2635
- children: Link({ href: 'https://x.com', children: 'X' }),
2561
+ children: Link({ href: "https://x.com", children: "X" }),
2636
2562
  })
2637
- const json = (await render(doc, 'notion')) as string
2563
+ const json = (await render(doc, "notion")) as string
2638
2564
  const parsed = JSON.parse(json)
2639
- expect(parsed.children[0].paragraph.rich_text[0].text.link.url).toBe(
2640
- 'https://x.com',
2641
- )
2565
+ expect(parsed.children[0].paragraph.rich_text[0].text.link.url).toBe("https://x.com")
2642
2566
  })
2643
2567
 
2644
- it('builder toNotion works', async () => {
2645
- const json = await createDocument().heading('Hi').toNotion()
2568
+ it("builder toNotion works", async () => {
2569
+ const json = await createDocument().heading("Hi").toNotion()
2646
2570
  const parsed = JSON.parse(json)
2647
2571
  expect(parsed.children.length).toBeGreaterThan(0)
2648
2572
  })
@@ -2650,271 +2574,299 @@ describe('Notion renderer', () => {
2650
2574
 
2651
2575
  // ─── Confluence/Jira Renderer ───────────────────────────────────────────────
2652
2576
 
2653
- describe('Confluence renderer', () => {
2654
- it('renders ADF document', async () => {
2655
- const doc = Document({ children: Heading({ children: 'Title' }) })
2656
- const json = (await render(doc, 'confluence')) as string
2577
+ describe("Confluence renderer", () => {
2578
+ it("renders ADF document", async () => {
2579
+ const doc = Document({ children: Heading({ children: "Title" }) })
2580
+ const json = (await render(doc, "confluence")) as string
2657
2581
  const adf = JSON.parse(json)
2658
2582
  expect(adf.version).toBe(1)
2659
- expect(adf.type).toBe('doc')
2660
- expect(adf.content[0].type).toBe('heading')
2583
+ expect(adf.type).toBe("doc")
2584
+ expect(adf.content[0].type).toBe("heading")
2661
2585
  })
2662
2586
 
2663
- it('renders text with marks', async () => {
2587
+ it("renders text with marks", async () => {
2664
2588
  const doc = Document({
2665
- children: Text({ bold: true, italic: true, children: 'styled' }),
2589
+ children: Text({ bold: true, italic: true, children: "styled" }),
2666
2590
  })
2667
- const json = (await render(doc, 'confluence')) as string
2591
+ const json = (await render(doc, "confluence")) as string
2668
2592
  const adf = JSON.parse(json)
2669
2593
  const marks = adf.content[0].content[0].marks
2670
- expect(marks.some((m: any) => m.type === 'strong')).toBe(true)
2671
- expect(marks.some((m: any) => m.type === 'em')).toBe(true)
2594
+ expect(marks.some((m: any) => m.type === "strong")).toBe(true)
2595
+ expect(marks.some((m: any) => m.type === "em")).toBe(true)
2672
2596
  })
2673
2597
 
2674
- it('renders table', async () => {
2598
+ it("renders table", async () => {
2675
2599
  const doc = Document({
2676
- children: Table({ columns: ['A'], rows: [['1']] }),
2600
+ children: Table({ columns: ["A"], rows: [["1"]] }),
2677
2601
  })
2678
- const json = (await render(doc, 'confluence')) as string
2602
+ const json = (await render(doc, "confluence")) as string
2679
2603
  const adf = JSON.parse(json)
2680
- expect(adf.content[0].type).toBe('table')
2681
- expect(adf.content[0].content[0].content[0].type).toBe('tableHeader')
2604
+ expect(adf.content[0].type).toBe("table")
2605
+ expect(adf.content[0].content[0].content[0].type).toBe("tableHeader")
2682
2606
  })
2683
2607
 
2684
- it('renders ordered list', async () => {
2608
+ it("renders ordered list", async () => {
2685
2609
  const doc = Document({
2686
2610
  children: List({
2687
2611
  ordered: true,
2688
- children: [ListItem({ children: 'a' })],
2612
+ children: [ListItem({ children: "a" })],
2689
2613
  }),
2690
2614
  })
2691
- const json = (await render(doc, 'confluence')) as string
2615
+ const json = (await render(doc, "confluence")) as string
2692
2616
  const adf = JSON.parse(json)
2693
- expect(adf.content[0].type).toBe('orderedList')
2617
+ expect(adf.content[0].type).toBe("orderedList")
2694
2618
  })
2695
2619
 
2696
- it('renders code block', async () => {
2620
+ it("renders code block", async () => {
2697
2621
  const doc = Document({
2698
- children: Code({ language: 'java', children: 'int x = 1;' }),
2622
+ children: Code({ language: "java", children: "int x = 1;" }),
2699
2623
  })
2700
- const json = (await render(doc, 'confluence')) as string
2624
+ const json = (await render(doc, "confluence")) as string
2701
2625
  const adf = JSON.parse(json)
2702
- expect(adf.content[0].type).toBe('codeBlock')
2703
- expect(adf.content[0].attrs.language).toBe('java')
2626
+ expect(adf.content[0].type).toBe("codeBlock")
2627
+ expect(adf.content[0].attrs.language).toBe("java")
2704
2628
  })
2705
2629
 
2706
- it('renders blockquote', async () => {
2707
- const doc = Document({ children: Quote({ children: 'wise' }) })
2708
- const json = (await render(doc, 'confluence')) as string
2630
+ it("renders blockquote", async () => {
2631
+ const doc = Document({ children: Quote({ children: "wise" }) })
2632
+ const json = (await render(doc, "confluence")) as string
2709
2633
  const adf = JSON.parse(json)
2710
- expect(adf.content[0].type).toBe('blockquote')
2634
+ expect(adf.content[0].type).toBe("blockquote")
2711
2635
  })
2712
2636
 
2713
- it('renders rule (divider)', async () => {
2637
+ it("renders rule (divider)", async () => {
2714
2638
  const doc = Document({ children: Divider() })
2715
- const json = (await render(doc, 'confluence')) as string
2639
+ const json = (await render(doc, "confluence")) as string
2716
2640
  const adf = JSON.parse(json)
2717
- expect(adf.content[0].type).toBe('rule')
2641
+ expect(adf.content[0].type).toBe("rule")
2718
2642
  })
2719
2643
 
2720
- it('renders link with href', async () => {
2644
+ it("renders link with href", async () => {
2721
2645
  const doc = Document({
2722
- children: Link({ href: 'https://x.com', children: 'X' }),
2646
+ children: Link({ href: "https://x.com", children: "X" }),
2723
2647
  })
2724
- const json = (await render(doc, 'confluence')) as string
2648
+ const json = (await render(doc, "confluence")) as string
2725
2649
  const adf = JSON.parse(json)
2726
- expect(adf.content[0].content[0].marks[0].attrs.href).toBe('https://x.com')
2650
+ expect(adf.content[0].content[0].marks[0].attrs.href).toBe("https://x.com")
2727
2651
  })
2728
2652
 
2729
- it('builder toConfluence works', async () => {
2730
- const json = await createDocument().heading('Hi').toConfluence()
2653
+ it("builder toConfluence works", async () => {
2654
+ const json = await createDocument().heading("Hi").toConfluence()
2731
2655
  const adf = JSON.parse(json)
2732
- expect(adf.type).toBe('doc')
2656
+ expect(adf.type).toBe("doc")
2733
2657
  })
2734
2658
  })
2735
2659
 
2736
2660
  // ─── WhatsApp Renderer ──────────────────────────────────────────────────────
2737
2661
 
2738
- describe('WhatsApp renderer', () => {
2739
- it('renders heading as bold', async () => {
2740
- const doc = Document({ children: Heading({ children: 'Title' }) })
2741
- const text = (await render(doc, 'whatsapp')) as string
2742
- expect(text).toContain('*Title*')
2662
+ describe("WhatsApp renderer", () => {
2663
+ it("renders heading as bold", async () => {
2664
+ const doc = Document({ children: Heading({ children: "Title" }) })
2665
+ const text = (await render(doc, "whatsapp")) as string
2666
+ expect(text).toContain("*Title*")
2743
2667
  })
2744
2668
 
2745
- it('renders bold, italic, strikethrough', async () => {
2669
+ it("renders bold, italic, strikethrough", async () => {
2746
2670
  const doc = Document({
2747
2671
  children: [
2748
- Text({ bold: true, children: 'Bold' }),
2749
- Text({ italic: true, children: 'Italic' }),
2750
- Text({ strikethrough: true, children: 'Struck' }),
2672
+ Text({ bold: true, children: "Bold" }),
2673
+ Text({ italic: true, children: "Italic" }),
2674
+ Text({ strikethrough: true, children: "Struck" }),
2751
2675
  ],
2752
2676
  })
2753
- const text = (await render(doc, 'whatsapp')) as string
2754
- expect(text).toContain('*Bold*')
2755
- expect(text).toContain('_Italic_')
2756
- expect(text).toContain('~Struck~')
2677
+ const text = (await render(doc, "whatsapp")) as string
2678
+ expect(text).toContain("*Bold*")
2679
+ expect(text).toContain("_Italic_")
2680
+ expect(text).toContain("~Struck~")
2757
2681
  })
2758
2682
 
2759
- it('renders code as triple backticks', async () => {
2760
- const doc = Document({ children: Code({ children: 'x = 1' }) })
2761
- const text = (await render(doc, 'whatsapp')) as string
2762
- expect(text).toContain('```x = 1```')
2683
+ it("renders code as triple backticks", async () => {
2684
+ const doc = Document({ children: Code({ children: "x = 1" }) })
2685
+ const text = (await render(doc, "whatsapp")) as string
2686
+ expect(text).toContain("```x = 1```")
2763
2687
  })
2764
2688
 
2765
- it('renders quote with >', async () => {
2766
- const doc = Document({ children: Quote({ children: 'wise' }) })
2767
- const text = (await render(doc, 'whatsapp')) as string
2768
- expect(text).toContain('> wise')
2689
+ it("renders quote with >", async () => {
2690
+ const doc = Document({ children: Quote({ children: "wise" }) })
2691
+ const text = (await render(doc, "whatsapp")) as string
2692
+ expect(text).toContain("> wise")
2769
2693
  })
2770
2694
 
2771
- it('renders link as text + URL', async () => {
2695
+ it("renders link as text + URL", async () => {
2772
2696
  const doc = Document({
2773
- children: Link({ href: 'https://x.com', children: 'X' }),
2697
+ children: Link({ href: "https://x.com", children: "X" }),
2774
2698
  })
2775
- const text = (await render(doc, 'whatsapp')) as string
2776
- expect(text).toContain('X: https://x.com')
2699
+ const text = (await render(doc, "whatsapp")) as string
2700
+ expect(text).toContain("X: https://x.com")
2777
2701
  })
2778
2702
 
2779
- it('renders table', async () => {
2703
+ it("renders table", async () => {
2780
2704
  const doc = Document({
2781
- children: Table({ columns: ['A', 'B'], rows: [['1', '2']] }),
2705
+ children: Table({ columns: ["A", "B"], rows: [["1", "2"]] }),
2782
2706
  })
2783
- const text = (await render(doc, 'whatsapp')) as string
2784
- expect(text).toContain('*A* | *B*')
2785
- expect(text).toContain('1 | 2')
2707
+ const text = (await render(doc, "whatsapp")) as string
2708
+ expect(text).toContain("*A* | *B*")
2709
+ expect(text).toContain("1 | 2")
2786
2710
  })
2787
2711
 
2788
- it('renders list', async () => {
2712
+ it("renders list", async () => {
2789
2713
  const doc = Document({
2790
- children: List({ children: [ListItem({ children: 'one' })] }),
2714
+ children: List({ children: [ListItem({ children: "one" })] }),
2791
2715
  })
2792
- const text = (await render(doc, 'whatsapp')) as string
2793
- expect(text).toContain('• one')
2716
+ const text = (await render(doc, "whatsapp")) as string
2717
+ expect(text).toContain("• one")
2794
2718
  })
2795
2719
 
2796
- it('skips images', async () => {
2797
- const doc = Document({ children: Image({ src: 'https://x.com/img.png' }) })
2798
- const text = (await render(doc, 'whatsapp')) as string
2799
- expect(text).toBe('')
2720
+ it("skips images", async () => {
2721
+ const doc = Document({ children: Image({ src: "https://x.com/img.png" }) })
2722
+ const text = (await render(doc, "whatsapp")) as string
2723
+ expect(text).toBe("")
2800
2724
  })
2801
2725
 
2802
- it('builder toWhatsApp works', async () => {
2803
- const text = await createDocument().heading('Hi').text('World').toWhatsApp()
2804
- expect(text).toContain('*Hi*')
2805
- expect(text).toContain('World')
2726
+ it("builder toWhatsApp works", async () => {
2727
+ const text = await createDocument().heading("Hi").text("World").toWhatsApp()
2728
+ expect(text).toContain("*Hi*")
2729
+ expect(text).toContain("World")
2806
2730
  })
2807
2731
  })
2808
2732
 
2809
2733
  // ─── Google Chat Renderer ───────────────────────────────────────────────────
2810
2734
 
2811
- describe('Google Chat renderer', () => {
2812
- it('renders card with header', async () => {
2735
+ describe("Google Chat renderer", () => {
2736
+ it("renders card with header", async () => {
2813
2737
  const doc = Document({
2814
- title: 'Report',
2815
- children: Text({ children: 'Body' }),
2738
+ title: "Report",
2739
+ children: Text({ children: "Body" }),
2816
2740
  })
2817
- const json = (await render(doc, 'google-chat')) as string
2741
+ const json = (await render(doc, "google-chat")) as string
2818
2742
  const card = JSON.parse(json)
2819
- expect(card.cardsV2[0].card.header.title).toBe('Report')
2743
+ expect(card.cardsV2[0].card.header.title).toBe("Report")
2820
2744
  })
2821
2745
 
2822
- it('renders heading as decorated text', async () => {
2823
- const doc = Document({ children: Heading({ children: 'Title' }) })
2824
- const json = (await render(doc, 'google-chat')) as string
2746
+ it("renders heading as decorated text", async () => {
2747
+ const doc = Document({ children: Heading({ children: "Title" }) })
2748
+ const json = (await render(doc, "google-chat")) as string
2825
2749
  const card = JSON.parse(json)
2826
- expect(
2827
- card.cardsV2[0].card.sections[0].widgets[0].decoratedText.text,
2828
- ).toContain('<b>Title</b>')
2750
+ expect(card.cardsV2[0].card.sections[0].widgets[0].decoratedText.text).toContain("<b>Title</b>")
2829
2751
  })
2830
2752
 
2831
- it('renders text paragraph', async () => {
2832
- const doc = Document({ children: Text({ bold: true, children: 'Bold' }) })
2833
- const json = (await render(doc, 'google-chat')) as string
2753
+ it("renders text paragraph", async () => {
2754
+ const doc = Document({ children: Text({ bold: true, children: "Bold" }) })
2755
+ const json = (await render(doc, "google-chat")) as string
2834
2756
  const card = JSON.parse(json)
2835
- expect(
2836
- card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text,
2837
- ).toContain('<b>Bold</b>')
2757
+ expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain("<b>Bold</b>")
2838
2758
  })
2839
2759
 
2840
- it('renders button', async () => {
2760
+ it("renders button", async () => {
2841
2761
  const doc = Document({
2842
- children: Button({ href: '/go', children: 'Click' }),
2762
+ children: Button({ href: "/go", children: "Click" }),
2843
2763
  })
2844
- const json = (await render(doc, 'google-chat')) as string
2764
+ const json = (await render(doc, "google-chat")) as string
2845
2765
  const card = JSON.parse(json)
2846
- expect(
2847
- card.cardsV2[0].card.sections[0].widgets[0].buttonList.buttons[0].text,
2848
- ).toBe('Click')
2766
+ expect(card.cardsV2[0].card.sections[0].widgets[0].buttonList.buttons[0].text).toBe("Click")
2849
2767
  })
2850
2768
 
2851
- it('renders divider', async () => {
2769
+ it("renders divider", async () => {
2852
2770
  const doc = Document({ children: Divider() })
2853
- const json = (await render(doc, 'google-chat')) as string
2771
+ const json = (await render(doc, "google-chat")) as string
2854
2772
  const card = JSON.parse(json)
2855
2773
  expect(card.cardsV2[0].card.sections[0].widgets[0].divider).toBeDefined()
2856
2774
  })
2857
2775
 
2858
- it('renders image', async () => {
2776
+ it("renders image", async () => {
2859
2777
  const doc = Document({
2860
- children: Image({ src: 'https://x.com/img.png', alt: 'Photo' }),
2778
+ children: Image({ src: "https://x.com/img.png", alt: "Photo" }),
2861
2779
  })
2862
- const json = (await render(doc, 'google-chat')) as string
2780
+ const json = (await render(doc, "google-chat")) as string
2863
2781
  const card = JSON.parse(json)
2864
- expect(card.cardsV2[0].card.sections[0].widgets[0].image.imageUrl).toBe(
2865
- 'https://x.com/img.png',
2866
- )
2782
+ expect(card.cardsV2[0].card.sections[0].widgets[0].image.imageUrl).toBe("https://x.com/img.png")
2867
2783
  })
2868
2784
 
2869
- it('renders link', async () => {
2785
+ it("renders link", async () => {
2870
2786
  const doc = Document({
2871
- children: Link({ href: 'https://x.com', children: 'X' }),
2787
+ children: Link({ href: "https://x.com", children: "X" }),
2872
2788
  })
2873
- const json = (await render(doc, 'google-chat')) as string
2789
+ const json = (await render(doc, "google-chat")) as string
2874
2790
  const card = JSON.parse(json)
2875
- expect(
2876
- card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text,
2877
- ).toContain('href="https://x.com"')
2791
+ expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain(
2792
+ 'href="https://x.com"',
2793
+ )
2878
2794
  })
2879
2795
 
2880
- it('renders list', async () => {
2796
+ it("renders list", async () => {
2881
2797
  const doc = Document({
2882
- children: List({ children: [ListItem({ children: 'one' })] }),
2798
+ children: List({ children: [ListItem({ children: "one" })] }),
2883
2799
  })
2884
- const json = (await render(doc, 'google-chat')) as string
2800
+ const json = (await render(doc, "google-chat")) as string
2885
2801
  const card = JSON.parse(json)
2886
- expect(
2887
- card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text,
2888
- ).toContain('• one')
2802
+ expect(card.cardsV2[0].card.sections[0].widgets[0].textParagraph.text).toContain("• one")
2889
2803
  })
2890
2804
 
2891
- it('uses first heading as title when no title prop', async () => {
2805
+ it("uses first heading as title when no title prop", async () => {
2892
2806
  const doc = Document({
2893
- children: [
2894
- Heading({ children: 'Auto Title' }),
2895
- Text({ children: 'body' }),
2896
- ],
2807
+ children: [Heading({ children: "Auto Title" }), Text({ children: "body" })],
2897
2808
  })
2898
- const json = (await render(doc, 'google-chat')) as string
2809
+ const json = (await render(doc, "google-chat")) as string
2899
2810
  const card = JSON.parse(json)
2900
- expect(card.cardsV2[0].card.header.title).toBe('Auto Title')
2811
+ expect(card.cardsV2[0].card.header.title).toBe("Auto Title")
2901
2812
  })
2902
2813
 
2903
- it('builder toGoogleChat works', async () => {
2904
- const json = await createDocument({ title: 'Hi' })
2905
- .text('World')
2906
- .toGoogleChat()
2814
+ it("builder toGoogleChat works", async () => {
2815
+ const json = await createDocument({ title: "Hi" }).text("World").toGoogleChat()
2907
2816
  const card = JSON.parse(json)
2908
- expect(card.cardsV2[0].card.header.title).toBe('Hi')
2817
+ expect(card.cardsV2[0].card.header.title).toBe("Hi")
2909
2818
  })
2910
2819
  })
2911
2820
 
2912
- describe('builder toSlack', () => {
2913
- it('renders to Slack JSON', async () => {
2914
- const result = await createDocument().heading('Hi').text('World').toSlack()
2821
+ describe("builder toSlack", () => {
2822
+ it("renders to Slack JSON", async () => {
2823
+ const result = await createDocument().heading("Hi").text("World").toSlack()
2915
2824
  const parsed = JSON.parse(result)
2916
2825
  expect(parsed.blocks).toHaveLength(2)
2917
- expect(parsed.blocks[0].type).toBe('header')
2918
- expect(parsed.blocks[1].type).toBe('section')
2826
+ expect(parsed.blocks[0].type).toBe("header")
2827
+ expect(parsed.blocks[1].type).toBe("section")
2828
+ })
2829
+ })
2830
+
2831
+ // ─── Builder .add() and .section() ──────────────────────────────────────────
2832
+
2833
+ describe("builder .add() and .section()", () => {
2834
+ // build() wraps sections in Document({ children: [Page({ children: sections })] })
2835
+ // so actual content nodes are inside the page's children
2836
+ function getPageChildren(doc: ReturnType<typeof createDocument>) {
2837
+ const node = doc.build()
2838
+ expect(node.type).toBe("document")
2839
+ const page = node.children[0]!
2840
+ expect(typeof page !== "string" && page.type).toBe("page")
2841
+ return typeof page !== "string" ? page.children : []
2842
+ }
2843
+
2844
+ it("add() with a single node adds it to the document", () => {
2845
+ const doc = createDocument().add(Text({ children: "hello" }))
2846
+ const children = getPageChildren(doc)
2847
+ expect(children).toHaveLength(1)
2848
+ const child = children[0]!
2849
+ expect(typeof child !== "string" && child.type).toBe("text")
2850
+ })
2851
+
2852
+ it("add() with an array adds multiple nodes", () => {
2853
+ const doc = createDocument().add([Heading({ children: "a" }), Text({ children: "b" })])
2854
+ const children = getPageChildren(doc)
2855
+ expect(children).toHaveLength(2)
2856
+ const first = children[0]!
2857
+ const second = children[1]!
2858
+ expect(typeof first !== "string" && first.type).toBe("heading")
2859
+ expect(typeof second !== "string" && second.type).toBe("text")
2860
+ })
2861
+
2862
+ it("section() wraps children in a Section node", () => {
2863
+ const doc = createDocument().section([Text({ children: "a" }), Text({ children: "b" })])
2864
+ const children = getPageChildren(doc)
2865
+ expect(children).toHaveLength(1)
2866
+ const section = children[0]!
2867
+ expect(typeof section !== "string" && section.type).toBe("section")
2868
+ if (typeof section !== "string") {
2869
+ expect(section.children).toHaveLength(2)
2870
+ }
2919
2871
  })
2920
2872
  })