@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,427 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ /**
10
+ * PDF renderer — lazy-loads pdfmake on first use.
11
+ * pdfmake handles pagination, tables, text wrapping, and font embedding.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { render, Document, Page, Heading } from '@pyreon/document'
16
+ *
17
+ * const doc = Document({
18
+ * title: 'Report',
19
+ * children: Page({ children: Heading({ children: 'Hello' }) }),
20
+ * })
21
+ * const pdf = await render(doc, 'pdf') // → Uint8Array
22
+ * ```
23
+ */
24
+
25
+ function resolveColumn(col: string | TableColumn): TableColumn {
26
+ return typeof col === 'string' ? { header: col } : col
27
+ }
28
+
29
+ function getTextContent(children: DocChild[]): string {
30
+ return children
31
+ .map((c) =>
32
+ typeof c === 'string' ? c : getTextContent((c as DocNode).children),
33
+ )
34
+ .join('')
35
+ }
36
+
37
+ type PdfContent = Record<string, unknown> | string | PdfContent[]
38
+
39
+ /** pdfmake expects page sizes as `{ width, height }` objects. */
40
+ const PAGE_SIZES: Record<string, { width: number; height: number }> = {
41
+ A3: { width: 841.89, height: 1190.55 },
42
+ A4: { width: 595.28, height: 841.89 },
43
+ A5: { width: 419.53, height: 595.28 },
44
+ letter: { width: 612, height: 792 },
45
+ legal: { width: 612, height: 1008 },
46
+ tabloid: { width: 792, height: 1224 },
47
+ }
48
+
49
+ /**
50
+ * Resolve an image `src` for pdfmake.
51
+ *
52
+ * - `data:` URIs are passed through directly (pdfmake supports base64).
53
+ * - `http(s)://` URLs cannot be resolved at render time in the browser.
54
+ * A placeholder text node is returned instead.
55
+ * - Relative / absolute paths (e.g. `/logo.png`) cannot be resolved without
56
+ * a server-side fetch, so they are skipped with a placeholder.
57
+ */
58
+ function resolveImageSrc(
59
+ src: string,
60
+ ): { image: string } | { text: string; italics: true; color: string } {
61
+ if (src.startsWith('data:')) {
62
+ return { image: src }
63
+ }
64
+ if (src.startsWith('http://') || src.startsWith('https://')) {
65
+ return { text: `[Image: ${src}]`, italics: true, color: '#999999' }
66
+ }
67
+ // Local path — cannot resolve in browser
68
+ return { text: `[Image: ${src}]`, italics: true, color: '#999999' }
69
+ }
70
+
71
+ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
72
+ const p = node.props
73
+
74
+ switch (node.type) {
75
+ case 'document':
76
+ case 'page':
77
+ return node.children
78
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
79
+ .filter((c): c is PdfContent => c != null)
80
+
81
+ case 'section': {
82
+ const content = node.children
83
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
84
+ .filter((c): c is PdfContent => c != null)
85
+ .flat()
86
+
87
+ if (p.direction === 'row') {
88
+ return {
89
+ columns: node.children
90
+ .filter((c): c is DocNode => typeof c !== 'string')
91
+ .map((child) => ({
92
+ stack: [nodeToContent(child)].flat().filter(Boolean),
93
+ width:
94
+ child.props.width === '*' || !child.props.width
95
+ ? '*'
96
+ : child.props.width,
97
+ })),
98
+ columnGap: (p.gap as number) ?? 0,
99
+ }
100
+ }
101
+
102
+ return content
103
+ }
104
+
105
+ case 'row': {
106
+ return {
107
+ columns: node.children
108
+ .filter((c): c is DocNode => typeof c !== 'string')
109
+ .map((child) => ({
110
+ stack: [nodeToContent(child)].flat().filter(Boolean),
111
+ width: child.props.width ?? '*',
112
+ })),
113
+ columnGap: (p.gap as number) ?? 0,
114
+ }
115
+ }
116
+
117
+ case 'column':
118
+ return node.children
119
+ .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
120
+ .filter((c): c is PdfContent => c != null)
121
+ .flat()
122
+
123
+ case 'heading': {
124
+ const level = (p.level as number) ?? 1
125
+ const sizes: Record<number, number> = {
126
+ 1: 24,
127
+ 2: 20,
128
+ 3: 18,
129
+ 4: 16,
130
+ 5: 14,
131
+ 6: 12,
132
+ }
133
+ return {
134
+ text: getTextContent(node.children),
135
+ fontSize: sizes[level] ?? 18,
136
+ bold: true,
137
+ color: (p.color as string) ?? '#000000',
138
+ alignment: (p.align as string) ?? 'left',
139
+ margin: [0, level === 1 ? 0 : 8, 0, 8],
140
+ }
141
+ }
142
+
143
+ case 'text':
144
+ return {
145
+ text: getTextContent(node.children),
146
+ fontSize: (p.size as number) ?? 12,
147
+ color: (p.color as string) ?? '#333333',
148
+ bold: p.bold ?? false,
149
+ italics: p.italic ?? false,
150
+ decoration: p.underline
151
+ ? 'underline'
152
+ : p.strikethrough
153
+ ? 'lineThrough'
154
+ : undefined,
155
+ alignment: (p.align as string) ?? 'left',
156
+ lineHeight: (p.lineHeight as number) ?? 1.4,
157
+ margin: [0, 0, 0, 8],
158
+ }
159
+
160
+ case 'link':
161
+ return {
162
+ text: getTextContent(node.children),
163
+ link: p.href as string,
164
+ color: (p.color as string) ?? '#4f46e5',
165
+ decoration: 'underline',
166
+ }
167
+
168
+ case 'image': {
169
+ const src = p.src as string
170
+ const resolved = resolveImageSrc(src)
171
+
172
+ if ('image' in resolved) {
173
+ const result: Record<string, unknown> = {
174
+ image: resolved.image,
175
+ fit: [p.width ?? 500, p.height ?? 400],
176
+ margin: [0, 0, 0, 8],
177
+ }
178
+ if (p.align === 'center') result.alignment = 'center'
179
+ if (p.align === 'right') result.alignment = 'right'
180
+ return result
181
+ }
182
+
183
+ // Placeholder for non-resolvable images
184
+ return { ...resolved, margin: [0, 0, 0, 8] }
185
+ }
186
+
187
+ case 'table': {
188
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
189
+ resolveColumn,
190
+ )
191
+ const rows = (p.rows ?? []) as (string | number)[][]
192
+ const hs = p.headerStyle as
193
+ | { background?: string; color?: string }
194
+ | undefined
195
+
196
+ const headerRow = columns.map((col) => ({
197
+ text: col.header,
198
+ bold: true,
199
+ fillColor: hs?.background ?? '#f5f5f5',
200
+ color: hs?.color ?? '#000000',
201
+ alignment: col.align ?? 'left',
202
+ }))
203
+
204
+ const dataRows = rows.map((row, rowIdx) =>
205
+ columns.map((col, colIdx) => ({
206
+ text: String(row[colIdx] ?? ''),
207
+ alignment: col.align ?? 'left',
208
+ fillColor: p.striped && rowIdx % 2 === 1 ? '#f9f9f9' : undefined,
209
+ })),
210
+ )
211
+
212
+ const widths = columns.map((col) => {
213
+ if (!col.width) return '*'
214
+ if (typeof col.width === 'string' && col.width.endsWith('%')) {
215
+ return col.width
216
+ }
217
+ return col.width
218
+ })
219
+
220
+ return {
221
+ table: {
222
+ headerRows: 1,
223
+ widths,
224
+ body: [headerRow, ...dataRows],
225
+ },
226
+ layout: p.bordered ? undefined : 'lightHorizontalLines',
227
+ unbreakable: p.keepTogether ?? false,
228
+ margin: [0, 0, 0, 12],
229
+ }
230
+ }
231
+
232
+ case 'list': {
233
+ const items = node.children
234
+ .filter((c): c is DocNode => typeof c !== 'string')
235
+ .map((item) => getTextContent(item.children))
236
+
237
+ return p.ordered
238
+ ? { ol: items, margin: [0, 0, 0, 8] }
239
+ : { ul: items, margin: [0, 0, 0, 8] }
240
+ }
241
+
242
+ case 'list-item':
243
+ return getTextContent(node.children)
244
+
245
+ case 'code':
246
+ return {
247
+ text: getTextContent(node.children),
248
+ font: 'Courier',
249
+ fontSize: 10,
250
+ background: '#f5f5f5',
251
+ margin: [0, 0, 0, 8],
252
+ }
253
+
254
+ case 'page-break':
255
+ return { text: '', pageBreak: 'after' }
256
+
257
+ case 'divider':
258
+ return {
259
+ canvas: [
260
+ {
261
+ type: 'line',
262
+ x1: 0,
263
+ y1: 0,
264
+ x2: 515,
265
+ y2: 0,
266
+ lineWidth: (p.thickness as number) ?? 1,
267
+ lineColor: (p.color as string) ?? '#dddddd',
268
+ },
269
+ ],
270
+ margin: [0, 8, 0, 8],
271
+ }
272
+
273
+ case 'spacer':
274
+ return { text: '', margin: [0, (p.height as number) ?? 12, 0, 0] }
275
+
276
+ case 'button':
277
+ return {
278
+ text: getTextContent(node.children),
279
+ link: p.href as string,
280
+ bold: true,
281
+ color: (p.color as string) ?? '#ffffff',
282
+ background: (p.background as string) ?? '#4f46e5',
283
+ margin: [0, 8, 0, 8],
284
+ }
285
+
286
+ case 'quote':
287
+ return {
288
+ table: {
289
+ widths: [4, '*'],
290
+ body: [
291
+ [
292
+ { text: '', fillColor: (p.borderColor as string) ?? '#dddddd' },
293
+ {
294
+ text: getTextContent(node.children),
295
+ italics: true,
296
+ color: '#555555',
297
+ margin: [8, 4, 0, 4],
298
+ },
299
+ ],
300
+ ],
301
+ },
302
+ layout: 'noBorders',
303
+ margin: [0, 4, 0, 8],
304
+ }
305
+
306
+ default:
307
+ return null
308
+ }
309
+ }
310
+
311
+ function resolveMargin(
312
+ margin:
313
+ | number
314
+ | [number, number]
315
+ | [number, number, number, number]
316
+ | undefined,
317
+ ): [number, number, number, number] {
318
+ if (margin == null) return [40, 40, 40, 40]
319
+ if (typeof margin === 'number') return [margin, margin, margin, margin]
320
+ if (margin.length === 2) return [margin[1], margin[0], margin[1], margin[0]]
321
+ return margin
322
+ }
323
+
324
+ /**
325
+ * Render header/footer DocNodes into pdfmake content for page headers/footers.
326
+ *
327
+ * pdfmake header/footer functions receive `(currentPage, pageCount, pageSize)`
328
+ * and must return a content object. We flatten the DocNode into static content.
329
+ */
330
+ function renderHeaderFooter(node: DocNode | undefined): PdfContent | undefined {
331
+ if (!node) return undefined
332
+ const content = nodeToContent(node)
333
+ if (content == null) return undefined
334
+ if (Array.isArray(content)) return { stack: content, margin: [40, 10, 40, 0] }
335
+ if (typeof content === 'object')
336
+ return { ...content, margin: [40, 10, 40, 0] }
337
+ return { text: content, margin: [40, 10, 40, 0] }
338
+ }
339
+
340
+ export const pdfRenderer: DocumentRenderer = {
341
+ async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
342
+ // Lazy-load pdfmake — handle ESM/CJS interop
343
+ const pdfMakeModule = await import('pdfmake/build/pdfmake')
344
+ const pdfFontsModule = await import('pdfmake/build/vfs_fonts')
345
+
346
+ // Resolve the actual exports (handle .default for ESM wrappers).
347
+ // pdfmake's default export is a singleton instance of browser_extensions_pdfmake.
348
+ // ESM interop may wrap it in an extra .default layer.
349
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
350
+ let pdfMake: any = pdfMakeModule.default ?? pdfMakeModule
351
+ if (pdfMake.default && typeof pdfMake.default.createPdf === 'function') {
352
+ pdfMake = pdfMake.default
353
+ }
354
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
355
+ const pdfFonts: any = pdfFontsModule.default ?? pdfFontsModule
356
+
357
+ // Assign virtual filesystem for fonts
358
+ if (pdfMake.vfs == null) {
359
+ pdfMake.vfs = pdfFonts.pdfMake?.vfs ?? pdfFonts.vfs
360
+ }
361
+
362
+ // Find page config
363
+ const pageNode = node.children.find(
364
+ (c): c is DocNode => typeof c !== 'string' && c.type === 'page',
365
+ )
366
+ const pageSize = (pageNode?.props.size as string) ?? 'A4'
367
+ const pageOrientation =
368
+ (pageNode?.props.orientation as string) ?? 'portrait'
369
+ const pageMargin = resolveMargin(
370
+ pageNode?.props.margin as
371
+ | number
372
+ | [number, number]
373
+ | [number, number, number, number]
374
+ | undefined,
375
+ )
376
+
377
+ const content = [nodeToContent(node)].flat().filter(Boolean) as PdfContent[]
378
+
379
+ // Build header/footer from PageProps if present
380
+ const headerFn = renderHeaderFooter(
381
+ pageNode?.props.header as DocNode | undefined,
382
+ )
383
+ const footerFn = renderHeaderFooter(
384
+ pageNode?.props.footer as DocNode | undefined,
385
+ )
386
+
387
+ const docDefinition: Record<string, unknown> = {
388
+ pageSize: PAGE_SIZES[pageSize] ?? PAGE_SIZES.A4,
389
+ pageOrientation,
390
+ pageMargins: pageMargin,
391
+ info: {
392
+ title: (node.props.title as string) ?? '',
393
+ author: (node.props.author as string) ?? '',
394
+ subject: (node.props.subject as string) ?? '',
395
+ keywords: (node.props.keywords as string[])?.join(', ') ?? '',
396
+ },
397
+ content,
398
+ defaultStyle: {
399
+ fontSize: 12,
400
+ lineHeight: 1.4,
401
+ },
402
+ // Keep sections together — break before a heading if it would be
403
+ // orphaned at the bottom of a page.
404
+ pageBreakBefore: (
405
+ currentNode: { headlineLevel?: number },
406
+ helpers: { getFollowingNodesOnPage?: () => unknown[] },
407
+ ) => {
408
+ if (currentNode.headlineLevel && helpers.getFollowingNodesOnPage) {
409
+ const following = helpers.getFollowingNodesOnPage()
410
+ if (following.length === 0) return true
411
+ }
412
+ return false
413
+ },
414
+ }
415
+
416
+ if (headerFn) docDefinition.header = headerFn
417
+ if (footerFn) docDefinition.footer = footerFn
418
+
419
+ try {
420
+ const pdf = pdfMake.createPdf(docDefinition)
421
+ const buffer = await pdf.getBuffer()
422
+ return new Uint8Array(buffer)
423
+ } catch (err) {
424
+ throw new Error(`[@pyreon/document] PDF generation failed: ${err}`)
425
+ }
426
+ },
427
+ }