@pyreon/document 0.0.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/LICENSE +21 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/chunk-ErZ26oRB.js +48 -0
- package/lib/confluence-Va8e7RxQ.js +192 -0
- package/lib/confluence-Va8e7RxQ.js.map +1 -0
- package/lib/csv-2c38ub-Y.js +32 -0
- package/lib/csv-2c38ub-Y.js.map +1 -0
- package/lib/discord-DAoUZqvE.js +134 -0
- package/lib/discord-DAoUZqvE.js.map +1 -0
- package/lib/dist-BsqdI2nY.js +20179 -0
- package/lib/dist-BsqdI2nY.js.map +1 -0
- package/lib/docx-CorFwEH9.js +450 -0
- package/lib/docx-CorFwEH9.js.map +1 -0
- package/lib/email-Bn_Brjdp.js +131 -0
- package/lib/email-Bn_Brjdp.js.map +1 -0
- package/lib/exceljs-BoIDUUaw.js +34377 -0
- package/lib/exceljs-BoIDUUaw.js.map +1 -0
- package/lib/google-chat-B6I017I1.js +125 -0
- package/lib/google-chat-B6I017I1.js.map +1 -0
- package/lib/html-De_iS_f0.js +151 -0
- package/lib/html-De_iS_f0.js.map +1 -0
- package/lib/index.js +619 -0
- package/lib/index.js.map +1 -0
- package/lib/markdown-BYC_3C9i.js +75 -0
- package/lib/markdown-BYC_3C9i.js.map +1 -0
- package/lib/notion-DHaQHO6P.js +187 -0
- package/lib/notion-DHaQHO6P.js.map +1 -0
- package/lib/pdf-CDPc5Itc.js +419 -0
- package/lib/pdf-CDPc5Itc.js.map +1 -0
- package/lib/pdfmake-DnmLxK4Q.js +55511 -0
- package/lib/pdfmake-DnmLxK4Q.js.map +1 -0
- package/lib/pptx-DKQU6bjq.js +252 -0
- package/lib/pptx-DKQU6bjq.js.map +1 -0
- package/lib/pptxgen.es-COcgXsyx.js +5697 -0
- package/lib/pptxgen.es-COcgXsyx.js.map +1 -0
- package/lib/slack-CJRJgkag.js +139 -0
- package/lib/slack-CJRJgkag.js.map +1 -0
- package/lib/svg-BM8biZmL.js +187 -0
- package/lib/svg-BM8biZmL.js.map +1 -0
- package/lib/teams-S99tonRG.js +176 -0
- package/lib/teams-S99tonRG.js.map +1 -0
- package/lib/telegram-CbEO_2PN.js +77 -0
- package/lib/telegram-CbEO_2PN.js.map +1 -0
- package/lib/text-B5U8ucRr.js +75 -0
- package/lib/text-B5U8ucRr.js.map +1 -0
- package/lib/types/index.d.ts +528 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/vfs_fonts-Df1kkZ4Y.js +19 -0
- package/lib/vfs_fonts-Df1kkZ4Y.js.map +1 -0
- package/lib/whatsapp-DJ2D1jGG.js +64 -0
- package/lib/whatsapp-DJ2D1jGG.js.map +1 -0
- package/lib/xlsx-D47x-gZ5.js +199 -0
- package/lib/xlsx-D47x-gZ5.js.map +1 -0
- package/package.json +62 -0
- package/src/builder.ts +266 -0
- package/src/download.ts +76 -0
- package/src/env.d.ts +17 -0
- package/src/index.ts +98 -0
- package/src/nodes.ts +315 -0
- package/src/render.ts +222 -0
- package/src/renderers/confluence.ts +231 -0
- package/src/renderers/csv.ts +67 -0
- package/src/renderers/discord.ts +192 -0
- package/src/renderers/docx.ts +612 -0
- package/src/renderers/email.ts +230 -0
- package/src/renderers/google-chat.ts +211 -0
- package/src/renderers/html.ts +225 -0
- package/src/renderers/markdown.ts +144 -0
- package/src/renderers/notion.ts +264 -0
- package/src/renderers/pdf.ts +427 -0
- package/src/renderers/pptx.ts +353 -0
- package/src/renderers/slack.ts +192 -0
- package/src/renderers/svg.ts +254 -0
- package/src/renderers/teams.ts +234 -0
- package/src/renderers/telegram.ts +137 -0
- package/src/renderers/text.ts +154 -0
- package/src/renderers/whatsapp.ts +121 -0
- package/src/renderers/xlsx.ts +342 -0
- package/src/tests/document.test.ts +2920 -0
- package/src/types.ts +291 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Email renderer — generates table-based HTML with inline styles
|
|
11
|
+
* that works across Gmail, Outlook, Apple Mail, and other email clients.
|
|
12
|
+
*
|
|
13
|
+
* Key constraints:
|
|
14
|
+
* - No CSS classes (Gmail strips <style> tags)
|
|
15
|
+
* - Table-based layout (no flexbox/grid)
|
|
16
|
+
* - All styles inline
|
|
17
|
+
* - VML buttons for Outlook
|
|
18
|
+
* - Max width 600px for compatibility
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function esc(str: string): string {
|
|
22
|
+
return str
|
|
23
|
+
.replace(/&/g, '&')
|
|
24
|
+
.replace(/</g, '<')
|
|
25
|
+
.replace(/>/g, '>')
|
|
26
|
+
.replace(/"/g, '"')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
30
|
+
return typeof col === 'string' ? { header: col } : col
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderChild(child: DocChild): string {
|
|
34
|
+
if (typeof child === 'string') return esc(child)
|
|
35
|
+
return renderNode(child)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderChildren(children: DocChild[]): string {
|
|
39
|
+
return children.map(renderChild).join('')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wrapInTable(content: string, style = ''): string {
|
|
43
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` : ''}><tr><td>${content}</td></tr></table>`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderNode(node: DocNode): string {
|
|
47
|
+
const p = node.props
|
|
48
|
+
|
|
49
|
+
switch (node.type) {
|
|
50
|
+
case 'document': {
|
|
51
|
+
const title = p.title ? `<title>${esc(p.title as string)}</title>` : ''
|
|
52
|
+
const preview = p.subject
|
|
53
|
+
? `<div style="display:none;max-height:0;overflow:hidden">${esc(p.subject as string)}</div>`
|
|
54
|
+
: ''
|
|
55
|
+
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>`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'page':
|
|
59
|
+
// In email, pages are just content sections
|
|
60
|
+
return renderChildren(node.children)
|
|
61
|
+
|
|
62
|
+
case 'section': {
|
|
63
|
+
const bg = p.background ? `background-color:${p.background};` : ''
|
|
64
|
+
const pad = p.padding
|
|
65
|
+
? `padding:${typeof p.padding === 'number' ? `${p.padding}px` : Array.isArray(p.padding) ? (p.padding as number[]).map((v) => `${v}px`).join(' ') : '0'}`
|
|
66
|
+
: 'padding:0'
|
|
67
|
+
const radius = p.borderRadius ? `border-radius:${p.borderRadius}px;` : ''
|
|
68
|
+
|
|
69
|
+
if (p.direction === 'row') {
|
|
70
|
+
// Row layout via nested table
|
|
71
|
+
const children = node.children.filter(
|
|
72
|
+
(c): c is DocNode => typeof c !== 'string',
|
|
73
|
+
)
|
|
74
|
+
const colWidth = Math.floor(100 / Math.max(children.length, 1))
|
|
75
|
+
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>`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return wrapInTable(renderChildren(node.children), `${bg}${radius}${pad}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'row': {
|
|
82
|
+
const children = node.children.filter(
|
|
83
|
+
(c): c is DocNode => typeof c !== 'string',
|
|
84
|
+
)
|
|
85
|
+
const gap = (p.gap as number) ?? 0
|
|
86
|
+
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>`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'column':
|
|
90
|
+
return renderChildren(node.children)
|
|
91
|
+
|
|
92
|
+
case 'heading': {
|
|
93
|
+
const level = (p.level as number) ?? 1
|
|
94
|
+
const sizes: Record<number, number> = {
|
|
95
|
+
1: 28,
|
|
96
|
+
2: 24,
|
|
97
|
+
3: 20,
|
|
98
|
+
4: 18,
|
|
99
|
+
5: 16,
|
|
100
|
+
6: 14,
|
|
101
|
+
}
|
|
102
|
+
const size = sizes[level] ?? 24
|
|
103
|
+
const color = (p.color as string) ?? '#000000'
|
|
104
|
+
const align = (p.align as string) ?? 'left'
|
|
105
|
+
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}>`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'text': {
|
|
109
|
+
const size = (p.size as number) ?? 14
|
|
110
|
+
const color = (p.color as string) ?? '#333333'
|
|
111
|
+
const weight = p.bold ? 'bold' : 'normal'
|
|
112
|
+
const style = p.italic ? 'italic' : 'normal'
|
|
113
|
+
const decoration = p.underline
|
|
114
|
+
? 'underline'
|
|
115
|
+
: p.strikethrough
|
|
116
|
+
? 'line-through'
|
|
117
|
+
: 'none'
|
|
118
|
+
const align = (p.align as string) ?? 'left'
|
|
119
|
+
const lh = (p.lineHeight as number) ?? 1.5
|
|
120
|
+
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>`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'link':
|
|
124
|
+
return `<a href="${esc(p.href as string)}" style="color:${(p.color as string) ?? '#4f46e5'};text-decoration:underline" target="_blank">${renderChildren(node.children)}</a>`
|
|
125
|
+
|
|
126
|
+
case 'image': {
|
|
127
|
+
const align = (p.align as string) ?? 'left'
|
|
128
|
+
const img = `<img src="${esc(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` : ''}" />`
|
|
129
|
+
if (p.caption) {
|
|
130
|
+
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>`
|
|
131
|
+
}
|
|
132
|
+
if (align === 'center')
|
|
133
|
+
return `<div style="text-align:center">${img}</div>`
|
|
134
|
+
if (align === 'right') return `<div style="text-align:right">${img}</div>`
|
|
135
|
+
return img
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'table': {
|
|
139
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
140
|
+
resolveColumn,
|
|
141
|
+
)
|
|
142
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
143
|
+
const hs = p.headerStyle as
|
|
144
|
+
| { background?: string; color?: string; bold?: boolean }
|
|
145
|
+
| undefined
|
|
146
|
+
const striped = p.striped as boolean | undefined
|
|
147
|
+
|
|
148
|
+
let html =
|
|
149
|
+
'<table width="100%" cellpadding="8" cellspacing="0" border="0" style="border-collapse:collapse">'
|
|
150
|
+
if (p.caption)
|
|
151
|
+
html += `<caption style="font-size:12px;color:#666;padding:8px;text-align:left">${esc(p.caption as string)}</caption>`
|
|
152
|
+
|
|
153
|
+
html += '<tr>'
|
|
154
|
+
for (const col of columns) {
|
|
155
|
+
const bg = hs?.background
|
|
156
|
+
? `background-color:${hs.background};`
|
|
157
|
+
: 'background-color:#f5f5f5;'
|
|
158
|
+
const color = hs?.color ? `color:${hs.color};` : ''
|
|
159
|
+
const align = col.align ? `text-align:${col.align};` : ''
|
|
160
|
+
const width = col.width
|
|
161
|
+
? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`
|
|
162
|
+
: ''
|
|
163
|
+
html += `<th style="${bg}${color}font-weight:bold;${align}${width}padding:8px;border-bottom:2px solid #ddd">${esc(col.header)}</th>`
|
|
164
|
+
}
|
|
165
|
+
html += '</tr>'
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < rows.length; i++) {
|
|
168
|
+
const bg = striped && i % 2 === 1 ? 'background-color:#f9f9f9;' : ''
|
|
169
|
+
html += '<tr>'
|
|
170
|
+
for (let j = 0; j < columns.length; j++) {
|
|
171
|
+
const col = columns[j]
|
|
172
|
+
const align = col?.align ? `text-align:${col.align};` : ''
|
|
173
|
+
html += `<td style="${bg}${align}padding:8px;border-bottom:1px solid #eee">${esc(String(rows[i]?.[j] ?? ''))}</td>`
|
|
174
|
+
}
|
|
175
|
+
html += '</tr>'
|
|
176
|
+
}
|
|
177
|
+
html += '</table>'
|
|
178
|
+
return html
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'list': {
|
|
182
|
+
const tag = p.ordered ? 'ol' : 'ul'
|
|
183
|
+
return `<${tag} style="margin:0 0 12px 0;padding-left:24px">${renderChildren(node.children)}</${tag}>`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case 'list-item':
|
|
187
|
+
return `<li style="margin:0 0 4px 0;font-size:14px;color:#333">${renderChildren(node.children)}</li>`
|
|
188
|
+
|
|
189
|
+
case 'code':
|
|
190
|
+
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>`
|
|
191
|
+
|
|
192
|
+
case 'divider': {
|
|
193
|
+
const color = (p.color as string) ?? '#dddddd'
|
|
194
|
+
const thickness = (p.thickness as number) ?? 1
|
|
195
|
+
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>`
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'page-break':
|
|
199
|
+
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>`
|
|
200
|
+
|
|
201
|
+
case 'spacer':
|
|
202
|
+
return `<div style="height:${p.height}px;line-height:${p.height}px;font-size:0"> </div>`
|
|
203
|
+
|
|
204
|
+
case 'button': {
|
|
205
|
+
const bg = (p.background as string) ?? '#4f46e5'
|
|
206
|
+
const color = (p.color as string) ?? '#ffffff'
|
|
207
|
+
const radius = (p.borderRadius as number) ?? 4
|
|
208
|
+
const href = esc(p.href as string)
|
|
209
|
+
const text = renderChildren(node.children)
|
|
210
|
+
const align = (p.align as string) ?? 'left'
|
|
211
|
+
|
|
212
|
+
// Bulletproof button — works in Outlook via VML, CSS everywhere else
|
|
213
|
+
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>`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'quote': {
|
|
217
|
+
const borderColor = (p.borderColor as string) ?? '#dddddd'
|
|
218
|
+
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>`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
default:
|
|
222
|
+
return renderChildren(node.children)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const emailRenderer: DocumentRenderer = {
|
|
227
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<string> {
|
|
228
|
+
return renderNode(node)
|
|
229
|
+
},
|
|
230
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Google Chat renderer — outputs Card V2 JSON for Google Chat API.
|
|
11
|
+
* Cards can be sent via webhooks, Chat API, or Apps Script.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
15
|
+
return typeof col === 'string' ? { header: col } : col
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getTextContent(children: DocChild[]): string {
|
|
19
|
+
return children
|
|
20
|
+
.map((c) =>
|
|
21
|
+
typeof c === 'string' ? c : getTextContent((c as DocNode).children),
|
|
22
|
+
)
|
|
23
|
+
.join('')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CardWidget {
|
|
27
|
+
[key: string]: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
31
|
+
const p = node.props
|
|
32
|
+
const widgets: CardWidget[] = []
|
|
33
|
+
|
|
34
|
+
switch (node.type) {
|
|
35
|
+
case 'document':
|
|
36
|
+
case 'page':
|
|
37
|
+
case 'section':
|
|
38
|
+
case 'row':
|
|
39
|
+
case 'column':
|
|
40
|
+
for (const child of node.children) {
|
|
41
|
+
if (typeof child !== 'string') {
|
|
42
|
+
widgets.push(...nodeToWidgets(child))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
case 'heading': {
|
|
48
|
+
const text = getTextContent(node.children)
|
|
49
|
+
widgets.push({
|
|
50
|
+
decoratedText: {
|
|
51
|
+
topLabel: '',
|
|
52
|
+
text: `<b>${text}</b>`,
|
|
53
|
+
wrapText: true,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'text': {
|
|
60
|
+
let text = getTextContent(node.children)
|
|
61
|
+
if (p.bold) text = `<b>${text}</b>`
|
|
62
|
+
if (p.italic) text = `<i>${text}</i>`
|
|
63
|
+
if (p.strikethrough) text = `<s>${text}</s>`
|
|
64
|
+
widgets.push({
|
|
65
|
+
textParagraph: { text },
|
|
66
|
+
})
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
case 'link': {
|
|
71
|
+
const href = p.href as string
|
|
72
|
+
const text = getTextContent(node.children)
|
|
73
|
+
widgets.push({
|
|
74
|
+
textParagraph: { text: `<a href="${href}">${text}</a>` },
|
|
75
|
+
})
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case 'image': {
|
|
80
|
+
const src = p.src as string
|
|
81
|
+
if (src.startsWith('http')) {
|
|
82
|
+
widgets.push({
|
|
83
|
+
image: {
|
|
84
|
+
imageUrl: src,
|
|
85
|
+
altText: (p.alt as string) ?? 'Image',
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'table': {
|
|
93
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
94
|
+
resolveColumn,
|
|
95
|
+
)
|
|
96
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
97
|
+
|
|
98
|
+
// Google Chat Cards don't have native tables — use grid or formatted text
|
|
99
|
+
const header = columns.map((c) => `<b>${c.header}</b>`).join(' | ')
|
|
100
|
+
const body = rows
|
|
101
|
+
.map((row) => row.map((c) => String(c ?? '')).join(' | '))
|
|
102
|
+
.join('\n')
|
|
103
|
+
|
|
104
|
+
widgets.push({
|
|
105
|
+
textParagraph: { text: `${header}\n${body}` },
|
|
106
|
+
})
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'list': {
|
|
111
|
+
const ordered = p.ordered as boolean | undefined
|
|
112
|
+
const items = node.children
|
|
113
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
114
|
+
.map((item, i) => {
|
|
115
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
116
|
+
return `${prefix} ${getTextContent(item.children)}`
|
|
117
|
+
})
|
|
118
|
+
.join('\n')
|
|
119
|
+
widgets.push({
|
|
120
|
+
textParagraph: { text: items },
|
|
121
|
+
})
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'code': {
|
|
126
|
+
const text = getTextContent(node.children)
|
|
127
|
+
widgets.push({
|
|
128
|
+
textParagraph: {
|
|
129
|
+
text: `<font color="#333333"><code>${text}</code></font>`,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'divider':
|
|
136
|
+
case 'page-break':
|
|
137
|
+
widgets.push({ divider: {} })
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
case 'spacer':
|
|
141
|
+
// No direct equivalent — skip
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
case 'button': {
|
|
145
|
+
const href = p.href as string
|
|
146
|
+
const text = getTextContent(node.children)
|
|
147
|
+
widgets.push({
|
|
148
|
+
buttonList: {
|
|
149
|
+
buttons: [
|
|
150
|
+
{
|
|
151
|
+
text,
|
|
152
|
+
onClick: { openLink: { url: href } },
|
|
153
|
+
color: {
|
|
154
|
+
red: 0.31,
|
|
155
|
+
green: 0.27,
|
|
156
|
+
blue: 0.89,
|
|
157
|
+
alpha: 1,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'quote': {
|
|
167
|
+
const text = getTextContent(node.children)
|
|
168
|
+
widgets.push({
|
|
169
|
+
textParagraph: { text: `<i>"${text}"</i>` },
|
|
170
|
+
})
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return widgets
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const googleChatRenderer: DocumentRenderer = {
|
|
179
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<string> {
|
|
180
|
+
const widgets = nodeToWidgets(node)
|
|
181
|
+
|
|
182
|
+
// Extract title from first heading or document title
|
|
183
|
+
let title = (node.props.title as string) ?? ''
|
|
184
|
+
if (!title) {
|
|
185
|
+
const firstHeading = node.children.find(
|
|
186
|
+
(c): c is DocNode => typeof c !== 'string' && c.type === 'heading',
|
|
187
|
+
)
|
|
188
|
+
if (firstHeading) title = getTextContent(firstHeading.children)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const card = {
|
|
192
|
+
cardsV2: [
|
|
193
|
+
{
|
|
194
|
+
cardId: 'document',
|
|
195
|
+
card: {
|
|
196
|
+
header: title
|
|
197
|
+
? { title, subtitle: (node.props.subject as string) ?? undefined }
|
|
198
|
+
: undefined,
|
|
199
|
+
sections: [
|
|
200
|
+
{
|
|
201
|
+
widgets,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return JSON.stringify(card, null, 2)
|
|
210
|
+
},
|
|
211
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
function escapeHtml(str: string): string {
|
|
10
|
+
return str
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
18
|
+
return typeof col === 'string' ? { header: col } : col
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function styleStr(styles: Record<string, string | number | undefined>): string {
|
|
22
|
+
const parts: string[] = []
|
|
23
|
+
for (const [k, v] of Object.entries(styles)) {
|
|
24
|
+
if (v != null && v !== '') {
|
|
25
|
+
const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
26
|
+
parts.push(`${prop}:${typeof v === 'number' ? `${v}px` : v}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return parts.length > 0 ? ` style="${parts.join(';')}"` : ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function padStr(
|
|
33
|
+
pad: number | [number, number] | [number, number, number, number] | undefined,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (pad == null) return undefined
|
|
36
|
+
if (typeof pad === 'number') return `${pad}px`
|
|
37
|
+
if (pad.length === 2) return `${pad[0]}px ${pad[1]}px`
|
|
38
|
+
return `${pad[0]}px ${pad[1]}px ${pad[2]}px ${pad[3]}px`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderChild(child: DocChild): string {
|
|
42
|
+
if (typeof child === 'string') return escapeHtml(child)
|
|
43
|
+
return renderNode(child)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderChildren(children: DocChild[]): string {
|
|
47
|
+
return children.map(renderChild).join('')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderNode(node: DocNode): string {
|
|
51
|
+
const p = node.props
|
|
52
|
+
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case 'document': {
|
|
55
|
+
const lang = (p.language as string) ?? 'en'
|
|
56
|
+
const title = p.title
|
|
57
|
+
? `<title>${escapeHtml(p.title as string)}</title>`
|
|
58
|
+
: ''
|
|
59
|
+
return `<!DOCTYPE html><html lang="${lang}"><head><meta charset="utf-8">${title}<meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderChildren(node.children)}</body></html>`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'page': {
|
|
63
|
+
const margin = padStr(p.margin as PageMargin)
|
|
64
|
+
return `<div${styleStr({ maxWidth: '800px', margin: margin ?? '0 auto', padding: margin ?? '40px' })}>${renderChildren(node.children)}</div>`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'section': {
|
|
68
|
+
const dir = (p.direction as string) ?? 'column'
|
|
69
|
+
return `<div${styleStr({
|
|
70
|
+
display: dir === 'row' ? 'flex' : 'block',
|
|
71
|
+
flexDirection: dir === 'row' ? 'row' : undefined,
|
|
72
|
+
gap: p.gap as number | undefined,
|
|
73
|
+
padding: padStr(p.padding as PageMargin),
|
|
74
|
+
background: p.background as string | undefined,
|
|
75
|
+
borderRadius: p.borderRadius as number | undefined,
|
|
76
|
+
})}>${renderChildren(node.children)}</div>`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case 'row':
|
|
80
|
+
return `<div${styleStr({ display: 'flex', gap: p.gap as number | undefined, alignItems: p.align as string | undefined })}>${renderChildren(node.children)}</div>`
|
|
81
|
+
|
|
82
|
+
case 'column':
|
|
83
|
+
return `<div${styleStr({ flex: p.width ? undefined : '1', width: p.width as string | undefined, textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</div>`
|
|
84
|
+
|
|
85
|
+
case 'heading': {
|
|
86
|
+
const level = (p.level as number) ?? 1
|
|
87
|
+
const tag = `h${Math.min(Math.max(level, 1), 6)}`
|
|
88
|
+
return `<${tag}${styleStr({ color: p.color as string | undefined, textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</${tag}>`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'text': {
|
|
92
|
+
return `<p${styleStr({
|
|
93
|
+
fontSize: p.size as number | undefined,
|
|
94
|
+
color: p.color as string | undefined,
|
|
95
|
+
fontWeight: p.bold ? 'bold' : undefined,
|
|
96
|
+
fontStyle: p.italic ? 'italic' : undefined,
|
|
97
|
+
textDecoration: p.underline
|
|
98
|
+
? 'underline'
|
|
99
|
+
: p.strikethrough
|
|
100
|
+
? 'line-through'
|
|
101
|
+
: undefined,
|
|
102
|
+
textAlign: p.align as string | undefined,
|
|
103
|
+
lineHeight: p.lineHeight as number | undefined,
|
|
104
|
+
})}>${renderChildren(node.children)}</p>`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'link':
|
|
108
|
+
return `<a href="${escapeHtml(p.href as string)}"${styleStr({ color: p.color as string | undefined })}>${renderChildren(node.children)}</a>`
|
|
109
|
+
|
|
110
|
+
case 'image': {
|
|
111
|
+
const alignStyle =
|
|
112
|
+
p.align === 'center'
|
|
113
|
+
? 'display:block;margin:0 auto'
|
|
114
|
+
: p.align === 'right'
|
|
115
|
+
? 'display:block;margin-left:auto'
|
|
116
|
+
: ''
|
|
117
|
+
const img = `<img src="${escapeHtml(p.src as string)}"${p.width ? ` width="${p.width}"` : ''}${p.height ? ` height="${p.height}"` : ''}${p.alt ? ` alt="${escapeHtml(p.alt as string)}"` : ''}${alignStyle ? ` style="${alignStyle}"` : ''} />`
|
|
118
|
+
if (p.caption) {
|
|
119
|
+
return `<figure${p.align === 'center' ? ' style="text-align:center"' : ''}>${img}<figcaption>${escapeHtml(p.caption as string)}</figcaption></figure>`
|
|
120
|
+
}
|
|
121
|
+
return img
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'table': {
|
|
125
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
126
|
+
resolveColumn,
|
|
127
|
+
)
|
|
128
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
129
|
+
const hs = p.headerStyle as
|
|
130
|
+
| { background?: string; color?: string; bold?: boolean }
|
|
131
|
+
| undefined
|
|
132
|
+
const striped = p.striped as boolean | undefined
|
|
133
|
+
const bordered = p.bordered as boolean | undefined
|
|
134
|
+
const borderStyle = bordered
|
|
135
|
+
? 'border:1px solid #ddd;border-collapse:collapse;'
|
|
136
|
+
: 'border-collapse:collapse;'
|
|
137
|
+
|
|
138
|
+
let html = `<table style="width:100%;${borderStyle}">`
|
|
139
|
+
if (p.caption)
|
|
140
|
+
html += `<caption>${escapeHtml(p.caption as string)}</caption>`
|
|
141
|
+
|
|
142
|
+
html += '<thead><tr>'
|
|
143
|
+
for (const col of columns) {
|
|
144
|
+
const cellBorder = bordered ? 'border:1px solid #ddd;' : ''
|
|
145
|
+
const bgStyle = hs?.background ? `background:${hs.background};` : ''
|
|
146
|
+
const colorStyle = hs?.color ? `color:${hs.color};` : ''
|
|
147
|
+
const fontStyle = hs?.bold !== false ? 'font-weight:bold;' : ''
|
|
148
|
+
const alignStyle = col.align ? `text-align:${col.align};` : ''
|
|
149
|
+
const widthStyle = col.width
|
|
150
|
+
? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`
|
|
151
|
+
: ''
|
|
152
|
+
html += `<th style="${cellBorder}${bgStyle}${colorStyle}${fontStyle}${alignStyle}${widthStyle}padding:8px">${escapeHtml(col.header)}</th>`
|
|
153
|
+
}
|
|
154
|
+
html += '</tr></thead>'
|
|
155
|
+
|
|
156
|
+
html += '<tbody>'
|
|
157
|
+
for (let i = 0; i < rows.length; i++) {
|
|
158
|
+
const rowBg =
|
|
159
|
+
striped && i % 2 === 1 ? ' style="background:#f9f9f9"' : ''
|
|
160
|
+
html += `<tr${rowBg}>`
|
|
161
|
+
for (let j = 0; j < columns.length; j++) {
|
|
162
|
+
const cellBorder = bordered ? 'border:1px solid #ddd;' : ''
|
|
163
|
+
const col = columns[j]
|
|
164
|
+
const alignStyle = col?.align ? `text-align:${col.align};` : ''
|
|
165
|
+
html += `<td style="${cellBorder}${alignStyle}padding:8px">${escapeHtml(String(rows[i]?.[j] ?? ''))}</td>`
|
|
166
|
+
}
|
|
167
|
+
html += '</tr>'
|
|
168
|
+
}
|
|
169
|
+
html += '</tbody></table>'
|
|
170
|
+
return html
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'list': {
|
|
174
|
+
const tag = p.ordered ? 'ol' : 'ul'
|
|
175
|
+
return `<${tag}>${renderChildren(node.children)}</${tag}>`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'list-item':
|
|
179
|
+
return `<li>${renderChildren(node.children)}</li>`
|
|
180
|
+
|
|
181
|
+
case 'code':
|
|
182
|
+
return `<pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto"><code>${escapeHtml(renderChildren(node.children))}</code></pre>`
|
|
183
|
+
|
|
184
|
+
case 'divider': {
|
|
185
|
+
const color = (p.color as string) ?? '#ddd'
|
|
186
|
+
const thickness = (p.thickness as number) ?? 1
|
|
187
|
+
return `<hr style="border:none;border-top:${thickness}px solid ${color};margin:16px 0" />`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
case 'page-break':
|
|
191
|
+
return '<div style="page-break-after:always;break-after:page"></div>'
|
|
192
|
+
|
|
193
|
+
case 'spacer':
|
|
194
|
+
return `<div style="height:${p.height}px"></div>`
|
|
195
|
+
|
|
196
|
+
case 'button': {
|
|
197
|
+
const bg = (p.background as string) ?? '#4f46e5'
|
|
198
|
+
const color = (p.color as string) ?? '#fff'
|
|
199
|
+
const radius = (p.borderRadius as number) ?? 4
|
|
200
|
+
const pad = padStr((p.padding ?? [12, 24]) as [number, number])
|
|
201
|
+
const align = (p.align as string) ?? 'left'
|
|
202
|
+
return `<div style="text-align:${align}"><a href="${escapeHtml(p.href as string)}" style="display:inline-block;background:${bg};color:${color};padding:${pad};border-radius:${radius}px;text-decoration:none;font-weight:bold">${renderChildren(node.children)}</a></div>`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'quote': {
|
|
206
|
+
const borderColor = (p.borderColor as string) ?? '#ddd'
|
|
207
|
+
return `<blockquote style="margin:0;padding:12px 20px;border-left:4px solid ${borderColor};color:#555">${renderChildren(node.children)}</blockquote>`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
return renderChildren(node.children)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
type PageMargin = number | [number, number] | [number, number, number, number]
|
|
216
|
+
|
|
217
|
+
export const htmlRenderer: DocumentRenderer = {
|
|
218
|
+
async render(node: DocNode, options?: RenderOptions): Promise<string> {
|
|
219
|
+
let html = renderNode(node)
|
|
220
|
+
if (options?.direction === 'rtl') {
|
|
221
|
+
html = html.replace('<body>', '<body dir="rtl" style="direction:rtl">')
|
|
222
|
+
}
|
|
223
|
+
return html
|
|
224
|
+
},
|
|
225
|
+
}
|