@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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/lib/analysis/index.js.html +5406 -0
  3. package/lib/chunk-ErZ26oRB.js +48 -0
  4. package/lib/confluence-Va8e7RxQ.js +192 -0
  5. package/lib/confluence-Va8e7RxQ.js.map +1 -0
  6. package/lib/csv-2c38ub-Y.js +32 -0
  7. package/lib/csv-2c38ub-Y.js.map +1 -0
  8. package/lib/discord-DAoUZqvE.js +134 -0
  9. package/lib/discord-DAoUZqvE.js.map +1 -0
  10. package/lib/dist-BsqdI2nY.js +20179 -0
  11. package/lib/dist-BsqdI2nY.js.map +1 -0
  12. package/lib/docx-CorFwEH9.js +450 -0
  13. package/lib/docx-CorFwEH9.js.map +1 -0
  14. package/lib/email-Bn_Brjdp.js +131 -0
  15. package/lib/email-Bn_Brjdp.js.map +1 -0
  16. package/lib/exceljs-BoIDUUaw.js +34377 -0
  17. package/lib/exceljs-BoIDUUaw.js.map +1 -0
  18. package/lib/google-chat-B6I017I1.js +125 -0
  19. package/lib/google-chat-B6I017I1.js.map +1 -0
  20. package/lib/html-De_iS_f0.js +151 -0
  21. package/lib/html-De_iS_f0.js.map +1 -0
  22. package/lib/index.js +619 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/markdown-BYC_3C9i.js +75 -0
  25. package/lib/markdown-BYC_3C9i.js.map +1 -0
  26. package/lib/notion-DHaQHO6P.js +187 -0
  27. package/lib/notion-DHaQHO6P.js.map +1 -0
  28. package/lib/pdf-CDPc5Itc.js +419 -0
  29. package/lib/pdf-CDPc5Itc.js.map +1 -0
  30. package/lib/pdfmake-DnmLxK4Q.js +55511 -0
  31. package/lib/pdfmake-DnmLxK4Q.js.map +1 -0
  32. package/lib/pptx-DKQU6bjq.js +252 -0
  33. package/lib/pptx-DKQU6bjq.js.map +1 -0
  34. package/lib/pptxgen.es-COcgXsyx.js +5697 -0
  35. package/lib/pptxgen.es-COcgXsyx.js.map +1 -0
  36. package/lib/slack-CJRJgkag.js +139 -0
  37. package/lib/slack-CJRJgkag.js.map +1 -0
  38. package/lib/svg-BM8biZmL.js +187 -0
  39. package/lib/svg-BM8biZmL.js.map +1 -0
  40. package/lib/teams-S99tonRG.js +176 -0
  41. package/lib/teams-S99tonRG.js.map +1 -0
  42. package/lib/telegram-CbEO_2PN.js +77 -0
  43. package/lib/telegram-CbEO_2PN.js.map +1 -0
  44. package/lib/text-B5U8ucRr.js +75 -0
  45. package/lib/text-B5U8ucRr.js.map +1 -0
  46. package/lib/types/index.d.ts +528 -0
  47. package/lib/types/index.d.ts.map +1 -0
  48. package/lib/vfs_fonts-Df1kkZ4Y.js +19 -0
  49. package/lib/vfs_fonts-Df1kkZ4Y.js.map +1 -0
  50. package/lib/whatsapp-DJ2D1jGG.js +64 -0
  51. package/lib/whatsapp-DJ2D1jGG.js.map +1 -0
  52. package/lib/xlsx-D47x-gZ5.js +199 -0
  53. package/lib/xlsx-D47x-gZ5.js.map +1 -0
  54. package/package.json +62 -0
  55. package/src/builder.ts +266 -0
  56. package/src/download.ts +76 -0
  57. package/src/env.d.ts +17 -0
  58. package/src/index.ts +98 -0
  59. package/src/nodes.ts +315 -0
  60. package/src/render.ts +222 -0
  61. package/src/renderers/confluence.ts +231 -0
  62. package/src/renderers/csv.ts +67 -0
  63. package/src/renderers/discord.ts +192 -0
  64. package/src/renderers/docx.ts +612 -0
  65. package/src/renderers/email.ts +230 -0
  66. package/src/renderers/google-chat.ts +211 -0
  67. package/src/renderers/html.ts +225 -0
  68. package/src/renderers/markdown.ts +144 -0
  69. package/src/renderers/notion.ts +264 -0
  70. package/src/renderers/pdf.ts +427 -0
  71. package/src/renderers/pptx.ts +353 -0
  72. package/src/renderers/slack.ts +192 -0
  73. package/src/renderers/svg.ts +254 -0
  74. package/src/renderers/teams.ts +234 -0
  75. package/src/renderers/telegram.ts +137 -0
  76. package/src/renderers/text.ts +154 -0
  77. package/src/renderers/whatsapp.ts +121 -0
  78. package/src/renderers/xlsx.ts +342 -0
  79. package/src/tests/document.test.ts +2920 -0
  80. 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, '&lt;')
23
+ .replace(/>/g, '&gt;')
24
+ .replace(/"/g, '&quot;')
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, '&amp;')
22
+ .replace(/</g, '&lt;')
23
+ .replace(/>/g, '&gt;')
24
+ .replace(/"/g, '&quot;')
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
+ }