@pyreon/document 0.11.4 → 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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sanitizeHref, sanitizeImageSrc } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeHref, sanitizeImageSrc } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Google Chat renderer — outputs Card V2 JSON for Google Chat API.
|
|
@@ -7,13 +7,13 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
10
|
-
return typeof col ===
|
|
10
|
+
return typeof col === 'string' ? { header: col } : col
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function getTextContent(children: DocChild[]): string {
|
|
14
14
|
return children
|
|
15
|
-
.map((c) => (typeof c ===
|
|
16
|
-
.join(
|
|
15
|
+
.map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
|
|
16
|
+
.join('')
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface CardWidget {
|
|
@@ -25,23 +25,23 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
25
25
|
const widgets: CardWidget[] = []
|
|
26
26
|
|
|
27
27
|
switch (node.type) {
|
|
28
|
-
case
|
|
29
|
-
case
|
|
30
|
-
case
|
|
31
|
-
case
|
|
32
|
-
case
|
|
28
|
+
case 'document':
|
|
29
|
+
case 'page':
|
|
30
|
+
case 'section':
|
|
31
|
+
case 'row':
|
|
32
|
+
case 'column':
|
|
33
33
|
for (const child of node.children) {
|
|
34
|
-
if (typeof child !==
|
|
34
|
+
if (typeof child !== 'string') {
|
|
35
35
|
widgets.push(...nodeToWidgets(child))
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
break
|
|
39
39
|
|
|
40
|
-
case
|
|
40
|
+
case 'heading': {
|
|
41
41
|
const text = getTextContent(node.children)
|
|
42
42
|
widgets.push({
|
|
43
43
|
decoratedText: {
|
|
44
|
-
topLabel:
|
|
44
|
+
topLabel: '',
|
|
45
45
|
text: `<b>${text}</b>`,
|
|
46
46
|
wrapText: true,
|
|
47
47
|
},
|
|
@@ -49,7 +49,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
49
49
|
break
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
case
|
|
52
|
+
case 'text': {
|
|
53
53
|
let text = getTextContent(node.children)
|
|
54
54
|
if (p.bold) text = `<b>${text}</b>`
|
|
55
55
|
if (p.italic) text = `<i>${text}</i>`
|
|
@@ -60,7 +60,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
60
60
|
break
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
case
|
|
63
|
+
case 'link': {
|
|
64
64
|
const href = sanitizeHref(p.href as string)
|
|
65
65
|
const text = getTextContent(node.children)
|
|
66
66
|
widgets.push({
|
|
@@ -69,26 +69,26 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
69
69
|
break
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
case
|
|
72
|
+
case 'image': {
|
|
73
73
|
const src = sanitizeImageSrc(p.src as string)
|
|
74
|
-
if (src.startsWith(
|
|
74
|
+
if (src.startsWith('http')) {
|
|
75
75
|
widgets.push({
|
|
76
76
|
image: {
|
|
77
77
|
imageUrl: src,
|
|
78
|
-
altText: (p.alt as string) ??
|
|
78
|
+
altText: (p.alt as string) ?? 'Image',
|
|
79
79
|
},
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
82
|
break
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
case
|
|
85
|
+
case 'table': {
|
|
86
86
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
87
87
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
88
88
|
|
|
89
89
|
// Google Chat Cards don't have native tables — use grid or formatted text
|
|
90
|
-
const header = columns.map((c) => `<b>${c.header}</b>`).join(
|
|
91
|
-
const body = rows.map((row) => row.map((c) => String(c ??
|
|
90
|
+
const header = columns.map((c) => `<b>${c.header}</b>`).join(' | ')
|
|
91
|
+
const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
|
|
92
92
|
|
|
93
93
|
widgets.push({
|
|
94
94
|
textParagraph: { text: `${header}\n${body}` },
|
|
@@ -96,22 +96,22 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
96
96
|
break
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
case
|
|
99
|
+
case 'list': {
|
|
100
100
|
const ordered = p.ordered as boolean | undefined
|
|
101
101
|
const items = node.children
|
|
102
|
-
.filter((c): c is DocNode => typeof c !==
|
|
102
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
103
103
|
.map((item, i) => {
|
|
104
|
-
const prefix = ordered ? `${i + 1}.` :
|
|
104
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
105
105
|
return `${prefix} ${getTextContent(item.children)}`
|
|
106
106
|
})
|
|
107
|
-
.join(
|
|
107
|
+
.join('\n')
|
|
108
108
|
widgets.push({
|
|
109
109
|
textParagraph: { text: items },
|
|
110
110
|
})
|
|
111
111
|
break
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
case
|
|
114
|
+
case 'code': {
|
|
115
115
|
const text = getTextContent(node.children)
|
|
116
116
|
widgets.push({
|
|
117
117
|
textParagraph: {
|
|
@@ -121,16 +121,16 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
121
121
|
break
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
case
|
|
125
|
-
case
|
|
124
|
+
case 'divider':
|
|
125
|
+
case 'page-break':
|
|
126
126
|
widgets.push({ divider: {} })
|
|
127
127
|
break
|
|
128
128
|
|
|
129
|
-
case
|
|
129
|
+
case 'spacer':
|
|
130
130
|
// No direct equivalent — skip
|
|
131
131
|
break
|
|
132
132
|
|
|
133
|
-
case
|
|
133
|
+
case 'button': {
|
|
134
134
|
const href = sanitizeHref(p.href as string)
|
|
135
135
|
const text = getTextContent(node.children)
|
|
136
136
|
widgets.push({
|
|
@@ -152,7 +152,7 @@ function nodeToWidgets(node: DocNode): CardWidget[] {
|
|
|
152
152
|
break
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
case
|
|
155
|
+
case 'quote': {
|
|
156
156
|
const text = getTextContent(node.children)
|
|
157
157
|
widgets.push({
|
|
158
158
|
textParagraph: { text: `<i>"${text}"</i>` },
|
|
@@ -169,10 +169,10 @@ export const googleChatRenderer: DocumentRenderer = {
|
|
|
169
169
|
const widgets = nodeToWidgets(node)
|
|
170
170
|
|
|
171
171
|
// Extract title from first heading or document title
|
|
172
|
-
let title = (node.props.title as string) ??
|
|
172
|
+
let title = (node.props.title as string) ?? ''
|
|
173
173
|
if (!title) {
|
|
174
174
|
const firstHeading = node.children.find(
|
|
175
|
-
(c): c is DocNode => typeof c !==
|
|
175
|
+
(c): c is DocNode => typeof c !== 'string' && c.type === 'heading',
|
|
176
176
|
)
|
|
177
177
|
if (firstHeading) title = getTextContent(firstHeading.children)
|
|
178
178
|
}
|
|
@@ -180,7 +180,7 @@ export const googleChatRenderer: DocumentRenderer = {
|
|
|
180
180
|
const card = {
|
|
181
181
|
cardsV2: [
|
|
182
182
|
{
|
|
183
|
-
cardId:
|
|
183
|
+
cardId: 'document',
|
|
184
184
|
card: {
|
|
185
185
|
header: title
|
|
186
186
|
? { title, subtitle: (node.props.subject as string) ?? undefined }
|
package/src/renderers/html.ts
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { sanitizeColor, sanitizeHref, sanitizeImageSrc, sanitizeStyle } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeColor, sanitizeHref, sanitizeImageSrc, sanitizeStyle } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
function escapeHtml(str: string): string {
|
|
5
5
|
return str
|
|
6
|
-
.replace(/&/g,
|
|
7
|
-
.replace(/</g,
|
|
8
|
-
.replace(/>/g,
|
|
9
|
-
.replace(/"/g,
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
13
|
-
return typeof col ===
|
|
13
|
+
return typeof col === 'string' ? { header: col } : col
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function styleStr(styles: Record<string, string | number | undefined>): string {
|
|
17
17
|
const parts: string[] = []
|
|
18
18
|
for (const [k, v] of Object.entries(styles)) {
|
|
19
|
-
if (v != null && v !==
|
|
20
|
-
const prop = k.replace(/([A-Z])/g,
|
|
21
|
-
parts.push(`${prop}:${typeof v ===
|
|
19
|
+
if (v != null && v !== '') {
|
|
20
|
+
const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
21
|
+
parts.push(`${prop}:${typeof v === 'number' ? `${v}px` : v}`)
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
return parts.length > 0 ? ` style="${parts.join(
|
|
24
|
+
return parts.length > 0 ? ` style="${parts.join(';')}"` : ''
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function padStr(
|
|
28
28
|
pad: number | [number, number] | [number, number, number, number] | undefined,
|
|
29
29
|
): string | undefined {
|
|
30
30
|
if (pad == null) return undefined
|
|
31
|
-
if (typeof pad ===
|
|
31
|
+
if (typeof pad === 'number') return `${pad}px`
|
|
32
32
|
if (pad.length === 2) return `${pad[0]}px ${pad[1]}px`
|
|
33
33
|
return `${pad[0]}px ${pad[1]}px ${pad[2]}px ${pad[3]}px`
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function renderChild(child: DocChild): string {
|
|
37
|
-
if (typeof child ===
|
|
37
|
+
if (typeof child === 'string') return escapeHtml(child)
|
|
38
38
|
return renderNode(child)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function renderChildren(children: DocChild[]): string {
|
|
42
|
-
return children.map(renderChild).join(
|
|
42
|
+
return children.map(renderChild).join('')
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function renderNode(node: DocNode): string {
|
|
46
46
|
const p = node.props
|
|
47
47
|
|
|
48
48
|
switch (node.type) {
|
|
49
|
-
case
|
|
50
|
-
const lang = (p.language as string) ??
|
|
51
|
-
const title = p.title ? `<title>${escapeHtml(p.title as string)}</title>` :
|
|
49
|
+
case 'document': {
|
|
50
|
+
const lang = (p.language as string) ?? 'en'
|
|
51
|
+
const title = p.title ? `<title>${escapeHtml(p.title as string)}</title>` : ''
|
|
52
52
|
return `<!DOCTYPE html><html lang="${lang}"><head><meta charset="utf-8">${title}<meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderChildren(node.children)}</body></html>`
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
case
|
|
55
|
+
case 'page': {
|
|
56
56
|
const margin = padStr(p.margin as PageMargin)
|
|
57
|
-
return `<div${styleStr({ maxWidth:
|
|
57
|
+
return `<div${styleStr({ maxWidth: '800px', margin: margin ?? '0 auto', padding: margin ?? '40px' })}>${renderChildren(node.children)}</div>`
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
case
|
|
61
|
-
const dir = (p.direction as string) ??
|
|
60
|
+
case 'section': {
|
|
61
|
+
const dir = (p.direction as string) ?? 'column'
|
|
62
62
|
return `<div${styleStr({
|
|
63
|
-
display: dir ===
|
|
64
|
-
flexDirection: dir ===
|
|
63
|
+
display: dir === 'row' ? 'flex' : 'block',
|
|
64
|
+
flexDirection: dir === 'row' ? 'row' : undefined,
|
|
65
65
|
gap: p.gap as number | undefined,
|
|
66
66
|
padding: padStr(p.padding as PageMargin),
|
|
67
67
|
background: sanitizeColor(p.background as string | undefined),
|
|
@@ -69,48 +69,48 @@ function renderNode(node: DocNode): string {
|
|
|
69
69
|
})}>${renderChildren(node.children)}</div>`
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
case
|
|
73
|
-
return `<div${styleStr({ display:
|
|
72
|
+
case 'row':
|
|
73
|
+
return `<div${styleStr({ display: 'flex', gap: p.gap as number | undefined, alignItems: p.align as string | undefined })}>${renderChildren(node.children)}</div>`
|
|
74
74
|
|
|
75
|
-
case
|
|
76
|
-
return `<div${styleStr({ flex: p.width ? undefined :
|
|
75
|
+
case 'column':
|
|
76
|
+
return `<div${styleStr({ flex: p.width ? undefined : '1', width: p.width as string | undefined, textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</div>`
|
|
77
77
|
|
|
78
|
-
case
|
|
78
|
+
case 'heading': {
|
|
79
79
|
const level = (p.level as number) ?? 1
|
|
80
80
|
const tag = `h${Math.min(Math.max(level, 1), 6)}`
|
|
81
81
|
return `<${tag}${styleStr({ color: sanitizeColor(p.color as string | undefined), textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</${tag}>`
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
case
|
|
84
|
+
case 'text': {
|
|
85
85
|
return `<p${styleStr({
|
|
86
86
|
fontSize: p.size as number | undefined,
|
|
87
87
|
color: sanitizeColor(p.color as string | undefined),
|
|
88
|
-
fontWeight: p.bold ?
|
|
89
|
-
fontStyle: p.italic ?
|
|
90
|
-
textDecoration: p.underline ?
|
|
88
|
+
fontWeight: p.bold ? 'bold' : undefined,
|
|
89
|
+
fontStyle: p.italic ? 'italic' : undefined,
|
|
90
|
+
textDecoration: p.underline ? 'underline' : p.strikethrough ? 'line-through' : undefined,
|
|
91
91
|
textAlign: p.align as string | undefined,
|
|
92
92
|
lineHeight: p.lineHeight as number | undefined,
|
|
93
93
|
})}>${renderChildren(node.children)}</p>`
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
case
|
|
96
|
+
case 'link':
|
|
97
97
|
return `<a href="${escapeHtml(sanitizeHref(p.href as string))}"${styleStr({ color: sanitizeColor(p.color as string | undefined) })}>${renderChildren(node.children)}</a>`
|
|
98
98
|
|
|
99
|
-
case
|
|
99
|
+
case 'image': {
|
|
100
100
|
const alignStyle =
|
|
101
|
-
p.align ===
|
|
102
|
-
?
|
|
103
|
-
: p.align ===
|
|
104
|
-
?
|
|
105
|
-
:
|
|
106
|
-
const img = `<img src="${escapeHtml(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` :
|
|
101
|
+
p.align === 'center'
|
|
102
|
+
? 'display:block;margin:0 auto'
|
|
103
|
+
: p.align === 'right'
|
|
104
|
+
? 'display:block;margin-left:auto'
|
|
105
|
+
: ''
|
|
106
|
+
const img = `<img src="${escapeHtml(sanitizeImageSrc(p.src as string))}"${p.width ? ` width="${p.width}"` : ''}${p.height ? ` height="${p.height}"` : ''}${p.alt ? ` alt="${escapeHtml(p.alt as string)}"` : ''}${alignStyle ? ` style="${sanitizeStyle(alignStyle)}"` : ''} />`
|
|
107
107
|
if (p.caption) {
|
|
108
|
-
return `<figure${p.align ===
|
|
108
|
+
return `<figure${p.align === 'center' ? ' style="text-align:center"' : ''}>${img}<figcaption>${escapeHtml(p.caption as string)}</figcaption></figure>`
|
|
109
109
|
}
|
|
110
110
|
return img
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
case
|
|
113
|
+
case 'table': {
|
|
114
114
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
115
115
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
116
116
|
const hs = p.headerStyle as
|
|
@@ -119,76 +119,76 @@ function renderNode(node: DocNode): string {
|
|
|
119
119
|
const striped = p.striped as boolean | undefined
|
|
120
120
|
const bordered = p.bordered as boolean | undefined
|
|
121
121
|
const borderStyle = bordered
|
|
122
|
-
?
|
|
123
|
-
:
|
|
122
|
+
? 'border:1px solid #ddd;border-collapse:collapse;'
|
|
123
|
+
: 'border-collapse:collapse;'
|
|
124
124
|
|
|
125
125
|
let html = `<table style="width:100%;${borderStyle}">`
|
|
126
126
|
if (p.caption) html += `<caption>${escapeHtml(p.caption as string)}</caption>`
|
|
127
127
|
|
|
128
|
-
html +=
|
|
128
|
+
html += '<thead><tr>'
|
|
129
129
|
for (const col of columns) {
|
|
130
|
-
const cellBorder = bordered ?
|
|
131
|
-
const bgStyle = hs?.background ? `background:${sanitizeColor(hs.background)};` :
|
|
132
|
-
const colorStyle = hs?.color ? `color:${sanitizeColor(hs.color)};` :
|
|
133
|
-
const fontStyle = hs?.bold !== false ?
|
|
134
|
-
const alignStyle = col.align ? `text-align:${col.align};` :
|
|
130
|
+
const cellBorder = bordered ? 'border:1px solid #ddd;' : ''
|
|
131
|
+
const bgStyle = hs?.background ? `background:${sanitizeColor(hs.background)};` : ''
|
|
132
|
+
const colorStyle = hs?.color ? `color:${sanitizeColor(hs.color)};` : ''
|
|
133
|
+
const fontStyle = hs?.bold !== false ? 'font-weight:bold;' : ''
|
|
134
|
+
const alignStyle = col.align ? `text-align:${col.align};` : ''
|
|
135
135
|
const widthStyle = col.width
|
|
136
|
-
? `width:${typeof col.width ===
|
|
137
|
-
:
|
|
136
|
+
? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`
|
|
137
|
+
: ''
|
|
138
138
|
html += `<th style="${cellBorder}${bgStyle}${colorStyle}${fontStyle}${alignStyle}${widthStyle}padding:8px">${escapeHtml(col.header)}</th>`
|
|
139
139
|
}
|
|
140
|
-
html +=
|
|
140
|
+
html += '</tr></thead>'
|
|
141
141
|
|
|
142
|
-
html +=
|
|
142
|
+
html += '<tbody>'
|
|
143
143
|
for (let i = 0; i < rows.length; i++) {
|
|
144
|
-
const rowBg = striped && i % 2 === 1 ? ' style="background:#f9f9f9"' :
|
|
144
|
+
const rowBg = striped && i % 2 === 1 ? ' style="background:#f9f9f9"' : ''
|
|
145
145
|
html += `<tr${rowBg}>`
|
|
146
146
|
for (let j = 0; j < columns.length; j++) {
|
|
147
|
-
const cellBorder = bordered ?
|
|
147
|
+
const cellBorder = bordered ? 'border:1px solid #ddd;' : ''
|
|
148
148
|
const col = columns[j]
|
|
149
|
-
const alignStyle = col?.align ? `text-align:${col.align};` :
|
|
150
|
-
html += `<td style="${cellBorder}${alignStyle}padding:8px">${escapeHtml(String(rows[i]?.[j] ??
|
|
149
|
+
const alignStyle = col?.align ? `text-align:${col.align};` : ''
|
|
150
|
+
html += `<td style="${cellBorder}${alignStyle}padding:8px">${escapeHtml(String(rows[i]?.[j] ?? ''))}</td>`
|
|
151
151
|
}
|
|
152
|
-
html +=
|
|
152
|
+
html += '</tr>'
|
|
153
153
|
}
|
|
154
|
-
html +=
|
|
154
|
+
html += '</tbody></table>'
|
|
155
155
|
return html
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
case
|
|
159
|
-
const tag = p.ordered ?
|
|
158
|
+
case 'list': {
|
|
159
|
+
const tag = p.ordered ? 'ol' : 'ul'
|
|
160
160
|
return `<${tag}>${renderChildren(node.children)}</${tag}>`
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
case
|
|
163
|
+
case 'list-item':
|
|
164
164
|
return `<li>${renderChildren(node.children)}</li>`
|
|
165
165
|
|
|
166
|
-
case
|
|
166
|
+
case 'code':
|
|
167
167
|
return `<pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto"><code>${escapeHtml(renderChildren(node.children))}</code></pre>`
|
|
168
168
|
|
|
169
|
-
case
|
|
170
|
-
const color = sanitizeColor((p.color as string) ??
|
|
169
|
+
case 'divider': {
|
|
170
|
+
const color = sanitizeColor((p.color as string) ?? '#ddd')
|
|
171
171
|
const thickness = (p.thickness as number) ?? 1
|
|
172
172
|
return `<hr style="border:none;border-top:${thickness}px solid ${color};margin:16px 0" />`
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
case
|
|
175
|
+
case 'page-break':
|
|
176
176
|
return '<div style="page-break-after:always;break-after:page"></div>'
|
|
177
177
|
|
|
178
|
-
case
|
|
178
|
+
case 'spacer':
|
|
179
179
|
return `<div style="height:${p.height}px"></div>`
|
|
180
180
|
|
|
181
|
-
case
|
|
182
|
-
const bg = sanitizeColor((p.background as string) ??
|
|
183
|
-
const color = sanitizeColor((p.color as string) ??
|
|
181
|
+
case 'button': {
|
|
182
|
+
const bg = sanitizeColor((p.background as string) ?? '#4f46e5')
|
|
183
|
+
const color = sanitizeColor((p.color as string) ?? '#fff')
|
|
184
184
|
const radius = (p.borderRadius as number) ?? 4
|
|
185
185
|
const pad = padStr((p.padding ?? [12, 24]) as [number, number])
|
|
186
|
-
const align = (p.align as string) ??
|
|
186
|
+
const align = (p.align as string) ?? 'left'
|
|
187
187
|
return `<div style="text-align:${align}"><a href="${escapeHtml(sanitizeHref(p.href as string))}" style="display:inline-block;background:${bg};color:${color};padding:${pad};border-radius:${radius}px;text-decoration:none;font-weight:bold">${renderChildren(node.children)}</a></div>`
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
case
|
|
191
|
-
const borderColor = sanitizeColor((p.borderColor as string) ??
|
|
190
|
+
case 'quote': {
|
|
191
|
+
const borderColor = sanitizeColor((p.borderColor as string) ?? '#ddd')
|
|
192
192
|
return `<blockquote style="margin:0;padding:12px 20px;border-left:4px solid ${borderColor};color:#555">${renderChildren(node.children)}</blockquote>`
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -202,8 +202,8 @@ type PageMargin = number | [number, number] | [number, number, number, number]
|
|
|
202
202
|
export const htmlRenderer: DocumentRenderer = {
|
|
203
203
|
async render(node: DocNode, options?: RenderOptions): Promise<string> {
|
|
204
204
|
let html = renderNode(node)
|
|
205
|
-
if (options?.direction ===
|
|
206
|
-
html = html.replace(
|
|
205
|
+
if (options?.direction === 'rtl') {
|
|
206
|
+
html = html.replace('<body>', '<body dir="rtl" style="direction:rtl">')
|
|
207
207
|
}
|
|
208
208
|
return html
|
|
209
209
|
},
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { sanitizeHref, sanitizeImageSrc } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeHref, sanitizeImageSrc } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
5
|
-
return typeof col ===
|
|
5
|
+
return typeof col === 'string' ? { header: col } : col
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function renderChild(child: DocChild): string {
|
|
9
|
-
if (typeof child ===
|
|
9
|
+
if (typeof child === 'string') return child
|
|
10
10
|
return renderNode(child)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function renderChildren(children: DocChild[]): string {
|
|
14
|
-
return children.map(renderChild).join(
|
|
14
|
+
return children.map(renderChild).join('')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function renderInline(children: DocChild[]): string {
|
|
18
|
-
return children.map(renderChild).join(
|
|
18
|
+
return children.map(renderChild).join('')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function renderNode(node: DocNode): string {
|
|
22
22
|
const p = node.props
|
|
23
23
|
|
|
24
24
|
switch (node.type) {
|
|
25
|
-
case
|
|
25
|
+
case 'document':
|
|
26
26
|
return renderChildren(node.children)
|
|
27
27
|
|
|
28
|
-
case
|
|
28
|
+
case 'page':
|
|
29
29
|
return renderChildren(node.children)
|
|
30
30
|
|
|
31
|
-
case
|
|
31
|
+
case 'section':
|
|
32
32
|
return `${renderChildren(node.children)}\n`
|
|
33
33
|
|
|
34
|
-
case
|
|
35
|
-
case
|
|
34
|
+
case 'row':
|
|
35
|
+
case 'column':
|
|
36
36
|
return renderChildren(node.children)
|
|
37
37
|
|
|
38
|
-
case
|
|
38
|
+
case 'heading': {
|
|
39
39
|
const level = (p.level as number) ?? 1
|
|
40
|
-
const prefix =
|
|
40
|
+
const prefix = '#'.repeat(Math.min(Math.max(level, 1), 6))
|
|
41
41
|
return `${prefix} ${renderInline(node.children)}\n\n`
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
case
|
|
44
|
+
case 'text': {
|
|
45
45
|
let text = renderInline(node.children)
|
|
46
46
|
if (p.bold) text = `**${text}**`
|
|
47
47
|
if (p.italic) text = `*${text}*`
|
|
@@ -49,78 +49,78 @@ function renderNode(node: DocNode): string {
|
|
|
49
49
|
return `${text}\n\n`
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
case
|
|
52
|
+
case 'link':
|
|
53
53
|
return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})`
|
|
54
54
|
|
|
55
|
-
case
|
|
56
|
-
const alt = (p.alt as string) ??
|
|
55
|
+
case 'image': {
|
|
56
|
+
const alt = (p.alt as string) ?? ''
|
|
57
57
|
let md = `})`
|
|
58
58
|
if (p.caption) md += `\n*${p.caption}*`
|
|
59
59
|
return `${md}\n\n`
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
case
|
|
62
|
+
case 'table': {
|
|
63
63
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
64
64
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
65
65
|
|
|
66
|
-
if (columns.length === 0) return
|
|
66
|
+
if (columns.length === 0) return ''
|
|
67
67
|
|
|
68
68
|
// Header
|
|
69
|
-
const header = `| ${columns.map((c) => c.header).join(
|
|
69
|
+
const header = `| ${columns.map((c) => c.header).join(' | ')} |`
|
|
70
70
|
|
|
71
71
|
// Separator with alignment
|
|
72
72
|
const separator = `| ${columns
|
|
73
73
|
.map((c) => {
|
|
74
|
-
const align = c.align ??
|
|
75
|
-
if (align ===
|
|
76
|
-
if (align ===
|
|
77
|
-
return
|
|
74
|
+
const align = c.align ?? 'left'
|
|
75
|
+
if (align === 'center') return ':---:'
|
|
76
|
+
if (align === 'right') return '---:'
|
|
77
|
+
return '---'
|
|
78
78
|
})
|
|
79
|
-
.join(
|
|
79
|
+
.join(' | ')} |`
|
|
80
80
|
|
|
81
81
|
// Rows
|
|
82
82
|
const body = rows
|
|
83
|
-
.map((row) => `| ${row.map((cell) => String(cell ??
|
|
84
|
-
.join(
|
|
83
|
+
.map((row) => `| ${row.map((cell) => String(cell ?? '')).join(' | ')} |`)
|
|
84
|
+
.join('\n')
|
|
85
85
|
|
|
86
86
|
let md = `${header}\n${separator}\n${body}\n\n`
|
|
87
87
|
if (p.caption) md = `*${p.caption}*\n\n${md}`
|
|
88
88
|
return md
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
case
|
|
91
|
+
case 'list': {
|
|
92
92
|
const ordered = p.ordered as boolean | undefined
|
|
93
93
|
return `${node.children
|
|
94
|
-
.filter((c): c is DocNode => typeof c !==
|
|
94
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
95
95
|
.map((item, i) => {
|
|
96
|
-
const prefix = ordered ? `${i + 1}.` :
|
|
96
|
+
const prefix = ordered ? `${i + 1}.` : '-'
|
|
97
97
|
return `${prefix} ${renderInline(item.children)}`
|
|
98
98
|
})
|
|
99
|
-
.join(
|
|
99
|
+
.join('\n')}\n\n`
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
case
|
|
102
|
+
case 'list-item':
|
|
103
103
|
return renderInline(node.children)
|
|
104
104
|
|
|
105
|
-
case
|
|
106
|
-
const lang = (p.language as string) ??
|
|
105
|
+
case 'code': {
|
|
106
|
+
const lang = (p.language as string) ?? ''
|
|
107
107
|
const content = renderInline(node.children)
|
|
108
108
|
return `\`\`\`${lang}\n${content}\n\`\`\`\n\n`
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
case
|
|
112
|
-
return
|
|
111
|
+
case 'divider':
|
|
112
|
+
return '---\n\n'
|
|
113
113
|
|
|
114
|
-
case
|
|
115
|
-
return
|
|
114
|
+
case 'page-break':
|
|
115
|
+
return '---\n\n'
|
|
116
116
|
|
|
117
|
-
case
|
|
118
|
-
return
|
|
117
|
+
case 'spacer':
|
|
118
|
+
return '\n'
|
|
119
119
|
|
|
120
|
-
case
|
|
120
|
+
case 'button':
|
|
121
121
|
return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})\n\n`
|
|
122
122
|
|
|
123
|
-
case
|
|
123
|
+
case 'quote':
|
|
124
124
|
return `> ${renderInline(node.children)}\n\n`
|
|
125
125
|
|
|
126
126
|
default:
|