@pyreon/document 0.0.1

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/lib/analysis/index.js.html +5406 -0
  3. package/lib/chunk-ErZ26oRB.js +48 -0
  4. package/lib/confluence-Va8e7RxQ.js +192 -0
  5. package/lib/confluence-Va8e7RxQ.js.map +1 -0
  6. package/lib/csv-2c38ub-Y.js +32 -0
  7. package/lib/csv-2c38ub-Y.js.map +1 -0
  8. package/lib/discord-DAoUZqvE.js +134 -0
  9. package/lib/discord-DAoUZqvE.js.map +1 -0
  10. package/lib/dist-BsqdI2nY.js +20179 -0
  11. package/lib/dist-BsqdI2nY.js.map +1 -0
  12. package/lib/docx-CorFwEH9.js +450 -0
  13. package/lib/docx-CorFwEH9.js.map +1 -0
  14. package/lib/email-Bn_Brjdp.js +131 -0
  15. package/lib/email-Bn_Brjdp.js.map +1 -0
  16. package/lib/exceljs-BoIDUUaw.js +34377 -0
  17. package/lib/exceljs-BoIDUUaw.js.map +1 -0
  18. package/lib/google-chat-B6I017I1.js +125 -0
  19. package/lib/google-chat-B6I017I1.js.map +1 -0
  20. package/lib/html-De_iS_f0.js +151 -0
  21. package/lib/html-De_iS_f0.js.map +1 -0
  22. package/lib/index.js +619 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/markdown-BYC_3C9i.js +75 -0
  25. package/lib/markdown-BYC_3C9i.js.map +1 -0
  26. package/lib/notion-DHaQHO6P.js +187 -0
  27. package/lib/notion-DHaQHO6P.js.map +1 -0
  28. package/lib/pdf-CDPc5Itc.js +419 -0
  29. package/lib/pdf-CDPc5Itc.js.map +1 -0
  30. package/lib/pdfmake-DnmLxK4Q.js +55511 -0
  31. package/lib/pdfmake-DnmLxK4Q.js.map +1 -0
  32. package/lib/pptx-DKQU6bjq.js +252 -0
  33. package/lib/pptx-DKQU6bjq.js.map +1 -0
  34. package/lib/pptxgen.es-COcgXsyx.js +5697 -0
  35. package/lib/pptxgen.es-COcgXsyx.js.map +1 -0
  36. package/lib/slack-CJRJgkag.js +139 -0
  37. package/lib/slack-CJRJgkag.js.map +1 -0
  38. package/lib/svg-BM8biZmL.js +187 -0
  39. package/lib/svg-BM8biZmL.js.map +1 -0
  40. package/lib/teams-S99tonRG.js +176 -0
  41. package/lib/teams-S99tonRG.js.map +1 -0
  42. package/lib/telegram-CbEO_2PN.js +77 -0
  43. package/lib/telegram-CbEO_2PN.js.map +1 -0
  44. package/lib/text-B5U8ucRr.js +75 -0
  45. package/lib/text-B5U8ucRr.js.map +1 -0
  46. package/lib/types/index.d.ts +528 -0
  47. package/lib/types/index.d.ts.map +1 -0
  48. package/lib/vfs_fonts-Df1kkZ4Y.js +19 -0
  49. package/lib/vfs_fonts-Df1kkZ4Y.js.map +1 -0
  50. package/lib/whatsapp-DJ2D1jGG.js +64 -0
  51. package/lib/whatsapp-DJ2D1jGG.js.map +1 -0
  52. package/lib/xlsx-D47x-gZ5.js +199 -0
  53. package/lib/xlsx-D47x-gZ5.js.map +1 -0
  54. package/package.json +62 -0
  55. package/src/builder.ts +266 -0
  56. package/src/download.ts +76 -0
  57. package/src/env.d.ts +17 -0
  58. package/src/index.ts +98 -0
  59. package/src/nodes.ts +315 -0
  60. package/src/render.ts +222 -0
  61. package/src/renderers/confluence.ts +231 -0
  62. package/src/renderers/csv.ts +67 -0
  63. package/src/renderers/discord.ts +192 -0
  64. package/src/renderers/docx.ts +612 -0
  65. package/src/renderers/email.ts +230 -0
  66. package/src/renderers/google-chat.ts +211 -0
  67. package/src/renderers/html.ts +225 -0
  68. package/src/renderers/markdown.ts +144 -0
  69. package/src/renderers/notion.ts +264 -0
  70. package/src/renderers/pdf.ts +427 -0
  71. package/src/renderers/pptx.ts +353 -0
  72. package/src/renderers/slack.ts +192 -0
  73. package/src/renderers/svg.ts +254 -0
  74. package/src/renderers/teams.ts +234 -0
  75. package/src/renderers/telegram.ts +137 -0
  76. package/src/renderers/text.ts +154 -0
  77. package/src/renderers/whatsapp.ts +121 -0
  78. package/src/renderers/xlsx.ts +342 -0
  79. package/src/tests/document.test.ts +2920 -0
  80. package/src/types.ts +291 -0
@@ -0,0 +1,154 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ function resolveColumn(col: string | TableColumn): TableColumn {
10
+ return typeof col === 'string' ? { header: col } : col
11
+ }
12
+
13
+ function renderChild(child: DocChild): string {
14
+ if (typeof child === 'string') return child
15
+ return renderNode(child)
16
+ }
17
+
18
+ function renderChildren(children: DocChild[]): string {
19
+ return children.map(renderChild).join('')
20
+ }
21
+
22
+ function pad(
23
+ str: string,
24
+ width: number,
25
+ align: 'left' | 'center' | 'right' = 'left',
26
+ ): string {
27
+ if (str.length >= width) return str.slice(0, width)
28
+ const diff = width - str.length
29
+ if (align === 'center') {
30
+ const left = Math.floor(diff / 2)
31
+ return ' '.repeat(left) + str + ' '.repeat(diff - left)
32
+ }
33
+ if (align === 'right') return ' '.repeat(diff) + str
34
+ return str + ' '.repeat(diff)
35
+ }
36
+
37
+ function renderNode(node: DocNode): string {
38
+ const p = node.props
39
+
40
+ switch (node.type) {
41
+ case 'document':
42
+ return renderChildren(node.children)
43
+
44
+ case 'page':
45
+ return renderChildren(node.children)
46
+
47
+ case 'section':
48
+ case 'row':
49
+ case 'column':
50
+ return renderChildren(node.children)
51
+
52
+ case 'heading': {
53
+ const text = renderChildren(node.children)
54
+ const level = (p.level as number) ?? 1
55
+ if (level === 1)
56
+ return `${text.toUpperCase()}\n${'='.repeat(text.length)}\n\n`
57
+ if (level === 2) return `${text}\n${'-'.repeat(text.length)}\n\n`
58
+ return `${text}\n\n`
59
+ }
60
+
61
+ case 'text':
62
+ return `${renderChildren(node.children)}\n\n`
63
+
64
+ case 'link':
65
+ return `${renderChildren(node.children)} (${p.href})`
66
+
67
+ case 'image': {
68
+ const alt = (p.alt as string) ?? 'Image'
69
+ const caption = p.caption ? ` — ${p.caption}` : ''
70
+ return `[${alt}${caption}]\n\n`
71
+ }
72
+
73
+ case 'table': {
74
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
75
+ resolveColumn,
76
+ )
77
+ const rows = (p.rows ?? []) as (string | number)[][]
78
+
79
+ if (columns.length === 0) return ''
80
+
81
+ // Calculate column widths
82
+ const widths = columns.map((col, i) => {
83
+ const headerLen = col.header.length
84
+ const maxDataLen = rows.reduce(
85
+ (max, row) => Math.max(max, String(row[i] ?? '').length),
86
+ 0,
87
+ )
88
+ return Math.max(headerLen, maxDataLen, 3)
89
+ })
90
+
91
+ // Header
92
+ const header = columns
93
+ .map((col, i) => pad(col.header, widths[i] ?? 3, col.align))
94
+ .join(' | ')
95
+ const separator = widths.map((w) => '-'.repeat(w ?? 3)).join('-+-')
96
+
97
+ // Rows
98
+ const body = rows
99
+ .map((row) =>
100
+ columns
101
+ .map((col, i) =>
102
+ pad(String(row[i] ?? ''), widths[i] ?? 3, col.align),
103
+ )
104
+ .join(' | '),
105
+ )
106
+ .join('\n')
107
+
108
+ let result = `${header}\n${separator}\n${body}\n\n`
109
+ if (p.caption) result = `${p.caption}\n\n${result}`
110
+ return result
111
+ }
112
+
113
+ case 'list': {
114
+ const ordered = p.ordered as boolean | undefined
115
+ return `${node.children
116
+ .filter((c): c is DocNode => typeof c !== 'string')
117
+ .map((item, i) => {
118
+ const prefix = ordered ? `${i + 1}.` : '*'
119
+ return ` ${prefix} ${renderChildren(item.children)}`
120
+ })
121
+ .join('\n')}\n\n`
122
+ }
123
+
124
+ case 'list-item':
125
+ return renderChildren(node.children)
126
+
127
+ case 'code':
128
+ return `${renderChildren(node.children)}\n\n`
129
+
130
+ case 'divider':
131
+ return `${'─'.repeat(40)}\n\n`
132
+
133
+ case 'page-break':
134
+ return `\n${'═'.repeat(40)}\n\n`
135
+
136
+ case 'spacer':
137
+ return '\n'
138
+
139
+ case 'button':
140
+ return `[${renderChildren(node.children)}] → ${p.href}\n\n`
141
+
142
+ case 'quote':
143
+ return ` "${renderChildren(node.children)}"\n\n`
144
+
145
+ default:
146
+ return renderChildren(node.children)
147
+ }
148
+ }
149
+
150
+ export const textRenderer: DocumentRenderer = {
151
+ async render(node: DocNode, _options?: RenderOptions): Promise<string> {
152
+ return `${renderNode(node).trim()}\n`
153
+ },
154
+ }
@@ -0,0 +1,121 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ /**
10
+ * WhatsApp renderer — outputs formatted text using WhatsApp's markup.
11
+ * WhatsApp supports: *bold*, _italic_, ~strikethrough~, ```code```, > quote
12
+ */
13
+
14
+ function resolveColumn(col: string | TableColumn): TableColumn {
15
+ return typeof col === 'string' ? { header: col } : col
16
+ }
17
+
18
+ function getTextContent(children: DocChild[]): string {
19
+ return children
20
+ .map((c) =>
21
+ typeof c === 'string' ? c : getTextContent((c as DocNode).children),
22
+ )
23
+ .join('')
24
+ }
25
+
26
+ function renderNode(node: DocNode): string {
27
+ const p = node.props
28
+
29
+ switch (node.type) {
30
+ case 'document':
31
+ case 'page':
32
+ case 'section':
33
+ case 'row':
34
+ case 'column':
35
+ return node.children
36
+ .map((c) => (typeof c === 'string' ? c : renderNode(c)))
37
+ .join('')
38
+
39
+ case 'heading': {
40
+ const text = getTextContent(node.children)
41
+ return `*${text}*\n\n`
42
+ }
43
+
44
+ case 'text': {
45
+ let text = getTextContent(node.children)
46
+ if (p.bold) text = `*${text}*`
47
+ if (p.italic) text = `_${text}_`
48
+ if (p.strikethrough) text = `~${text}~`
49
+ return `${text}\n\n`
50
+ }
51
+
52
+ case 'link': {
53
+ const href = p.href as string
54
+ const text = getTextContent(node.children)
55
+ return `${text}: ${href}\n\n`
56
+ }
57
+
58
+ case 'image':
59
+ // WhatsApp doesn't support inline images in text
60
+ return ''
61
+
62
+ case 'table': {
63
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
64
+ resolveColumn,
65
+ )
66
+ const rows = (p.rows ?? []) as (string | number)[][]
67
+
68
+ const header = columns.map((c) => `*${c.header}*`).join(' | ')
69
+ const body = rows
70
+ .map((row) => row.map((c) => String(c ?? '')).join(' | '))
71
+ .join('\n')
72
+
73
+ let result = `${header}\n${body}\n\n`
74
+ if (p.caption) result = `_${p.caption}_\n${result}`
75
+ return result
76
+ }
77
+
78
+ case 'list': {
79
+ const ordered = p.ordered as boolean | undefined
80
+ return `${node.children
81
+ .filter((c): c is DocNode => typeof c !== 'string')
82
+ .map((item, i) => {
83
+ const prefix = ordered ? `${i + 1}.` : '•'
84
+ return `${prefix} ${getTextContent(item.children)}`
85
+ })
86
+ .join('\n')}\n\n`
87
+ }
88
+
89
+ case 'code': {
90
+ const text = getTextContent(node.children)
91
+ return `\`\`\`${text}\`\`\`\n\n`
92
+ }
93
+
94
+ case 'divider':
95
+ case 'page-break':
96
+ return '───────────\n\n'
97
+
98
+ case 'spacer':
99
+ return '\n'
100
+
101
+ case 'button': {
102
+ const href = p.href as string
103
+ const text = getTextContent(node.children)
104
+ return `*${text}*: ${href}\n\n`
105
+ }
106
+
107
+ case 'quote': {
108
+ const text = getTextContent(node.children)
109
+ return `> ${text}\n\n`
110
+ }
111
+
112
+ default:
113
+ return ''
114
+ }
115
+ }
116
+
117
+ export const whatsappRenderer: DocumentRenderer = {
118
+ async render(node: DocNode, _options?: RenderOptions): Promise<string> {
119
+ return renderNode(node).trim()
120
+ },
121
+ }
@@ -0,0 +1,342 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ /**
10
+ * XLSX renderer — lazy-loads ExcelJS on first use.
11
+ * Extracts tables from the document and renders each as a worksheet.
12
+ * Non-table content (headings, text) becomes header rows.
13
+ */
14
+
15
+ function resolveColumn(col: string | TableColumn): TableColumn {
16
+ return typeof col === 'string' ? { header: col } : col
17
+ }
18
+
19
+ function getTextContent(children: DocChild[]): string {
20
+ return children
21
+ .map((c) =>
22
+ typeof c === 'string' ? c : getTextContent((c as DocNode).children),
23
+ )
24
+ .join('')
25
+ }
26
+
27
+ interface ExtractedSheet {
28
+ name: string
29
+ headings: string[]
30
+ tables: DocNode[]
31
+ }
32
+
33
+ /** Walk the tree and group content into sheets (one per page, or one global). */
34
+ function extractSheets(node: DocNode): ExtractedSheet[] {
35
+ const sheets: ExtractedSheet[] = []
36
+ let currentSheet: ExtractedSheet = {
37
+ name: 'Sheet 1',
38
+ headings: [],
39
+ tables: [],
40
+ }
41
+
42
+ function walk(n: DocNode): void {
43
+ switch (n.type) {
44
+ case 'document':
45
+ walkChildren(n)
46
+ break
47
+
48
+ case 'page':
49
+ pushCurrentSheet()
50
+ currentSheet = {
51
+ name: `Sheet ${sheets.length + 1}`,
52
+ headings: [],
53
+ tables: [],
54
+ }
55
+ walkChildren(n)
56
+ break
57
+
58
+ case 'heading':
59
+ addHeading(n)
60
+ break
61
+
62
+ case 'table':
63
+ currentSheet.tables.push(n)
64
+ break
65
+
66
+ default:
67
+ walkChildren(n)
68
+ }
69
+ }
70
+
71
+ function walkChildren(n: DocNode): void {
72
+ for (const child of n.children) {
73
+ if (typeof child !== 'string') walk(child)
74
+ }
75
+ }
76
+
77
+ function pushCurrentSheet(): void {
78
+ if (currentSheet.tables.length > 0 || currentSheet.headings.length > 0) {
79
+ sheets.push(currentSheet)
80
+ }
81
+ }
82
+
83
+ function addHeading(n: DocNode): void {
84
+ const text = getTextContent(n.children)
85
+ currentSheet.headings.push(text)
86
+ if (currentSheet.headings.length === 1) {
87
+ currentSheet.name = text.slice(0, 31) // Excel sheet name max 31 chars
88
+ }
89
+ }
90
+
91
+ walk(node)
92
+ pushCurrentSheet()
93
+
94
+ return sheets
95
+ }
96
+
97
+ /** Parse a cell value, handling currencies, percentages, and plain numbers. */
98
+ function parseCellValue(value: string | number | undefined): string | number {
99
+ if (value == null) return ''
100
+ if (typeof value === 'number') return value
101
+
102
+ const trimmed = value.trim()
103
+
104
+ // Percentage: "45%" or "12.5%"
105
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) {
106
+ return Number.parseFloat(trimmed) / 100
107
+ }
108
+
109
+ // Currency: "$1,234.56", "$1234", "-$500"
110
+ const currencyMatch = trimmed.match(/^-?\$[\d,]+(\.\d+)?$/)
111
+ if (currencyMatch) {
112
+ return Number.parseFloat(trimmed.replace(/[$,]/g, ''))
113
+ }
114
+
115
+ // Plain number: "1,234.56", "1234", "-500.5"
116
+ const plainNum = Number(trimmed.replace(/,/g, ''))
117
+ if (!Number.isNaN(plainNum) && /^-?[\d,]+(\.\d+)?$/.test(trimmed)) {
118
+ return plainNum
119
+ }
120
+
121
+ return value
122
+ }
123
+
124
+ /** Get ExcelJS number format string for a value. */
125
+ function getCellFormat(
126
+ originalValue: string | number | undefined,
127
+ ): string | undefined {
128
+ if (typeof originalValue !== 'string') return undefined
129
+ const trimmed = originalValue.trim()
130
+
131
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return '0.00%'
132
+ if (/^-?\$/.test(trimmed)) return '$#,##0.00'
133
+ return undefined
134
+ }
135
+
136
+ /** Map alignment string to ExcelJS horizontal alignment. */
137
+ function mapAlignment(align?: string): 'left' | 'center' | 'right' | undefined {
138
+ if (align === 'left' || align === 'center' || align === 'right') return align
139
+ return undefined
140
+ }
141
+
142
+ /** Thin border style for ExcelJS. */
143
+ function thinBorder(): { style: 'thin'; color: { argb: string } } {
144
+ return { style: 'thin', color: { argb: 'FFDDDDDD' } }
145
+ }
146
+
147
+ /** Apply header styling to a cell. */
148
+ function styleHeaderCell(
149
+ cell: {
150
+ font: unknown
151
+ fill: unknown
152
+ alignment: unknown
153
+ border: unknown
154
+ value: unknown
155
+ },
156
+ col: TableColumn,
157
+ hs: { background?: string; color?: string } | undefined,
158
+ bordered: boolean,
159
+ ): void {
160
+ cell.value = col.header
161
+ cell.font = {
162
+ bold: true,
163
+ color: { argb: hs?.color?.replace('#', 'FF') ?? 'FF000000' },
164
+ }
165
+ if (hs?.background) {
166
+ cell.fill = {
167
+ type: 'pattern',
168
+ pattern: 'solid',
169
+ fgColor: { argb: hs.background.replace('#', 'FF') },
170
+ }
171
+ }
172
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
173
+ if (bordered) {
174
+ cell.border = {
175
+ top: thinBorder(),
176
+ bottom: thinBorder(),
177
+ left: thinBorder(),
178
+ right: thinBorder(),
179
+ }
180
+ }
181
+ }
182
+
183
+ /** Apply data cell value and styling. */
184
+ function styleDataCell(
185
+ cell: {
186
+ value: unknown
187
+ numFmt: unknown
188
+ alignment: unknown
189
+ fill: unknown
190
+ border: unknown
191
+ },
192
+ rawValue: string | number | undefined,
193
+ col: TableColumn,
194
+ striped: boolean,
195
+ isOddRow: boolean,
196
+ bordered: boolean,
197
+ ): void {
198
+ cell.value = parseCellValue(rawValue)
199
+ const fmt = getCellFormat(rawValue)
200
+ if (fmt) cell.numFmt = fmt
201
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
202
+ if (striped && isOddRow) {
203
+ cell.fill = {
204
+ type: 'pattern',
205
+ pattern: 'solid',
206
+ fgColor: { argb: 'FFF9F9F9' },
207
+ }
208
+ }
209
+ if (bordered) {
210
+ cell.border = {
211
+ top: thinBorder(),
212
+ bottom: thinBorder(),
213
+ left: thinBorder(),
214
+ right: thinBorder(),
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Render a single table node into the worksheet starting at the given row. Returns the next row number. */
220
+ function renderTable(
221
+ ws: {
222
+ getRow: (n: number) => { getCell: (n: number) => Record<string, unknown> }
223
+ columns: unknown[]
224
+ },
225
+ tableNode: DocNode,
226
+ startRow: number,
227
+ ): number {
228
+ let rowNum = startRow
229
+ const columns = (
230
+ (tableNode.props.columns ?? []) as (string | TableColumn)[]
231
+ ).map(resolveColumn)
232
+ const rows = (tableNode.props.rows ?? []) as (string | number)[][]
233
+ const hs = tableNode.props.headerStyle as
234
+ | { background?: string; color?: string }
235
+ | undefined
236
+ const bordered = (tableNode.props.bordered as boolean) ?? false
237
+
238
+ // Caption
239
+ if (tableNode.props.caption) {
240
+ const captionRow = ws.getRow(rowNum)
241
+ const captionCell = captionRow.getCell(1)
242
+ captionCell.value = tableNode.props.caption as string
243
+ captionCell.font = { italic: true, size: 10 }
244
+ rowNum++
245
+ }
246
+
247
+ // Header row
248
+ const headerRow = ws.getRow(rowNum)
249
+ for (let i = 0; i < columns.length; i++) {
250
+ const col = columns[i]
251
+ if (!col) continue
252
+ styleHeaderCell(headerRow.getCell(i + 1) as any, col, hs, bordered)
253
+ }
254
+ rowNum++
255
+
256
+ // Data rows
257
+ for (let r = 0; r < rows.length; r++) {
258
+ const dataRow = ws.getRow(rowNum)
259
+ for (let c = 0; c < columns.length; c++) {
260
+ const col = columns[c]
261
+ if (!col) continue
262
+ styleDataCell(
263
+ dataRow.getCell(c + 1) as any,
264
+ rows[r]?.[c],
265
+ col,
266
+ (tableNode.props.striped as boolean) ?? false,
267
+ r % 2 === 1,
268
+ bordered,
269
+ )
270
+ }
271
+ rowNum++
272
+ }
273
+
274
+ return rowNum + 1 // gap after table
275
+ }
276
+
277
+ /** Auto-fit column widths based on content. */
278
+ function autoFitColumns(ws: {
279
+ columns: {
280
+ width: number
281
+ eachCell?: (
282
+ opts: { includeEmpty: boolean },
283
+ cb: (cell: { value: unknown }) => void,
284
+ ) => void
285
+ }[]
286
+ }): void {
287
+ for (const col of ws.columns) {
288
+ let maxLen = 10
289
+ col.eachCell?.({ includeEmpty: false }, (cell) => {
290
+ const len = String(cell.value ?? '').length
291
+ if (len > maxLen) maxLen = len
292
+ })
293
+ col.width = Math.min(maxLen + 2, 50)
294
+ }
295
+ }
296
+
297
+ export const xlsxRenderer: DocumentRenderer = {
298
+ async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
299
+ const ExcelJS = await import('exceljs')
300
+ const workbook = new ExcelJS.default.Workbook()
301
+
302
+ workbook.creator = (node.props.author as string) ?? ''
303
+ workbook.title = (node.props.title as string) ?? ''
304
+
305
+ const sheets = extractSheets(node)
306
+
307
+ if (sheets.length === 0) {
308
+ workbook.addWorksheet('Sheet 1')
309
+ }
310
+
311
+ for (const sheet of sheets) {
312
+ const ws = workbook.addWorksheet(sheet.name)
313
+
314
+ let rowNum = 1
315
+
316
+ // Add headings as title rows
317
+ for (const heading of sheet.headings) {
318
+ const row = ws.getRow(rowNum)
319
+ row.getCell(1).value = heading
320
+ row.getCell(1).font = { bold: true, size: 14 }
321
+ rowNum++
322
+ }
323
+
324
+ if (sheet.headings.length > 0) rowNum++ // gap after headings
325
+
326
+ // Add tables
327
+ for (const tableNode of sheet.tables) {
328
+ rowNum = renderTable(
329
+ ws as unknown as Parameters<typeof renderTable>[0],
330
+ tableNode,
331
+ rowNum,
332
+ )
333
+ }
334
+
335
+ // Auto-fit columns (approximate)
336
+ autoFitColumns(ws as unknown as Parameters<typeof autoFitColumns>[0])
337
+ }
338
+
339
+ const buffer = await workbook.xlsx.writeBuffer()
340
+ return new Uint8Array(buffer as ArrayBuffer)
341
+ },
342
+ }