@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,353 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PPTX renderer — lazy-loads pptxgenjs on first use.
|
|
11
|
+
* Each `<Page>` becomes a slide. Document nodes map to PPTX elements.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { render, Document, Page, Heading, Text } from '@pyreon/document'
|
|
16
|
+
*
|
|
17
|
+
* const doc = Document({
|
|
18
|
+
* title: 'Presentation',
|
|
19
|
+
* children: [
|
|
20
|
+
* Page({ children: [Heading({ children: 'Slide 1' }), Text({ children: 'Hello' })] }),
|
|
21
|
+
* Page({ children: [Heading({ children: 'Slide 2' })] }),
|
|
22
|
+
* ],
|
|
23
|
+
* })
|
|
24
|
+
* const pptx = await render(doc, 'pptx') // → Uint8Array
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
29
|
+
return typeof col === 'string' ? { header: col } : col
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getTextContent(children: DocChild[]): string {
|
|
33
|
+
return children
|
|
34
|
+
.map((c) =>
|
|
35
|
+
typeof c === 'string' ? c : getTextContent((c as DocNode).children),
|
|
36
|
+
)
|
|
37
|
+
.join('')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Vertical position tracker for placing elements on a slide. */
|
|
41
|
+
interface SlideContext {
|
|
42
|
+
slide: PptxSlide
|
|
43
|
+
y: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Duck-typed pptxgenjs interfaces to avoid hard dependency on types
|
|
47
|
+
interface PptxSlide {
|
|
48
|
+
addText(text: string | PptxTextProps[], opts?: Record<string, unknown>): void
|
|
49
|
+
addImage(opts: Record<string, unknown>): void
|
|
50
|
+
addTable(rows: unknown[][], opts?: Record<string, unknown>): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PptxTextProps {
|
|
54
|
+
text: string
|
|
55
|
+
options?: Record<string, unknown>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface PptxGen {
|
|
59
|
+
addSlide(): PptxSlide
|
|
60
|
+
write(outputType: string): Promise<unknown>
|
|
61
|
+
title: string
|
|
62
|
+
author: string
|
|
63
|
+
subject: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const HEADING_SIZES: Record<number, number> = {
|
|
67
|
+
1: 28,
|
|
68
|
+
2: 24,
|
|
69
|
+
3: 20,
|
|
70
|
+
4: 18,
|
|
71
|
+
5: 16,
|
|
72
|
+
6: 14,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const SLIDE_WIDTH = 10 // inches
|
|
76
|
+
const CONTENT_MARGIN = 0.5
|
|
77
|
+
const CONTENT_WIDTH = SLIDE_WIDTH - CONTENT_MARGIN * 2
|
|
78
|
+
|
|
79
|
+
function processNode(node: DocNode, ctx: SlideContext): void {
|
|
80
|
+
const p = node.props
|
|
81
|
+
|
|
82
|
+
switch (node.type) {
|
|
83
|
+
case 'heading': {
|
|
84
|
+
const level = (p.level as number) ?? 1
|
|
85
|
+
const fontSize = HEADING_SIZES[level] ?? 20
|
|
86
|
+
ctx.slide.addText(getTextContent(node.children), {
|
|
87
|
+
x: CONTENT_MARGIN,
|
|
88
|
+
y: ctx.y,
|
|
89
|
+
w: CONTENT_WIDTH,
|
|
90
|
+
h: 0.6,
|
|
91
|
+
fontSize,
|
|
92
|
+
bold: true,
|
|
93
|
+
color: ((p.color as string) ?? '#000000').replace('#', ''),
|
|
94
|
+
align: (p.align as string) ?? 'left',
|
|
95
|
+
})
|
|
96
|
+
ctx.y += 0.7
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'text': {
|
|
101
|
+
const text = getTextContent(node.children)
|
|
102
|
+
ctx.slide.addText(text, {
|
|
103
|
+
x: CONTENT_MARGIN,
|
|
104
|
+
y: ctx.y,
|
|
105
|
+
w: CONTENT_WIDTH,
|
|
106
|
+
h: 0.4,
|
|
107
|
+
fontSize: (p.size as number) ?? 14,
|
|
108
|
+
bold: p.bold ?? false,
|
|
109
|
+
italic: p.italic ?? false,
|
|
110
|
+
underline: p.underline ? { style: 'sng' } : undefined,
|
|
111
|
+
strike: p.strikethrough ? 'sngStrike' : undefined,
|
|
112
|
+
color: ((p.color as string) ?? '#333333').replace('#', ''),
|
|
113
|
+
align: (p.align as string) ?? 'left',
|
|
114
|
+
})
|
|
115
|
+
ctx.y += 0.5
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case 'image': {
|
|
120
|
+
const src = p.src as string
|
|
121
|
+
const w = Math.min(((p.width as number) ?? 400) / 96, CONTENT_WIDTH)
|
|
122
|
+
const h = ((p.height as number) ?? 300) / 96
|
|
123
|
+
|
|
124
|
+
if (src.startsWith('data:')) {
|
|
125
|
+
ctx.slide.addImage({
|
|
126
|
+
data: src,
|
|
127
|
+
x: CONTENT_MARGIN,
|
|
128
|
+
y: ctx.y,
|
|
129
|
+
w,
|
|
130
|
+
h,
|
|
131
|
+
})
|
|
132
|
+
ctx.y += h + 0.2
|
|
133
|
+
}
|
|
134
|
+
// HTTP URLs and local paths are not supported — skip silently
|
|
135
|
+
break
|
|
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 }
|
|
145
|
+
| undefined
|
|
146
|
+
|
|
147
|
+
const headerRow = columns.map((col) => ({
|
|
148
|
+
text: col.header,
|
|
149
|
+
options: {
|
|
150
|
+
bold: true,
|
|
151
|
+
fill: { color: (hs?.background ?? '#f5f5f5').replace('#', '') },
|
|
152
|
+
color: (hs?.color ?? '#000000').replace('#', ''),
|
|
153
|
+
align: col.align ?? 'left',
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
},
|
|
156
|
+
}))
|
|
157
|
+
|
|
158
|
+
const dataRows = rows.map((row, rowIdx) =>
|
|
159
|
+
columns.map((col, colIdx) => ({
|
|
160
|
+
text: String(row[colIdx] ?? ''),
|
|
161
|
+
options: {
|
|
162
|
+
align: col.align ?? 'left',
|
|
163
|
+
fontSize: 11,
|
|
164
|
+
fill:
|
|
165
|
+
p.striped && rowIdx % 2 === 1 ? { color: 'F9F9F9' } : undefined,
|
|
166
|
+
},
|
|
167
|
+
})),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const allRows = [headerRow, ...dataRows]
|
|
171
|
+
const rowHeight = 0.35
|
|
172
|
+
const tableHeight = allRows.length * rowHeight
|
|
173
|
+
|
|
174
|
+
ctx.slide.addTable(allRows, {
|
|
175
|
+
x: CONTENT_MARGIN,
|
|
176
|
+
y: ctx.y,
|
|
177
|
+
w: CONTENT_WIDTH,
|
|
178
|
+
border: { pt: 0.5, color: 'DDDDDD' },
|
|
179
|
+
rowH: rowHeight,
|
|
180
|
+
})
|
|
181
|
+
ctx.y += tableHeight + 0.2
|
|
182
|
+
break
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
case 'list': {
|
|
186
|
+
const items = node.children
|
|
187
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
188
|
+
.map((item) => getTextContent(item.children))
|
|
189
|
+
|
|
190
|
+
const isOrdered = p.ordered as boolean
|
|
191
|
+
const listText = items.map((item, i) => ({
|
|
192
|
+
text: isOrdered ? `${i + 1}. ${item}\n` : `\u2022 ${item}\n`,
|
|
193
|
+
options: { fontSize: 13, bullet: false },
|
|
194
|
+
}))
|
|
195
|
+
|
|
196
|
+
ctx.slide.addText(listText, {
|
|
197
|
+
x: CONTENT_MARGIN,
|
|
198
|
+
y: ctx.y,
|
|
199
|
+
w: CONTENT_WIDTH,
|
|
200
|
+
h: items.length * 0.35,
|
|
201
|
+
})
|
|
202
|
+
ctx.y += items.length * 0.35 + 0.1
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
case 'code': {
|
|
207
|
+
const text = getTextContent(node.children)
|
|
208
|
+
ctx.slide.addText(text, {
|
|
209
|
+
x: CONTENT_MARGIN,
|
|
210
|
+
y: ctx.y,
|
|
211
|
+
w: CONTENT_WIDTH,
|
|
212
|
+
h: 0.5,
|
|
213
|
+
fontSize: 10,
|
|
214
|
+
fontFace: 'Courier New',
|
|
215
|
+
fill: { color: 'F5F5F5' },
|
|
216
|
+
color: '333333',
|
|
217
|
+
})
|
|
218
|
+
ctx.y += 0.6
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'quote': {
|
|
223
|
+
const text = getTextContent(node.children)
|
|
224
|
+
ctx.slide.addText(text, {
|
|
225
|
+
x: CONTENT_MARGIN + 0.3,
|
|
226
|
+
y: ctx.y,
|
|
227
|
+
w: CONTENT_WIDTH - 0.3,
|
|
228
|
+
h: 0.5,
|
|
229
|
+
fontSize: 13,
|
|
230
|
+
italic: true,
|
|
231
|
+
color: '555555',
|
|
232
|
+
})
|
|
233
|
+
ctx.y += 0.6
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case 'link': {
|
|
238
|
+
ctx.slide.addText(getTextContent(node.children), {
|
|
239
|
+
x: CONTENT_MARGIN,
|
|
240
|
+
y: ctx.y,
|
|
241
|
+
w: CONTENT_WIDTH,
|
|
242
|
+
h: 0.4,
|
|
243
|
+
fontSize: 13,
|
|
244
|
+
color: '4F46E5',
|
|
245
|
+
underline: { style: 'sng' },
|
|
246
|
+
hyperlink: { url: p.href as string },
|
|
247
|
+
})
|
|
248
|
+
ctx.y += 0.5
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'button': {
|
|
253
|
+
ctx.slide.addText(getTextContent(node.children), {
|
|
254
|
+
x: CONTENT_MARGIN,
|
|
255
|
+
y: ctx.y,
|
|
256
|
+
w: 3,
|
|
257
|
+
h: 0.5,
|
|
258
|
+
fontSize: 14,
|
|
259
|
+
bold: true,
|
|
260
|
+
color: ((p.color as string) ?? '#ffffff').replace('#', ''),
|
|
261
|
+
fill: {
|
|
262
|
+
color: ((p.background as string) ?? '#4f46e5').replace('#', ''),
|
|
263
|
+
},
|
|
264
|
+
align: 'center',
|
|
265
|
+
hyperlink: { url: p.href as string },
|
|
266
|
+
})
|
|
267
|
+
ctx.y += 0.6
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case 'spacer': {
|
|
272
|
+
ctx.y += ((p.height as number) ?? 12) / 72
|
|
273
|
+
break
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'divider': {
|
|
277
|
+
// Render as a thin line using a text element with top border
|
|
278
|
+
ctx.slide.addText('', {
|
|
279
|
+
x: CONTENT_MARGIN,
|
|
280
|
+
y: ctx.y,
|
|
281
|
+
w: CONTENT_WIDTH,
|
|
282
|
+
h: 0.02,
|
|
283
|
+
fill: { color: ((p.color as string) ?? '#DDDDDD').replace('#', '') },
|
|
284
|
+
})
|
|
285
|
+
ctx.y += 0.2
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Container types — recurse into children
|
|
290
|
+
case 'section':
|
|
291
|
+
case 'row':
|
|
292
|
+
case 'column':
|
|
293
|
+
for (const child of node.children) {
|
|
294
|
+
if (typeof child !== 'string') {
|
|
295
|
+
processNode(child, ctx)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
default:
|
|
301
|
+
break
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function processSlide(pageNode: DocNode, pptx: PptxGen): void {
|
|
306
|
+
const slide = pptx.addSlide()
|
|
307
|
+
const ctx: SlideContext = { slide, y: CONTENT_MARGIN }
|
|
308
|
+
|
|
309
|
+
for (const child of pageNode.children) {
|
|
310
|
+
if (typeof child !== 'string') {
|
|
311
|
+
processNode(child, ctx)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const pptxRenderer: DocumentRenderer = {
|
|
317
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
|
|
318
|
+
const PptxGenJS = await import('pptxgenjs')
|
|
319
|
+
const PptxGenClass = PptxGenJS.default ?? PptxGenJS
|
|
320
|
+
|
|
321
|
+
const pptx = new PptxGenClass() as PptxGen
|
|
322
|
+
|
|
323
|
+
// Set metadata
|
|
324
|
+
if (node.props.title) pptx.title = node.props.title as string
|
|
325
|
+
if (node.props.author) pptx.author = node.props.author as string
|
|
326
|
+
if (node.props.subject) pptx.subject = node.props.subject as string
|
|
327
|
+
|
|
328
|
+
// Collect pages — each becomes a slide
|
|
329
|
+
const pages: DocNode[] = []
|
|
330
|
+
for (const child of node.children) {
|
|
331
|
+
if (typeof child !== 'string' && child.type === 'page') {
|
|
332
|
+
pages.push(child)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// If no explicit pages, treat entire document content as one slide
|
|
337
|
+
if (pages.length === 0) {
|
|
338
|
+
const syntheticPage: DocNode = {
|
|
339
|
+
type: 'page',
|
|
340
|
+
props: {},
|
|
341
|
+
children: node.children,
|
|
342
|
+
}
|
|
343
|
+
pages.push(syntheticPage)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const page of pages) {
|
|
347
|
+
processSlide(page, pptx)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const output = await pptx.write('arraybuffer')
|
|
351
|
+
return new Uint8Array(output as ArrayBuffer)
|
|
352
|
+
},
|
|
353
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
RenderOptions,
|
|
6
|
+
TableColumn,
|
|
7
|
+
} from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.
|
|
11
|
+
* Maps document nodes to Slack blocks (section, header, divider, image, etc.).
|
|
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 SlackBlock {
|
|
27
|
+
type: string
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mrkdwn(text: string): { type: 'mrkdwn'; text: string } {
|
|
32
|
+
return { type: 'mrkdwn', text }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function plainText(text: string): { type: 'plain_text'; text: string } {
|
|
36
|
+
return { type: 'plain_text', text }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nodeToBlocks(node: DocNode): SlackBlock[] {
|
|
40
|
+
const p = node.props
|
|
41
|
+
const blocks: SlackBlock[] = []
|
|
42
|
+
|
|
43
|
+
switch (node.type) {
|
|
44
|
+
case 'document':
|
|
45
|
+
case 'page':
|
|
46
|
+
case 'section':
|
|
47
|
+
case 'row':
|
|
48
|
+
case 'column':
|
|
49
|
+
for (const child of node.children) {
|
|
50
|
+
if (typeof child !== 'string') {
|
|
51
|
+
blocks.push(...nodeToBlocks(child))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
case 'heading':
|
|
57
|
+
blocks.push({
|
|
58
|
+
type: 'header',
|
|
59
|
+
text: plainText(getTextContent(node.children)),
|
|
60
|
+
})
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
case 'text': {
|
|
64
|
+
let text = getTextContent(node.children)
|
|
65
|
+
if (p.bold) text = `*${text}*`
|
|
66
|
+
if (p.italic) text = `_${text}_`
|
|
67
|
+
if (p.strikethrough) text = `~${text}~`
|
|
68
|
+
blocks.push({
|
|
69
|
+
type: 'section',
|
|
70
|
+
text: mrkdwn(text),
|
|
71
|
+
})
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'link': {
|
|
76
|
+
const href = p.href as string
|
|
77
|
+
const text = getTextContent(node.children)
|
|
78
|
+
blocks.push({
|
|
79
|
+
type: 'section',
|
|
80
|
+
text: mrkdwn(`<${href}|${text}>`),
|
|
81
|
+
})
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'image': {
|
|
86
|
+
const src = p.src as string
|
|
87
|
+
// Slack only supports public URLs for images
|
|
88
|
+
if (src.startsWith('http')) {
|
|
89
|
+
blocks.push({
|
|
90
|
+
type: 'image',
|
|
91
|
+
image_url: src,
|
|
92
|
+
alt_text: (p.alt as string) ?? 'Image',
|
|
93
|
+
...(p.caption ? { title: plainText(p.caption as string) } : {}),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case 'table': {
|
|
100
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
101
|
+
resolveColumn,
|
|
102
|
+
)
|
|
103
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
104
|
+
|
|
105
|
+
// Slack doesn't have native tables — render as formatted text
|
|
106
|
+
const header = columns.map((c) => `*${c.header}*`).join(' | ')
|
|
107
|
+
const separator = columns.map(() => '---').join(' | ')
|
|
108
|
+
const body = rows
|
|
109
|
+
.map((row) => row.map((cell) => String(cell ?? '')).join(' | '))
|
|
110
|
+
.join('\n')
|
|
111
|
+
|
|
112
|
+
let text = `${header}\n${separator}\n${body}`
|
|
113
|
+
if (p.caption) text = `_${p.caption}_\n${text}`
|
|
114
|
+
|
|
115
|
+
blocks.push({
|
|
116
|
+
type: 'section',
|
|
117
|
+
text: mrkdwn(`\`\`\`\n${text}\n\`\`\``),
|
|
118
|
+
})
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'list': {
|
|
123
|
+
const ordered = p.ordered as boolean | undefined
|
|
124
|
+
const items = node.children
|
|
125
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
126
|
+
.map((item, i) => {
|
|
127
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
128
|
+
return `${prefix} ${getTextContent(item.children)}`
|
|
129
|
+
})
|
|
130
|
+
.join('\n')
|
|
131
|
+
blocks.push({
|
|
132
|
+
type: 'section',
|
|
133
|
+
text: mrkdwn(items),
|
|
134
|
+
})
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'code': {
|
|
139
|
+
const text = getTextContent(node.children)
|
|
140
|
+
const lang = (p.language as string) ?? ''
|
|
141
|
+
blocks.push({
|
|
142
|
+
type: 'section',
|
|
143
|
+
text: mrkdwn(`\`\`\`${lang}\n${text}\n\`\`\``),
|
|
144
|
+
})
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'divider':
|
|
149
|
+
case 'page-break':
|
|
150
|
+
blocks.push({ type: 'divider' })
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
case 'spacer':
|
|
154
|
+
// No equivalent in Slack — skip
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
case 'button': {
|
|
158
|
+
const href = p.href as string
|
|
159
|
+
const text = getTextContent(node.children)
|
|
160
|
+
blocks.push({
|
|
161
|
+
type: 'actions',
|
|
162
|
+
elements: [
|
|
163
|
+
{
|
|
164
|
+
type: 'button',
|
|
165
|
+
text: plainText(text),
|
|
166
|
+
url: href,
|
|
167
|
+
style: 'primary',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
})
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'quote': {
|
|
175
|
+
const text = getTextContent(node.children)
|
|
176
|
+
blocks.push({
|
|
177
|
+
type: 'section',
|
|
178
|
+
text: mrkdwn(`> ${text}`),
|
|
179
|
+
})
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return blocks
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const slackRenderer: DocumentRenderer = {
|
|
188
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<string> {
|
|
189
|
+
const blocks = nodeToBlocks(node)
|
|
190
|
+
return JSON.stringify({ blocks }, null, 2)
|
|
191
|
+
},
|
|
192
|
+
}
|