@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,11 +1,5 @@
1
- import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from '../sanitize'
2
- import type {
3
- DocChild,
4
- DocNode,
5
- DocumentRenderer,
6
- RenderOptions,
7
- TableColumn,
8
- } from '../types'
1
+ import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from "../sanitize"
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
9
3
 
10
4
  /**
11
5
  * Email renderer — generates table-based HTML with inline styles
@@ -21,78 +15,72 @@ import type {
21
15
 
22
16
  function esc(str: string): string {
23
17
  return str
24
- .replace(/&/g, '&')
25
- .replace(/</g, '&lt;')
26
- .replace(/>/g, '&gt;')
27
- .replace(/"/g, '&quot;')
18
+ .replace(/&/g, "&amp;")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+ .replace(/"/g, "&quot;")
28
22
  }
29
23
 
30
24
  function resolveColumn(col: string | TableColumn): TableColumn {
31
- return typeof col === 'string' ? { header: col } : col
25
+ return typeof col === "string" ? { header: col } : col
32
26
  }
33
27
 
34
28
  function renderChild(child: DocChild): string {
35
- if (typeof child === 'string') return esc(child)
29
+ if (typeof child === "string") return esc(child)
36
30
  return renderNode(child)
37
31
  }
38
32
 
39
33
  function renderChildren(children: DocChild[]): string {
40
- return children.map(renderChild).join('')
34
+ return children.map(renderChild).join("")
41
35
  }
42
36
 
43
- function wrapInTable(content: string, style = ''): string {
44
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` : ''}><tr><td>${content}</td></tr></table>`
37
+ function wrapInTable(content: string, style = ""): string {
38
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` : ""}><tr><td>${content}</td></tr></table>`
45
39
  }
46
40
 
47
41
  function renderNode(node: DocNode): string {
48
42
  const p = node.props
49
43
 
50
44
  switch (node.type) {
51
- case 'document': {
52
- const title = p.title ? `<title>${esc(p.title as string)}</title>` : ''
45
+ case "document": {
46
+ const title = p.title ? `<title>${esc(p.title as string)}</title>` : ""
53
47
  const preview = p.subject
54
48
  ? `<div style="display:none;max-height:0;overflow:hidden">${esc(p.subject as string)}</div>`
55
- : ''
49
+ : ""
56
50
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${title}<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]--></head><body style="margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif">${preview}<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f4f4f4"><tr><td align="center" style="padding:20px 0"><table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;max-width:600px;width:100%"><tr><td>${renderChildren(node.children)}</td></tr></table></td></tr></table></body></html>`
57
51
  }
58
52
 
59
- case 'page':
53
+ case "page":
60
54
  // In email, pages are just content sections
61
55
  return renderChildren(node.children)
62
56
 
63
- case 'section': {
64
- const bg = p.background
65
- ? `background-color:${sanitizeColor(p.background as string)};`
66
- : ''
57
+ case "section": {
58
+ const bg = p.background ? `background-color:${sanitizeColor(p.background as string)};` : ""
67
59
  const pad = p.padding
68
- ? `padding:${typeof p.padding === 'number' ? `${p.padding}px` : Array.isArray(p.padding) ? (p.padding as number[]).map((v) => `${v}px`).join(' ') : '0'}`
69
- : 'padding:0'
70
- const radius = p.borderRadius ? `border-radius:${p.borderRadius}px;` : ''
60
+ ? `padding:${typeof p.padding === "number" ? `${p.padding}px` : Array.isArray(p.padding) ? (p.padding as number[]).map((v) => `${v}px`).join(" ") : "0"}`
61
+ : "padding:0"
62
+ const radius = p.borderRadius ? `border-radius:${p.borderRadius}px;` : ""
71
63
 
72
- if (p.direction === 'row') {
64
+ if (p.direction === "row") {
73
65
  // Row layout via nested table
74
- const children = node.children.filter(
75
- (c): c is DocNode => typeof c !== 'string',
76
- )
66
+ const children = node.children.filter((c): c is DocNode => typeof c !== "string")
77
67
  const colWidth = Math.floor(100 / Math.max(children.length, 1))
78
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="${bg}${radius}${pad}"><tr>${children.map((child) => `<td width="${colWidth}%" valign="top" style="padding:${(p.gap as number | undefined) ? `0 ${(p.gap as number) / 2}px` : '0'}">${renderNode(child)}</td>`).join('')}</tr></table>`
68
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="${bg}${radius}${pad}"><tr>${children.map((child) => `<td width="${colWidth}%" valign="top" style="padding:${(p.gap as number | undefined) ? `0 ${(p.gap as number) / 2}px` : "0"}">${renderNode(child)}</td>`).join("")}</tr></table>`
79
69
  }
80
70
 
81
71
  return wrapInTable(renderChildren(node.children), `${bg}${radius}${pad}`)
82
72
  }
83
73
 
84
- case 'row': {
85
- const children = node.children.filter(
86
- (c): c is DocNode => typeof c !== 'string',
87
- )
74
+ case "row": {
75
+ const children = node.children.filter((c): c is DocNode => typeof c !== "string")
88
76
  const gap = (p.gap as number) ?? 0
89
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>${children.map((child) => `<td valign="top" style="padding:0 ${gap / 2}px">${renderNode(child)}</td>`).join('')}</tr></table>`
77
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>${children.map((child) => `<td valign="top" style="padding:0 ${gap / 2}px">${renderNode(child)}</td>`).join("")}</tr></table>`
90
78
  }
91
79
 
92
- case 'column':
80
+ case "column":
93
81
  return renderChildren(node.children)
94
82
 
95
- case 'heading': {
83
+ case "heading": {
96
84
  const level = (p.level as number) ?? 1
97
85
  const sizes: Record<number, number> = {
98
86
  1: 28,
@@ -103,45 +91,38 @@ function renderNode(node: DocNode): string {
103
91
  6: 14,
104
92
  }
105
93
  const size = sizes[level] ?? 24
106
- const color = sanitizeColor((p.color as string) ?? '#000000')
107
- const align = (p.align as string) ?? 'left'
94
+ const color = sanitizeColor((p.color as string) ?? "#000000")
95
+ const align = (p.align as string) ?? "left"
108
96
  return `<h${level} style="margin:0 0 12px 0;font-size:${size}px;color:${color};text-align:${align};font-weight:bold;line-height:1.3">${renderChildren(node.children)}</h${level}>`
109
97
  }
110
98
 
111
- case 'text': {
99
+ case "text": {
112
100
  const size = (p.size as number) ?? 14
113
- const color = sanitizeColor((p.color as string) ?? '#333333')
114
- const weight = p.bold ? 'bold' : 'normal'
115
- const style = p.italic ? 'italic' : 'normal'
116
- const decoration = p.underline
117
- ? 'underline'
118
- : p.strikethrough
119
- ? 'line-through'
120
- : 'none'
121
- const align = (p.align as string) ?? 'left'
101
+ const color = sanitizeColor((p.color as string) ?? "#333333")
102
+ const weight = p.bold ? "bold" : "normal"
103
+ const style = p.italic ? "italic" : "normal"
104
+ const decoration = p.underline ? "underline" : p.strikethrough ? "line-through" : "none"
105
+ const align = (p.align as string) ?? "left"
122
106
  const lh = (p.lineHeight as number) ?? 1.5
123
107
  return `<p style="margin:0 0 12px 0;font-size:${size}px;color:${color};font-weight:${weight};font-style:${style};text-decoration:${decoration};text-align:${align};line-height:${lh}">${renderChildren(node.children)}</p>`
124
108
  }
125
109
 
126
- case 'link':
127
- return `<a href="${esc(sanitizeHref(p.href as string))}" style="color:${sanitizeColor((p.color as string) ?? '#4f46e5')};text-decoration:underline" target="_blank">${renderChildren(node.children)}</a>`
110
+ case "link":
111
+ return `<a href="${esc(sanitizeHref(p.href as string))}" style="color:${sanitizeColor((p.color as string) ?? "#4f46e5")};text-decoration:underline" target="_blank">${renderChildren(node.children)}</a>`
128
112
 
129
- case 'image': {
130
- const align = (p.align as string) ?? 'left'
131
- const img = `<img src="${esc(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` : ''}${p.height ? ` height="${p.height}"` : ''} alt="${esc((p.alt as string) ?? '')}" style="display:block;outline:none;border:none;text-decoration:none${p.width ? `;max-width:${p.width}px` : ''}" />`
113
+ case "image": {
114
+ const align = (p.align as string) ?? "left"
115
+ const img = `<img src="${esc(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` : ""}${p.height ? ` height="${p.height}"` : ""} alt="${esc((p.alt as string) ?? "")}" style="display:block;outline:none;border:none;text-decoration:none${p.width ? `;max-width:${p.width}px` : ""}" />`
132
116
  if (p.caption) {
133
- return `<table cellpadding="0" cellspacing="0" border="0"${align === 'center' ? ' align="center"' : ''}><tr><td>${img}</td></tr><tr><td style="font-size:12px;color:#666;padding-top:4px;text-align:center">${esc(p.caption as string)}</td></tr></table>`
117
+ return `<table cellpadding="0" cellspacing="0" border="0"${align === "center" ? ' align="center"' : ""}><tr><td>${img}</td></tr><tr><td style="font-size:12px;color:#666;padding-top:4px;text-align:center">${esc(p.caption as string)}</td></tr></table>`
134
118
  }
135
- if (align === 'center')
136
- return `<div style="text-align:center">${img}</div>`
137
- if (align === 'right') return `<div style="text-align:right">${img}</div>`
119
+ if (align === "center") return `<div style="text-align:center">${img}</div>`
120
+ if (align === "right") return `<div style="text-align:right">${img}</div>`
138
121
  return img
139
122
  }
140
123
 
141
- case 'table': {
142
- const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
143
- resolveColumn,
144
- )
124
+ case "table": {
125
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
145
126
  const rows = (p.rows ?? []) as (string | number)[][]
146
127
  const hs = p.headerStyle as
147
128
  | { background?: string; color?: string; bold?: boolean }
@@ -153,71 +134,71 @@ function renderNode(node: DocNode): string {
153
134
  if (p.caption)
154
135
  html += `<caption style="font-size:12px;color:#666;padding:8px;text-align:left">${esc(p.caption as string)}</caption>`
155
136
 
156
- html += '<tr>'
137
+ html += "<tr>"
157
138
  for (const col of columns) {
158
139
  const bg = hs?.background
159
140
  ? `background-color:${sanitizeColor(hs.background)};`
160
- : 'background-color:#f5f5f5;'
161
- const color = hs?.color ? `color:${sanitizeColor(hs.color)};` : ''
162
- const align = col.align ? `text-align:${col.align};` : ''
141
+ : "background-color:#f5f5f5;"
142
+ const color = hs?.color ? `color:${sanitizeColor(hs.color)};` : ""
143
+ const align = col.align ? `text-align:${col.align};` : ""
163
144
  const width = col.width
164
- ? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`
165
- : ''
145
+ ? `width:${typeof col.width === "number" ? `${col.width}px` : col.width};`
146
+ : ""
166
147
  html += `<th style="${bg}${color}font-weight:bold;${align}${width}padding:8px;border-bottom:2px solid #ddd">${esc(col.header)}</th>`
167
148
  }
168
- html += '</tr>'
149
+ html += "</tr>"
169
150
 
170
151
  for (let i = 0; i < rows.length; i++) {
171
- const bg = striped && i % 2 === 1 ? 'background-color:#f9f9f9;' : ''
172
- html += '<tr>'
152
+ const bg = striped && i % 2 === 1 ? "background-color:#f9f9f9;" : ""
153
+ html += "<tr>"
173
154
  for (let j = 0; j < columns.length; j++) {
174
155
  const col = columns[j]
175
- const align = col?.align ? `text-align:${col.align};` : ''
176
- html += `<td style="${bg}${align}padding:8px;border-bottom:1px solid #eee">${esc(String(rows[i]?.[j] ?? ''))}</td>`
156
+ const align = col?.align ? `text-align:${col.align};` : ""
157
+ html += `<td style="${bg}${align}padding:8px;border-bottom:1px solid #eee">${esc(String(rows[i]?.[j] ?? ""))}</td>`
177
158
  }
178
- html += '</tr>'
159
+ html += "</tr>"
179
160
  }
180
- html += '</table>'
161
+ html += "</table>"
181
162
  return html
182
163
  }
183
164
 
184
- case 'list': {
185
- const tag = p.ordered ? 'ol' : 'ul'
165
+ case "list": {
166
+ const tag = p.ordered ? "ol" : "ul"
186
167
  return `<${tag} style="margin:0 0 12px 0;padding-left:24px">${renderChildren(node.children)}</${tag}>`
187
168
  }
188
169
 
189
- case 'list-item':
170
+ case "list-item":
190
171
  return `<li style="margin:0 0 4px 0;font-size:14px;color:#333">${renderChildren(node.children)}</li>`
191
172
 
192
- case 'code':
173
+ case "code":
193
174
  return `<pre style="background-color:#f5f5f5;padding:12px;border-radius:4px;font-family:Courier New,monospace;font-size:13px;color:#333;overflow-x:auto;margin:0 0 12px 0"><code>${esc(renderChildren(node.children))}</code></pre>`
194
175
 
195
- case 'divider': {
196
- const color = sanitizeColor((p.color as string) ?? '#dddddd')
176
+ case "divider": {
177
+ const color = sanitizeColor((p.color as string) ?? "#dddddd")
197
178
  const thickness = (p.thickness as number) ?? 1
198
179
  return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0"><tr><td style="border-top:${thickness}px solid ${color};font-size:0;line-height:0">&nbsp;</td></tr></table>`
199
180
  }
200
181
 
201
- case 'page-break':
182
+ case "page-break":
202
183
  return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0"><tr><td style="border-top:2px solid #dddddd;font-size:0;line-height:0">&nbsp;</td></tr></table>`
203
184
 
204
- case 'spacer':
185
+ case "spacer":
205
186
  return `<div style="height:${p.height}px;line-height:${p.height}px;font-size:0">&nbsp;</div>`
206
187
 
207
- case 'button': {
208
- const bg = sanitizeColor((p.background as string) ?? '#4f46e5')
209
- const color = sanitizeColor((p.color as string) ?? '#ffffff')
188
+ case "button": {
189
+ const bg = sanitizeColor((p.background as string) ?? "#4f46e5")
190
+ const color = sanitizeColor((p.color as string) ?? "#ffffff")
210
191
  const radius = (p.borderRadius as number) ?? 4
211
192
  const href = esc(sanitizeHref(p.href as string))
212
193
  const text = renderChildren(node.children)
213
- const align = (p.align as string) ?? 'left'
194
+ const align = (p.align as string) ?? "left"
214
195
 
215
196
  // Bulletproof button — works in Outlook via VML, CSS everywhere else
216
197
  return `<div style="text-align:${align};margin:12px 0"><!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:200px" arcsize="10%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${color};font-family:Arial,sans-serif;font-size:14px;font-weight:bold">${text}</center></v:roundrect><![endif]--><!--[if !mso]><!--><a href="${href}" style="display:inline-block;background-color:${bg};color:${color};padding:12px 24px;border-radius:${radius}px;text-decoration:none;font-weight:bold;font-size:14px;font-family:Arial,sans-serif" target="_blank">${text}</a><!--<![endif]--></div>`
217
198
  }
218
199
 
219
- case 'quote': {
220
- const borderColor = sanitizeColor((p.borderColor as string) ?? '#dddddd')
200
+ case "quote": {
201
+ const borderColor = sanitizeColor((p.borderColor as string) ?? "#dddddd")
221
202
  return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:12px 0"><tr><td style="border-left:4px solid ${borderColor};padding:12px 20px;color:#555555;font-style:italic">${renderChildren(node.children)}</td></tr></table>`
222
203
  }
223
204
 
@@ -1,11 +1,5 @@
1
- import { sanitizeHref, sanitizeImageSrc } from '../sanitize'
2
- import type {
3
- DocChild,
4
- DocNode,
5
- DocumentRenderer,
6
- RenderOptions,
7
- TableColumn,
8
- } from '../types'
1
+ import { sanitizeHref, sanitizeImageSrc } from "../sanitize"
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
9
3
 
10
4
  /**
11
5
  * Google Chat renderer — outputs Card V2 JSON for Google Chat API.
@@ -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 CardWidget {
@@ -33,23 +25,23 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
33
25
  const widgets: CardWidget[] = []
34
26
 
35
27
  switch (node.type) {
36
- case 'document':
37
- case 'page':
38
- case 'section':
39
- case 'row':
40
- case 'column':
28
+ case "document":
29
+ case "page":
30
+ case "section":
31
+ case "row":
32
+ case "column":
41
33
  for (const child of node.children) {
42
- if (typeof child !== 'string') {
34
+ if (typeof child !== "string") {
43
35
  widgets.push(...nodeToWidgets(child))
44
36
  }
45
37
  }
46
38
  break
47
39
 
48
- case 'heading': {
40
+ case "heading": {
49
41
  const text = getTextContent(node.children)
50
42
  widgets.push({
51
43
  decoratedText: {
52
- topLabel: '',
44
+ topLabel: "",
53
45
  text: `<b>${text}</b>`,
54
46
  wrapText: true,
55
47
  },
@@ -57,7 +49,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
57
49
  break
58
50
  }
59
51
 
60
- case 'text': {
52
+ case "text": {
61
53
  let text = getTextContent(node.children)
62
54
  if (p.bold) text = `<b>${text}</b>`
63
55
  if (p.italic) text = `<i>${text}</i>`
@@ -68,7 +60,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
68
60
  break
69
61
  }
70
62
 
71
- case 'link': {
63
+ case "link": {
72
64
  const href = sanitizeHref(p.href as string)
73
65
  const text = getTextContent(node.children)
74
66
  widgets.push({
@@ -77,30 +69,26 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
77
69
  break
78
70
  }
79
71
 
80
- case 'image': {
72
+ case "image": {
81
73
  const src = sanitizeImageSrc(p.src as string)
82
- if (src.startsWith('http')) {
74
+ if (src.startsWith("http")) {
83
75
  widgets.push({
84
76
  image: {
85
77
  imageUrl: src,
86
- altText: (p.alt as string) ?? 'Image',
78
+ altText: (p.alt as string) ?? "Image",
87
79
  },
88
80
  })
89
81
  }
90
82
  break
91
83
  }
92
84
 
93
- case 'table': {
94
- const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
95
- resolveColumn,
96
- )
85
+ case "table": {
86
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
97
87
  const rows = (p.rows ?? []) as (string | number)[][]
98
88
 
99
89
  // Google Chat Cards don't have native tables — use grid or formatted text
100
- const header = columns.map((c) => `<b>${c.header}</b>`).join(' | ')
101
- const body = rows
102
- .map((row) => row.map((c) => String(c ?? '')).join(' | '))
103
- .join('\n')
90
+ const header = columns.map((c) => `<b>${c.header}</b>`).join(" | ")
91
+ const body = rows.map((row) => row.map((c) => String(c ?? "")).join(" | ")).join("\n")
104
92
 
105
93
  widgets.push({
106
94
  textParagraph: { text: `${header}\n${body}` },
@@ -108,22 +96,22 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
108
96
  break
109
97
  }
110
98
 
111
- case 'list': {
99
+ case "list": {
112
100
  const ordered = p.ordered as boolean | undefined
113
101
  const items = node.children
114
- .filter((c): c is DocNode => typeof c !== 'string')
102
+ .filter((c): c is DocNode => typeof c !== "string")
115
103
  .map((item, i) => {
116
- const prefix = ordered ? `${i + 1}.` : ''
104
+ const prefix = ordered ? `${i + 1}.` : ""
117
105
  return `${prefix} ${getTextContent(item.children)}`
118
106
  })
119
- .join('\n')
107
+ .join("\n")
120
108
  widgets.push({
121
109
  textParagraph: { text: items },
122
110
  })
123
111
  break
124
112
  }
125
113
 
126
- case 'code': {
114
+ case "code": {
127
115
  const text = getTextContent(node.children)
128
116
  widgets.push({
129
117
  textParagraph: {
@@ -133,16 +121,16 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
133
121
  break
134
122
  }
135
123
 
136
- case 'divider':
137
- case 'page-break':
124
+ case "divider":
125
+ case "page-break":
138
126
  widgets.push({ divider: {} })
139
127
  break
140
128
 
141
- case 'spacer':
129
+ case "spacer":
142
130
  // No direct equivalent — skip
143
131
  break
144
132
 
145
- case 'button': {
133
+ case "button": {
146
134
  const href = sanitizeHref(p.href as string)
147
135
  const text = getTextContent(node.children)
148
136
  widgets.push({
@@ -164,7 +152,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
164
152
  break
165
153
  }
166
154
 
167
- case 'quote': {
155
+ case "quote": {
168
156
  const text = getTextContent(node.children)
169
157
  widgets.push({
170
158
  textParagraph: { text: `<i>"${text}"</i>` },
@@ -181,10 +169,10 @@ export const googleChatRenderer: DocumentRenderer = {
181
169
  const widgets = nodeToWidgets(node)
182
170
 
183
171
  // Extract title from first heading or document title
184
- let title = (node.props.title as string) ?? ''
172
+ let title = (node.props.title as string) ?? ""
185
173
  if (!title) {
186
174
  const firstHeading = node.children.find(
187
- (c): c is DocNode => typeof c !== 'string' && c.type === 'heading',
175
+ (c): c is DocNode => typeof c !== "string" && c.type === "heading",
188
176
  )
189
177
  if (firstHeading) title = getTextContent(firstHeading.children)
190
178
  }
@@ -192,7 +180,7 @@ export const googleChatRenderer: DocumentRenderer = {
192
180
  const card = {
193
181
  cardsV2: [
194
182
  {
195
- cardId: 'document',
183
+ cardId: "document",
196
184
  card: {
197
185
  header: title
198
186
  ? { title, subtitle: (node.props.subject as string) ?? undefined }