@pyreon/document 0.11.5 → 0.11.6
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/README.md +7 -4
- 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/docx-uNAel545.js.map +1 -1
- package/lib/email-D0bbfWq4.js.map +1 -1
- package/lib/google-chat-CkKCBUWC.js.map +1 -1
- package/lib/html-B5biprN2.js.map +1 -1
- 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-IuBgTb3T.js.map +1 -1
- package/lib/pptx-DXiMiYFM.js.map +1 -1
- 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 +27 -27
- package/lib/whatsapp-CjSGoOKx.js.map +1 -1
- package/lib/xlsx-Cvu4LBNy.js.map +1 -1
- package/package.json +21 -21
- package/src/builder.ts +36 -36
- package/src/download.ts +32 -32
- package/src/index.ts +5 -10
- package/src/nodes.ts +45 -45
- package/src/render.ts +43 -43
- package/src/renderers/confluence.ts +63 -63
- package/src/renderers/csv.ts +10 -10
- package/src/renderers/discord.ts +37 -37
- package/src/renderers/docx.ts +57 -57
- package/src/renderers/email.ts +72 -72
- package/src/renderers/google-chat.ts +34 -34
- package/src/renderers/html.ts +76 -76
- package/src/renderers/markdown.ts +42 -42
- package/src/renderers/notion.ts +60 -60
- package/src/renderers/pdf.ts +78 -78
- package/src/renderers/pptx.ts +51 -51
- package/src/renderers/slack.ts +48 -48
- package/src/renderers/svg.ts +47 -47
- package/src/renderers/teams.ts +67 -67
- package/src/renderers/telegram.ts +39 -39
- package/src/renderers/text.ts +43 -43
- package/src/renderers/whatsapp.ts +33 -33
- package/src/renderers/xlsx.ts +35 -35
- package/src/sanitize.ts +20 -20
- package/src/tests/document.test.ts +1302 -1302
- package/src/tests/stress.test.ts +110 -110
- package/src/types.ts +61 -61
package/src/renderers/docx.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeHref, sanitizeXmlColor } from
|
|
1
|
+
import { sanitizeHref, sanitizeXmlColor } from '../sanitize'
|
|
2
2
|
import type {
|
|
3
3
|
DocChild,
|
|
4
4
|
DocNode,
|
|
@@ -7,20 +7,20 @@ import type {
|
|
|
7
7
|
PageSize,
|
|
8
8
|
RenderOptions,
|
|
9
9
|
TableColumn,
|
|
10
|
-
} from
|
|
10
|
+
} from '../types'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* DOCX renderer — lazy-loads the 'docx' npm package on first use.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
17
|
-
return typeof col ===
|
|
17
|
+
return typeof col === 'string' ? { header: col } : col
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function getTextContent(children: DocChild[]): string {
|
|
21
21
|
return children
|
|
22
|
-
.map((c) => (typeof c ===
|
|
23
|
-
.join(
|
|
22
|
+
.map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
|
|
23
|
+
.join('')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Parse a data URL and return the base64 data and media type, or null for external URLs. */
|
|
@@ -46,7 +46,7 @@ function getPageSize(
|
|
|
46
46
|
}
|
|
47
47
|
const dims = sizes[size]
|
|
48
48
|
if (!dims) return undefined
|
|
49
|
-
if (orientation ===
|
|
49
|
+
if (orientation === 'landscape') {
|
|
50
50
|
return { width: dims.height, height: dims.width }
|
|
51
51
|
}
|
|
52
52
|
return dims
|
|
@@ -57,7 +57,7 @@ function getPageMargins(
|
|
|
57
57
|
margin?: number | [number, number] | [number, number, number, number],
|
|
58
58
|
): object | undefined {
|
|
59
59
|
if (margin == null) return undefined
|
|
60
|
-
if (typeof margin ===
|
|
60
|
+
if (typeof margin === 'number') {
|
|
61
61
|
const twips = margin * 20
|
|
62
62
|
return { top: twips, right: twips, bottom: twips, left: twips }
|
|
63
63
|
}
|
|
@@ -80,15 +80,15 @@ function getPageMargins(
|
|
|
80
80
|
/** Map percentage column width to DOCX table column width. */
|
|
81
81
|
function getColumnWidth(width?: number | string): { size: number; type: unknown } | undefined {
|
|
82
82
|
if (width == null) return undefined
|
|
83
|
-
if (typeof width ===
|
|
83
|
+
if (typeof width === 'number') return undefined
|
|
84
84
|
const match = width.match(/^(\d+)%$/)
|
|
85
85
|
if (!match) return undefined
|
|
86
|
-
return { size: Number.parseInt(match[1]!, 10) * 100, type:
|
|
86
|
+
return { size: Number.parseInt(match[1]!, 10) * 100, type: 'pct' as unknown }
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** Shared context passed to per-node-type render helpers. */
|
|
90
90
|
interface DocxCtx {
|
|
91
|
-
docx: typeof import(
|
|
91
|
+
docx: typeof import('docx')
|
|
92
92
|
children: unknown[]
|
|
93
93
|
alignmentMap: (align?: string) => unknown
|
|
94
94
|
processListItems: (n: DocNode, listRef: string, level: number, ordered: boolean) => void
|
|
@@ -135,7 +135,7 @@ function renderTextNode(ctx: DocxCtx, n: DocNode): void {
|
|
|
135
135
|
...(p.underline ? { underline: {} } : {}),
|
|
136
136
|
...(p.strikethrough != null ? { strike: p.strikethrough as boolean } : {}),
|
|
137
137
|
...(p.size != null ? { size: (p.size as number) * 2 } : {}),
|
|
138
|
-
color: sanitizeXmlColor(p.color as string,
|
|
138
|
+
color: sanitizeXmlColor(p.color as string, '333333'),
|
|
139
139
|
}),
|
|
140
140
|
],
|
|
141
141
|
alignment: alignmentMap(p.align as string) as any,
|
|
@@ -155,7 +155,7 @@ function renderLink(ctx: DocxCtx, n: DocNode): void {
|
|
|
155
155
|
children: [
|
|
156
156
|
new docx.TextRun({
|
|
157
157
|
text: getTextContent(n.children),
|
|
158
|
-
color: sanitizeXmlColor(p.color as string,
|
|
158
|
+
color: sanitizeXmlColor(p.color as string, '4f46e5'),
|
|
159
159
|
underline: { type: docx.UnderlineType.SINGLE },
|
|
160
160
|
}),
|
|
161
161
|
],
|
|
@@ -178,9 +178,9 @@ function renderImage(ctx: DocxCtx, n: DocNode): void {
|
|
|
178
178
|
new docx.Paragraph({
|
|
179
179
|
children: [
|
|
180
180
|
new docx.ImageRun({
|
|
181
|
-
data: Buffer.from(parsed.data,
|
|
181
|
+
data: Buffer.from(parsed.data, 'base64'),
|
|
182
182
|
transformation: { width: imgWidth, height: imgHeight },
|
|
183
|
-
type: parsed.mime ===
|
|
183
|
+
type: parsed.mime === 'image/png' ? 'png' : 'jpg',
|
|
184
184
|
}),
|
|
185
185
|
],
|
|
186
186
|
alignment: alignmentMap(p.align as string) as any,
|
|
@@ -194,7 +194,7 @@ function renderImage(ctx: DocxCtx, n: DocNode): void {
|
|
|
194
194
|
text: p.caption as string,
|
|
195
195
|
italics: true,
|
|
196
196
|
size: 20,
|
|
197
|
-
color:
|
|
197
|
+
color: '666666',
|
|
198
198
|
}),
|
|
199
199
|
],
|
|
200
200
|
alignment: alignmentMap(p.align as string) as any,
|
|
@@ -203,15 +203,15 @@ function renderImage(ctx: DocxCtx, n: DocNode): void {
|
|
|
203
203
|
)
|
|
204
204
|
}
|
|
205
205
|
} else {
|
|
206
|
-
const alt = (p.alt as string) ??
|
|
207
|
-
const caption = p.caption ? ` — ${p.caption}` :
|
|
206
|
+
const alt = (p.alt as string) ?? 'Image'
|
|
207
|
+
const caption = p.caption ? ` — ${p.caption}` : ''
|
|
208
208
|
children.push(
|
|
209
209
|
new docx.Paragraph({
|
|
210
210
|
children: [
|
|
211
211
|
new docx.TextRun({
|
|
212
212
|
text: `[${alt}${caption}]`,
|
|
213
213
|
italics: true,
|
|
214
|
-
color:
|
|
214
|
+
color: '999999',
|
|
215
215
|
}),
|
|
216
216
|
],
|
|
217
217
|
}),
|
|
@@ -227,7 +227,7 @@ function renderDocxTable(ctx: DocxCtx, n: DocNode): void {
|
|
|
227
227
|
const hs = p.headerStyle as { background?: string; color?: string } | undefined
|
|
228
228
|
const bordered = p.bordered as boolean | undefined
|
|
229
229
|
const borderStyle = bordered
|
|
230
|
-
? { style: docx.BorderStyle.SINGLE, size: 1, color:
|
|
230
|
+
? { style: docx.BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
|
|
231
231
|
: undefined
|
|
232
232
|
const cellBorders = borderStyle
|
|
233
233
|
? {
|
|
@@ -277,12 +277,12 @@ function renderDocxTable(ctx: DocxCtx, n: DocNode): void {
|
|
|
277
277
|
new docx.TableCell({
|
|
278
278
|
children: [
|
|
279
279
|
new docx.Paragraph({
|
|
280
|
-
children: [new docx.TextRun({ text: String(row[colIdx] ??
|
|
280
|
+
children: [new docx.TextRun({ text: String(row[colIdx] ?? '') })],
|
|
281
281
|
alignment: alignmentMap(col.align) as any,
|
|
282
282
|
}),
|
|
283
283
|
],
|
|
284
284
|
...(p.striped && rowIdx % 2 === 1
|
|
285
|
-
? { shading: { fill:
|
|
285
|
+
? { shading: { fill: 'F9F9F9', type: docx.ShadingType.SOLID } }
|
|
286
286
|
: {}),
|
|
287
287
|
...(cellBorders != null ? { borders: cellBorders } : {}),
|
|
288
288
|
width: getColumnWidth(col.width as string | undefined) as any,
|
|
@@ -312,7 +312,7 @@ function renderDocxTable(ctx: DocxCtx, n: DocNode): void {
|
|
|
312
312
|
width: { size: 100, type: docx.WidthType.PERCENTAGE },
|
|
313
313
|
}),
|
|
314
314
|
)
|
|
315
|
-
children.push(new docx.Paragraph({ text:
|
|
315
|
+
children.push(new docx.Paragraph({ text: '', spacing: { after: 120 } }))
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
function renderList(ctx: DocxCtx, n: DocNode): void {
|
|
@@ -320,14 +320,14 @@ function renderList(ctx: DocxCtx, n: DocNode): void {
|
|
|
320
320
|
const ordered = n.props.ordered as boolean | undefined
|
|
321
321
|
const listRef = nextListId()
|
|
322
322
|
processListItems(n, listRef, 0, ordered ?? false)
|
|
323
|
-
children.push(new docx.Paragraph({ text:
|
|
323
|
+
children.push(new docx.Paragraph({ text: '', spacing: { after: 60 } }))
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
function renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {
|
|
327
327
|
const { docx, children } = ctx
|
|
328
328
|
const p = n.props
|
|
329
329
|
const text = getTextContent(n.children)
|
|
330
|
-
if (n.type ===
|
|
330
|
+
if (n.type === 'button') {
|
|
331
331
|
children.push(
|
|
332
332
|
new docx.Paragraph({
|
|
333
333
|
children: [
|
|
@@ -337,7 +337,7 @@ function renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {
|
|
|
337
337
|
new docx.TextRun({
|
|
338
338
|
text,
|
|
339
339
|
bold: true,
|
|
340
|
-
color:
|
|
340
|
+
color: '4F46E5',
|
|
341
341
|
underline: { type: docx.UnderlineType.SINGLE },
|
|
342
342
|
}),
|
|
343
343
|
],
|
|
@@ -349,13 +349,13 @@ function renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {
|
|
|
349
349
|
} else {
|
|
350
350
|
children.push(
|
|
351
351
|
new docx.Paragraph({
|
|
352
|
-
children: [new docx.TextRun({ text, italics: true, color:
|
|
352
|
+
children: [new docx.TextRun({ text, italics: true, color: '555555' })],
|
|
353
353
|
indent: { left: 720 },
|
|
354
354
|
border: {
|
|
355
355
|
left: {
|
|
356
356
|
style: docx.BorderStyle.SINGLE,
|
|
357
357
|
size: 6,
|
|
358
|
-
color: sanitizeXmlColor(p.borderColor as string,
|
|
358
|
+
color: sanitizeXmlColor(p.borderColor as string, 'DDDDDD'),
|
|
359
359
|
},
|
|
360
360
|
},
|
|
361
361
|
spacing: { after: 120 },
|
|
@@ -366,9 +366,9 @@ function renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {
|
|
|
366
366
|
|
|
367
367
|
export const docxRenderer: DocumentRenderer = {
|
|
368
368
|
async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
|
|
369
|
-
let docx: typeof import(
|
|
369
|
+
let docx: typeof import('docx')
|
|
370
370
|
try {
|
|
371
|
-
docx = await import(
|
|
371
|
+
docx = await import('docx')
|
|
372
372
|
} catch {
|
|
373
373
|
throw new Error(
|
|
374
374
|
'[@pyreon/document] DOCX renderer requires "docx" package. Install it: bun add docx',
|
|
@@ -389,13 +389,13 @@ export const docxRenderer: DocumentRenderer = {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
function processListItems(n: DocNode, listRef: string, level: number, ordered: boolean): void {
|
|
392
|
-
const items = n.children.filter((c): c is DocNode => typeof c !==
|
|
392
|
+
const items = n.children.filter((c): c is DocNode => typeof c !== 'string')
|
|
393
393
|
for (const item of items) {
|
|
394
394
|
const nestedList = item.children.find(
|
|
395
|
-
(c): c is DocNode => typeof c !==
|
|
395
|
+
(c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'list',
|
|
396
396
|
)
|
|
397
397
|
const textChildren = item.children.filter(
|
|
398
|
-
(c) => typeof c ===
|
|
398
|
+
(c) => typeof c === 'string' || (c as DocNode).type !== 'list',
|
|
399
399
|
)
|
|
400
400
|
children.push(
|
|
401
401
|
new docx.Paragraph({
|
|
@@ -420,73 +420,73 @@ export const docxRenderer: DocumentRenderer = {
|
|
|
420
420
|
|
|
421
421
|
function processNode(n: DocNode): void {
|
|
422
422
|
switch (n.type) {
|
|
423
|
-
case
|
|
424
|
-
case
|
|
425
|
-
case
|
|
426
|
-
case
|
|
427
|
-
case
|
|
423
|
+
case 'document':
|
|
424
|
+
case 'page':
|
|
425
|
+
case 'section':
|
|
426
|
+
case 'row':
|
|
427
|
+
case 'column':
|
|
428
428
|
for (const child of n.children) {
|
|
429
|
-
if (typeof child !==
|
|
429
|
+
if (typeof child !== 'string') processNode(child)
|
|
430
430
|
else children.push(new docx.Paragraph({ text: child }))
|
|
431
431
|
}
|
|
432
432
|
break
|
|
433
|
-
case
|
|
433
|
+
case 'heading':
|
|
434
434
|
renderHeading(ctx, n)
|
|
435
435
|
break
|
|
436
|
-
case
|
|
436
|
+
case 'text':
|
|
437
437
|
renderTextNode(ctx, n)
|
|
438
438
|
break
|
|
439
|
-
case
|
|
439
|
+
case 'link':
|
|
440
440
|
renderLink(ctx, n)
|
|
441
441
|
break
|
|
442
|
-
case
|
|
442
|
+
case 'image':
|
|
443
443
|
renderImage(ctx, n)
|
|
444
444
|
break
|
|
445
|
-
case
|
|
445
|
+
case 'table':
|
|
446
446
|
renderDocxTable(ctx, n)
|
|
447
447
|
break
|
|
448
|
-
case
|
|
448
|
+
case 'list':
|
|
449
449
|
renderList(ctx, n)
|
|
450
450
|
break
|
|
451
|
-
case
|
|
451
|
+
case 'code':
|
|
452
452
|
children.push(
|
|
453
453
|
new docx.Paragraph({
|
|
454
454
|
children: [
|
|
455
455
|
new docx.TextRun({
|
|
456
456
|
text: getTextContent(n.children),
|
|
457
|
-
font:
|
|
457
|
+
font: 'Courier New',
|
|
458
458
|
size: 20,
|
|
459
459
|
}),
|
|
460
460
|
],
|
|
461
|
-
shading: { fill:
|
|
461
|
+
shading: { fill: 'F5F5F5', type: docx.ShadingType.SOLID },
|
|
462
462
|
spacing: { after: 120 },
|
|
463
463
|
}),
|
|
464
464
|
)
|
|
465
465
|
break
|
|
466
|
-
case
|
|
466
|
+
case 'divider':
|
|
467
467
|
children.push(
|
|
468
468
|
new docx.Paragraph({
|
|
469
469
|
border: {
|
|
470
470
|
bottom: {
|
|
471
471
|
style: docx.BorderStyle.SINGLE,
|
|
472
472
|
size: (n.props.thickness as number | undefined) ?? 1,
|
|
473
|
-
color: sanitizeXmlColor(n.props.color as string,
|
|
473
|
+
color: sanitizeXmlColor(n.props.color as string, 'DDDDDD'),
|
|
474
474
|
},
|
|
475
475
|
},
|
|
476
476
|
spacing: { before: 120, after: 120 },
|
|
477
477
|
}),
|
|
478
478
|
)
|
|
479
479
|
break
|
|
480
|
-
case
|
|
480
|
+
case 'spacer':
|
|
481
481
|
children.push(
|
|
482
482
|
new docx.Paragraph({
|
|
483
|
-
text:
|
|
483
|
+
text: '',
|
|
484
484
|
spacing: { after: (n.props.height as number) * 20 },
|
|
485
485
|
}),
|
|
486
486
|
)
|
|
487
487
|
break
|
|
488
|
-
case
|
|
489
|
-
case
|
|
488
|
+
case 'button':
|
|
489
|
+
case 'quote':
|
|
490
490
|
renderButtonOrQuote(ctx, n)
|
|
491
491
|
break
|
|
492
492
|
}
|
|
@@ -513,11 +513,11 @@ export const docxRenderer: DocumentRenderer = {
|
|
|
513
513
|
|
|
514
514
|
// Extract page properties from first page node
|
|
515
515
|
const pageNode =
|
|
516
|
-
node.type ===
|
|
516
|
+
node.type === 'document'
|
|
517
517
|
? (node.children.find(
|
|
518
|
-
(c): c is DocNode => typeof c !==
|
|
518
|
+
(c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'page',
|
|
519
519
|
) as DocNode | undefined)
|
|
520
|
-
: node.type ===
|
|
520
|
+
: node.type === 'page'
|
|
521
521
|
? node
|
|
522
522
|
: undefined
|
|
523
523
|
|
|
@@ -536,7 +536,7 @@ export const docxRenderer: DocumentRenderer = {
|
|
|
536
536
|
if (!text) return undefined
|
|
537
537
|
return [
|
|
538
538
|
new docx.Paragraph({
|
|
539
|
-
children: [new docx.TextRun({ text, size: 18, color:
|
|
539
|
+
children: [new docx.TextRun({ text, size: 18, color: '999999' })],
|
|
540
540
|
alignment: docx.AlignmentType.CENTER,
|
|
541
541
|
}),
|
|
542
542
|
]
|
package/src/renderers/email.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Email renderer — generates table-based HTML with inline styles
|
|
@@ -15,72 +15,72 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
|
|
|
15
15
|
|
|
16
16
|
function esc(str: string): string {
|
|
17
17
|
return str
|
|
18
|
-
.replace(/&/g,
|
|
19
|
-
.replace(/</g,
|
|
20
|
-
.replace(/>/g,
|
|
21
|
-
.replace(/"/g,
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
25
|
-
return typeof col ===
|
|
25
|
+
return typeof col === 'string' ? { header: col } : col
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function renderChild(child: DocChild): string {
|
|
29
|
-
if (typeof child ===
|
|
29
|
+
if (typeof child === 'string') return esc(child)
|
|
30
30
|
return renderNode(child)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function renderChildren(children: DocChild[]): string {
|
|
34
|
-
return children.map(renderChild).join(
|
|
34
|
+
return children.map(renderChild).join('')
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function wrapInTable(content: string, style =
|
|
38
|
-
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` :
|
|
37
|
+
function wrapInTable(content: string, style = ''): string {
|
|
38
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"${style ? ` style="${style}"` : ''}><tr><td>${content}</td></tr></table>`
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function renderNode(node: DocNode): string {
|
|
42
42
|
const p = node.props
|
|
43
43
|
|
|
44
44
|
switch (node.type) {
|
|
45
|
-
case
|
|
46
|
-
const title = p.title ? `<title>${esc(p.title as string)}</title>` :
|
|
45
|
+
case 'document': {
|
|
46
|
+
const title = p.title ? `<title>${esc(p.title as string)}</title>` : ''
|
|
47
47
|
const preview = p.subject
|
|
48
48
|
? `<div style="display:none;max-height:0;overflow:hidden">${esc(p.subject as string)}</div>`
|
|
49
|
-
:
|
|
49
|
+
: ''
|
|
50
50
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${title}<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]--></head><body style="margin:0;padding:0;background-color:#f4f4f4;font-family:Arial,Helvetica,sans-serif">${preview}<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f4f4f4"><tr><td align="center" style="padding:20px 0"><table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;max-width:600px;width:100%"><tr><td>${renderChildren(node.children)}</td></tr></table></td></tr></table></body></html>`
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
case
|
|
53
|
+
case 'page':
|
|
54
54
|
// In email, pages are just content sections
|
|
55
55
|
return renderChildren(node.children)
|
|
56
56
|
|
|
57
|
-
case
|
|
58
|
-
const bg = p.background ? `background-color:${sanitizeColor(p.background as string)};` :
|
|
57
|
+
case 'section': {
|
|
58
|
+
const bg = p.background ? `background-color:${sanitizeColor(p.background as string)};` : ''
|
|
59
59
|
const pad = p.padding
|
|
60
|
-
? `padding:${typeof p.padding ===
|
|
61
|
-
:
|
|
62
|
-
const radius = p.borderRadius ? `border-radius:${p.borderRadius}px;` :
|
|
60
|
+
? `padding:${typeof p.padding === 'number' ? `${p.padding}px` : Array.isArray(p.padding) ? (p.padding as number[]).map((v) => `${v}px`).join(' ') : '0'}`
|
|
61
|
+
: 'padding:0'
|
|
62
|
+
const radius = p.borderRadius ? `border-radius:${p.borderRadius}px;` : ''
|
|
63
63
|
|
|
64
|
-
if (p.direction ===
|
|
64
|
+
if (p.direction === 'row') {
|
|
65
65
|
// Row layout via nested table
|
|
66
|
-
const children = node.children.filter((c): c is DocNode => typeof c !==
|
|
66
|
+
const children = node.children.filter((c): c is DocNode => typeof c !== 'string')
|
|
67
67
|
const colWidth = Math.floor(100 / Math.max(children.length, 1))
|
|
68
|
-
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="${bg}${radius}${pad}"><tr>${children.map((child) => `<td width="${colWidth}%" valign="top" style="padding:${(p.gap as number | undefined) ? `0 ${(p.gap as number) / 2}px` :
|
|
68
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="${bg}${radius}${pad}"><tr>${children.map((child) => `<td width="${colWidth}%" valign="top" style="padding:${(p.gap as number | undefined) ? `0 ${(p.gap as number) / 2}px` : '0'}">${renderNode(child)}</td>`).join('')}</tr></table>`
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
return wrapInTable(renderChildren(node.children), `${bg}${radius}${pad}`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
case
|
|
75
|
-
const children = node.children.filter((c): c is DocNode => typeof c !==
|
|
74
|
+
case 'row': {
|
|
75
|
+
const children = node.children.filter((c): c is DocNode => typeof c !== 'string')
|
|
76
76
|
const gap = (p.gap as number) ?? 0
|
|
77
|
-
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>${children.map((child) => `<td valign="top" style="padding:0 ${gap / 2}px">${renderNode(child)}</td>`).join(
|
|
77
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>${children.map((child) => `<td valign="top" style="padding:0 ${gap / 2}px">${renderNode(child)}</td>`).join('')}</tr></table>`
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
case
|
|
80
|
+
case 'column':
|
|
81
81
|
return renderChildren(node.children)
|
|
82
82
|
|
|
83
|
-
case
|
|
83
|
+
case 'heading': {
|
|
84
84
|
const level = (p.level as number) ?? 1
|
|
85
85
|
const sizes: Record<number, number> = {
|
|
86
86
|
1: 28,
|
|
@@ -91,37 +91,37 @@ function renderNode(node: DocNode): string {
|
|
|
91
91
|
6: 14,
|
|
92
92
|
}
|
|
93
93
|
const size = sizes[level] ?? 24
|
|
94
|
-
const color = sanitizeColor((p.color as string) ??
|
|
95
|
-
const align = (p.align as string) ??
|
|
94
|
+
const color = sanitizeColor((p.color as string) ?? '#000000')
|
|
95
|
+
const align = (p.align as string) ?? 'left'
|
|
96
96
|
return `<h${level} style="margin:0 0 12px 0;font-size:${size}px;color:${color};text-align:${align};font-weight:bold;line-height:1.3">${renderChildren(node.children)}</h${level}>`
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
case
|
|
99
|
+
case 'text': {
|
|
100
100
|
const size = (p.size as number) ?? 14
|
|
101
|
-
const color = sanitizeColor((p.color as string) ??
|
|
102
|
-
const weight = p.bold ?
|
|
103
|
-
const style = p.italic ?
|
|
104
|
-
const decoration = p.underline ?
|
|
105
|
-
const align = (p.align as string) ??
|
|
101
|
+
const color = sanitizeColor((p.color as string) ?? '#333333')
|
|
102
|
+
const weight = p.bold ? 'bold' : 'normal'
|
|
103
|
+
const style = p.italic ? 'italic' : 'normal'
|
|
104
|
+
const decoration = p.underline ? 'underline' : p.strikethrough ? 'line-through' : 'none'
|
|
105
|
+
const align = (p.align as string) ?? 'left'
|
|
106
106
|
const lh = (p.lineHeight as number) ?? 1.5
|
|
107
107
|
return `<p style="margin:0 0 12px 0;font-size:${size}px;color:${color};font-weight:${weight};font-style:${style};text-decoration:${decoration};text-align:${align};line-height:${lh}">${renderChildren(node.children)}</p>`
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
case
|
|
111
|
-
return `<a href="${esc(sanitizeHref(p.href as string))}" style="color:${sanitizeColor((p.color as string) ??
|
|
110
|
+
case 'link':
|
|
111
|
+
return `<a href="${esc(sanitizeHref(p.href as string))}" style="color:${sanitizeColor((p.color as string) ?? '#4f46e5')};text-decoration:underline" target="_blank">${renderChildren(node.children)}</a>`
|
|
112
112
|
|
|
113
|
-
case
|
|
114
|
-
const align = (p.align as string) ??
|
|
115
|
-
const img = `<img src="${esc(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` :
|
|
113
|
+
case 'image': {
|
|
114
|
+
const align = (p.align as string) ?? 'left'
|
|
115
|
+
const img = `<img src="${esc(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` : ''}${p.height ? ` height="${p.height}"` : ''} alt="${esc((p.alt as string) ?? '')}" style="display:block;outline:none;border:none;text-decoration:none${p.width ? `;max-width:${p.width}px` : ''}" />`
|
|
116
116
|
if (p.caption) {
|
|
117
|
-
return `<table cellpadding="0" cellspacing="0" border="0"${align ===
|
|
117
|
+
return `<table cellpadding="0" cellspacing="0" border="0"${align === 'center' ? ' align="center"' : ''}><tr><td>${img}</td></tr><tr><td style="font-size:12px;color:#666;padding-top:4px;text-align:center">${esc(p.caption as string)}</td></tr></table>`
|
|
118
118
|
}
|
|
119
|
-
if (align ===
|
|
120
|
-
if (align ===
|
|
119
|
+
if (align === 'center') return `<div style="text-align:center">${img}</div>`
|
|
120
|
+
if (align === 'right') return `<div style="text-align:right">${img}</div>`
|
|
121
121
|
return img
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
case
|
|
124
|
+
case 'table': {
|
|
125
125
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
126
126
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
127
127
|
const hs = p.headerStyle as
|
|
@@ -134,71 +134,71 @@ function renderNode(node: DocNode): string {
|
|
|
134
134
|
if (p.caption)
|
|
135
135
|
html += `<caption style="font-size:12px;color:#666;padding:8px;text-align:left">${esc(p.caption as string)}</caption>`
|
|
136
136
|
|
|
137
|
-
html +=
|
|
137
|
+
html += '<tr>'
|
|
138
138
|
for (const col of columns) {
|
|
139
139
|
const bg = hs?.background
|
|
140
140
|
? `background-color:${sanitizeColor(hs.background)};`
|
|
141
|
-
:
|
|
142
|
-
const color = hs?.color ? `color:${sanitizeColor(hs.color)};` :
|
|
143
|
-
const align = col.align ? `text-align:${col.align};` :
|
|
141
|
+
: 'background-color:#f5f5f5;'
|
|
142
|
+
const color = hs?.color ? `color:${sanitizeColor(hs.color)};` : ''
|
|
143
|
+
const align = col.align ? `text-align:${col.align};` : ''
|
|
144
144
|
const width = col.width
|
|
145
|
-
? `width:${typeof col.width ===
|
|
146
|
-
:
|
|
145
|
+
? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`
|
|
146
|
+
: ''
|
|
147
147
|
html += `<th style="${bg}${color}font-weight:bold;${align}${width}padding:8px;border-bottom:2px solid #ddd">${esc(col.header)}</th>`
|
|
148
148
|
}
|
|
149
|
-
html +=
|
|
149
|
+
html += '</tr>'
|
|
150
150
|
|
|
151
151
|
for (let i = 0; i < rows.length; i++) {
|
|
152
|
-
const bg = striped && i % 2 === 1 ?
|
|
153
|
-
html +=
|
|
152
|
+
const bg = striped && i % 2 === 1 ? 'background-color:#f9f9f9;' : ''
|
|
153
|
+
html += '<tr>'
|
|
154
154
|
for (let j = 0; j < columns.length; j++) {
|
|
155
155
|
const col = columns[j]
|
|
156
|
-
const align = col?.align ? `text-align:${col.align};` :
|
|
157
|
-
html += `<td style="${bg}${align}padding:8px;border-bottom:1px solid #eee">${esc(String(rows[i]?.[j] ??
|
|
156
|
+
const align = col?.align ? `text-align:${col.align};` : ''
|
|
157
|
+
html += `<td style="${bg}${align}padding:8px;border-bottom:1px solid #eee">${esc(String(rows[i]?.[j] ?? ''))}</td>`
|
|
158
158
|
}
|
|
159
|
-
html +=
|
|
159
|
+
html += '</tr>'
|
|
160
160
|
}
|
|
161
|
-
html +=
|
|
161
|
+
html += '</table>'
|
|
162
162
|
return html
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
case
|
|
166
|
-
const tag = p.ordered ?
|
|
165
|
+
case 'list': {
|
|
166
|
+
const tag = p.ordered ? 'ol' : 'ul'
|
|
167
167
|
return `<${tag} style="margin:0 0 12px 0;padding-left:24px">${renderChildren(node.children)}</${tag}>`
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
case
|
|
170
|
+
case 'list-item':
|
|
171
171
|
return `<li style="margin:0 0 4px 0;font-size:14px;color:#333">${renderChildren(node.children)}</li>`
|
|
172
172
|
|
|
173
|
-
case
|
|
173
|
+
case 'code':
|
|
174
174
|
return `<pre style="background-color:#f5f5f5;padding:12px;border-radius:4px;font-family:Courier New,monospace;font-size:13px;color:#333;overflow-x:auto;margin:0 0 12px 0"><code>${esc(renderChildren(node.children))}</code></pre>`
|
|
175
175
|
|
|
176
|
-
case
|
|
177
|
-
const color = sanitizeColor((p.color as string) ??
|
|
176
|
+
case 'divider': {
|
|
177
|
+
const color = sanitizeColor((p.color as string) ?? '#dddddd')
|
|
178
178
|
const thickness = (p.thickness as number) ?? 1
|
|
179
179
|
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0"><tr><td style="border-top:${thickness}px solid ${color};font-size:0;line-height:0"> </td></tr></table>`
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
case
|
|
182
|
+
case 'page-break':
|
|
183
183
|
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0"><tr><td style="border-top:2px solid #dddddd;font-size:0;line-height:0"> </td></tr></table>`
|
|
184
184
|
|
|
185
|
-
case
|
|
185
|
+
case 'spacer':
|
|
186
186
|
return `<div style="height:${p.height}px;line-height:${p.height}px;font-size:0"> </div>`
|
|
187
187
|
|
|
188
|
-
case
|
|
189
|
-
const bg = sanitizeColor((p.background as string) ??
|
|
190
|
-
const color = sanitizeColor((p.color as string) ??
|
|
188
|
+
case 'button': {
|
|
189
|
+
const bg = sanitizeColor((p.background as string) ?? '#4f46e5')
|
|
190
|
+
const color = sanitizeColor((p.color as string) ?? '#ffffff')
|
|
191
191
|
const radius = (p.borderRadius as number) ?? 4
|
|
192
192
|
const href = esc(sanitizeHref(p.href as string))
|
|
193
193
|
const text = renderChildren(node.children)
|
|
194
|
-
const align = (p.align as string) ??
|
|
194
|
+
const align = (p.align as string) ?? 'left'
|
|
195
195
|
|
|
196
196
|
// Bulletproof button — works in Outlook via VML, CSS everywhere else
|
|
197
197
|
return `<div style="text-align:${align};margin:12px 0"><!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:200px" arcsize="10%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${color};font-family:Arial,sans-serif;font-size:14px;font-weight:bold">${text}</center></v:roundrect><![endif]--><!--[if !mso]><!--><a href="${href}" style="display:inline-block;background-color:${bg};color:${color};padding:12px 24px;border-radius:${radius}px;text-decoration:none;font-weight:bold;font-size:14px;font-family:Arial,sans-serif" target="_blank">${text}</a><!--<![endif]--></div>`
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
case
|
|
201
|
-
const borderColor = sanitizeColor((p.borderColor as string) ??
|
|
200
|
+
case 'quote': {
|
|
201
|
+
const borderColor = sanitizeColor((p.borderColor as string) ?? '#dddddd')
|
|
202
202
|
return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:12px 0"><tr><td style="border-left:4px solid ${borderColor};padding:12px 20px;color:#555555;font-style:italic">${renderChildren(node.children)}</td></tr></table>`
|
|
203
203
|
}
|
|
204
204
|
|