@pyreon/document 0.11.4 → 0.11.6

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 (50) hide show
  1. package/README.md +7 -4
  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/docx-uNAel545.js.map +1 -1
  6. package/lib/email-D0bbfWq4.js.map +1 -1
  7. package/lib/google-chat-CkKCBUWC.js.map +1 -1
  8. package/lib/html-B5biprN2.js.map +1 -1
  9. package/lib/index.js.map +1 -1
  10. package/lib/markdown-CdtlFGC0.js.map +1 -1
  11. package/lib/notion-iG2C5bEY.js.map +1 -1
  12. package/lib/pdf-IuBgTb3T.js.map +1 -1
  13. package/lib/pptx-DXiMiYFM.js.map +1 -1
  14. package/lib/sanitize-O_3j1mNJ.js.map +1 -1
  15. package/lib/slack-BI3EQwYm.js.map +1 -1
  16. package/lib/svg-BKxumy-p.js.map +1 -1
  17. package/lib/teams-Cwz9lce0.js.map +1 -1
  18. package/lib/telegram-gYFqyMXb.js.map +1 -1
  19. package/lib/text-l1XNXBOC.js.map +1 -1
  20. package/lib/types/index.d.ts +27 -27
  21. package/lib/whatsapp-CjSGoOKx.js.map +1 -1
  22. package/lib/xlsx-Cvu4LBNy.js.map +1 -1
  23. package/package.json +21 -21
  24. package/src/builder.ts +36 -36
  25. package/src/download.ts +32 -32
  26. package/src/index.ts +5 -10
  27. package/src/nodes.ts +45 -45
  28. package/src/render.ts +43 -43
  29. package/src/renderers/confluence.ts +63 -63
  30. package/src/renderers/csv.ts +10 -10
  31. package/src/renderers/discord.ts +37 -37
  32. package/src/renderers/docx.ts +57 -57
  33. package/src/renderers/email.ts +72 -72
  34. package/src/renderers/google-chat.ts +34 -34
  35. package/src/renderers/html.ts +76 -76
  36. package/src/renderers/markdown.ts +42 -42
  37. package/src/renderers/notion.ts +60 -60
  38. package/src/renderers/pdf.ts +78 -78
  39. package/src/renderers/pptx.ts +51 -51
  40. package/src/renderers/slack.ts +48 -48
  41. package/src/renderers/svg.ts +47 -47
  42. package/src/renderers/teams.ts +67 -67
  43. package/src/renderers/telegram.ts +39 -39
  44. package/src/renderers/text.ts +43 -43
  45. package/src/renderers/whatsapp.ts +33 -33
  46. package/src/renderers/xlsx.ts +35 -35
  47. package/src/sanitize.ts +20 -20
  48. package/src/tests/document.test.ts +1302 -1302
  49. package/src/tests/stress.test.ts +110 -110
  50. package/src/types.ts +61 -61
@@ -1,5 +1,5 @@
1
- import { sanitizeHref, sanitizeImageSrc } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import { sanitizeHref, sanitizeImageSrc } from '../sanitize'
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
3
3
 
4
4
  /**
5
5
  * Notion renderer — outputs Notion Block JSON for the Notion API.
@@ -7,17 +7,17 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
7
7
  */
8
8
 
9
9
  function resolveColumn(col: string | TableColumn): TableColumn {
10
- return typeof col === "string" ? { header: col } : col
10
+ return typeof col === 'string' ? { header: col } : col
11
11
  }
12
12
 
13
13
  function getTextContent(children: DocChild[]): string {
14
14
  return children
15
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
16
- .join("")
15
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
16
+ .join('')
17
17
  }
18
18
 
19
19
  interface RichText {
20
- type: "text"
20
+ type: 'text'
21
21
  text: { content: string; link?: { url: string } }
22
22
  annotations?: {
23
23
  bold?: boolean
@@ -28,10 +28,10 @@ interface RichText {
28
28
  }
29
29
  }
30
30
 
31
- function textToRichText(text: string, annotations?: RichText["annotations"]): RichText[] {
31
+ function textToRichText(text: string, annotations?: RichText['annotations']): RichText[] {
32
32
  return [
33
33
  {
34
- type: "text",
34
+ type: 'text',
35
35
  text: { content: text },
36
36
  ...(annotations ? { annotations } : {}),
37
37
  },
@@ -39,7 +39,7 @@ function textToRichText(text: string, annotations?: RichText["annotations"]): Ri
39
39
  }
40
40
 
41
41
  interface NotionBlock {
42
- object: "block"
42
+ object: 'block'
43
43
  type: string
44
44
  [key: string]: unknown
45
45
  }
@@ -49,40 +49,40 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
49
49
  const blocks: NotionBlock[] = []
50
50
 
51
51
  switch (node.type) {
52
- case "document":
53
- case "page":
54
- case "section":
55
- case "row":
56
- case "column":
52
+ case 'document':
53
+ case 'page':
54
+ case 'section':
55
+ case 'row':
56
+ case 'column':
57
57
  for (const child of node.children) {
58
- if (typeof child !== "string") {
58
+ if (typeof child !== 'string') {
59
59
  blocks.push(...nodeToBlocks(child))
60
60
  }
61
61
  }
62
62
  break
63
63
 
64
- case "heading": {
64
+ case 'heading': {
65
65
  const level = (p.level as number) ?? 1
66
66
  const text = getTextContent(node.children)
67
- const type = level <= 1 ? "heading_1" : level === 2 ? "heading_2" : "heading_3"
67
+ const type = level <= 1 ? 'heading_1' : level === 2 ? 'heading_2' : 'heading_3'
68
68
  blocks.push({
69
- object: "block",
69
+ object: 'block',
70
70
  type,
71
71
  [type]: { rich_text: textToRichText(text) },
72
72
  })
73
73
  break
74
74
  }
75
75
 
76
- case "text": {
76
+ case 'text': {
77
77
  const text = getTextContent(node.children)
78
- const annotations: RichText["annotations"] = {}
78
+ const annotations: RichText['annotations'] = {}
79
79
  if (p.bold) annotations.bold = true
80
80
  if (p.italic) annotations.italic = true
81
81
  if (p.strikethrough) annotations.strikethrough = true
82
82
  if (p.underline) annotations.underline = true
83
83
  blocks.push({
84
- object: "block",
85
- type: "paragraph",
84
+ object: 'block',
85
+ type: 'paragraph',
86
86
  paragraph: {
87
87
  rich_text: textToRichText(
88
88
  text,
@@ -93,27 +93,27 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
93
93
  break
94
94
  }
95
95
 
96
- case "link": {
96
+ case 'link': {
97
97
  const href = sanitizeHref(p.href as string)
98
98
  const text = getTextContent(node.children)
99
99
  blocks.push({
100
- object: "block",
101
- type: "paragraph",
100
+ object: 'block',
101
+ type: 'paragraph',
102
102
  paragraph: {
103
- rich_text: [{ type: "text", text: { content: text, link: { url: href } } }],
103
+ rich_text: [{ type: 'text', text: { content: text, link: { url: href } } }],
104
104
  },
105
105
  })
106
106
  break
107
107
  }
108
108
 
109
- case "image": {
109
+ case 'image': {
110
110
  const src = sanitizeImageSrc(p.src as string)
111
- if (src.startsWith("http")) {
111
+ if (src.startsWith('http')) {
112
112
  blocks.push({
113
- object: "block",
114
- type: "image",
113
+ object: 'block',
114
+ type: 'image',
115
115
  image: {
116
- type: "external",
116
+ type: 'external',
117
117
  external: { url: src },
118
118
  ...(p.caption ? { caption: textToRichText(p.caption as string) } : {}),
119
119
  },
@@ -122,7 +122,7 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
122
122
  break
123
123
  }
124
124
 
125
- case "table": {
125
+ case 'table': {
126
126
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
127
127
  const rows = (p.rows ?? []) as (string | number)[][]
128
128
 
@@ -130,8 +130,8 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
130
130
 
131
131
  // Header row
132
132
  tableRows.push({
133
- object: "block",
134
- type: "table_row",
133
+ object: 'block',
134
+ type: 'table_row',
135
135
  table_row: {
136
136
  cells: columns.map((col) => textToRichText(col.header, { bold: true })),
137
137
  },
@@ -140,17 +140,17 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
140
140
  // Data rows
141
141
  for (const row of rows) {
142
142
  tableRows.push({
143
- object: "block",
144
- type: "table_row",
143
+ object: 'block',
144
+ type: 'table_row',
145
145
  table_row: {
146
- cells: columns.map((_, i) => textToRichText(String(row[i] ?? ""))),
146
+ cells: columns.map((_, i) => textToRichText(String(row[i] ?? ''))),
147
147
  },
148
148
  })
149
149
  }
150
150
 
151
151
  blocks.push({
152
- object: "block",
153
- type: "table",
152
+ object: 'block',
153
+ type: 'table',
154
154
  table: {
155
155
  table_width: columns.length,
156
156
  has_column_header: true,
@@ -160,14 +160,14 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
160
160
  break
161
161
  }
162
162
 
163
- case "list": {
163
+ case 'list': {
164
164
  const ordered = p.ordered as boolean | undefined
165
- const items = node.children.filter((c): c is DocNode => typeof c !== "string")
165
+ const items = node.children.filter((c): c is DocNode => typeof c !== 'string')
166
166
  for (const item of items) {
167
167
  const text = getTextContent(item.children)
168
- const type = ordered ? "numbered_list_item" : "bulleted_list_item"
168
+ const type = ordered ? 'numbered_list_item' : 'bulleted_list_item'
169
169
  blocks.push({
170
- object: "block",
170
+ object: 'block',
171
171
  type,
172
172
  [type]: { rich_text: textToRichText(text) },
173
173
  })
@@ -175,12 +175,12 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
175
175
  break
176
176
  }
177
177
 
178
- case "code": {
178
+ case 'code': {
179
179
  const text = getTextContent(node.children)
180
- const lang = (p.language as string) ?? "plain text"
180
+ const lang = (p.language as string) ?? 'plain text'
181
181
  blocks.push({
182
- object: "block",
183
- type: "code",
182
+ object: 'block',
183
+ type: 'code',
184
184
  code: {
185
185
  rich_text: textToRichText(text),
186
186
  language: lang,
@@ -189,29 +189,29 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
189
189
  break
190
190
  }
191
191
 
192
- case "divider":
193
- case "page-break":
194
- blocks.push({ object: "block", type: "divider", divider: {} })
192
+ case 'divider':
193
+ case 'page-break':
194
+ blocks.push({ object: 'block', type: 'divider', divider: {} })
195
195
  break
196
196
 
197
- case "spacer":
197
+ case 'spacer':
198
198
  blocks.push({
199
- object: "block",
200
- type: "paragraph",
199
+ object: 'block',
200
+ type: 'paragraph',
201
201
  paragraph: { rich_text: [] },
202
202
  })
203
203
  break
204
204
 
205
- case "button": {
205
+ case 'button': {
206
206
  const href = sanitizeHref(p.href as string)
207
207
  const text = getTextContent(node.children)
208
208
  blocks.push({
209
- object: "block",
210
- type: "paragraph",
209
+ object: 'block',
210
+ type: 'paragraph',
211
211
  paragraph: {
212
212
  rich_text: [
213
213
  {
214
- type: "text",
214
+ type: 'text',
215
215
  text: { content: text, link: { url: href } },
216
216
  annotations: { bold: true },
217
217
  },
@@ -221,11 +221,11 @@ function nodeToBlocks(node: DocNode): NotionBlock[] {
221
221
  break
222
222
  }
223
223
 
224
- case "quote": {
224
+ case 'quote': {
225
225
  const text = getTextContent(node.children)
226
226
  blocks.push({
227
- object: "block",
228
- type: "quote",
227
+ object: 'block',
228
+ type: 'quote',
229
229
  quote: { rich_text: textToRichText(text) },
230
230
  })
231
231
  break
@@ -1,4 +1,4 @@
1
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
2
2
 
3
3
  /**
4
4
  * PDF renderer — lazy-loads pdfmake on first use.
@@ -17,13 +17,13 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
17
17
  */
18
18
 
19
19
  function resolveColumn(col: string | TableColumn): TableColumn {
20
- return typeof col === "string" ? { header: col } : col
20
+ return typeof col === 'string' ? { header: col } : col
21
21
  }
22
22
 
23
23
  function getTextContent(children: DocChild[]): string {
24
24
  return children
25
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
26
- .join("")
25
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
26
+ .join('')
27
27
  }
28
28
 
29
29
  type PdfContent = Record<string, unknown> | string | PdfContent[]
@@ -50,39 +50,39 @@ const PAGE_SIZES: Record<string, { width: number; height: number }> = {
50
50
  function resolveImageSrc(
51
51
  src: string,
52
52
  ): { image: string } | { text: string; italics: true; color: string } {
53
- if (src.startsWith("data:")) {
53
+ if (src.startsWith('data:')) {
54
54
  return { image: src }
55
55
  }
56
- if (src.startsWith("http://") || src.startsWith("https://")) {
57
- return { text: `[Image: ${src}]`, italics: true, color: "#999999" }
56
+ if (src.startsWith('http://') || src.startsWith('https://')) {
57
+ return { text: `[Image: ${src}]`, italics: true, color: '#999999' }
58
58
  }
59
59
  // Local path — cannot resolve in browser
60
- return { text: `[Image: ${src}]`, italics: true, color: "#999999" }
60
+ return { text: `[Image: ${src}]`, italics: true, color: '#999999' }
61
61
  }
62
62
 
63
63
  function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
64
64
  const p = node.props
65
65
 
66
66
  switch (node.type) {
67
- case "document":
68
- case "page":
67
+ case 'document':
68
+ case 'page':
69
69
  return node.children
70
- .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
70
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
71
71
  .filter((c): c is PdfContent => c != null)
72
72
 
73
- case "section": {
73
+ case 'section': {
74
74
  const content = node.children
75
- .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
75
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
76
76
  .filter((c): c is PdfContent => c != null)
77
77
  .flat()
78
78
 
79
- if (p.direction === "row") {
79
+ if (p.direction === 'row') {
80
80
  return {
81
81
  columns: node.children
82
- .filter((c): c is DocNode => typeof c !== "string")
82
+ .filter((c): c is DocNode => typeof c !== 'string')
83
83
  .map((child) => ({
84
84
  stack: [nodeToContent(child)].flat().filter(Boolean),
85
- width: child.props.width === "*" || !child.props.width ? "*" : child.props.width,
85
+ width: child.props.width === '*' || !child.props.width ? '*' : child.props.width,
86
86
  })),
87
87
  columnGap: (p.gap as number) ?? 0,
88
88
  }
@@ -91,25 +91,25 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
91
91
  return content
92
92
  }
93
93
 
94
- case "row": {
94
+ case 'row': {
95
95
  return {
96
96
  columns: node.children
97
- .filter((c): c is DocNode => typeof c !== "string")
97
+ .filter((c): c is DocNode => typeof c !== 'string')
98
98
  .map((child) => ({
99
99
  stack: [nodeToContent(child)].flat().filter(Boolean),
100
- width: child.props.width ?? "*",
100
+ width: child.props.width ?? '*',
101
101
  })),
102
102
  columnGap: (p.gap as number) ?? 0,
103
103
  }
104
104
  }
105
105
 
106
- case "column":
106
+ case 'column':
107
107
  return node.children
108
- .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
108
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
109
109
  .filter((c): c is PdfContent => c != null)
110
110
  .flat()
111
111
 
112
- case "heading": {
112
+ case 'heading': {
113
113
  const level = (p.level as number) ?? 1
114
114
  const sizes: Record<number, number> = {
115
115
  1: 24,
@@ -123,45 +123,45 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
123
123
  text: getTextContent(node.children),
124
124
  fontSize: sizes[level] ?? 18,
125
125
  bold: true,
126
- color: (p.color as string) ?? "#000000",
127
- alignment: (p.align as string) ?? "left",
126
+ color: (p.color as string) ?? '#000000',
127
+ alignment: (p.align as string) ?? 'left',
128
128
  margin: [0, level === 1 ? 0 : 8, 0, 8],
129
129
  }
130
130
  }
131
131
 
132
- case "text":
132
+ case 'text':
133
133
  return {
134
134
  text: getTextContent(node.children),
135
135
  fontSize: (p.size as number) ?? 12,
136
- color: (p.color as string) ?? "#333333",
136
+ color: (p.color as string) ?? '#333333',
137
137
  bold: p.bold ?? false,
138
138
  italics: p.italic ?? false,
139
- decoration: p.underline ? "underline" : p.strikethrough ? "lineThrough" : undefined,
140
- alignment: (p.align as string) ?? "left",
139
+ decoration: p.underline ? 'underline' : p.strikethrough ? 'lineThrough' : undefined,
140
+ alignment: (p.align as string) ?? 'left',
141
141
  lineHeight: (p.lineHeight as number) ?? 1.4,
142
142
  margin: [0, 0, 0, 8],
143
143
  }
144
144
 
145
- case "link":
145
+ case 'link':
146
146
  return {
147
147
  text: getTextContent(node.children),
148
148
  link: p.href as string,
149
- color: (p.color as string) ?? "#4f46e5",
150
- decoration: "underline",
149
+ color: (p.color as string) ?? '#4f46e5',
150
+ decoration: 'underline',
151
151
  }
152
152
 
153
- case "image": {
153
+ case 'image': {
154
154
  const src = p.src as string
155
155
  const resolved = resolveImageSrc(src)
156
156
 
157
- if ("image" in resolved) {
157
+ if ('image' in resolved) {
158
158
  const result: Record<string, unknown> = {
159
159
  image: resolved.image,
160
160
  fit: [p.width ?? 500, p.height ?? 400],
161
161
  margin: [0, 0, 0, 8],
162
162
  }
163
- if (p.align === "center") result.alignment = "center"
164
- if (p.align === "right") result.alignment = "right"
163
+ if (p.align === 'center') result.alignment = 'center'
164
+ if (p.align === 'right') result.alignment = 'right'
165
165
  return result
166
166
  }
167
167
 
@@ -169,7 +169,7 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
169
169
  return { ...resolved, margin: [0, 0, 0, 8] }
170
170
  }
171
171
 
172
- case "table": {
172
+ case 'table': {
173
173
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
174
174
  const rows = (p.rows ?? []) as (string | number)[][]
175
175
  const hs = p.headerStyle as { background?: string; color?: string } | undefined
@@ -177,22 +177,22 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
177
177
  const headerRow = columns.map((col) => ({
178
178
  text: col.header,
179
179
  bold: true,
180
- fillColor: hs?.background ?? "#f5f5f5",
181
- color: hs?.color ?? "#000000",
182
- alignment: col.align ?? "left",
180
+ fillColor: hs?.background ?? '#f5f5f5',
181
+ color: hs?.color ?? '#000000',
182
+ alignment: col.align ?? 'left',
183
183
  }))
184
184
 
185
185
  const dataRows = rows.map((row, rowIdx) =>
186
186
  columns.map((col, colIdx) => ({
187
- text: String(row[colIdx] ?? ""),
188
- alignment: col.align ?? "left",
189
- fillColor: p.striped && rowIdx % 2 === 1 ? "#f9f9f9" : undefined,
187
+ text: String(row[colIdx] ?? ''),
188
+ alignment: col.align ?? 'left',
189
+ fillColor: p.striped && rowIdx % 2 === 1 ? '#f9f9f9' : undefined,
190
190
  })),
191
191
  )
192
192
 
193
193
  const widths = columns.map((col) => {
194
- if (!col.width) return "*"
195
- if (typeof col.width === "string" && col.width.endsWith("%")) {
194
+ if (!col.width) return '*'
195
+ if (typeof col.width === 'string' && col.width.endsWith('%')) {
196
196
  return col.width
197
197
  }
198
198
  return col.width
@@ -204,81 +204,81 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
204
204
  widths,
205
205
  body: [headerRow, ...dataRows],
206
206
  },
207
- layout: p.bordered ? undefined : "lightHorizontalLines",
207
+ layout: p.bordered ? undefined : 'lightHorizontalLines',
208
208
  unbreakable: p.keepTogether ?? false,
209
209
  margin: [0, 0, 0, 12],
210
210
  }
211
211
  }
212
212
 
213
- case "list": {
213
+ case 'list': {
214
214
  const items = node.children
215
- .filter((c): c is DocNode => typeof c !== "string")
215
+ .filter((c): c is DocNode => typeof c !== 'string')
216
216
  .map((item) => getTextContent(item.children))
217
217
 
218
218
  return p.ordered ? { ol: items, margin: [0, 0, 0, 8] } : { ul: items, margin: [0, 0, 0, 8] }
219
219
  }
220
220
 
221
- case "list-item":
221
+ case 'list-item':
222
222
  return getTextContent(node.children)
223
223
 
224
- case "code":
224
+ case 'code':
225
225
  return {
226
226
  text: getTextContent(node.children),
227
- font: "Courier",
227
+ font: 'Courier',
228
228
  fontSize: 10,
229
- background: "#f5f5f5",
229
+ background: '#f5f5f5',
230
230
  margin: [0, 0, 0, 8],
231
231
  }
232
232
 
233
- case "page-break":
234
- return { text: "", pageBreak: "after" }
233
+ case 'page-break':
234
+ return { text: '', pageBreak: 'after' }
235
235
 
236
- case "divider":
236
+ case 'divider':
237
237
  return {
238
238
  canvas: [
239
239
  {
240
- type: "line",
240
+ type: 'line',
241
241
  x1: 0,
242
242
  y1: 0,
243
243
  x2: 515,
244
244
  y2: 0,
245
245
  lineWidth: (p.thickness as number) ?? 1,
246
- lineColor: (p.color as string) ?? "#dddddd",
246
+ lineColor: (p.color as string) ?? '#dddddd',
247
247
  },
248
248
  ],
249
249
  margin: [0, 8, 0, 8],
250
250
  }
251
251
 
252
- case "spacer":
253
- return { text: "", margin: [0, (p.height as number) ?? 12, 0, 0] }
252
+ case 'spacer':
253
+ return { text: '', margin: [0, (p.height as number) ?? 12, 0, 0] }
254
254
 
255
- case "button":
255
+ case 'button':
256
256
  return {
257
257
  text: getTextContent(node.children),
258
258
  link: p.href as string,
259
259
  bold: true,
260
- color: (p.color as string) ?? "#ffffff",
261
- background: (p.background as string) ?? "#4f46e5",
260
+ color: (p.color as string) ?? '#ffffff',
261
+ background: (p.background as string) ?? '#4f46e5',
262
262
  margin: [0, 8, 0, 8],
263
263
  }
264
264
 
265
- case "quote":
265
+ case 'quote':
266
266
  return {
267
267
  table: {
268
- widths: [4, "*"],
268
+ widths: [4, '*'],
269
269
  body: [
270
270
  [
271
- { text: "", fillColor: (p.borderColor as string) ?? "#dddddd" },
271
+ { text: '', fillColor: (p.borderColor as string) ?? '#dddddd' },
272
272
  {
273
273
  text: getTextContent(node.children),
274
274
  italics: true,
275
- color: "#555555",
275
+ color: '#555555',
276
276
  margin: [8, 4, 0, 4],
277
277
  },
278
278
  ],
279
279
  ],
280
280
  },
281
- layout: "noBorders",
281
+ layout: 'noBorders',
282
282
  margin: [0, 4, 0, 8],
283
283
  }
284
284
 
@@ -291,7 +291,7 @@ function resolveMargin(
291
291
  margin: number | [number, number] | [number, number, number, number] | undefined,
292
292
  ): [number, number, number, number] {
293
293
  if (margin == null) return [40, 40, 40, 40]
294
- if (typeof margin === "number") return [margin, margin, margin, margin]
294
+ if (typeof margin === 'number') return [margin, margin, margin, margin]
295
295
  if (margin.length === 2) return [margin[1], margin[0], margin[1], margin[0]]
296
296
  return margin
297
297
  }
@@ -307,7 +307,7 @@ function renderHeaderFooter(node: DocNode | undefined): PdfContent | undefined {
307
307
  const content = nodeToContent(node)
308
308
  if (content == null) return undefined
309
309
  if (Array.isArray(content)) return { stack: content, margin: [40, 10, 40, 0] }
310
- if (typeof content === "object") return { ...content, margin: [40, 10, 40, 0] }
310
+ if (typeof content === 'object') return { ...content, margin: [40, 10, 40, 0] }
311
311
  return { text: content, margin: [40, 10, 40, 0] }
312
312
  }
313
313
 
@@ -317,8 +317,8 @@ export const pdfRenderer: DocumentRenderer = {
317
317
  let pdfMakeModule: any
318
318
  let pdfFontsModule: any
319
319
  try {
320
- pdfMakeModule = await import("pdfmake/build/pdfmake")
321
- pdfFontsModule = await import("pdfmake/build/vfs_fonts")
320
+ pdfMakeModule = await import('pdfmake/build/pdfmake')
321
+ pdfFontsModule = await import('pdfmake/build/vfs_fonts')
322
322
  } catch {
323
323
  throw new Error(
324
324
  '[@pyreon/document] PDF renderer requires "pdfmake" package. Install it: bun add pdfmake',
@@ -330,7 +330,7 @@ export const pdfRenderer: DocumentRenderer = {
330
330
  // ESM interop may wrap it in an extra .default layer.
331
331
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
332
332
  let pdfMake: any = pdfMakeModule.default ?? pdfMakeModule
333
- if (pdfMake.default && typeof pdfMake.default.createPdf === "function") {
333
+ if (pdfMake.default && typeof pdfMake.default.createPdf === 'function') {
334
334
  pdfMake = pdfMake.default
335
335
  }
336
336
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
@@ -343,10 +343,10 @@ export const pdfRenderer: DocumentRenderer = {
343
343
 
344
344
  // Find page config
345
345
  const pageNode = node.children.find(
346
- (c): c is DocNode => typeof c !== "string" && c.type === "page",
346
+ (c): c is DocNode => typeof c !== 'string' && c.type === 'page',
347
347
  )
348
- const pageSize = (pageNode?.props.size as string) ?? "A4"
349
- const pageOrientation = (pageNode?.props.orientation as string) ?? "portrait"
348
+ const pageSize = (pageNode?.props.size as string) ?? 'A4'
349
+ const pageOrientation = (pageNode?.props.orientation as string) ?? 'portrait'
350
350
  const pageMargin = resolveMargin(
351
351
  pageNode?.props.margin as
352
352
  | number
@@ -366,10 +366,10 @@ export const pdfRenderer: DocumentRenderer = {
366
366
  pageOrientation,
367
367
  pageMargins: pageMargin,
368
368
  info: {
369
- title: (node.props.title as string) ?? "",
370
- author: (node.props.author as string) ?? "",
371
- subject: (node.props.subject as string) ?? "",
372
- keywords: (node.props.keywords as string[])?.join(", ") ?? "",
369
+ title: (node.props.title as string) ?? '',
370
+ author: (node.props.author as string) ?? '',
371
+ subject: (node.props.subject as string) ?? '',
372
+ keywords: (node.props.keywords as string[])?.join(', ') ?? '',
373
373
  },
374
374
  content,
375
375
  defaultStyle: {