@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,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SVG renderer — generates a standalone SVG document from the node tree.
|
|
11
|
+
* Useful for thumbnails, social cards, and preview images.
|
|
12
|
+
* No external dependencies — pure SVG string generation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
16
|
+
return typeof col === 'string' ? { header: col } : col
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function escapeXml(str: string): string {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTextContent(children: DocChild[]): string {
|
|
28
|
+
return children
|
|
29
|
+
.map((c) =>
|
|
30
|
+
typeof c === 'string' ? c : getTextContent((c as DocNode).children),
|
|
31
|
+
)
|
|
32
|
+
.join('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RenderContext {
|
|
36
|
+
y: number
|
|
37
|
+
width: number
|
|
38
|
+
padding: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderNode(node: DocNode, ctx: RenderContext): string {
|
|
42
|
+
const p = node.props
|
|
43
|
+
const contentWidth = ctx.width - ctx.padding * 2
|
|
44
|
+
let svg = ''
|
|
45
|
+
|
|
46
|
+
switch (node.type) {
|
|
47
|
+
case 'document':
|
|
48
|
+
case 'page':
|
|
49
|
+
case 'section':
|
|
50
|
+
case 'row':
|
|
51
|
+
case 'column':
|
|
52
|
+
for (const child of node.children) {
|
|
53
|
+
if (typeof child !== 'string') {
|
|
54
|
+
svg += renderNode(child, ctx)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
case 'heading': {
|
|
60
|
+
const level = (p.level as number) ?? 1
|
|
61
|
+
const sizes: Record<number, number> = {
|
|
62
|
+
1: 28,
|
|
63
|
+
2: 24,
|
|
64
|
+
3: 20,
|
|
65
|
+
4: 18,
|
|
66
|
+
5: 16,
|
|
67
|
+
6: 14,
|
|
68
|
+
}
|
|
69
|
+
const size = sizes[level] ?? 24
|
|
70
|
+
const color = (p.color as string) ?? '#000000'
|
|
71
|
+
const text = escapeXml(getTextContent(node.children))
|
|
72
|
+
ctx.y += size + 8
|
|
73
|
+
svg += `<text x="${ctx.padding}" y="${ctx.y}" font-size="${size}" font-weight="bold" fill="${color}" font-family="system-ui, -apple-system, sans-serif">${text}</text>`
|
|
74
|
+
ctx.y += 12
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'text': {
|
|
79
|
+
const size = (p.size as number) ?? 14
|
|
80
|
+
const color = (p.color as string) ?? '#333333'
|
|
81
|
+
const weight = p.bold ? 'bold' : 'normal'
|
|
82
|
+
const style = p.italic ? 'italic' : 'normal'
|
|
83
|
+
const text = escapeXml(getTextContent(node.children))
|
|
84
|
+
ctx.y += size + 4
|
|
85
|
+
svg += `<text x="${ctx.padding}" y="${ctx.y}" font-size="${size}" font-weight="${weight}" font-style="${style}" fill="${color}" font-family="system-ui, -apple-system, sans-serif">${text}</text>`
|
|
86
|
+
ctx.y += 10
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'link': {
|
|
91
|
+
const href = p.href as string
|
|
92
|
+
const text = escapeXml(getTextContent(node.children))
|
|
93
|
+
const color = (p.color as string) ?? '#4f46e5'
|
|
94
|
+
ctx.y += 18
|
|
95
|
+
svg += `<a href="${escapeXml(href)}"><text x="${ctx.padding}" y="${ctx.y}" font-size="14" fill="${color}" text-decoration="underline" font-family="system-ui, -apple-system, sans-serif">${text}</text></a>`
|
|
96
|
+
ctx.y += 10
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'image': {
|
|
101
|
+
const width = (p.width as number) ?? Math.min(contentWidth, 400)
|
|
102
|
+
const height = (p.height as number) ?? 200
|
|
103
|
+
const src = p.src as string
|
|
104
|
+
|
|
105
|
+
if (src.startsWith('data:') || src.startsWith('http')) {
|
|
106
|
+
svg += `<image x="${ctx.padding}" y="${ctx.y}" width="${width}" height="${height}" href="${escapeXml(src)}" />`
|
|
107
|
+
} else {
|
|
108
|
+
// Placeholder rectangle for local paths
|
|
109
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${width}" height="${height}" fill="#f0f0f0" stroke="#ddd" rx="4" />`
|
|
110
|
+
svg += `<text x="${ctx.padding + width / 2}" y="${ctx.y + height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999" font-family="system-ui, sans-serif">${escapeXml((p.alt as string) ?? 'Image')}</text>`
|
|
111
|
+
}
|
|
112
|
+
ctx.y += height + 8
|
|
113
|
+
|
|
114
|
+
if (p.caption) {
|
|
115
|
+
ctx.y += 14
|
|
116
|
+
svg += `<text x="${ctx.padding}" y="${ctx.y}" font-size="12" fill="#666" font-style="italic" font-family="system-ui, sans-serif">${escapeXml(p.caption as string)}</text>`
|
|
117
|
+
ctx.y += 8
|
|
118
|
+
}
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'table': {
|
|
123
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
124
|
+
resolveColumn,
|
|
125
|
+
)
|
|
126
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
127
|
+
const hs = p.headerStyle as
|
|
128
|
+
| { background?: string; color?: string }
|
|
129
|
+
| undefined
|
|
130
|
+
const striped = p.striped as boolean | undefined
|
|
131
|
+
|
|
132
|
+
const colWidth = contentWidth / columns.length
|
|
133
|
+
const rowHeight = 28
|
|
134
|
+
const headerBg = hs?.background ?? '#f5f5f5'
|
|
135
|
+
const headerColor = hs?.color ?? '#000000'
|
|
136
|
+
|
|
137
|
+
// Header
|
|
138
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${contentWidth}" height="${rowHeight}" fill="${headerBg}" />`
|
|
139
|
+
for (let i = 0; i < columns.length; i++) {
|
|
140
|
+
const col = columns[i]
|
|
141
|
+
if (!col) continue
|
|
142
|
+
svg += `<text x="${ctx.padding + i * colWidth + 8}" y="${ctx.y + 18}" font-size="12" font-weight="bold" fill="${headerColor}" font-family="system-ui, sans-serif">${escapeXml(col.header)}</text>`
|
|
143
|
+
}
|
|
144
|
+
ctx.y += rowHeight
|
|
145
|
+
|
|
146
|
+
// Rows
|
|
147
|
+
for (let r = 0; r < rows.length; r++) {
|
|
148
|
+
if (striped && r % 2 === 1) {
|
|
149
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${contentWidth}" height="${rowHeight}" fill="#f9f9f9" />`
|
|
150
|
+
}
|
|
151
|
+
for (let c = 0; c < columns.length; c++) {
|
|
152
|
+
svg += `<text x="${ctx.padding + c * colWidth + 8}" y="${ctx.y + 18}" font-size="12" fill="#333" font-family="system-ui, sans-serif">${escapeXml(String(rows[r]?.[c] ?? ''))}</text>`
|
|
153
|
+
}
|
|
154
|
+
ctx.y += rowHeight
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Bottom border
|
|
158
|
+
svg += `<line x1="${ctx.padding}" y1="${ctx.y}" x2="${ctx.padding + contentWidth}" y2="${ctx.y}" stroke="#ddd" stroke-width="1" />`
|
|
159
|
+
ctx.y += 12
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'list': {
|
|
164
|
+
const ordered = p.ordered as boolean | undefined
|
|
165
|
+
const items = node.children.filter(
|
|
166
|
+
(c): c is DocNode => typeof c !== 'string',
|
|
167
|
+
)
|
|
168
|
+
for (let i = 0; i < items.length; i++) {
|
|
169
|
+
const item = items[i]
|
|
170
|
+
if (!item) continue
|
|
171
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
172
|
+
const text = escapeXml(getTextContent(item.children))
|
|
173
|
+
ctx.y += 18
|
|
174
|
+
svg += `<text x="${ctx.padding + 16}" y="${ctx.y}" font-size="13" fill="#333" font-family="system-ui, sans-serif">${prefix} ${text}</text>`
|
|
175
|
+
}
|
|
176
|
+
ctx.y += 10
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'code': {
|
|
181
|
+
const text = getTextContent(node.children)
|
|
182
|
+
const lines = text.split('\n')
|
|
183
|
+
const codeHeight = lines.length * 18 + 16
|
|
184
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${contentWidth}" height="${codeHeight}" fill="#f5f5f5" rx="4" />`
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
svg += `<text x="${ctx.padding + 12}" y="${ctx.y + 20 + i * 18}" font-size="12" fill="#333" font-family="monospace">${escapeXml(lines[i] ?? '')}</text>`
|
|
187
|
+
}
|
|
188
|
+
ctx.y += codeHeight + 8
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'divider': {
|
|
193
|
+
const color = (p.color as string) ?? '#ddd'
|
|
194
|
+
const thickness = (p.thickness as number) ?? 1
|
|
195
|
+
ctx.y += 12
|
|
196
|
+
svg += `<line x1="${ctx.padding}" y1="${ctx.y}" x2="${ctx.padding + contentWidth}" y2="${ctx.y}" stroke="${color}" stroke-width="${thickness}" />`
|
|
197
|
+
ctx.y += 12
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'page-break':
|
|
202
|
+
ctx.y += 16
|
|
203
|
+
svg += `<line x1="${ctx.padding}" y1="${ctx.y}" x2="${ctx.padding + contentWidth}" y2="${ctx.y}" stroke="#ccc" stroke-width="2" stroke-dasharray="8,4" />`
|
|
204
|
+
ctx.y += 16
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
case 'spacer':
|
|
208
|
+
ctx.y += (p.height as number) ?? 12
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
case 'button': {
|
|
212
|
+
const bg = (p.background as string) ?? '#4f46e5'
|
|
213
|
+
const color = (p.color as string) ?? '#ffffff'
|
|
214
|
+
const text = escapeXml(getTextContent(node.children))
|
|
215
|
+
const btnWidth = Math.min(text.length * 10 + 48, contentWidth)
|
|
216
|
+
const btnHeight = 40
|
|
217
|
+
ctx.y += 8
|
|
218
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${btnWidth}" height="${btnHeight}" fill="${bg}" rx="4" />`
|
|
219
|
+
svg += `<text x="${ctx.padding + btnWidth / 2}" y="${ctx.y + 25}" text-anchor="middle" font-size="14" font-weight="bold" fill="${color}" font-family="system-ui, sans-serif">${text}</text>`
|
|
220
|
+
ctx.y += btnHeight + 12
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'quote': {
|
|
225
|
+
const borderColor = (p.borderColor as string) ?? '#ddd'
|
|
226
|
+
const text = escapeXml(getTextContent(node.children))
|
|
227
|
+
ctx.y += 4
|
|
228
|
+
svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="4" height="20" fill="${borderColor}" />`
|
|
229
|
+
svg += `<text x="${ctx.padding + 16}" y="${ctx.y + 15}" font-size="13" fill="#555" font-style="italic" font-family="system-ui, sans-serif">${text}</text>`
|
|
230
|
+
ctx.y += 28
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return svg
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export const svgRenderer: DocumentRenderer = {
|
|
239
|
+
async render(node: DocNode, options?: RenderOptions): Promise<string> {
|
|
240
|
+
const width = 800
|
|
241
|
+
const padding = 40
|
|
242
|
+
const ctx: RenderContext = { y: padding, width, padding }
|
|
243
|
+
|
|
244
|
+
const content = renderNode(node, ctx)
|
|
245
|
+
const height = ctx.y + padding
|
|
246
|
+
|
|
247
|
+
const dir = options?.direction === 'rtl' ? ' direction="rtl"' : ''
|
|
248
|
+
|
|
249
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"${dir}>
|
|
250
|
+
<rect width="${width}" height="${height}" fill="#ffffff" />
|
|
251
|
+
${content}
|
|
252
|
+
</svg>`
|
|
253
|
+
},
|
|
254
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Microsoft Teams renderer — outputs Adaptive Cards JSON.
|
|
11
|
+
* Can be posted via Teams Webhooks, Bot Framework, or Power Automate.
|
|
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 AdaptiveElement {
|
|
27
|
+
type: string
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function nodeToElements(node: DocNode): AdaptiveElement[] {
|
|
32
|
+
const p = node.props
|
|
33
|
+
const elements: AdaptiveElement[] = []
|
|
34
|
+
|
|
35
|
+
switch (node.type) {
|
|
36
|
+
case 'document':
|
|
37
|
+
case 'page':
|
|
38
|
+
case 'section':
|
|
39
|
+
case 'row':
|
|
40
|
+
case 'column':
|
|
41
|
+
for (const child of node.children) {
|
|
42
|
+
if (typeof child !== 'string') {
|
|
43
|
+
elements.push(...nodeToElements(child))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
case 'heading': {
|
|
49
|
+
const level = (p.level as number) ?? 1
|
|
50
|
+
const sizeMap: Record<number, string> = {
|
|
51
|
+
1: 'extraLarge',
|
|
52
|
+
2: 'large',
|
|
53
|
+
3: 'medium',
|
|
54
|
+
4: 'default',
|
|
55
|
+
5: 'small',
|
|
56
|
+
6: 'small',
|
|
57
|
+
}
|
|
58
|
+
elements.push({
|
|
59
|
+
type: 'TextBlock',
|
|
60
|
+
text: getTextContent(node.children),
|
|
61
|
+
size: sizeMap[level] ?? 'large',
|
|
62
|
+
weight: 'bolder',
|
|
63
|
+
wrap: true,
|
|
64
|
+
})
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'text': {
|
|
69
|
+
let text = getTextContent(node.children)
|
|
70
|
+
if (p.bold) text = `**${text}**`
|
|
71
|
+
if (p.italic) text = `_${text}_`
|
|
72
|
+
if (p.strikethrough) text = `~~${text}~~`
|
|
73
|
+
elements.push({
|
|
74
|
+
type: 'TextBlock',
|
|
75
|
+
text,
|
|
76
|
+
wrap: true,
|
|
77
|
+
...(p.color ? { color: 'default' } : {}),
|
|
78
|
+
...(p.size
|
|
79
|
+
? { size: (p.size as number) >= 18 ? 'large' : 'default' }
|
|
80
|
+
: {}),
|
|
81
|
+
})
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'link': {
|
|
86
|
+
const href = p.href as string
|
|
87
|
+
const text = getTextContent(node.children)
|
|
88
|
+
elements.push({
|
|
89
|
+
type: 'TextBlock',
|
|
90
|
+
text: `[${text}](${href})`,
|
|
91
|
+
wrap: true,
|
|
92
|
+
})
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'image': {
|
|
97
|
+
const src = p.src as string
|
|
98
|
+
if (src.startsWith('http')) {
|
|
99
|
+
elements.push({
|
|
100
|
+
type: 'Image',
|
|
101
|
+
url: src,
|
|
102
|
+
altText: (p.alt as string) ?? 'Image',
|
|
103
|
+
size: 'large',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'table': {
|
|
110
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
111
|
+
resolveColumn,
|
|
112
|
+
)
|
|
113
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
114
|
+
|
|
115
|
+
// Adaptive Cards have native Table support (schema 1.5+)
|
|
116
|
+
const tableColumns = columns.map((col) => ({
|
|
117
|
+
type: 'Column',
|
|
118
|
+
width: 'stretch',
|
|
119
|
+
items: [
|
|
120
|
+
{
|
|
121
|
+
type: 'TextBlock',
|
|
122
|
+
text: `**${col.header}**`,
|
|
123
|
+
weight: 'bolder',
|
|
124
|
+
wrap: true,
|
|
125
|
+
},
|
|
126
|
+
...rows.map((row, i) => ({
|
|
127
|
+
type: 'TextBlock',
|
|
128
|
+
text: String(row[columns.indexOf(col)] ?? ''),
|
|
129
|
+
wrap: true,
|
|
130
|
+
separator: i === 0,
|
|
131
|
+
})),
|
|
132
|
+
],
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
elements.push({
|
|
136
|
+
type: 'ColumnSet',
|
|
137
|
+
columns: tableColumns,
|
|
138
|
+
})
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'list': {
|
|
143
|
+
const ordered = p.ordered as boolean | undefined
|
|
144
|
+
const items = node.children
|
|
145
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
146
|
+
.map((item, i) => {
|
|
147
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
148
|
+
return `${prefix} ${getTextContent(item.children)}`
|
|
149
|
+
})
|
|
150
|
+
.join('\n')
|
|
151
|
+
elements.push({
|
|
152
|
+
type: 'TextBlock',
|
|
153
|
+
text: items,
|
|
154
|
+
wrap: true,
|
|
155
|
+
})
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'code': {
|
|
160
|
+
const text = getTextContent(node.children)
|
|
161
|
+
elements.push({
|
|
162
|
+
type: 'TextBlock',
|
|
163
|
+
text: `\`\`\`\n${text}\n\`\`\``,
|
|
164
|
+
fontType: 'monospace',
|
|
165
|
+
wrap: true,
|
|
166
|
+
})
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case 'divider':
|
|
171
|
+
case 'page-break':
|
|
172
|
+
elements.push({
|
|
173
|
+
type: 'TextBlock',
|
|
174
|
+
text: ' ',
|
|
175
|
+
separator: true,
|
|
176
|
+
})
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
case 'spacer':
|
|
180
|
+
elements.push({
|
|
181
|
+
type: 'TextBlock',
|
|
182
|
+
text: ' ',
|
|
183
|
+
spacing: 'large',
|
|
184
|
+
})
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
case 'button': {
|
|
188
|
+
elements.push({
|
|
189
|
+
type: 'ActionSet',
|
|
190
|
+
actions: [
|
|
191
|
+
{
|
|
192
|
+
type: 'Action.OpenUrl',
|
|
193
|
+
title: getTextContent(node.children),
|
|
194
|
+
url: p.href as string,
|
|
195
|
+
style: 'positive',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'quote': {
|
|
203
|
+
const text = getTextContent(node.children)
|
|
204
|
+
elements.push({
|
|
205
|
+
type: 'Container',
|
|
206
|
+
style: 'emphasis',
|
|
207
|
+
items: [
|
|
208
|
+
{
|
|
209
|
+
type: 'TextBlock',
|
|
210
|
+
text: `_${text}_`,
|
|
211
|
+
wrap: true,
|
|
212
|
+
isSubtle: true,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
})
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return elements
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const teamsRenderer: DocumentRenderer = {
|
|
224
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<string> {
|
|
225
|
+
const body = nodeToElements(node)
|
|
226
|
+
const card = {
|
|
227
|
+
type: 'AdaptiveCard',
|
|
228
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
229
|
+
version: '1.5',
|
|
230
|
+
body,
|
|
231
|
+
}
|
|
232
|
+
return JSON.stringify(card, null, 2)
|
|
233
|
+
},
|
|
234
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Telegram renderer — outputs HTML using Telegram's supported subset.
|
|
11
|
+
* Telegram Bot API supports: <b>, <i>, <u>, <s>, <a>, <code>, <pre>, <blockquote>.
|
|
12
|
+
* No tables, no images inline — images sent separately via sendPhoto.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
16
|
+
return typeof col === 'string' ? { header: col } : col
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function esc(str: string): string {
|
|
20
|
+
return str
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTextContent(children: DocChild[]): string {
|
|
28
|
+
return children
|
|
29
|
+
.map((c) =>
|
|
30
|
+
typeof c === 'string' ? c : getTextContent((c as DocNode).children),
|
|
31
|
+
)
|
|
32
|
+
.join('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderNode(node: DocNode): string {
|
|
36
|
+
const p = node.props
|
|
37
|
+
|
|
38
|
+
switch (node.type) {
|
|
39
|
+
case 'document':
|
|
40
|
+
case 'page':
|
|
41
|
+
case 'section':
|
|
42
|
+
case 'row':
|
|
43
|
+
case 'column':
|
|
44
|
+
return node.children
|
|
45
|
+
.map((c) => (typeof c === 'string' ? esc(c) : renderNode(c)))
|
|
46
|
+
.join('')
|
|
47
|
+
|
|
48
|
+
case 'heading': {
|
|
49
|
+
const text = esc(getTextContent(node.children))
|
|
50
|
+
return `<b>${text}</b>\n\n`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'text': {
|
|
54
|
+
let text = esc(getTextContent(node.children))
|
|
55
|
+
if (p.bold) text = `<b>${text}</b>`
|
|
56
|
+
if (p.italic) text = `<i>${text}</i>`
|
|
57
|
+
if (p.underline) text = `<u>${text}</u>`
|
|
58
|
+
if (p.strikethrough) text = `<s>${text}</s>`
|
|
59
|
+
return `${text}\n\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
case 'link': {
|
|
63
|
+
const href = p.href as string
|
|
64
|
+
const text = esc(getTextContent(node.children))
|
|
65
|
+
return `<a href="${esc(href)}">${text}</a>\n\n`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'image':
|
|
69
|
+
// Telegram doesn't support inline images in HTML
|
|
70
|
+
// Images need to be sent separately via sendPhoto
|
|
71
|
+
return ''
|
|
72
|
+
|
|
73
|
+
case 'table': {
|
|
74
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
75
|
+
resolveColumn,
|
|
76
|
+
)
|
|
77
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
78
|
+
|
|
79
|
+
// Render as preformatted text since Telegram has no table support
|
|
80
|
+
const header = columns.map((c) => c.header).join(' | ')
|
|
81
|
+
const separator = columns.map(() => '---').join('-+-')
|
|
82
|
+
const body = rows
|
|
83
|
+
.map((row) => row.map((c) => String(c ?? '')).join(' | '))
|
|
84
|
+
.join('\n')
|
|
85
|
+
|
|
86
|
+
return `<pre>${esc(header)}\n${esc(separator)}\n${esc(body)}</pre>\n\n`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case 'list': {
|
|
90
|
+
const ordered = p.ordered as boolean | undefined
|
|
91
|
+
const items = node.children
|
|
92
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
93
|
+
.map((item, i) => {
|
|
94
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
95
|
+
return `${prefix} ${esc(getTextContent(item.children))}`
|
|
96
|
+
})
|
|
97
|
+
.join('\n')
|
|
98
|
+
return `${items}\n\n`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'code': {
|
|
102
|
+
const lang = (p.language as string) ?? ''
|
|
103
|
+
const text = esc(getTextContent(node.children))
|
|
104
|
+
if (lang) {
|
|
105
|
+
return `<pre><code class="language-${esc(lang)}">${text}</code></pre>\n\n`
|
|
106
|
+
}
|
|
107
|
+
return `<pre>${text}</pre>\n\n`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'divider':
|
|
111
|
+
case 'page-break':
|
|
112
|
+
return '───────────\n\n'
|
|
113
|
+
|
|
114
|
+
case 'spacer':
|
|
115
|
+
return '\n'
|
|
116
|
+
|
|
117
|
+
case 'button': {
|
|
118
|
+
const href = p.href as string
|
|
119
|
+
const text = esc(getTextContent(node.children))
|
|
120
|
+
return `<a href="${esc(href)}">${text}</a>\n\n`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'quote': {
|
|
124
|
+
const text = esc(getTextContent(node.children))
|
|
125
|
+
return `<blockquote>${text}</blockquote>\n\n`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
return ''
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const telegramRenderer: DocumentRenderer = {
|
|
134
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<string> {
|
|
135
|
+
return renderNode(node).trim()
|
|
136
|
+
},
|
|
137
|
+
}
|