@pyreon/document 0.10.0 → 0.11.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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/confluence-Bd3ua1Ut.js.map +1 -1
- package/lib/csv-COrS4qdy.js.map +1 -1
- package/lib/discord-BLUnkEh9.js.map +1 -1
- package/lib/{dist-BsqdI2nY.js → dist-CYL41kqQ.js} +2 -2
- package/lib/dist-CYL41kqQ.js.map +1 -0
- package/lib/{docx-BEBOihjl.js → docx-uNAel545.js} +7 -2
- package/lib/docx-uNAel545.js.map +1 -0
- package/lib/email-D0bbfWq4.js.map +1 -1
- package/lib/{exceljs-BoIDUUaw.js → exceljs-BYETsesT.js} +314 -314
- package/lib/exceljs-BYETsesT.js.map +1 -0
- package/lib/google-chat-CkKCBUWC.js.map +1 -1
- package/lib/html-B5biprN2.js.map +1 -1
- package/lib/index.js +17 -8
- package/lib/index.js.map +1 -1
- package/lib/markdown-CdtlFGC0.js.map +1 -1
- package/lib/notion-iG2C5bEY.js.map +1 -1
- package/lib/{pdf-DIUQUEdj.js → pdf-IuBgTb3T.js} +9 -3
- package/lib/pdf-IuBgTb3T.js.map +1 -0
- package/lib/{pdfmake-DnmLxK4Q.js → pdfmake-CKMX5URW.js} +2 -4
- package/lib/pdfmake-CKMX5URW.js.map +1 -0
- package/lib/{pptx-Dd33oL3_.js → pptx-DXiMiYFM.js} +7 -2
- package/lib/pptx-DXiMiYFM.js.map +1 -0
- package/lib/{pptxgen.es-COcgXsyx.js → pptxgen.es-FsqHs8mD.js} +3 -6
- package/lib/pptxgen.es-FsqHs8mD.js.map +1 -0
- package/lib/sanitize-O_3j1mNJ.js.map +1 -1
- package/lib/slack-BI3EQwYm.js.map +1 -1
- package/lib/svg-BKxumy-p.js.map +1 -1
- package/lib/teams-Cwz9lce0.js.map +1 -1
- package/lib/telegram-gYFqyMXb.js.map +1 -1
- package/lib/text-l1XNXBOC.js.map +1 -1
- package/lib/types/index.d.ts +43 -39
- package/lib/types/index.d.ts.map +1 -1
- package/lib/{vfs_fonts-Df1kkZ4Y.js → vfs_fonts-Cap07Jg3.js} +2 -2
- package/lib/vfs_fonts-Cap07Jg3.js.map +1 -0
- package/lib/whatsapp-CjSGoOKx.js.map +1 -1
- package/lib/{xlsx-Bb5TWyXQ.js → xlsx-Cvu4LBNy.js} +8 -2
- package/lib/xlsx-Cvu4LBNy.js.map +1 -0
- package/package.json +19 -7
- package/src/builder.ts +53 -44
- package/src/download.ts +32 -36
- package/src/env.d.ts +3 -17
- package/src/index.ts +6 -8
- package/src/nodes.ts +45 -45
- package/src/render.ts +45 -118
- package/src/renderers/confluence.ts +64 -80
- package/src/renderers/csv.ts +11 -18
- package/src/renderers/discord.ts +38 -50
- package/src/renderers/docx.ts +78 -120
- package/src/renderers/email.ts +73 -92
- package/src/renderers/google-chat.ts +35 -47
- package/src/renderers/html.ts +78 -101
- package/src/renderers/markdown.ts +43 -53
- package/src/renderers/notion.ts +63 -85
- package/src/renderers/pdf.ts +92 -115
- package/src/renderers/pptx.ts +60 -66
- package/src/renderers/slack.ts +49 -61
- package/src/renderers/svg.ts +49 -63
- package/src/renderers/teams.ts +68 -80
- package/src/renderers/telegram.ts +40 -54
- package/src/renderers/text.ts +44 -66
- package/src/renderers/whatsapp.ts +34 -48
- package/src/renderers/xlsx.ts +47 -61
- package/src/sanitize.ts +21 -25
- package/src/tests/document.test.ts +1337 -1385
- package/src/tests/stress.test.ts +111 -111
- package/src/types.ts +66 -65
- package/lib/dist-BsqdI2nY.js.map +0 -1
- package/lib/docx-BEBOihjl.js.map +0 -1
- package/lib/exceljs-BoIDUUaw.js.map +0 -1
- package/lib/pdf-DIUQUEdj.js.map +0 -1
- package/lib/pdfmake-DnmLxK4Q.js.map +0 -1
- package/lib/pptx-Dd33oL3_.js.map +0 -1
- package/lib/pptxgen.es-COcgXsyx.js.map +0 -1
- package/lib/vfs_fonts-Df1kkZ4Y.js.map +0 -1
- package/lib/xlsx-Bb5TWyXQ.js.map +0 -1
package/src/renderers/email.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from
|
|
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,
|
|
26
|
-
.replace(/>/g,
|
|
27
|
-
.replace(/"/g,
|
|
18
|
+
.replace(/&/g, "&")
|
|
19
|
+
.replace(/</g, "<")
|
|
20
|
+
.replace(/>/g, ">")
|
|
21
|
+
.replace(/"/g, """)
|
|
28
22
|
}
|
|
29
23
|
|
|
30
24
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
31
|
-
return typeof col ===
|
|
25
|
+
return typeof col === "string" ? { header: col } : col
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
function renderChild(child: DocChild): string {
|
|
35
|
-
if (typeof 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 =
|
|
44
|
-
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` :
|
|
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
|
|
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
|
|
53
|
+
case "page":
|
|
60
54
|
// In email, pages are just content sections
|
|
61
55
|
return renderChildren(node.children)
|
|
62
56
|
|
|
63
|
-
case
|
|
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 ===
|
|
69
|
-
:
|
|
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 ===
|
|
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` :
|
|
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
|
|
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(
|
|
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
|
|
80
|
+
case "column":
|
|
93
81
|
return renderChildren(node.children)
|
|
94
82
|
|
|
95
|
-
case
|
|
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) ??
|
|
107
|
-
const align = (p.align as string) ??
|
|
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
|
|
99
|
+
case "text": {
|
|
112
100
|
const size = (p.size as number) ?? 14
|
|
113
|
-
const color = sanitizeColor((p.color as string) ??
|
|
114
|
-
const weight = p.bold ?
|
|
115
|
-
const style = p.italic ?
|
|
116
|
-
const decoration = p.underline
|
|
117
|
-
|
|
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
|
|
127
|
-
return `<a href="${esc(sanitizeHref(p.href as string))}" style="color:${sanitizeColor((p.color as string) ??
|
|
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
|
|
130
|
-
const align = (p.align as string) ??
|
|
131
|
-
const img = `<img src="${esc(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` :
|
|
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 ===
|
|
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 ===
|
|
136
|
-
|
|
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
|
|
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 +=
|
|
137
|
+
html += "<tr>"
|
|
157
138
|
for (const col of columns) {
|
|
158
139
|
const bg = hs?.background
|
|
159
140
|
? `background-color:${sanitizeColor(hs.background)};`
|
|
160
|
-
:
|
|
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 ===
|
|
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 +=
|
|
149
|
+
html += "</tr>"
|
|
169
150
|
|
|
170
151
|
for (let i = 0; i < rows.length; i++) {
|
|
171
|
-
const bg = striped && i % 2 === 1 ?
|
|
172
|
-
html +=
|
|
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] ??
|
|
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 +=
|
|
159
|
+
html += "</tr>"
|
|
179
160
|
}
|
|
180
|
-
html +=
|
|
161
|
+
html += "</table>"
|
|
181
162
|
return html
|
|
182
163
|
}
|
|
183
164
|
|
|
184
|
-
case
|
|
185
|
-
const tag = p.ordered ?
|
|
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
|
|
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
|
|
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
|
|
196
|
-
const color = sanitizeColor((p.color as string) ??
|
|
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"> </td></tr></table>`
|
|
199
180
|
}
|
|
200
181
|
|
|
201
|
-
case
|
|
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"> </td></tr></table>`
|
|
203
184
|
|
|
204
|
-
case
|
|
185
|
+
case "spacer":
|
|
205
186
|
return `<div style="height:${p.height}px;line-height:${p.height}px;font-size:0"> </div>`
|
|
206
187
|
|
|
207
|
-
case
|
|
208
|
-
const bg = sanitizeColor((p.background as string) ??
|
|
209
|
-
const color = sanitizeColor((p.color as string) ??
|
|
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) ??
|
|
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
|
|
220
|
-
const borderColor = sanitizeColor((p.borderColor as string) ??
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
37
|
-
case
|
|
38
|
-
case
|
|
39
|
-
case
|
|
40
|
-
case
|
|
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 !==
|
|
34
|
+
if (typeof child !== "string") {
|
|
43
35
|
widgets.push(...nodeToWidgets(child))
|
|
44
36
|
}
|
|
45
37
|
}
|
|
46
38
|
break
|
|
47
39
|
|
|
48
|
-
case
|
|
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
|
|
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
|
|
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
|
|
72
|
+
case "image": {
|
|
81
73
|
const src = sanitizeImageSrc(p.src as string)
|
|
82
|
-
if (src.startsWith(
|
|
74
|
+
if (src.startsWith("http")) {
|
|
83
75
|
widgets.push({
|
|
84
76
|
image: {
|
|
85
77
|
imageUrl: src,
|
|
86
|
-
altText: (p.alt as string) ??
|
|
78
|
+
altText: (p.alt as string) ?? "Image",
|
|
87
79
|
},
|
|
88
80
|
})
|
|
89
81
|
}
|
|
90
82
|
break
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
case
|
|
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
|
|
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 !==
|
|
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(
|
|
107
|
+
.join("\n")
|
|
120
108
|
widgets.push({
|
|
121
109
|
textParagraph: { text: items },
|
|
122
110
|
})
|
|
123
111
|
break
|
|
124
112
|
}
|
|
125
113
|
|
|
126
|
-
case
|
|
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
|
|
137
|
-
case
|
|
124
|
+
case "divider":
|
|
125
|
+
case "page-break":
|
|
138
126
|
widgets.push({ divider: {} })
|
|
139
127
|
break
|
|
140
128
|
|
|
141
|
-
case
|
|
129
|
+
case "spacer":
|
|
142
130
|
// No direct equivalent — skip
|
|
143
131
|
break
|
|
144
132
|
|
|
145
|
-
case
|
|
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
|
|
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 !==
|
|
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:
|
|
183
|
+
cardId: "document",
|
|
196
184
|
card: {
|
|
197
185
|
header: title
|
|
198
186
|
? { title, subtitle: (node.props.subject as string) ?? undefined }
|