@pyreon/document 0.9.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 +350 -0
  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,10 +1,4 @@
1
- import type {
2
- DocChild,
3
- DocNode,
4
- DocumentRenderer,
5
- RenderOptions,
6
- TableColumn,
7
- } from '../types'
1
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
8
2
 
9
3
  /**
10
4
  * PDF renderer — lazy-loads pdfmake on first use.
@@ -23,15 +17,13 @@ import type {
23
17
  */
24
18
 
25
19
  function resolveColumn(col: string | TableColumn): TableColumn {
26
- return typeof col === 'string' ? { header: col } : col
20
+ return typeof col === "string" ? { header: col } : col
27
21
  }
28
22
 
29
23
  function getTextContent(children: DocChild[]): string {
30
24
  return children
31
- .map((c) =>
32
- typeof c === 'string' ? c : getTextContent((c as DocNode).children),
33
- )
34
- .join('')
25
+ .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
26
+ .join("")
35
27
  }
36
28
 
37
29
  type PdfContent = Record<string, unknown> | string | PdfContent[]
@@ -58,42 +50,39 @@ const PAGE_SIZES: Record<string, { width: number; height: number }> = {
58
50
  function resolveImageSrc(
59
51
  src: string,
60
52
  ): { image: string } | { text: string; italics: true; color: string } {
61
- if (src.startsWith('data:')) {
53
+ if (src.startsWith("data:")) {
62
54
  return { image: src }
63
55
  }
64
- if (src.startsWith('http://') || src.startsWith('https://')) {
65
- 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" }
66
58
  }
67
59
  // Local path — cannot resolve in browser
68
- return { text: `[Image: ${src}]`, italics: true, color: '#999999' }
60
+ return { text: `[Image: ${src}]`, italics: true, color: "#999999" }
69
61
  }
70
62
 
71
63
  function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
72
64
  const p = node.props
73
65
 
74
66
  switch (node.type) {
75
- case 'document':
76
- case 'page':
67
+ case "document":
68
+ case "page":
77
69
  return node.children
78
- .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
70
+ .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
79
71
  .filter((c): c is PdfContent => c != null)
80
72
 
81
- case 'section': {
73
+ case "section": {
82
74
  const content = node.children
83
- .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
75
+ .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
84
76
  .filter((c): c is PdfContent => c != null)
85
77
  .flat()
86
78
 
87
- if (p.direction === 'row') {
79
+ if (p.direction === "row") {
88
80
  return {
89
81
  columns: node.children
90
- .filter((c): c is DocNode => typeof c !== 'string')
82
+ .filter((c): c is DocNode => typeof c !== "string")
91
83
  .map((child) => ({
92
84
  stack: [nodeToContent(child)].flat().filter(Boolean),
93
- width:
94
- child.props.width === '*' || !child.props.width
95
- ? '*'
96
- : child.props.width,
85
+ width: child.props.width === "*" || !child.props.width ? "*" : child.props.width,
97
86
  })),
98
87
  columnGap: (p.gap as number) ?? 0,
99
88
  }
@@ -102,25 +91,25 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
102
91
  return content
103
92
  }
104
93
 
105
- case 'row': {
94
+ case "row": {
106
95
  return {
107
96
  columns: node.children
108
- .filter((c): c is DocNode => typeof c !== 'string')
97
+ .filter((c): c is DocNode => typeof c !== "string")
109
98
  .map((child) => ({
110
99
  stack: [nodeToContent(child)].flat().filter(Boolean),
111
- width: child.props.width ?? '*',
100
+ width: child.props.width ?? "*",
112
101
  })),
113
102
  columnGap: (p.gap as number) ?? 0,
114
103
  }
115
104
  }
116
105
 
117
- case 'column':
106
+ case "column":
118
107
  return node.children
119
- .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))
108
+ .map((c) => (typeof c === "string" ? c : nodeToContent(c)))
120
109
  .filter((c): c is PdfContent => c != null)
121
110
  .flat()
122
111
 
123
- case 'heading': {
112
+ case "heading": {
124
113
  const level = (p.level as number) ?? 1
125
114
  const sizes: Record<number, number> = {
126
115
  1: 24,
@@ -134,49 +123,45 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
134
123
  text: getTextContent(node.children),
135
124
  fontSize: sizes[level] ?? 18,
136
125
  bold: true,
137
- color: (p.color as string) ?? '#000000',
138
- alignment: (p.align as string) ?? 'left',
126
+ color: (p.color as string) ?? "#000000",
127
+ alignment: (p.align as string) ?? "left",
139
128
  margin: [0, level === 1 ? 0 : 8, 0, 8],
140
129
  }
141
130
  }
142
131
 
143
- case 'text':
132
+ case "text":
144
133
  return {
145
134
  text: getTextContent(node.children),
146
135
  fontSize: (p.size as number) ?? 12,
147
- color: (p.color as string) ?? '#333333',
136
+ color: (p.color as string) ?? "#333333",
148
137
  bold: p.bold ?? false,
149
138
  italics: p.italic ?? false,
150
- decoration: p.underline
151
- ? 'underline'
152
- : p.strikethrough
153
- ? 'lineThrough'
154
- : undefined,
155
- alignment: (p.align as string) ?? 'left',
139
+ decoration: p.underline ? "underline" : p.strikethrough ? "lineThrough" : undefined,
140
+ alignment: (p.align as string) ?? "left",
156
141
  lineHeight: (p.lineHeight as number) ?? 1.4,
157
142
  margin: [0, 0, 0, 8],
158
143
  }
159
144
 
160
- case 'link':
145
+ case "link":
161
146
  return {
162
147
  text: getTextContent(node.children),
163
148
  link: p.href as string,
164
- color: (p.color as string) ?? '#4f46e5',
165
- decoration: 'underline',
149
+ color: (p.color as string) ?? "#4f46e5",
150
+ decoration: "underline",
166
151
  }
167
152
 
168
- case 'image': {
153
+ case "image": {
169
154
  const src = p.src as string
170
155
  const resolved = resolveImageSrc(src)
171
156
 
172
- if ('image' in resolved) {
157
+ if ("image" in resolved) {
173
158
  const result: Record<string, unknown> = {
174
159
  image: resolved.image,
175
160
  fit: [p.width ?? 500, p.height ?? 400],
176
161
  margin: [0, 0, 0, 8],
177
162
  }
178
- if (p.align === 'center') result.alignment = 'center'
179
- if (p.align === 'right') result.alignment = 'right'
163
+ if (p.align === "center") result.alignment = "center"
164
+ if (p.align === "right") result.alignment = "right"
180
165
  return result
181
166
  }
182
167
 
@@ -184,34 +169,30 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
184
169
  return { ...resolved, margin: [0, 0, 0, 8] }
185
170
  }
186
171
 
187
- case 'table': {
188
- const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
189
- resolveColumn,
190
- )
172
+ case "table": {
173
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
191
174
  const rows = (p.rows ?? []) as (string | number)[][]
192
- const hs = p.headerStyle as
193
- | { background?: string; color?: string }
194
- | undefined
175
+ const hs = p.headerStyle as { background?: string; color?: string } | undefined
195
176
 
196
177
  const headerRow = columns.map((col) => ({
197
178
  text: col.header,
198
179
  bold: true,
199
- fillColor: hs?.background ?? '#f5f5f5',
200
- color: hs?.color ?? '#000000',
201
- alignment: col.align ?? 'left',
180
+ fillColor: hs?.background ?? "#f5f5f5",
181
+ color: hs?.color ?? "#000000",
182
+ alignment: col.align ?? "left",
202
183
  }))
203
184
 
204
185
  const dataRows = rows.map((row, rowIdx) =>
205
186
  columns.map((col, colIdx) => ({
206
- text: String(row[colIdx] ?? ''),
207
- alignment: col.align ?? 'left',
208
- 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,
209
190
  })),
210
191
  )
211
192
 
212
193
  const widths = columns.map((col) => {
213
- if (!col.width) return '*'
214
- if (typeof col.width === 'string' && col.width.endsWith('%')) {
194
+ if (!col.width) return "*"
195
+ if (typeof col.width === "string" && col.width.endsWith("%")) {
215
196
  return col.width
216
197
  }
217
198
  return col.width
@@ -223,83 +204,81 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
223
204
  widths,
224
205
  body: [headerRow, ...dataRows],
225
206
  },
226
- layout: p.bordered ? undefined : 'lightHorizontalLines',
207
+ layout: p.bordered ? undefined : "lightHorizontalLines",
227
208
  unbreakable: p.keepTogether ?? false,
228
209
  margin: [0, 0, 0, 12],
229
210
  }
230
211
  }
231
212
 
232
- case 'list': {
213
+ case "list": {
233
214
  const items = node.children
234
- .filter((c): c is DocNode => typeof c !== 'string')
215
+ .filter((c): c is DocNode => typeof c !== "string")
235
216
  .map((item) => getTextContent(item.children))
236
217
 
237
- return p.ordered
238
- ? { ol: items, margin: [0, 0, 0, 8] }
239
- : { ul: items, margin: [0, 0, 0, 8] }
218
+ return p.ordered ? { ol: items, margin: [0, 0, 0, 8] } : { ul: items, margin: [0, 0, 0, 8] }
240
219
  }
241
220
 
242
- case 'list-item':
221
+ case "list-item":
243
222
  return getTextContent(node.children)
244
223
 
245
- case 'code':
224
+ case "code":
246
225
  return {
247
226
  text: getTextContent(node.children),
248
- font: 'Courier',
227
+ font: "Courier",
249
228
  fontSize: 10,
250
- background: '#f5f5f5',
229
+ background: "#f5f5f5",
251
230
  margin: [0, 0, 0, 8],
252
231
  }
253
232
 
254
- case 'page-break':
255
- return { text: '', pageBreak: 'after' }
233
+ case "page-break":
234
+ return { text: "", pageBreak: "after" }
256
235
 
257
- case 'divider':
236
+ case "divider":
258
237
  return {
259
238
  canvas: [
260
239
  {
261
- type: 'line',
240
+ type: "line",
262
241
  x1: 0,
263
242
  y1: 0,
264
243
  x2: 515,
265
244
  y2: 0,
266
245
  lineWidth: (p.thickness as number) ?? 1,
267
- lineColor: (p.color as string) ?? '#dddddd',
246
+ lineColor: (p.color as string) ?? "#dddddd",
268
247
  },
269
248
  ],
270
249
  margin: [0, 8, 0, 8],
271
250
  }
272
251
 
273
- case 'spacer':
274
- 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] }
275
254
 
276
- case 'button':
255
+ case "button":
277
256
  return {
278
257
  text: getTextContent(node.children),
279
258
  link: p.href as string,
280
259
  bold: true,
281
- color: (p.color as string) ?? '#ffffff',
282
- background: (p.background as string) ?? '#4f46e5',
260
+ color: (p.color as string) ?? "#ffffff",
261
+ background: (p.background as string) ?? "#4f46e5",
283
262
  margin: [0, 8, 0, 8],
284
263
  }
285
264
 
286
- case 'quote':
265
+ case "quote":
287
266
  return {
288
267
  table: {
289
- widths: [4, '*'],
268
+ widths: [4, "*"],
290
269
  body: [
291
270
  [
292
- { text: '', fillColor: (p.borderColor as string) ?? '#dddddd' },
271
+ { text: "", fillColor: (p.borderColor as string) ?? "#dddddd" },
293
272
  {
294
273
  text: getTextContent(node.children),
295
274
  italics: true,
296
- color: '#555555',
275
+ color: "#555555",
297
276
  margin: [8, 4, 0, 4],
298
277
  },
299
278
  ],
300
279
  ],
301
280
  },
302
- layout: 'noBorders',
281
+ layout: "noBorders",
303
282
  margin: [0, 4, 0, 8],
304
283
  }
305
284
 
@@ -309,14 +288,10 @@ function nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {
309
288
  }
310
289
 
311
290
  function resolveMargin(
312
- margin:
313
- | number
314
- | [number, number]
315
- | [number, number, number, number]
316
- | undefined,
291
+ margin: number | [number, number] | [number, number, number, number] | undefined,
317
292
  ): [number, number, number, number] {
318
293
  if (margin == null) return [40, 40, 40, 40]
319
- if (typeof margin === 'number') return [margin, margin, margin, margin]
294
+ if (typeof margin === "number") return [margin, margin, margin, margin]
320
295
  if (margin.length === 2) return [margin[1], margin[0], margin[1], margin[0]]
321
296
  return margin
322
297
  }
@@ -332,23 +307,30 @@ function renderHeaderFooter(node: DocNode | undefined): PdfContent | undefined {
332
307
  const content = nodeToContent(node)
333
308
  if (content == null) return undefined
334
309
  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] }
310
+ if (typeof content === "object") return { ...content, margin: [40, 10, 40, 0] }
337
311
  return { text: content, margin: [40, 10, 40, 0] }
338
312
  }
339
313
 
340
314
  export const pdfRenderer: DocumentRenderer = {
341
315
  async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
342
316
  // Lazy-load pdfmake — handle ESM/CJS interop
343
- const pdfMakeModule = await import('pdfmake/build/pdfmake')
344
- const pdfFontsModule = await import('pdfmake/build/vfs_fonts')
317
+ let pdfMakeModule: any
318
+ let pdfFontsModule: any
319
+ try {
320
+ pdfMakeModule = await import("pdfmake/build/pdfmake")
321
+ pdfFontsModule = await import("pdfmake/build/vfs_fonts")
322
+ } catch {
323
+ throw new Error(
324
+ '[@pyreon/document] PDF renderer requires "pdfmake" package. Install it: bun add pdfmake',
325
+ )
326
+ }
345
327
 
346
328
  // Resolve the actual exports (handle .default for ESM wrappers).
347
329
  // pdfmake's default export is a singleton instance of browser_extensions_pdfmake.
348
330
  // ESM interop may wrap it in an extra .default layer.
349
331
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
350
332
  let pdfMake: any = pdfMakeModule.default ?? pdfMakeModule
351
- if (pdfMake.default && typeof pdfMake.default.createPdf === 'function') {
333
+ if (pdfMake.default && typeof pdfMake.default.createPdf === "function") {
352
334
  pdfMake = pdfMake.default
353
335
  }
354
336
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete
@@ -361,11 +343,10 @@ export const pdfRenderer: DocumentRenderer = {
361
343
 
362
344
  // Find page config
363
345
  const pageNode = node.children.find(
364
- (c): c is DocNode => typeof c !== 'string' && c.type === 'page',
346
+ (c): c is DocNode => typeof c !== "string" && c.type === "page",
365
347
  )
366
- const pageSize = (pageNode?.props.size as string) ?? 'A4'
367
- const pageOrientation =
368
- (pageNode?.props.orientation as string) ?? 'portrait'
348
+ const pageSize = (pageNode?.props.size as string) ?? "A4"
349
+ const pageOrientation = (pageNode?.props.orientation as string) ?? "portrait"
369
350
  const pageMargin = resolveMargin(
370
351
  pageNode?.props.margin as
371
352
  | number
@@ -377,22 +358,18 @@ export const pdfRenderer: DocumentRenderer = {
377
358
  const content = [nodeToContent(node)].flat().filter(Boolean) as PdfContent[]
378
359
 
379
360
  // 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
- )
361
+ const headerFn = renderHeaderFooter(pageNode?.props.header as DocNode | undefined)
362
+ const footerFn = renderHeaderFooter(pageNode?.props.footer as DocNode | undefined)
386
363
 
387
364
  const docDefinition: Record<string, unknown> = {
388
365
  pageSize: PAGE_SIZES[pageSize] ?? PAGE_SIZES.A4,
389
366
  pageOrientation,
390
367
  pageMargins: pageMargin,
391
368
  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(', ') ?? '',
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(", ") ?? "",
396
373
  },
397
374
  content,
398
375
  defaultStyle: {