@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,612 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocChild,
|
|
3
|
+
DocNode,
|
|
4
|
+
DocumentRenderer,
|
|
5
|
+
PageOrientation,
|
|
6
|
+
PageSize,
|
|
7
|
+
RenderOptions,
|
|
8
|
+
TableColumn,
|
|
9
|
+
} from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DOCX renderer — lazy-loads the 'docx' npm package on first use.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
16
|
+
return typeof col === 'string' ? { header: col } : col
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTextContent(children: DocChild[]): string {
|
|
20
|
+
return children
|
|
21
|
+
.map((c) =>
|
|
22
|
+
typeof c === 'string' ? c : getTextContent((c as DocNode).children),
|
|
23
|
+
)
|
|
24
|
+
.join('')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Parse a data URL and return the base64 data and media type, or null for external URLs. */
|
|
28
|
+
function parseDataUrl(src: string): { data: string; mime: string } | null {
|
|
29
|
+
const match = src.match(/^data:(image\/[^;]+);base64,(.+)$/)
|
|
30
|
+
if (!match) return null
|
|
31
|
+
return { mime: match[1]!, data: match[2]! }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Convert page size name to DOCX page dimensions in twips (1 inch = 1440 twips). */
|
|
35
|
+
function getPageSize(
|
|
36
|
+
size?: PageSize,
|
|
37
|
+
orientation?: PageOrientation,
|
|
38
|
+
): { width: number; height: number } | undefined {
|
|
39
|
+
if (!size) return undefined
|
|
40
|
+
const sizes: Record<string, { width: number; height: number }> = {
|
|
41
|
+
A4: { width: 11906, height: 16838 },
|
|
42
|
+
A3: { width: 16838, height: 23811 },
|
|
43
|
+
A5: { width: 8391, height: 11906 },
|
|
44
|
+
letter: { width: 12240, height: 15840 },
|
|
45
|
+
legal: { width: 12240, height: 20160 },
|
|
46
|
+
tabloid: { width: 15840, height: 24480 },
|
|
47
|
+
}
|
|
48
|
+
const dims = sizes[size]
|
|
49
|
+
if (!dims) return undefined
|
|
50
|
+
if (orientation === 'landscape') {
|
|
51
|
+
return { width: dims.height, height: dims.width }
|
|
52
|
+
}
|
|
53
|
+
return dims
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Convert margin prop to DOCX section margin (in twips, 1pt ~= 20 twips). */
|
|
57
|
+
function getPageMargins(
|
|
58
|
+
margin?: number | [number, number] | [number, number, number, number],
|
|
59
|
+
): object | undefined {
|
|
60
|
+
if (margin == null) return undefined
|
|
61
|
+
if (typeof margin === 'number') {
|
|
62
|
+
const twips = margin * 20
|
|
63
|
+
return { top: twips, right: twips, bottom: twips, left: twips }
|
|
64
|
+
}
|
|
65
|
+
if (margin.length === 2) {
|
|
66
|
+
return {
|
|
67
|
+
top: margin[0] * 20,
|
|
68
|
+
right: margin[1] * 20,
|
|
69
|
+
bottom: margin[0] * 20,
|
|
70
|
+
left: margin[1] * 20,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
top: margin[0] * 20,
|
|
75
|
+
right: margin[1] * 20,
|
|
76
|
+
bottom: margin[2] * 20,
|
|
77
|
+
left: margin[3] * 20,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Map percentage column width to DOCX table column width. */
|
|
82
|
+
function getColumnWidth(
|
|
83
|
+
width?: number | string,
|
|
84
|
+
): { size: number; type: unknown } | undefined {
|
|
85
|
+
if (width == null) return undefined
|
|
86
|
+
if (typeof width === 'number') return undefined
|
|
87
|
+
const match = width.match(/^(\d+)%$/)
|
|
88
|
+
if (!match) return undefined
|
|
89
|
+
return { size: Number.parseInt(match[1]!, 10) * 100, type: 'pct' as unknown }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Shared context passed to per-node-type render helpers. */
|
|
93
|
+
interface DocxCtx {
|
|
94
|
+
docx: typeof import('docx')
|
|
95
|
+
children: unknown[]
|
|
96
|
+
alignmentMap: (align?: string) => unknown
|
|
97
|
+
processListItems: (
|
|
98
|
+
n: DocNode,
|
|
99
|
+
listRef: string,
|
|
100
|
+
level: number,
|
|
101
|
+
ordered: boolean,
|
|
102
|
+
) => void
|
|
103
|
+
nextListId: () => string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderHeading(ctx: DocxCtx, n: DocNode): void {
|
|
107
|
+
const { docx, children, alignmentMap } = ctx
|
|
108
|
+
const p = n.props
|
|
109
|
+
const level = (p.level as number) ?? 1
|
|
110
|
+
const headingMap: Record<number, unknown> = {
|
|
111
|
+
1: docx.HeadingLevel.HEADING_1,
|
|
112
|
+
2: docx.HeadingLevel.HEADING_2,
|
|
113
|
+
3: docx.HeadingLevel.HEADING_3,
|
|
114
|
+
4: docx.HeadingLevel.HEADING_4,
|
|
115
|
+
5: docx.HeadingLevel.HEADING_5,
|
|
116
|
+
6: docx.HeadingLevel.HEADING_6,
|
|
117
|
+
}
|
|
118
|
+
children.push(
|
|
119
|
+
new docx.Paragraph({
|
|
120
|
+
heading: (headingMap[level] ?? docx.HeadingLevel.HEADING_1) as any,
|
|
121
|
+
children: [
|
|
122
|
+
new docx.TextRun({
|
|
123
|
+
text: getTextContent(n.children),
|
|
124
|
+
bold: true,
|
|
125
|
+
color: (p.color as string)?.replace('#', '') ?? '000000',
|
|
126
|
+
}),
|
|
127
|
+
],
|
|
128
|
+
alignment: alignmentMap(p.align as string) as any,
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderTextNode(ctx: DocxCtx, n: DocNode): void {
|
|
134
|
+
const { docx, children, alignmentMap } = ctx
|
|
135
|
+
const p = n.props
|
|
136
|
+
children.push(
|
|
137
|
+
new docx.Paragraph({
|
|
138
|
+
children: [
|
|
139
|
+
new docx.TextRun({
|
|
140
|
+
text: getTextContent(n.children),
|
|
141
|
+
bold: p.bold as boolean | undefined,
|
|
142
|
+
italics: p.italic as boolean | undefined,
|
|
143
|
+
underline: p.underline ? {} : undefined,
|
|
144
|
+
strike: p.strikethrough as boolean | undefined,
|
|
145
|
+
size: p.size ? (p.size as number) * 2 : undefined,
|
|
146
|
+
color: (p.color as string)?.replace('#', '') ?? '333333',
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
alignment: alignmentMap(p.align as string) as any,
|
|
150
|
+
spacing: { after: 120 },
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderLink(ctx: DocxCtx, n: DocNode): void {
|
|
156
|
+
const { docx, children } = ctx
|
|
157
|
+
const p = n.props
|
|
158
|
+
children.push(
|
|
159
|
+
new docx.Paragraph({
|
|
160
|
+
children: [
|
|
161
|
+
new docx.ExternalHyperlink({
|
|
162
|
+
link: p.href as string,
|
|
163
|
+
children: [
|
|
164
|
+
new docx.TextRun({
|
|
165
|
+
text: getTextContent(n.children),
|
|
166
|
+
color: (p.color as string)?.replace('#', '') ?? '4f46e5',
|
|
167
|
+
underline: { type: docx.UnderlineType.SINGLE },
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderImage(ctx: DocxCtx, n: DocNode): void {
|
|
177
|
+
const { docx, children, alignmentMap } = ctx
|
|
178
|
+
const p = n.props
|
|
179
|
+
const src = p.src as string
|
|
180
|
+
const parsed = parseDataUrl(src)
|
|
181
|
+
|
|
182
|
+
if (parsed) {
|
|
183
|
+
const imgWidth = (p.width as number) ?? 300
|
|
184
|
+
const imgHeight = (p.height as number) ?? 200
|
|
185
|
+
children.push(
|
|
186
|
+
new docx.Paragraph({
|
|
187
|
+
children: [
|
|
188
|
+
new docx.ImageRun({
|
|
189
|
+
data: Buffer.from(parsed.data, 'base64'),
|
|
190
|
+
transformation: { width: imgWidth, height: imgHeight },
|
|
191
|
+
type: parsed.mime === 'image/png' ? 'png' : 'jpg',
|
|
192
|
+
}),
|
|
193
|
+
],
|
|
194
|
+
alignment: alignmentMap(p.align as string) as any,
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
if (p.caption) {
|
|
198
|
+
children.push(
|
|
199
|
+
new docx.Paragraph({
|
|
200
|
+
children: [
|
|
201
|
+
new docx.TextRun({
|
|
202
|
+
text: p.caption as string,
|
|
203
|
+
italics: true,
|
|
204
|
+
size: 20,
|
|
205
|
+
color: '666666',
|
|
206
|
+
}),
|
|
207
|
+
],
|
|
208
|
+
alignment: alignmentMap(p.align as string) as any,
|
|
209
|
+
spacing: { after: 120 },
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
const alt = (p.alt as string) ?? 'Image'
|
|
215
|
+
const caption = p.caption ? ` — ${p.caption}` : ''
|
|
216
|
+
children.push(
|
|
217
|
+
new docx.Paragraph({
|
|
218
|
+
children: [
|
|
219
|
+
new docx.TextRun({
|
|
220
|
+
text: `[${alt}${caption}]`,
|
|
221
|
+
italics: true,
|
|
222
|
+
color: '999999',
|
|
223
|
+
}),
|
|
224
|
+
],
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderDocxTable(ctx: DocxCtx, n: DocNode): void {
|
|
231
|
+
const { docx, children, alignmentMap } = ctx
|
|
232
|
+
const p = n.props
|
|
233
|
+
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
|
|
234
|
+
resolveColumn,
|
|
235
|
+
)
|
|
236
|
+
const rows = (p.rows ?? []) as (string | number)[][]
|
|
237
|
+
const hs = p.headerStyle as
|
|
238
|
+
| { background?: string; color?: string }
|
|
239
|
+
| undefined
|
|
240
|
+
const bordered = p.bordered as boolean | undefined
|
|
241
|
+
const borderStyle = bordered
|
|
242
|
+
? { style: docx.BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
|
|
243
|
+
: undefined
|
|
244
|
+
const cellBorders = borderStyle
|
|
245
|
+
? {
|
|
246
|
+
top: borderStyle,
|
|
247
|
+
bottom: borderStyle,
|
|
248
|
+
left: borderStyle,
|
|
249
|
+
right: borderStyle,
|
|
250
|
+
}
|
|
251
|
+
: undefined
|
|
252
|
+
|
|
253
|
+
const headerRow = new docx.TableRow({
|
|
254
|
+
tableHeader: true,
|
|
255
|
+
children: columns.map(
|
|
256
|
+
(col) =>
|
|
257
|
+
new docx.TableCell({
|
|
258
|
+
children: [
|
|
259
|
+
new docx.Paragraph({
|
|
260
|
+
children: [
|
|
261
|
+
new docx.TextRun({
|
|
262
|
+
text: col.header,
|
|
263
|
+
bold: true,
|
|
264
|
+
color: hs?.color?.replace('#', '') ?? '000000',
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
alignment: alignmentMap(col.align) as any,
|
|
268
|
+
}),
|
|
269
|
+
],
|
|
270
|
+
shading: hs?.background
|
|
271
|
+
? {
|
|
272
|
+
fill: hs.background.replace('#', ''),
|
|
273
|
+
type: docx.ShadingType.SOLID,
|
|
274
|
+
}
|
|
275
|
+
: undefined,
|
|
276
|
+
borders: cellBorders,
|
|
277
|
+
width: getColumnWidth(col.width as string | undefined) as any,
|
|
278
|
+
}),
|
|
279
|
+
),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const dataRows = rows.map(
|
|
283
|
+
(row, rowIdx) =>
|
|
284
|
+
new docx.TableRow({
|
|
285
|
+
children: columns.map(
|
|
286
|
+
(col, colIdx) =>
|
|
287
|
+
new docx.TableCell({
|
|
288
|
+
children: [
|
|
289
|
+
new docx.Paragraph({
|
|
290
|
+
children: [
|
|
291
|
+
new docx.TextRun({ text: String(row[colIdx] ?? '') }),
|
|
292
|
+
],
|
|
293
|
+
alignment: alignmentMap(col.align) as any,
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
shading:
|
|
297
|
+
p.striped && rowIdx % 2 === 1
|
|
298
|
+
? { fill: 'F9F9F9', type: docx.ShadingType.SOLID }
|
|
299
|
+
: undefined,
|
|
300
|
+
borders: cellBorders,
|
|
301
|
+
width: getColumnWidth(col.width as string | undefined) as any,
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
if (p.caption) {
|
|
308
|
+
children.push(
|
|
309
|
+
new docx.Paragraph({
|
|
310
|
+
children: [
|
|
311
|
+
new docx.TextRun({
|
|
312
|
+
text: p.caption as string,
|
|
313
|
+
italics: true,
|
|
314
|
+
size: 20,
|
|
315
|
+
}),
|
|
316
|
+
],
|
|
317
|
+
spacing: { after: 60 },
|
|
318
|
+
}),
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
children.push(
|
|
323
|
+
new docx.Table({
|
|
324
|
+
rows: [headerRow, ...dataRows],
|
|
325
|
+
width: { size: 100, type: docx.WidthType.PERCENTAGE },
|
|
326
|
+
}),
|
|
327
|
+
)
|
|
328
|
+
children.push(new docx.Paragraph({ text: '', spacing: { after: 120 } }))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderList(ctx: DocxCtx, n: DocNode): void {
|
|
332
|
+
const { docx, children, processListItems, nextListId } = ctx
|
|
333
|
+
const ordered = n.props.ordered as boolean | undefined
|
|
334
|
+
const listRef = nextListId()
|
|
335
|
+
processListItems(n, listRef, 0, ordered ?? false)
|
|
336
|
+
children.push(new docx.Paragraph({ text: '', spacing: { after: 60 } }))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {
|
|
340
|
+
const { docx, children } = ctx
|
|
341
|
+
const p = n.props
|
|
342
|
+
const text = getTextContent(n.children)
|
|
343
|
+
if (n.type === 'button') {
|
|
344
|
+
children.push(
|
|
345
|
+
new docx.Paragraph({
|
|
346
|
+
children: [
|
|
347
|
+
new docx.ExternalHyperlink({
|
|
348
|
+
link: p.href as string,
|
|
349
|
+
children: [
|
|
350
|
+
new docx.TextRun({
|
|
351
|
+
text,
|
|
352
|
+
bold: true,
|
|
353
|
+
color: '4F46E5',
|
|
354
|
+
underline: { type: docx.UnderlineType.SINGLE },
|
|
355
|
+
}),
|
|
356
|
+
],
|
|
357
|
+
}),
|
|
358
|
+
],
|
|
359
|
+
spacing: { after: 120 },
|
|
360
|
+
}),
|
|
361
|
+
)
|
|
362
|
+
} else {
|
|
363
|
+
children.push(
|
|
364
|
+
new docx.Paragraph({
|
|
365
|
+
children: [new docx.TextRun({ text, italics: true, color: '555555' })],
|
|
366
|
+
indent: { left: 720 },
|
|
367
|
+
border: {
|
|
368
|
+
left: {
|
|
369
|
+
style: docx.BorderStyle.SINGLE,
|
|
370
|
+
size: 6,
|
|
371
|
+
color: (p.borderColor as string)?.replace('#', '') ?? 'DDDDDD',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
spacing: { after: 120 },
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const docxRenderer: DocumentRenderer = {
|
|
381
|
+
async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
|
|
382
|
+
const docx = await import('docx')
|
|
383
|
+
const children: unknown[] = []
|
|
384
|
+
let listCounter = 0
|
|
385
|
+
|
|
386
|
+
function alignmentMap(align?: string): unknown {
|
|
387
|
+
if (!align) return undefined
|
|
388
|
+
const map: Record<string, unknown> = {
|
|
389
|
+
left: docx.AlignmentType.LEFT,
|
|
390
|
+
center: docx.AlignmentType.CENTER,
|
|
391
|
+
right: docx.AlignmentType.RIGHT,
|
|
392
|
+
justify: docx.AlignmentType.JUSTIFIED,
|
|
393
|
+
}
|
|
394
|
+
return map[align]
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function processListItems(
|
|
398
|
+
n: DocNode,
|
|
399
|
+
listRef: string,
|
|
400
|
+
level: number,
|
|
401
|
+
ordered: boolean,
|
|
402
|
+
): void {
|
|
403
|
+
const items = n.children.filter(
|
|
404
|
+
(c): c is DocNode => typeof c !== 'string',
|
|
405
|
+
)
|
|
406
|
+
for (const item of items) {
|
|
407
|
+
const nestedList = item.children.find(
|
|
408
|
+
(c): c is DocNode =>
|
|
409
|
+
typeof c !== 'string' && (c as DocNode).type === 'list',
|
|
410
|
+
)
|
|
411
|
+
const textChildren = item.children.filter(
|
|
412
|
+
(c) => typeof c === 'string' || (c as DocNode).type !== 'list',
|
|
413
|
+
)
|
|
414
|
+
children.push(
|
|
415
|
+
new docx.Paragraph({
|
|
416
|
+
children: [
|
|
417
|
+
new docx.TextRun({ text: getTextContent(textChildren) }),
|
|
418
|
+
],
|
|
419
|
+
numbering: ordered ? { reference: listRef, level } : undefined,
|
|
420
|
+
bullet: ordered ? undefined : { level },
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
423
|
+
if (nestedList) {
|
|
424
|
+
const nestedOrdered = (nestedList as DocNode).props.ordered as
|
|
425
|
+
| boolean
|
|
426
|
+
| undefined
|
|
427
|
+
processListItems(
|
|
428
|
+
nestedList as DocNode,
|
|
429
|
+
listRef,
|
|
430
|
+
level + 1,
|
|
431
|
+
nestedOrdered ?? false,
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const ctx: DocxCtx = {
|
|
438
|
+
docx,
|
|
439
|
+
children,
|
|
440
|
+
alignmentMap,
|
|
441
|
+
processListItems,
|
|
442
|
+
nextListId: () => `list-${listCounter++}`,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function processNode(n: DocNode): void {
|
|
446
|
+
switch (n.type) {
|
|
447
|
+
case 'document':
|
|
448
|
+
case 'page':
|
|
449
|
+
case 'section':
|
|
450
|
+
case 'row':
|
|
451
|
+
case 'column':
|
|
452
|
+
for (const child of n.children) {
|
|
453
|
+
if (typeof child !== 'string') processNode(child)
|
|
454
|
+
else children.push(new docx.Paragraph({ text: child }))
|
|
455
|
+
}
|
|
456
|
+
break
|
|
457
|
+
case 'heading':
|
|
458
|
+
renderHeading(ctx, n)
|
|
459
|
+
break
|
|
460
|
+
case 'text':
|
|
461
|
+
renderTextNode(ctx, n)
|
|
462
|
+
break
|
|
463
|
+
case 'link':
|
|
464
|
+
renderLink(ctx, n)
|
|
465
|
+
break
|
|
466
|
+
case 'image':
|
|
467
|
+
renderImage(ctx, n)
|
|
468
|
+
break
|
|
469
|
+
case 'table':
|
|
470
|
+
renderDocxTable(ctx, n)
|
|
471
|
+
break
|
|
472
|
+
case 'list':
|
|
473
|
+
renderList(ctx, n)
|
|
474
|
+
break
|
|
475
|
+
case 'code':
|
|
476
|
+
children.push(
|
|
477
|
+
new docx.Paragraph({
|
|
478
|
+
children: [
|
|
479
|
+
new docx.TextRun({
|
|
480
|
+
text: getTextContent(n.children),
|
|
481
|
+
font: 'Courier New',
|
|
482
|
+
size: 20,
|
|
483
|
+
}),
|
|
484
|
+
],
|
|
485
|
+
shading: { fill: 'F5F5F5', type: docx.ShadingType.SOLID },
|
|
486
|
+
spacing: { after: 120 },
|
|
487
|
+
}),
|
|
488
|
+
)
|
|
489
|
+
break
|
|
490
|
+
case 'divider':
|
|
491
|
+
children.push(
|
|
492
|
+
new docx.Paragraph({
|
|
493
|
+
border: {
|
|
494
|
+
bottom: {
|
|
495
|
+
style: docx.BorderStyle.SINGLE,
|
|
496
|
+
size: (n.props.thickness as number | undefined) ?? 1,
|
|
497
|
+
color:
|
|
498
|
+
(n.props.color as string)?.replace('#', '') ?? 'DDDDDD',
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
spacing: { before: 120, after: 120 },
|
|
502
|
+
}),
|
|
503
|
+
)
|
|
504
|
+
break
|
|
505
|
+
case 'spacer':
|
|
506
|
+
children.push(
|
|
507
|
+
new docx.Paragraph({
|
|
508
|
+
text: '',
|
|
509
|
+
spacing: { after: (n.props.height as number) * 20 },
|
|
510
|
+
}),
|
|
511
|
+
)
|
|
512
|
+
break
|
|
513
|
+
case 'button':
|
|
514
|
+
case 'quote':
|
|
515
|
+
renderButtonOrQuote(ctx, n)
|
|
516
|
+
break
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
processNode(node)
|
|
521
|
+
|
|
522
|
+
// Build numbering configs for all lists
|
|
523
|
+
const numberingConfigs: unknown[] = []
|
|
524
|
+
for (let i = 0; i < listCounter; i++) {
|
|
525
|
+
numberingConfigs.push({
|
|
526
|
+
reference: `list-${i}`,
|
|
527
|
+
levels: Array.from({ length: 9 }, (_, level) => ({
|
|
528
|
+
level,
|
|
529
|
+
format: docx.LevelFormat.DECIMAL,
|
|
530
|
+
text: `%${level + 1}.`,
|
|
531
|
+
alignment: docx.AlignmentType.LEFT,
|
|
532
|
+
style: {
|
|
533
|
+
paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } },
|
|
534
|
+
},
|
|
535
|
+
})),
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Extract page properties from first page node
|
|
540
|
+
const pageNode =
|
|
541
|
+
node.type === 'document'
|
|
542
|
+
? (node.children.find(
|
|
543
|
+
(c): c is DocNode =>
|
|
544
|
+
typeof c !== 'string' && (c as DocNode).type === 'page',
|
|
545
|
+
) as DocNode | undefined)
|
|
546
|
+
: node.type === 'page'
|
|
547
|
+
? node
|
|
548
|
+
: undefined
|
|
549
|
+
|
|
550
|
+
const pageProps = pageNode?.props ?? {}
|
|
551
|
+
const pageDims = getPageSize(
|
|
552
|
+
pageProps.size as PageSize | undefined,
|
|
553
|
+
pageProps.orientation as PageOrientation | undefined,
|
|
554
|
+
)
|
|
555
|
+
const pageMargins = getPageMargins(
|
|
556
|
+
pageProps.margin as
|
|
557
|
+
| number
|
|
558
|
+
| [number, number]
|
|
559
|
+
| [number, number, number, number]
|
|
560
|
+
| undefined,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
function buildHeaderFooter(
|
|
564
|
+
contentNode: DocNode | undefined,
|
|
565
|
+
): unknown[] | undefined {
|
|
566
|
+
if (!contentNode) return undefined
|
|
567
|
+
const text = getTextContent(contentNode.children)
|
|
568
|
+
if (!text) return undefined
|
|
569
|
+
return [
|
|
570
|
+
new docx.Paragraph({
|
|
571
|
+
children: [new docx.TextRun({ text, size: 18, color: '999999' })],
|
|
572
|
+
alignment: docx.AlignmentType.CENTER,
|
|
573
|
+
}),
|
|
574
|
+
]
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const headerContent = buildHeaderFooter(
|
|
578
|
+
pageProps.header as DocNode | undefined,
|
|
579
|
+
)
|
|
580
|
+
const footerContent = buildHeaderFooter(
|
|
581
|
+
pageProps.footer as DocNode | undefined,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
const sectionProperties: Record<string, unknown> = {}
|
|
585
|
+
if (pageDims) {
|
|
586
|
+
sectionProperties.page = { size: pageDims, margin: pageMargins }
|
|
587
|
+
} else if (pageMargins) {
|
|
588
|
+
sectionProperties.page = { margin: pageMargins }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const doc = new docx.Document({
|
|
592
|
+
numbering: (numberingConfigs.length > 0
|
|
593
|
+
? { config: numberingConfigs }
|
|
594
|
+
: undefined) as any,
|
|
595
|
+
sections: [
|
|
596
|
+
{
|
|
597
|
+
properties: sectionProperties,
|
|
598
|
+
headers: headerContent
|
|
599
|
+
? { default: new docx.Header({ children: headerContent as any }) }
|
|
600
|
+
: undefined,
|
|
601
|
+
footers: footerContent
|
|
602
|
+
? { default: new docx.Footer({ children: footerContent as any }) }
|
|
603
|
+
: undefined,
|
|
604
|
+
children: children as any,
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
const buffer = await docx.Packer.toBuffer(doc)
|
|
610
|
+
return new Uint8Array(buffer)
|
|
611
|
+
},
|
|
612
|
+
}
|