@pyreon/document 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/confluence-Bd3ua1Ut.js.map +1 -1
- package/lib/csv-COrS4qdy.js.map +1 -1
- package/lib/discord-BLUnkEh9.js.map +1 -1
- package/lib/{dist-BsqdI2nY.js → dist-CYL41kqQ.js} +2 -2
- package/lib/dist-CYL41kqQ.js.map +1 -0
- package/lib/{docx-BEBOihjl.js → docx-uNAel545.js} +7 -2
- package/lib/docx-uNAel545.js.map +1 -0
- package/lib/email-D0bbfWq4.js.map +1 -1
- package/lib/{exceljs-BoIDUUaw.js → exceljs-BYETsesT.js} +314 -314
- package/lib/exceljs-BYETsesT.js.map +1 -0
- package/lib/google-chat-CkKCBUWC.js.map +1 -1
- package/lib/html-B5biprN2.js.map +1 -1
- package/lib/index.js +17 -8
- package/lib/index.js.map +1 -1
- package/lib/markdown-CdtlFGC0.js.map +1 -1
- package/lib/notion-iG2C5bEY.js.map +1 -1
- package/lib/{pdf-DIUQUEdj.js → pdf-IuBgTb3T.js} +9 -3
- package/lib/pdf-IuBgTb3T.js.map +1 -0
- package/lib/{pdfmake-DnmLxK4Q.js → pdfmake-CKMX5URW.js} +2 -4
- package/lib/pdfmake-CKMX5URW.js.map +1 -0
- package/lib/{pptx-Dd33oL3_.js → pptx-DXiMiYFM.js} +7 -2
- package/lib/pptx-DXiMiYFM.js.map +1 -0
- package/lib/{pptxgen.es-COcgXsyx.js → pptxgen.es-FsqHs8mD.js} +3 -6
- package/lib/pptxgen.es-FsqHs8mD.js.map +1 -0
- package/lib/sanitize-O_3j1mNJ.js.map +1 -1
- package/lib/slack-BI3EQwYm.js.map +1 -1
- package/lib/svg-BKxumy-p.js.map +1 -1
- package/lib/teams-Cwz9lce0.js.map +1 -1
- package/lib/telegram-gYFqyMXb.js.map +1 -1
- package/lib/text-l1XNXBOC.js.map +1 -1
- package/lib/types/index.d.ts +43 -39
- package/lib/types/index.d.ts.map +1 -1
- package/lib/{vfs_fonts-Df1kkZ4Y.js → vfs_fonts-Cap07Jg3.js} +2 -2
- package/lib/vfs_fonts-Cap07Jg3.js.map +1 -0
- package/lib/whatsapp-CjSGoOKx.js.map +1 -1
- package/lib/{xlsx-Bb5TWyXQ.js → xlsx-Cvu4LBNy.js} +8 -2
- package/lib/xlsx-Cvu4LBNy.js.map +1 -0
- package/package.json +19 -7
- package/src/builder.ts +53 -44
- package/src/download.ts +32 -36
- package/src/env.d.ts +3 -17
- package/src/index.ts +6 -8
- package/src/nodes.ts +45 -45
- package/src/render.ts +45 -118
- package/src/renderers/confluence.ts +64 -80
- package/src/renderers/csv.ts +11 -18
- package/src/renderers/discord.ts +38 -50
- package/src/renderers/docx.ts +78 -120
- package/src/renderers/email.ts +73 -92
- package/src/renderers/google-chat.ts +35 -47
- package/src/renderers/html.ts +78 -101
- package/src/renderers/markdown.ts +43 -53
- package/src/renderers/notion.ts +63 -85
- package/src/renderers/pdf.ts +92 -115
- package/src/renderers/pptx.ts +60 -66
- package/src/renderers/slack.ts +49 -61
- package/src/renderers/svg.ts +49 -63
- package/src/renderers/teams.ts +68 -80
- package/src/renderers/telegram.ts +40 -54
- package/src/renderers/text.ts +44 -66
- package/src/renderers/whatsapp.ts +34 -48
- package/src/renderers/xlsx.ts +47 -61
- package/src/sanitize.ts +21 -25
- package/src/tests/document.test.ts +1337 -1385
- package/src/tests/stress.test.ts +111 -111
- package/src/types.ts +66 -65
- package/lib/dist-BsqdI2nY.js.map +0 -1
- package/lib/docx-BEBOihjl.js.map +0 -1
- package/lib/exceljs-BoIDUUaw.js.map +0 -1
- package/lib/pdf-DIUQUEdj.js.map +0 -1
- package/lib/pdfmake-DnmLxK4Q.js.map +0 -1
- package/lib/pptx-Dd33oL3_.js.map +0 -1
- package/lib/pptxgen.es-COcgXsyx.js.map +0 -1
- package/lib/vfs_fonts-Df1kkZ4Y.js.map +0 -1
- package/lib/xlsx-Bb5TWyXQ.js.map +0 -1
package/src/renderers/pptx.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { sanitizeHref, sanitizeImageSrc, sanitizeXmlColor } from
|
|
2
|
-
import type {
|
|
3
|
-
DocChild,
|
|
4
|
-
DocNode,
|
|
5
|
-
DocumentRenderer,
|
|
6
|
-
RenderOptions,
|
|
7
|
-
TableColumn,
|
|
8
|
-
} from '../types'
|
|
1
|
+
import { sanitizeHref, sanitizeImageSrc, sanitizeXmlColor } from "../sanitize"
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
|
|
9
3
|
|
|
10
4
|
/**
|
|
11
5
|
* PPTX renderer — lazy-loads pptxgenjs on first use.
|
|
@@ -27,15 +21,13 @@ import type {
|
|
|
27
21
|
*/
|
|
28
22
|
|
|
29
23
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
30
|
-
return typeof col ===
|
|
24
|
+
return typeof col === "string" ? { header: col } : col
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
function getTextContent(children: DocChild[]): string {
|
|
34
28
|
return children
|
|
35
|
-
.map((c) =>
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
.join('')
|
|
29
|
+
.map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
|
|
30
|
+
.join("")
|
|
39
31
|
}
|
|
40
32
|
|
|
41
33
|
/** Vertical position tracker for placing elements on a slide. */
|
|
@@ -81,7 +73,7 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
81
73
|
const p = node.props
|
|
82
74
|
|
|
83
75
|
switch (node.type) {
|
|
84
|
-
case
|
|
76
|
+
case "heading": {
|
|
85
77
|
const level = (p.level as number) ?? 1
|
|
86
78
|
const fontSize = HEADING_SIZES[level] ?? 20
|
|
87
79
|
ctx.slide.addText(getTextContent(node.children), {
|
|
@@ -91,14 +83,14 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
91
83
|
h: 0.6,
|
|
92
84
|
fontSize,
|
|
93
85
|
bold: true,
|
|
94
|
-
color: sanitizeXmlColor((p.color as string) ??
|
|
95
|
-
align: (p.align as string) ??
|
|
86
|
+
color: sanitizeXmlColor((p.color as string) ?? "#000000"),
|
|
87
|
+
align: (p.align as string) ?? "left",
|
|
96
88
|
})
|
|
97
89
|
ctx.y += 0.7
|
|
98
90
|
break
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
case
|
|
93
|
+
case "text": {
|
|
102
94
|
const text = getTextContent(node.children)
|
|
103
95
|
ctx.slide.addText(text, {
|
|
104
96
|
x: CONTENT_MARGIN,
|
|
@@ -108,21 +100,21 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
108
100
|
fontSize: (p.size as number) ?? 14,
|
|
109
101
|
bold: p.bold ?? false,
|
|
110
102
|
italic: p.italic ?? false,
|
|
111
|
-
underline: p.underline ? { style:
|
|
112
|
-
strike: p.strikethrough ?
|
|
113
|
-
color: sanitizeXmlColor((p.color as string) ??
|
|
114
|
-
align: (p.align as string) ??
|
|
103
|
+
underline: p.underline ? { style: "sng" } : undefined,
|
|
104
|
+
strike: p.strikethrough ? "sngStrike" : undefined,
|
|
105
|
+
color: sanitizeXmlColor((p.color as string) ?? "#333333"),
|
|
106
|
+
align: (p.align as string) ?? "left",
|
|
115
107
|
})
|
|
116
108
|
ctx.y += 0.5
|
|
117
109
|
break
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
case
|
|
112
|
+
case "image": {
|
|
121
113
|
const src = sanitizeImageSrc(p.src as string)
|
|
122
114
|
const w = Math.min(((p.width as number) ?? 400) / 96, CONTENT_WIDTH)
|
|
123
115
|
const h = ((p.height as number) ?? 300) / 96
|
|
124
116
|
|
|
125
|
-
if (src.startsWith(
|
|
117
|
+
if (src.startsWith("data:")) {
|
|
126
118
|
ctx.slide.addImage({
|
|
127
119
|
data: src,
|
|
128
120
|
x: CONTENT_MARGIN,
|
|
@@ -136,34 +128,29 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
136
128
|
break
|
|
137
129
|
}
|
|
138
130
|
|
|
139
|
-
case
|
|
140
|
-
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
141
|
-
resolveColumn,
|
|
142
|
-
)
|
|
131
|
+
case "table": {
|
|
132
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
143
133
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
144
|
-
const hs = p.headerStyle as
|
|
145
|
-
| { background?: string; color?: string }
|
|
146
|
-
| undefined
|
|
134
|
+
const hs = p.headerStyle as { background?: string; color?: string } | undefined
|
|
147
135
|
|
|
148
136
|
const headerRow = columns.map((col) => ({
|
|
149
137
|
text: col.header,
|
|
150
138
|
options: {
|
|
151
139
|
bold: true,
|
|
152
|
-
fill: { color: sanitizeXmlColor(hs?.background ??
|
|
153
|
-
color: sanitizeXmlColor(hs?.color ??
|
|
154
|
-
align: col.align ??
|
|
140
|
+
fill: { color: sanitizeXmlColor(hs?.background ?? "#f5f5f5") },
|
|
141
|
+
color: sanitizeXmlColor(hs?.color ?? "#000000"),
|
|
142
|
+
align: col.align ?? "left",
|
|
155
143
|
fontSize: 12,
|
|
156
144
|
},
|
|
157
145
|
}))
|
|
158
146
|
|
|
159
147
|
const dataRows = rows.map((row, rowIdx) =>
|
|
160
148
|
columns.map((col, colIdx) => ({
|
|
161
|
-
text: String(row[colIdx] ??
|
|
149
|
+
text: String(row[colIdx] ?? ""),
|
|
162
150
|
options: {
|
|
163
|
-
align: col.align ??
|
|
151
|
+
align: col.align ?? "left",
|
|
164
152
|
fontSize: 11,
|
|
165
|
-
fill:
|
|
166
|
-
p.striped && rowIdx % 2 === 1 ? { color: 'F9F9F9' } : undefined,
|
|
153
|
+
fill: p.striped && rowIdx % 2 === 1 ? { color: "F9F9F9" } : undefined,
|
|
167
154
|
},
|
|
168
155
|
})),
|
|
169
156
|
)
|
|
@@ -176,16 +163,16 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
176
163
|
x: CONTENT_MARGIN,
|
|
177
164
|
y: ctx.y,
|
|
178
165
|
w: CONTENT_WIDTH,
|
|
179
|
-
border: { pt: 0.5, color:
|
|
166
|
+
border: { pt: 0.5, color: "DDDDDD" },
|
|
180
167
|
rowH: rowHeight,
|
|
181
168
|
})
|
|
182
169
|
ctx.y += tableHeight + 0.2
|
|
183
170
|
break
|
|
184
171
|
}
|
|
185
172
|
|
|
186
|
-
case
|
|
173
|
+
case "list": {
|
|
187
174
|
const items = node.children
|
|
188
|
-
.filter((c): c is DocNode => typeof c !==
|
|
175
|
+
.filter((c): c is DocNode => typeof c !== "string")
|
|
189
176
|
.map((item) => getTextContent(item.children))
|
|
190
177
|
|
|
191
178
|
const isOrdered = p.ordered as boolean
|
|
@@ -204,7 +191,7 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
204
191
|
break
|
|
205
192
|
}
|
|
206
193
|
|
|
207
|
-
case
|
|
194
|
+
case "code": {
|
|
208
195
|
const text = getTextContent(node.children)
|
|
209
196
|
ctx.slide.addText(text, {
|
|
210
197
|
x: CONTENT_MARGIN,
|
|
@@ -212,15 +199,15 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
212
199
|
w: CONTENT_WIDTH,
|
|
213
200
|
h: 0.5,
|
|
214
201
|
fontSize: 10,
|
|
215
|
-
fontFace:
|
|
216
|
-
fill: { color:
|
|
217
|
-
color:
|
|
202
|
+
fontFace: "Courier New",
|
|
203
|
+
fill: { color: "F5F5F5" },
|
|
204
|
+
color: "333333",
|
|
218
205
|
})
|
|
219
206
|
ctx.y += 0.6
|
|
220
207
|
break
|
|
221
208
|
}
|
|
222
209
|
|
|
223
|
-
case
|
|
210
|
+
case "quote": {
|
|
224
211
|
const text = getTextContent(node.children)
|
|
225
212
|
ctx.slide.addText(text, {
|
|
226
213
|
x: CONTENT_MARGIN + 0.3,
|
|
@@ -229,28 +216,28 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
229
216
|
h: 0.5,
|
|
230
217
|
fontSize: 13,
|
|
231
218
|
italic: true,
|
|
232
|
-
color:
|
|
219
|
+
color: "555555",
|
|
233
220
|
})
|
|
234
221
|
ctx.y += 0.6
|
|
235
222
|
break
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
case
|
|
225
|
+
case "link": {
|
|
239
226
|
ctx.slide.addText(getTextContent(node.children), {
|
|
240
227
|
x: CONTENT_MARGIN,
|
|
241
228
|
y: ctx.y,
|
|
242
229
|
w: CONTENT_WIDTH,
|
|
243
230
|
h: 0.4,
|
|
244
231
|
fontSize: 13,
|
|
245
|
-
color:
|
|
246
|
-
underline: { style:
|
|
232
|
+
color: "4F46E5",
|
|
233
|
+
underline: { style: "sng" },
|
|
247
234
|
hyperlink: { url: sanitizeHref(p.href as string) },
|
|
248
235
|
})
|
|
249
236
|
ctx.y += 0.5
|
|
250
237
|
break
|
|
251
238
|
}
|
|
252
239
|
|
|
253
|
-
case
|
|
240
|
+
case "button": {
|
|
254
241
|
ctx.slide.addText(getTextContent(node.children), {
|
|
255
242
|
x: CONTENT_MARGIN,
|
|
256
243
|
y: ctx.y,
|
|
@@ -258,41 +245,41 @@ function processNode(node: DocNode, ctx: SlideContext): void {
|
|
|
258
245
|
h: 0.5,
|
|
259
246
|
fontSize: 14,
|
|
260
247
|
bold: true,
|
|
261
|
-
color: sanitizeXmlColor((p.color as string) ??
|
|
248
|
+
color: sanitizeXmlColor((p.color as string) ?? "#ffffff"),
|
|
262
249
|
fill: {
|
|
263
|
-
color: sanitizeXmlColor((p.background as string) ??
|
|
250
|
+
color: sanitizeXmlColor((p.background as string) ?? "#4f46e5"),
|
|
264
251
|
},
|
|
265
|
-
align:
|
|
252
|
+
align: "center",
|
|
266
253
|
hyperlink: { url: sanitizeHref(p.href as string) },
|
|
267
254
|
})
|
|
268
255
|
ctx.y += 0.6
|
|
269
256
|
break
|
|
270
257
|
}
|
|
271
258
|
|
|
272
|
-
case
|
|
259
|
+
case "spacer": {
|
|
273
260
|
ctx.y += ((p.height as number) ?? 12) / 72
|
|
274
261
|
break
|
|
275
262
|
}
|
|
276
263
|
|
|
277
|
-
case
|
|
264
|
+
case "divider": {
|
|
278
265
|
// Render as a thin line using a text element with top border
|
|
279
|
-
ctx.slide.addText(
|
|
266
|
+
ctx.slide.addText("", {
|
|
280
267
|
x: CONTENT_MARGIN,
|
|
281
268
|
y: ctx.y,
|
|
282
269
|
w: CONTENT_WIDTH,
|
|
283
270
|
h: 0.02,
|
|
284
|
-
fill: { color: sanitizeXmlColor((p.color as string) ??
|
|
271
|
+
fill: { color: sanitizeXmlColor((p.color as string) ?? "#DDDDDD") },
|
|
285
272
|
})
|
|
286
273
|
ctx.y += 0.2
|
|
287
274
|
break
|
|
288
275
|
}
|
|
289
276
|
|
|
290
277
|
// Container types — recurse into children
|
|
291
|
-
case
|
|
292
|
-
case
|
|
293
|
-
case
|
|
278
|
+
case "section":
|
|
279
|
+
case "row":
|
|
280
|
+
case "column":
|
|
294
281
|
for (const child of node.children) {
|
|
295
|
-
if (typeof child !==
|
|
282
|
+
if (typeof child !== "string") {
|
|
296
283
|
processNode(child, ctx)
|
|
297
284
|
}
|
|
298
285
|
}
|
|
@@ -308,7 +295,7 @@ function processSlide(pageNode: DocNode, pptx: PptxGen): void {
|
|
|
308
295
|
const ctx: SlideContext = { slide, y: CONTENT_MARGIN }
|
|
309
296
|
|
|
310
297
|
for (const child of pageNode.children) {
|
|
311
|
-
if (typeof child !==
|
|
298
|
+
if (typeof child !== "string") {
|
|
312
299
|
processNode(child, ctx)
|
|
313
300
|
}
|
|
314
301
|
}
|
|
@@ -316,7 +303,14 @@ function processSlide(pageNode: DocNode, pptx: PptxGen): void {
|
|
|
316
303
|
|
|
317
304
|
export const pptxRenderer: DocumentRenderer = {
|
|
318
305
|
async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
|
|
319
|
-
|
|
306
|
+
let PptxGenJS: any
|
|
307
|
+
try {
|
|
308
|
+
PptxGenJS = await import("pptxgenjs")
|
|
309
|
+
} catch {
|
|
310
|
+
throw new Error(
|
|
311
|
+
'[@pyreon/document] PPTX renderer requires "pptxgenjs" package. Install it: bun add pptxgenjs',
|
|
312
|
+
)
|
|
313
|
+
}
|
|
320
314
|
const PptxGenClass = PptxGenJS.default ?? PptxGenJS
|
|
321
315
|
|
|
322
316
|
const pptx = new PptxGenClass() as PptxGen
|
|
@@ -329,7 +323,7 @@ export const pptxRenderer: DocumentRenderer = {
|
|
|
329
323
|
// Collect pages — each becomes a slide
|
|
330
324
|
const pages: DocNode[] = []
|
|
331
325
|
for (const child of node.children) {
|
|
332
|
-
if (typeof child !==
|
|
326
|
+
if (typeof child !== "string" && child.type === "page") {
|
|
333
327
|
pages.push(child)
|
|
334
328
|
}
|
|
335
329
|
}
|
|
@@ -337,7 +331,7 @@ export const pptxRenderer: DocumentRenderer = {
|
|
|
337
331
|
// If no explicit pages, treat entire document content as one slide
|
|
338
332
|
if (pages.length === 0) {
|
|
339
333
|
const syntheticPage: DocNode = {
|
|
340
|
-
type:
|
|
334
|
+
type: "page",
|
|
341
335
|
props: {},
|
|
342
336
|
children: node.children,
|
|
343
337
|
}
|
|
@@ -348,7 +342,7 @@ export const pptxRenderer: DocumentRenderer = {
|
|
|
348
342
|
processSlide(page, pptx)
|
|
349
343
|
}
|
|
350
344
|
|
|
351
|
-
const output = await pptx.write(
|
|
345
|
+
const output = await pptx.write("arraybuffer")
|
|
352
346
|
return new Uint8Array(output as ArrayBuffer)
|
|
353
347
|
},
|
|
354
348
|
}
|
package/src/renderers/slack.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { sanitizeHref, sanitizeImageSrc } from
|
|
2
|
-
import type {
|
|
3
|
-
DocChild,
|
|
4
|
-
DocNode,
|
|
5
|
-
DocumentRenderer,
|
|
6
|
-
RenderOptions,
|
|
7
|
-
TableColumn,
|
|
8
|
-
} from '../types'
|
|
1
|
+
import { sanitizeHref, sanitizeImageSrc } from "../sanitize"
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
|
|
9
3
|
|
|
10
4
|
/**
|
|
11
5
|
* Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.
|
|
@@ -13,15 +7,13 @@ import type {
|
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
16
|
-
return typeof col ===
|
|
10
|
+
return typeof col === "string" ? { header: col } : col
|
|
17
11
|
}
|
|
18
12
|
|
|
19
13
|
function getTextContent(children: DocChild[]): string {
|
|
20
14
|
return children
|
|
21
|
-
.map((c) =>
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
.join('')
|
|
15
|
+
.map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
|
|
16
|
+
.join("")
|
|
25
17
|
}
|
|
26
18
|
|
|
27
19
|
interface SlackBlock {
|
|
@@ -29,12 +21,12 @@ interface SlackBlock {
|
|
|
29
21
|
[key: string]: unknown
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
function mrkdwn(text: string): { type:
|
|
33
|
-
return { type:
|
|
24
|
+
function mrkdwn(text: string): { type: "mrkdwn"; text: string } {
|
|
25
|
+
return { type: "mrkdwn", text }
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
function plainText(text: string): { type:
|
|
37
|
-
return { type:
|
|
28
|
+
function plainText(text: string): { type: "plain_text"; text: string } {
|
|
29
|
+
return { type: "plain_text", text }
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
function nodeToBlocks(node: DocNode): SlackBlock[] {
|
|
@@ -42,140 +34,136 @@ function nodeToBlocks(node: DocNode): SlackBlock[] {
|
|
|
42
34
|
const blocks: SlackBlock[] = []
|
|
43
35
|
|
|
44
36
|
switch (node.type) {
|
|
45
|
-
case
|
|
46
|
-
case
|
|
47
|
-
case
|
|
48
|
-
case
|
|
49
|
-
case
|
|
37
|
+
case "document":
|
|
38
|
+
case "page":
|
|
39
|
+
case "section":
|
|
40
|
+
case "row":
|
|
41
|
+
case "column":
|
|
50
42
|
for (const child of node.children) {
|
|
51
|
-
if (typeof child !==
|
|
43
|
+
if (typeof child !== "string") {
|
|
52
44
|
blocks.push(...nodeToBlocks(child))
|
|
53
45
|
}
|
|
54
46
|
}
|
|
55
47
|
break
|
|
56
48
|
|
|
57
|
-
case
|
|
49
|
+
case "heading":
|
|
58
50
|
blocks.push({
|
|
59
|
-
type:
|
|
51
|
+
type: "header",
|
|
60
52
|
text: plainText(getTextContent(node.children)),
|
|
61
53
|
})
|
|
62
54
|
break
|
|
63
55
|
|
|
64
|
-
case
|
|
56
|
+
case "text": {
|
|
65
57
|
let text = getTextContent(node.children)
|
|
66
58
|
if (p.bold) text = `*${text}*`
|
|
67
59
|
if (p.italic) text = `_${text}_`
|
|
68
60
|
if (p.strikethrough) text = `~${text}~`
|
|
69
61
|
blocks.push({
|
|
70
|
-
type:
|
|
62
|
+
type: "section",
|
|
71
63
|
text: mrkdwn(text),
|
|
72
64
|
})
|
|
73
65
|
break
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
case
|
|
68
|
+
case "link": {
|
|
77
69
|
const href = sanitizeHref(p.href as string)
|
|
78
70
|
const text = getTextContent(node.children)
|
|
79
71
|
blocks.push({
|
|
80
|
-
type:
|
|
72
|
+
type: "section",
|
|
81
73
|
text: mrkdwn(`<${href}|${text}>`),
|
|
82
74
|
})
|
|
83
75
|
break
|
|
84
76
|
}
|
|
85
77
|
|
|
86
|
-
case
|
|
78
|
+
case "image": {
|
|
87
79
|
const src = sanitizeImageSrc(p.src as string)
|
|
88
80
|
// Slack only supports public URLs for images
|
|
89
|
-
if (src.startsWith(
|
|
81
|
+
if (src.startsWith("http")) {
|
|
90
82
|
blocks.push({
|
|
91
|
-
type:
|
|
83
|
+
type: "image",
|
|
92
84
|
image_url: src,
|
|
93
|
-
alt_text: (p.alt as string) ??
|
|
85
|
+
alt_text: (p.alt as string) ?? "Image",
|
|
94
86
|
...(p.caption ? { title: plainText(p.caption as string) } : {}),
|
|
95
87
|
})
|
|
96
88
|
}
|
|
97
89
|
break
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
case
|
|
101
|
-
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
102
|
-
resolveColumn,
|
|
103
|
-
)
|
|
92
|
+
case "table": {
|
|
93
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
104
94
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
105
95
|
|
|
106
96
|
// Slack doesn't have native tables — render as formatted text
|
|
107
|
-
const header = columns.map((c) => `*${c.header}*`).join(
|
|
108
|
-
const separator = columns.map(() =>
|
|
109
|
-
const body = rows
|
|
110
|
-
.map((row) => row.map((cell) => String(cell ?? '')).join(' | '))
|
|
111
|
-
.join('\n')
|
|
97
|
+
const header = columns.map((c) => `*${c.header}*`).join(" | ")
|
|
98
|
+
const separator = columns.map(() => "---").join(" | ")
|
|
99
|
+
const body = rows.map((row) => row.map((cell) => String(cell ?? "")).join(" | ")).join("\n")
|
|
112
100
|
|
|
113
101
|
let text = `${header}\n${separator}\n${body}`
|
|
114
102
|
if (p.caption) text = `_${p.caption}_\n${text}`
|
|
115
103
|
|
|
116
104
|
blocks.push({
|
|
117
|
-
type:
|
|
105
|
+
type: "section",
|
|
118
106
|
text: mrkdwn(`\`\`\`\n${text}\n\`\`\``),
|
|
119
107
|
})
|
|
120
108
|
break
|
|
121
109
|
}
|
|
122
110
|
|
|
123
|
-
case
|
|
111
|
+
case "list": {
|
|
124
112
|
const ordered = p.ordered as boolean | undefined
|
|
125
113
|
const items = node.children
|
|
126
|
-
.filter((c): c is DocNode => typeof c !==
|
|
114
|
+
.filter((c): c is DocNode => typeof c !== "string")
|
|
127
115
|
.map((item, i) => {
|
|
128
|
-
const prefix = ordered ? `${i + 1}.` :
|
|
116
|
+
const prefix = ordered ? `${i + 1}.` : "•"
|
|
129
117
|
return `${prefix} ${getTextContent(item.children)}`
|
|
130
118
|
})
|
|
131
|
-
.join(
|
|
119
|
+
.join("\n")
|
|
132
120
|
blocks.push({
|
|
133
|
-
type:
|
|
121
|
+
type: "section",
|
|
134
122
|
text: mrkdwn(items),
|
|
135
123
|
})
|
|
136
124
|
break
|
|
137
125
|
}
|
|
138
126
|
|
|
139
|
-
case
|
|
127
|
+
case "code": {
|
|
140
128
|
const text = getTextContent(node.children)
|
|
141
|
-
const lang = (p.language as string) ??
|
|
129
|
+
const lang = (p.language as string) ?? ""
|
|
142
130
|
blocks.push({
|
|
143
|
-
type:
|
|
131
|
+
type: "section",
|
|
144
132
|
text: mrkdwn(`\`\`\`${lang}\n${text}\n\`\`\``),
|
|
145
133
|
})
|
|
146
134
|
break
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
case
|
|
150
|
-
case
|
|
151
|
-
blocks.push({ type:
|
|
137
|
+
case "divider":
|
|
138
|
+
case "page-break":
|
|
139
|
+
blocks.push({ type: "divider" })
|
|
152
140
|
break
|
|
153
141
|
|
|
154
|
-
case
|
|
142
|
+
case "spacer":
|
|
155
143
|
// No equivalent in Slack — skip
|
|
156
144
|
break
|
|
157
145
|
|
|
158
|
-
case
|
|
146
|
+
case "button": {
|
|
159
147
|
const href = sanitizeHref(p.href as string)
|
|
160
148
|
const text = getTextContent(node.children)
|
|
161
149
|
blocks.push({
|
|
162
|
-
type:
|
|
150
|
+
type: "actions",
|
|
163
151
|
elements: [
|
|
164
152
|
{
|
|
165
|
-
type:
|
|
153
|
+
type: "button",
|
|
166
154
|
text: plainText(text),
|
|
167
155
|
url: href,
|
|
168
|
-
style:
|
|
156
|
+
style: "primary",
|
|
169
157
|
},
|
|
170
158
|
],
|
|
171
159
|
})
|
|
172
160
|
break
|
|
173
161
|
}
|
|
174
162
|
|
|
175
|
-
case
|
|
163
|
+
case "quote": {
|
|
176
164
|
const text = getTextContent(node.children)
|
|
177
165
|
blocks.push({
|
|
178
|
-
type:
|
|
166
|
+
type: "section",
|
|
179
167
|
text: mrkdwn(`> ${text}`),
|
|
180
168
|
})
|
|
181
169
|
break
|