@pyreon/document 0.11.5 → 0.11.7

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 } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import { sanitizeHref } from '../sanitize'
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
3
3
 
4
4
  /**
5
5
  * Telegram renderer — outputs HTML using Telegram's supported subset.
@@ -8,40 +8,40 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
8
8
  */
9
9
 
10
10
  function resolveColumn(col: string | TableColumn): TableColumn {
11
- return typeof col === "string" ? { header: col } : col
11
+ return typeof col === 'string' ? { header: col } : col
12
12
  }
13
13
 
14
14
  function esc(str: string): string {
15
15
  return str
16
- .replace(/&/g, "&")
17
- .replace(/</g, "&lt;")
18
- .replace(/>/g, "&gt;")
19
- .replace(/"/g, "&quot;")
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;')
19
+ .replace(/"/g, '&quot;')
20
20
  }
21
21
 
22
22
  function getTextContent(children: DocChild[]): string {
23
23
  return children
24
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
25
- .join("")
24
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
25
+ .join('')
26
26
  }
27
27
 
28
28
  function renderNode(node: DocNode): string {
29
29
  const p = node.props
30
30
 
31
31
  switch (node.type) {
32
- case "document":
33
- case "page":
34
- case "section":
35
- case "row":
36
- case "column":
37
- return node.children.map((c) => (typeof c === "string" ? esc(c) : renderNode(c))).join("")
38
-
39
- case "heading": {
32
+ case 'document':
33
+ case 'page':
34
+ case 'section':
35
+ case 'row':
36
+ case 'column':
37
+ return node.children.map((c) => (typeof c === 'string' ? esc(c) : renderNode(c))).join('')
38
+
39
+ case 'heading': {
40
40
  const text = esc(getTextContent(node.children))
41
41
  return `<b>${text}</b>\n\n`
42
42
  }
43
43
 
44
- case "text": {
44
+ case 'text': {
45
45
  let text = esc(getTextContent(node.children))
46
46
  if (p.bold) text = `<b>${text}</b>`
47
47
  if (p.italic) text = `<i>${text}</i>`
@@ -50,43 +50,43 @@ function renderNode(node: DocNode): string {
50
50
  return `${text}\n\n`
51
51
  }
52
52
 
53
- case "link": {
53
+ case 'link': {
54
54
  const href = sanitizeHref(p.href as string)
55
55
  const text = esc(getTextContent(node.children))
56
56
  return `<a href="${esc(href)}">${text}</a>\n\n`
57
57
  }
58
58
 
59
- case "image":
59
+ case 'image':
60
60
  // Telegram doesn't support inline images in HTML
61
61
  // Images need to be sent separately via sendPhoto
62
- return ""
62
+ return ''
63
63
 
64
- case "table": {
64
+ case 'table': {
65
65
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
66
66
  const rows = (p.rows ?? []) as (string | number)[][]
67
67
 
68
68
  // Render as preformatted text since Telegram has no table support
69
- const header = columns.map((c) => c.header).join(" | ")
70
- const separator = columns.map(() => "---").join("-+-")
71
- const body = rows.map((row) => row.map((c) => String(c ?? "")).join(" | ")).join("\n")
69
+ const header = columns.map((c) => c.header).join(' | ')
70
+ const separator = columns.map(() => '---').join('-+-')
71
+ const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
72
72
 
73
73
  return `<pre>${esc(header)}\n${esc(separator)}\n${esc(body)}</pre>\n\n`
74
74
  }
75
75
 
76
- case "list": {
76
+ case 'list': {
77
77
  const ordered = p.ordered as boolean | undefined
78
78
  const items = node.children
79
- .filter((c): c is DocNode => typeof c !== "string")
79
+ .filter((c): c is DocNode => typeof c !== 'string')
80
80
  .map((item, i) => {
81
- const prefix = ordered ? `${i + 1}.` : ""
81
+ const prefix = ordered ? `${i + 1}.` : ''
82
82
  return `${prefix} ${esc(getTextContent(item.children))}`
83
83
  })
84
- .join("\n")
84
+ .join('\n')
85
85
  return `${items}\n\n`
86
86
  }
87
87
 
88
- case "code": {
89
- const lang = (p.language as string) ?? ""
88
+ case 'code': {
89
+ const lang = (p.language as string) ?? ''
90
90
  const text = esc(getTextContent(node.children))
91
91
  if (lang) {
92
92
  return `<pre><code class="language-${esc(lang)}">${text}</code></pre>\n\n`
@@ -94,26 +94,26 @@ function renderNode(node: DocNode): string {
94
94
  return `<pre>${text}</pre>\n\n`
95
95
  }
96
96
 
97
- case "divider":
98
- case "page-break":
99
- return "───────────\n\n"
97
+ case 'divider':
98
+ case 'page-break':
99
+ return '───────────\n\n'
100
100
 
101
- case "spacer":
102
- return "\n"
101
+ case 'spacer':
102
+ return '\n'
103
103
 
104
- case "button": {
104
+ case 'button': {
105
105
  const href = sanitizeHref(p.href as string)
106
106
  const text = esc(getTextContent(node.children))
107
107
  return `<a href="${esc(href)}">${text}</a>\n\n`
108
108
  }
109
109
 
110
- case "quote": {
110
+ case 'quote': {
111
111
  const text = esc(getTextContent(node.children))
112
112
  return `<blockquote>${text}</blockquote>\n\n`
113
113
  }
114
114
 
115
115
  default:
116
- return ""
116
+ return ''
117
117
  }
118
118
  }
119
119
 
@@ -1,123 +1,123 @@
1
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
2
2
 
3
3
  function resolveColumn(col: string | TableColumn): TableColumn {
4
- return typeof col === "string" ? { header: col } : col
4
+ return typeof col === 'string' ? { header: col } : col
5
5
  }
6
6
 
7
7
  function renderChild(child: DocChild): string {
8
- if (typeof child === "string") return child
8
+ if (typeof child === 'string') return child
9
9
  return renderNode(child)
10
10
  }
11
11
 
12
12
  function renderChildren(children: DocChild[]): string {
13
- return children.map(renderChild).join("")
13
+ return children.map(renderChild).join('')
14
14
  }
15
15
 
16
- function pad(str: string, width: number, align: "left" | "center" | "right" = "left"): string {
16
+ function pad(str: string, width: number, align: 'left' | 'center' | 'right' = 'left'): string {
17
17
  if (str.length >= width) return str.slice(0, width)
18
18
  const diff = width - str.length
19
- if (align === "center") {
19
+ if (align === 'center') {
20
20
  const left = Math.floor(diff / 2)
21
- return " ".repeat(left) + str + " ".repeat(diff - left)
21
+ return ' '.repeat(left) + str + ' '.repeat(diff - left)
22
22
  }
23
- if (align === "right") return " ".repeat(diff) + str
24
- return str + " ".repeat(diff)
23
+ if (align === 'right') return ' '.repeat(diff) + str
24
+ return str + ' '.repeat(diff)
25
25
  }
26
26
 
27
27
  function renderNode(node: DocNode): string {
28
28
  const p = node.props
29
29
 
30
30
  switch (node.type) {
31
- case "document":
31
+ case 'document':
32
32
  return renderChildren(node.children)
33
33
 
34
- case "page":
34
+ case 'page':
35
35
  return renderChildren(node.children)
36
36
 
37
- case "section":
38
- case "row":
39
- case "column":
37
+ case 'section':
38
+ case 'row':
39
+ case 'column':
40
40
  return renderChildren(node.children)
41
41
 
42
- case "heading": {
42
+ case 'heading': {
43
43
  const text = renderChildren(node.children)
44
44
  const level = (p.level as number) ?? 1
45
- if (level === 1) return `${text.toUpperCase()}\n${"=".repeat(text.length)}\n\n`
46
- if (level === 2) return `${text}\n${"-".repeat(text.length)}\n\n`
45
+ if (level === 1) return `${text.toUpperCase()}\n${'='.repeat(text.length)}\n\n`
46
+ if (level === 2) return `${text}\n${'-'.repeat(text.length)}\n\n`
47
47
  return `${text}\n\n`
48
48
  }
49
49
 
50
- case "text":
50
+ case 'text':
51
51
  return `${renderChildren(node.children)}\n\n`
52
52
 
53
- case "link":
53
+ case 'link':
54
54
  return `${renderChildren(node.children)} (${p.href})`
55
55
 
56
- case "image": {
57
- const alt = (p.alt as string) ?? "Image"
58
- const caption = p.caption ? ` — ${p.caption}` : ""
56
+ case 'image': {
57
+ const alt = (p.alt as string) ?? 'Image'
58
+ const caption = p.caption ? ` — ${p.caption}` : ''
59
59
  return `[${alt}${caption}]\n\n`
60
60
  }
61
61
 
62
- case "table": {
62
+ case 'table': {
63
63
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
64
64
  const rows = (p.rows ?? []) as (string | number)[][]
65
65
 
66
- if (columns.length === 0) return ""
66
+ if (columns.length === 0) return ''
67
67
 
68
68
  // Calculate column widths
69
69
  const widths = columns.map((col, i) => {
70
70
  const headerLen = col.header.length
71
- const maxDataLen = rows.reduce((max, row) => Math.max(max, String(row[i] ?? "").length), 0)
71
+ const maxDataLen = rows.reduce((max, row) => Math.max(max, String(row[i] ?? '').length), 0)
72
72
  return Math.max(headerLen, maxDataLen, 3)
73
73
  })
74
74
 
75
75
  // Header
76
- const header = columns.map((col, i) => pad(col.header, widths[i] ?? 3, col.align)).join(" | ")
77
- const separator = widths.map((w) => "-".repeat(w ?? 3)).join("-+-")
76
+ const header = columns.map((col, i) => pad(col.header, widths[i] ?? 3, col.align)).join(' | ')
77
+ const separator = widths.map((w) => '-'.repeat(w ?? 3)).join('-+-')
78
78
 
79
79
  // Rows
80
80
  const body = rows
81
81
  .map((row) =>
82
- columns.map((col, i) => pad(String(row[i] ?? ""), widths[i] ?? 3, col.align)).join(" | "),
82
+ columns.map((col, i) => pad(String(row[i] ?? ''), widths[i] ?? 3, col.align)).join(' | '),
83
83
  )
84
- .join("\n")
84
+ .join('\n')
85
85
 
86
86
  let result = `${header}\n${separator}\n${body}\n\n`
87
87
  if (p.caption) result = `${p.caption}\n\n${result}`
88
88
  return result
89
89
  }
90
90
 
91
- case "list": {
91
+ case 'list': {
92
92
  const ordered = p.ordered as boolean | undefined
93
93
  return `${node.children
94
- .filter((c): c is DocNode => typeof c !== "string")
94
+ .filter((c): c is DocNode => typeof c !== 'string')
95
95
  .map((item, i) => {
96
- const prefix = ordered ? `${i + 1}.` : "*"
96
+ const prefix = ordered ? `${i + 1}.` : '*'
97
97
  return ` ${prefix} ${renderChildren(item.children)}`
98
98
  })
99
- .join("\n")}\n\n`
99
+ .join('\n')}\n\n`
100
100
  }
101
101
 
102
- case "list-item":
102
+ case 'list-item':
103
103
  return renderChildren(node.children)
104
104
 
105
- case "code":
105
+ case 'code':
106
106
  return `${renderChildren(node.children)}\n\n`
107
107
 
108
- case "divider":
109
- return `${"".repeat(40)}\n\n`
108
+ case 'divider':
109
+ return `${''.repeat(40)}\n\n`
110
110
 
111
- case "page-break":
112
- return `\n${"".repeat(40)}\n\n`
111
+ case 'page-break':
112
+ return `\n${''.repeat(40)}\n\n`
113
113
 
114
- case "spacer":
115
- return "\n"
114
+ case 'spacer':
115
+ return '\n'
116
116
 
117
- case "button":
117
+ case 'button':
118
118
  return `[${renderChildren(node.children)}] → ${p.href}\n\n`
119
119
 
120
- case "quote":
120
+ case 'quote':
121
121
  return ` "${renderChildren(node.children)}"\n\n`
122
122
 
123
123
  default:
@@ -1,5 +1,5 @@
1
- import { sanitizeHref } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import { sanitizeHref } from '../sanitize'
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
3
3
 
4
4
  /**
5
5
  * WhatsApp renderer — outputs formatted text using WhatsApp's markup.
@@ -7,32 +7,32 @@ 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
  function renderNode(node: DocNode): string {
20
20
  const p = node.props
21
21
 
22
22
  switch (node.type) {
23
- case "document":
24
- case "page":
25
- case "section":
26
- case "row":
27
- case "column":
28
- return node.children.map((c) => (typeof c === "string" ? c : renderNode(c))).join("")
29
-
30
- case "heading": {
23
+ case 'document':
24
+ case 'page':
25
+ case 'section':
26
+ case 'row':
27
+ case 'column':
28
+ return node.children.map((c) => (typeof c === 'string' ? c : renderNode(c))).join('')
29
+
30
+ case 'heading': {
31
31
  const text = getTextContent(node.children)
32
32
  return `*${text}*\n\n`
33
33
  }
34
34
 
35
- case "text": {
35
+ case 'text': {
36
36
  let text = getTextContent(node.children)
37
37
  if (p.bold) text = `*${text}*`
38
38
  if (p.italic) text = `_${text}_`
@@ -40,64 +40,64 @@ function renderNode(node: DocNode): string {
40
40
  return `${text}\n\n`
41
41
  }
42
42
 
43
- case "link": {
43
+ case 'link': {
44
44
  const href = sanitizeHref(p.href as string)
45
45
  const text = getTextContent(node.children)
46
46
  return `${text}: ${href}\n\n`
47
47
  }
48
48
 
49
- case "image":
49
+ case 'image':
50
50
  // WhatsApp doesn't support inline images in text
51
- return ""
51
+ return ''
52
52
 
53
- case "table": {
53
+ case 'table': {
54
54
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
55
55
  const rows = (p.rows ?? []) as (string | number)[][]
56
56
 
57
- const header = columns.map((c) => `*${c.header}*`).join(" | ")
58
- const body = rows.map((row) => row.map((c) => String(c ?? "")).join(" | ")).join("\n")
57
+ const header = columns.map((c) => `*${c.header}*`).join(' | ')
58
+ const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
59
59
 
60
60
  let result = `${header}\n${body}\n\n`
61
61
  if (p.caption) result = `_${p.caption}_\n${result}`
62
62
  return result
63
63
  }
64
64
 
65
- case "list": {
65
+ case 'list': {
66
66
  const ordered = p.ordered as boolean | undefined
67
67
  return `${node.children
68
- .filter((c): c is DocNode => typeof c !== "string")
68
+ .filter((c): c is DocNode => typeof c !== 'string')
69
69
  .map((item, i) => {
70
- const prefix = ordered ? `${i + 1}.` : ""
70
+ const prefix = ordered ? `${i + 1}.` : ''
71
71
  return `${prefix} ${getTextContent(item.children)}`
72
72
  })
73
- .join("\n")}\n\n`
73
+ .join('\n')}\n\n`
74
74
  }
75
75
 
76
- case "code": {
76
+ case 'code': {
77
77
  const text = getTextContent(node.children)
78
78
  return `\`\`\`${text}\`\`\`\n\n`
79
79
  }
80
80
 
81
- case "divider":
82
- case "page-break":
83
- return "───────────\n\n"
81
+ case 'divider':
82
+ case 'page-break':
83
+ return '───────────\n\n'
84
84
 
85
- case "spacer":
86
- return "\n"
85
+ case 'spacer':
86
+ return '\n'
87
87
 
88
- case "button": {
88
+ case 'button': {
89
89
  const href = sanitizeHref(p.href as string)
90
90
  const text = getTextContent(node.children)
91
91
  return `*${text}*: ${href}\n\n`
92
92
  }
93
93
 
94
- case "quote": {
94
+ case 'quote': {
95
95
  const text = getTextContent(node.children)
96
96
  return `> ${text}\n\n`
97
97
  }
98
98
 
99
99
  default:
100
- return ""
100
+ return ''
101
101
  }
102
102
  }
103
103
 
@@ -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
  * XLSX renderer — lazy-loads ExcelJS on first use.
@@ -7,13 +7,13 @@ 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 ExtractedSheet {
@@ -26,18 +26,18 @@ interface ExtractedSheet {
26
26
  function extractSheets(node: DocNode): ExtractedSheet[] {
27
27
  const sheets: ExtractedSheet[] = []
28
28
  let currentSheet: ExtractedSheet = {
29
- name: "Sheet 1",
29
+ name: 'Sheet 1',
30
30
  headings: [],
31
31
  tables: [],
32
32
  }
33
33
 
34
34
  function walk(n: DocNode): void {
35
35
  switch (n.type) {
36
- case "document":
36
+ case 'document':
37
37
  walkChildren(n)
38
38
  break
39
39
 
40
- case "page":
40
+ case 'page':
41
41
  pushCurrentSheet()
42
42
  currentSheet = {
43
43
  name: `Sheet ${sheets.length + 1}`,
@@ -47,11 +47,11 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
47
47
  walkChildren(n)
48
48
  break
49
49
 
50
- case "heading":
50
+ case 'heading':
51
51
  addHeading(n)
52
52
  break
53
53
 
54
- case "table":
54
+ case 'table':
55
55
  currentSheet.tables.push(n)
56
56
  break
57
57
 
@@ -62,7 +62,7 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
62
62
 
63
63
  function walkChildren(n: DocNode): void {
64
64
  for (const child of n.children) {
65
- if (typeof child !== "string") walk(child)
65
+ if (typeof child !== 'string') walk(child)
66
66
  }
67
67
  }
68
68
 
@@ -88,8 +88,8 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
88
88
 
89
89
  /** Parse a cell value, handling currencies, percentages, and plain numbers. */
90
90
  function parseCellValue(value: string | number | undefined): string | number {
91
- if (value == null) return ""
92
- if (typeof value === "number") return value
91
+ if (value == null) return ''
92
+ if (typeof value === 'number') return value
93
93
 
94
94
  const trimmed = value.trim()
95
95
 
@@ -101,11 +101,11 @@ function parseCellValue(value: string | number | undefined): string | number {
101
101
  // Currency: "$1,234.56", "$1234", "-$500"
102
102
  const currencyMatch = trimmed.match(/^-?\$[\d,]+(\.\d+)?$/)
103
103
  if (currencyMatch) {
104
- return Number.parseFloat(trimmed.replace(/[$,]/g, ""))
104
+ return Number.parseFloat(trimmed.replace(/[$,]/g, ''))
105
105
  }
106
106
 
107
107
  // Plain number: "1,234.56", "1234", "-500.5"
108
- const plainNum = Number(trimmed.replace(/,/g, ""))
108
+ const plainNum = Number(trimmed.replace(/,/g, ''))
109
109
  if (!Number.isNaN(plainNum) && /^-?[\d,]+(\.\d+)?$/.test(trimmed)) {
110
110
  return plainNum
111
111
  }
@@ -115,23 +115,23 @@ function parseCellValue(value: string | number | undefined): string | number {
115
115
 
116
116
  /** Get ExcelJS number format string for a value. */
117
117
  function getCellFormat(originalValue: string | number | undefined): string | undefined {
118
- if (typeof originalValue !== "string") return undefined
118
+ if (typeof originalValue !== 'string') return undefined
119
119
  const trimmed = originalValue.trim()
120
120
 
121
- if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return "0.00%"
122
- if (/^-?\$/.test(trimmed)) return "$#,##0.00"
121
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return '0.00%'
122
+ if (/^-?\$/.test(trimmed)) return '$#,##0.00'
123
123
  return undefined
124
124
  }
125
125
 
126
126
  /** Map alignment string to ExcelJS horizontal alignment. */
127
- function mapAlignment(align?: string): "left" | "center" | "right" | undefined {
128
- 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
129
129
  return undefined
130
130
  }
131
131
 
132
132
  /** Thin border style for ExcelJS. */
133
- function thinBorder(): { style: "thin"; color: { argb: string } } {
134
- return { style: "thin", color: { argb: "FFDDDDDD" } }
133
+ function thinBorder(): { style: 'thin'; color: { argb: string } } {
134
+ return { style: 'thin', color: { argb: 'FFDDDDDD' } }
135
135
  }
136
136
 
137
137
  /** Apply header styling to a cell. */
@@ -150,16 +150,16 @@ function styleHeaderCell(
150
150
  cell.value = col.header
151
151
  cell.font = {
152
152
  bold: true,
153
- color: { argb: hs?.color?.replace("#", "FF") ?? "FF000000" },
153
+ color: { argb: hs?.color?.replace('#', 'FF') ?? 'FF000000' },
154
154
  }
155
155
  if (hs?.background) {
156
156
  cell.fill = {
157
- type: "pattern",
158
- pattern: "solid",
159
- fgColor: { argb: hs.background.replace("#", "FF") },
157
+ type: 'pattern',
158
+ pattern: 'solid',
159
+ fgColor: { argb: hs.background.replace('#', 'FF') },
160
160
  }
161
161
  }
162
- cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" }
162
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
163
163
  if (bordered) {
164
164
  cell.border = {
165
165
  top: thinBorder(),
@@ -188,12 +188,12 @@ function styleDataCell(
188
188
  cell.value = parseCellValue(rawValue)
189
189
  const fmt = getCellFormat(rawValue)
190
190
  if (fmt) cell.numFmt = fmt
191
- cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" }
191
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
192
192
  if (striped && isOddRow) {
193
193
  cell.fill = {
194
- type: "pattern",
195
- pattern: "solid",
196
- fgColor: { argb: "FFF9F9F9" },
194
+ type: 'pattern',
195
+ pattern: 'solid',
196
+ fgColor: { argb: 'FFF9F9F9' },
197
197
  }
198
198
  }
199
199
  if (bordered) {
@@ -270,7 +270,7 @@ function autoFitColumns(ws: {
270
270
  for (const col of ws.columns) {
271
271
  let maxLen = 10
272
272
  col.eachCell?.({ includeEmpty: false }, (cell) => {
273
- const len = String(cell.value ?? "").length
273
+ const len = String(cell.value ?? '').length
274
274
  if (len > maxLen) maxLen = len
275
275
  })
276
276
  col.width = Math.min(maxLen + 2, 50)
@@ -281,7 +281,7 @@ export const xlsxRenderer: DocumentRenderer = {
281
281
  async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
282
282
  let ExcelJS: any
283
283
  try {
284
- ExcelJS = await import("exceljs")
284
+ ExcelJS = await import('exceljs')
285
285
  } catch {
286
286
  throw new Error(
287
287
  '[@pyreon/document] XLSX renderer requires "exceljs" package. Install it: bun add exceljs',
@@ -289,13 +289,13 @@ export const xlsxRenderer: DocumentRenderer = {
289
289
  }
290
290
  const workbook = new ExcelJS.default.Workbook()
291
291
 
292
- workbook.creator = (node.props.author as string) ?? ""
293
- workbook.title = (node.props.title as string) ?? ""
292
+ workbook.creator = (node.props.author as string) ?? ''
293
+ workbook.title = (node.props.title as string) ?? ''
294
294
 
295
295
  const sheets = extractSheets(node)
296
296
 
297
297
  if (sheets.length === 0) {
298
- workbook.addWorksheet("Sheet 1")
298
+ workbook.addWorksheet('Sheet 1')
299
299
  }
300
300
 
301
301
  for (const sheet of sheets) {