@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,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
  * XLSX renderer — lazy-loads ExcelJS on first use.
@@ -13,15 +7,13 @@ import type {
13
7
  */
14
8
 
15
9
  function resolveColumn(col: string | TableColumn): TableColumn {
16
- return typeof col === 'string' ? { header: col } : col
10
+ return typeof col === "string" ? { header: col } : col
17
11
  }
18
12
 
19
13
  function getTextContent(children: DocChild[]): string {
20
14
  return children
21
- .map((c) =>
22
- typeof c === 'string' ? c : getTextContent((c as DocNode).children),
23
- )
24
- .join('')
15
+ .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
16
+ .join("")
25
17
  }
26
18
 
27
19
  interface ExtractedSheet {
@@ -34,18 +26,18 @@ interface ExtractedSheet {
34
26
  function extractSheets(node: DocNode): ExtractedSheet[] {
35
27
  const sheets: ExtractedSheet[] = []
36
28
  let currentSheet: ExtractedSheet = {
37
- name: 'Sheet 1',
29
+ name: "Sheet 1",
38
30
  headings: [],
39
31
  tables: [],
40
32
  }
41
33
 
42
34
  function walk(n: DocNode): void {
43
35
  switch (n.type) {
44
- case 'document':
36
+ case "document":
45
37
  walkChildren(n)
46
38
  break
47
39
 
48
- case 'page':
40
+ case "page":
49
41
  pushCurrentSheet()
50
42
  currentSheet = {
51
43
  name: `Sheet ${sheets.length + 1}`,
@@ -55,11 +47,11 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
55
47
  walkChildren(n)
56
48
  break
57
49
 
58
- case 'heading':
50
+ case "heading":
59
51
  addHeading(n)
60
52
  break
61
53
 
62
- case 'table':
54
+ case "table":
63
55
  currentSheet.tables.push(n)
64
56
  break
65
57
 
@@ -70,7 +62,7 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
70
62
 
71
63
  function walkChildren(n: DocNode): void {
72
64
  for (const child of n.children) {
73
- if (typeof child !== 'string') walk(child)
65
+ if (typeof child !== "string") walk(child)
74
66
  }
75
67
  }
76
68
 
@@ -96,8 +88,8 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
96
88
 
97
89
  /** Parse a cell value, handling currencies, percentages, and plain numbers. */
98
90
  function parseCellValue(value: string | number | undefined): string | number {
99
- if (value == null) return ''
100
- if (typeof value === 'number') return value
91
+ if (value == null) return ""
92
+ if (typeof value === "number") return value
101
93
 
102
94
  const trimmed = value.trim()
103
95
 
@@ -109,11 +101,11 @@ function parseCellValue(value: string | number | undefined): string | number {
109
101
  // Currency: "$1,234.56", "$1234", "-$500"
110
102
  const currencyMatch = trimmed.match(/^-?\$[\d,]+(\.\d+)?$/)
111
103
  if (currencyMatch) {
112
- return Number.parseFloat(trimmed.replace(/[$,]/g, ''))
104
+ return Number.parseFloat(trimmed.replace(/[$,]/g, ""))
113
105
  }
114
106
 
115
107
  // Plain number: "1,234.56", "1234", "-500.5"
116
- const plainNum = Number(trimmed.replace(/,/g, ''))
108
+ const plainNum = Number(trimmed.replace(/,/g, ""))
117
109
  if (!Number.isNaN(plainNum) && /^-?[\d,]+(\.\d+)?$/.test(trimmed)) {
118
110
  return plainNum
119
111
  }
@@ -122,26 +114,24 @@ function parseCellValue(value: string | number | undefined): string | number {
122
114
  }
123
115
 
124
116
  /** 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
117
+ function getCellFormat(originalValue: string | number | undefined): string | undefined {
118
+ if (typeof originalValue !== "string") return undefined
129
119
  const trimmed = originalValue.trim()
130
120
 
131
- if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return '0.00%'
132
- if (/^-?\$/.test(trimmed)) return '$#,##0.00'
121
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return "0.00%"
122
+ if (/^-?\$/.test(trimmed)) return "$#,##0.00"
133
123
  return undefined
134
124
  }
135
125
 
136
126
  /** 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
127
+ function mapAlignment(align?: string): "left" | "center" | "right" | undefined {
128
+ if (align === "left" || align === "center" || align === "right") return align
139
129
  return undefined
140
130
  }
141
131
 
142
132
  /** Thin border style for ExcelJS. */
143
- function thinBorder(): { style: 'thin'; color: { argb: string } } {
144
- return { style: 'thin', color: { argb: 'FFDDDDDD' } }
133
+ function thinBorder(): { style: "thin"; color: { argb: string } } {
134
+ return { style: "thin", color: { argb: "FFDDDDDD" } }
145
135
  }
146
136
 
147
137
  /** Apply header styling to a cell. */
@@ -160,16 +150,16 @@ function styleHeaderCell(
160
150
  cell.value = col.header
161
151
  cell.font = {
162
152
  bold: true,
163
- color: { argb: hs?.color?.replace('#', 'FF') ?? 'FF000000' },
153
+ color: { argb: hs?.color?.replace("#", "FF") ?? "FF000000" },
164
154
  }
165
155
  if (hs?.background) {
166
156
  cell.fill = {
167
- type: 'pattern',
168
- pattern: 'solid',
169
- fgColor: { argb: hs.background.replace('#', 'FF') },
157
+ type: "pattern",
158
+ pattern: "solid",
159
+ fgColor: { argb: hs.background.replace("#", "FF") },
170
160
  }
171
161
  }
172
- cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
162
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" }
173
163
  if (bordered) {
174
164
  cell.border = {
175
165
  top: thinBorder(),
@@ -198,12 +188,12 @@ function styleDataCell(
198
188
  cell.value = parseCellValue(rawValue)
199
189
  const fmt = getCellFormat(rawValue)
200
190
  if (fmt) cell.numFmt = fmt
201
- cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
191
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" }
202
192
  if (striped && isOddRow) {
203
193
  cell.fill = {
204
- type: 'pattern',
205
- pattern: 'solid',
206
- fgColor: { argb: 'FFF9F9F9' },
194
+ type: "pattern",
195
+ pattern: "solid",
196
+ fgColor: { argb: "FFF9F9F9" },
207
197
  }
208
198
  }
209
199
  if (bordered) {
@@ -226,13 +216,9 @@ function renderTable(
226
216
  startRow: number,
227
217
  ): number {
228
218
  let rowNum = startRow
229
- const columns = (
230
- (tableNode.props.columns ?? []) as (string | TableColumn)[]
231
- ).map(resolveColumn)
219
+ const columns = ((tableNode.props.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
232
220
  const rows = (tableNode.props.rows ?? []) as (string | number)[][]
233
- const hs = tableNode.props.headerStyle as
234
- | { background?: string; color?: string }
235
- | undefined
221
+ const hs = tableNode.props.headerStyle as { background?: string; color?: string } | undefined
236
222
  const bordered = (tableNode.props.bordered as boolean) ?? false
237
223
 
238
224
  // Caption
@@ -278,16 +264,13 @@ function renderTable(
278
264
  function autoFitColumns(ws: {
279
265
  columns: {
280
266
  width: number
281
- eachCell?: (
282
- opts: { includeEmpty: boolean },
283
- cb: (cell: { value: unknown }) => void,
284
- ) => void
267
+ eachCell?: (opts: { includeEmpty: boolean }, cb: (cell: { value: unknown }) => void) => void
285
268
  }[]
286
269
  }): void {
287
270
  for (const col of ws.columns) {
288
271
  let maxLen = 10
289
272
  col.eachCell?.({ includeEmpty: false }, (cell) => {
290
- const len = String(cell.value ?? '').length
273
+ const len = String(cell.value ?? "").length
291
274
  if (len > maxLen) maxLen = len
292
275
  })
293
276
  col.width = Math.min(maxLen + 2, 50)
@@ -296,16 +279,23 @@ function autoFitColumns(ws: {
296
279
 
297
280
  export const xlsxRenderer: DocumentRenderer = {
298
281
  async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
299
- const ExcelJS = await import('exceljs')
282
+ let ExcelJS: any
283
+ try {
284
+ ExcelJS = await import("exceljs")
285
+ } catch {
286
+ throw new Error(
287
+ '[@pyreon/document] XLSX renderer requires "exceljs" package. Install it: bun add exceljs',
288
+ )
289
+ }
300
290
  const workbook = new ExcelJS.default.Workbook()
301
291
 
302
- workbook.creator = (node.props.author as string) ?? ''
303
- workbook.title = (node.props.title as string) ?? ''
292
+ workbook.creator = (node.props.author as string) ?? ""
293
+ workbook.title = (node.props.title as string) ?? ""
304
294
 
305
295
  const sheets = extractSheets(node)
306
296
 
307
297
  if (sheets.length === 0) {
308
- workbook.addWorksheet('Sheet 1')
298
+ workbook.addWorksheet("Sheet 1")
309
299
  }
310
300
 
311
301
  for (const sheet of sheets) {
@@ -325,11 +315,7 @@ export const xlsxRenderer: DocumentRenderer = {
325
315
 
326
316
  // Add tables
327
317
  for (const tableNode of sheet.tables) {
328
- rowNum = renderTable(
329
- ws as unknown as Parameters<typeof renderTable>[0],
330
- tableNode,
331
- rowNum,
332
- )
318
+ rowNum = renderTable(ws as unknown as Parameters<typeof renderTable>[0], tableNode, rowNum)
333
319
  }
334
320
 
335
321
  // Auto-fit columns (approximate)
package/src/sanitize.ts CHANGED
@@ -8,13 +8,13 @@
8
8
  * Blocks: semicolons, braces, angle brackets, quotes, backslashes, expressions.
9
9
  */
10
10
  export function sanitizeCss(value: string | undefined): string {
11
- if (value == null) return ''
11
+ if (value == null) return ""
12
12
  // Remove anything that could break out of a CSS value
13
13
  return value
14
- .replace(/[;{}()<>\\'"]/g, '')
15
- .replace(/expression\s*\(/gi, '')
16
- .replace(/url\s*\(/gi, '')
17
- .replace(/javascript\s*:/gi, '')
14
+ .replace(/[;{}()<>\\'"]/g, "")
15
+ .replace(/expression\s*\(/gi, "")
16
+ .replace(/url\s*\(/gi, "")
17
+ .replace(/javascript\s*:/gi, "")
18
18
  }
19
19
 
20
20
  /**
@@ -22,7 +22,7 @@ export function sanitizeCss(value: string | undefined): string {
22
22
  * Returns the value if valid, empty string if not.
23
23
  */
24
24
  export function sanitizeColor(value: string | undefined): string {
25
- if (value == null) return ''
25
+ if (value == null) return ""
26
26
  const trimmed = value.trim()
27
27
  // Hex: #fff, #ffffff, #ffffffff
28
28
  if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed
@@ -31,21 +31,17 @@ export function sanitizeColor(value: string | undefined): string {
31
31
  // rgb/rgba/hsl/hsla
32
32
  if (/^(rgb|hsl)a?\(\s*[\d.,\s%]+\)$/.test(trimmed)) return trimmed
33
33
  // transparent, inherit, currentColor
34
- if (/^(transparent|inherit|currentColor|initial|unset)$/i.test(trimmed))
35
- return trimmed
36
- return ''
34
+ if (/^(transparent|inherit|currentColor|initial|unset)$/i.test(trimmed)) return trimmed
35
+ return ""
37
36
  }
38
37
 
39
38
  /**
40
39
  * Sanitize a color for XML attributes (DOCX/PPTX) — only hex without #.
41
40
  * Returns 6-char hex string or default.
42
41
  */
43
- export function sanitizeXmlColor(
44
- value: string | undefined,
45
- fallback = '000000',
46
- ): string {
42
+ export function sanitizeXmlColor(value: string | undefined, fallback = "000000"): string {
47
43
  if (value == null) return fallback
48
- const hex = value.replace('#', '')
44
+ const hex = value.replace("#", "")
49
45
  if (/^[0-9a-fA-F]{3,8}$/.test(hex)) return hex
50
46
  return fallback
51
47
  }
@@ -55,13 +51,13 @@ export function sanitizeXmlColor(
55
51
  * Returns the URL if safe, empty string if not.
56
52
  */
57
53
  export function sanitizeHref(url: string | undefined): string {
58
- if (url == null) return ''
54
+ if (url == null) return ""
59
55
  const trimmed = url.trim()
60
56
  // Block dangerous protocols
61
- const lower = trimmed.toLowerCase().replace(/\s/g, '')
62
- if (lower.startsWith('javascript:')) return ''
63
- if (lower.startsWith('vbscript:')) return ''
64
- if (lower.startsWith('data:') && !lower.startsWith('data:image/')) return ''
57
+ const lower = trimmed.toLowerCase().replace(/\s/g, "")
58
+ if (lower.startsWith("javascript:")) return ""
59
+ if (lower.startsWith("vbscript:")) return ""
60
+ if (lower.startsWith("data:") && !lower.startsWith("data:image/")) return ""
65
61
  return trimmed
66
62
  }
67
63
 
@@ -70,12 +66,12 @@ export function sanitizeHref(url: string | undefined): string {
70
66
  * Blocks javascript:, vbscript:, and non-image data: URIs.
71
67
  */
72
68
  export function sanitizeImageSrc(src: string | undefined): string {
73
- if (src == null) return ''
69
+ if (src == null) return ""
74
70
  const trimmed = src.trim()
75
- const lower = trimmed.toLowerCase().replace(/\s/g, '')
76
- if (lower.startsWith('javascript:')) return ''
77
- if (lower.startsWith('vbscript:')) return ''
78
- if (lower.startsWith('data:') && !lower.startsWith('data:image/')) return ''
71
+ const lower = trimmed.toLowerCase().replace(/\s/g, "")
72
+ if (lower.startsWith("javascript:")) return ""
73
+ if (lower.startsWith("vbscript:")) return ""
74
+ if (lower.startsWith("data:") && !lower.startsWith("data:image/")) return ""
79
75
  return trimmed
80
76
  }
81
77
 
@@ -83,6 +79,6 @@ export function sanitizeImageSrc(src: string | undefined): string {
83
79
  * Sanitize a style attribute value — validates it's safe CSS.
84
80
  */
85
81
  export function sanitizeStyle(value: string | undefined): string {
86
- if (value == null) return ''
82
+ if (value == null) return ""
87
83
  return sanitizeCss(value)
88
84
  }