@pyreon/document 0.11.5 → 0.11.7
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 } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeHref } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Telegram renderer — outputs HTML using Telegram's supported subset.
|
|
@@ -8,40 +8,40 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
11
|
-
return typeof col ===
|
|
11
|
+
return typeof col === 'string' ? { header: col } : col
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function esc(str: string): string {
|
|
15
15
|
return str
|
|
16
|
-
.replace(/&/g,
|
|
17
|
-
.replace(/</g,
|
|
18
|
-
.replace(/>/g,
|
|
19
|
-
.replace(/"/g,
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/</g, '<')
|
|
18
|
+
.replace(/>/g, '>')
|
|
19
|
+
.replace(/"/g, '"')
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getTextContent(children: DocChild[]): string {
|
|
23
23
|
return children
|
|
24
|
-
.map((c) => (typeof c ===
|
|
25
|
-
.join(
|
|
24
|
+
.map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
|
|
25
|
+
.join('')
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function renderNode(node: DocNode): string {
|
|
29
29
|
const p = node.props
|
|
30
30
|
|
|
31
31
|
switch (node.type) {
|
|
32
|
-
case
|
|
33
|
-
case
|
|
34
|
-
case
|
|
35
|
-
case
|
|
36
|
-
case
|
|
37
|
-
return node.children.map((c) => (typeof c ===
|
|
38
|
-
|
|
39
|
-
case
|
|
32
|
+
case 'document':
|
|
33
|
+
case 'page':
|
|
34
|
+
case 'section':
|
|
35
|
+
case 'row':
|
|
36
|
+
case 'column':
|
|
37
|
+
return node.children.map((c) => (typeof c === 'string' ? esc(c) : renderNode(c))).join('')
|
|
38
|
+
|
|
39
|
+
case 'heading': {
|
|
40
40
|
const text = esc(getTextContent(node.children))
|
|
41
41
|
return `<b>${text}</b>\n\n`
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
case
|
|
44
|
+
case 'text': {
|
|
45
45
|
let text = esc(getTextContent(node.children))
|
|
46
46
|
if (p.bold) text = `<b>${text}</b>`
|
|
47
47
|
if (p.italic) text = `<i>${text}</i>`
|
|
@@ -50,43 +50,43 @@ function renderNode(node: DocNode): string {
|
|
|
50
50
|
return `${text}\n\n`
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
case
|
|
53
|
+
case 'link': {
|
|
54
54
|
const href = sanitizeHref(p.href as string)
|
|
55
55
|
const text = esc(getTextContent(node.children))
|
|
56
56
|
return `<a href="${esc(href)}">${text}</a>\n\n`
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
case
|
|
59
|
+
case 'image':
|
|
60
60
|
// Telegram doesn't support inline images in HTML
|
|
61
61
|
// Images need to be sent separately via sendPhoto
|
|
62
|
-
return
|
|
62
|
+
return ''
|
|
63
63
|
|
|
64
|
-
case
|
|
64
|
+
case 'table': {
|
|
65
65
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
66
66
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
67
67
|
|
|
68
68
|
// Render as preformatted text since Telegram has no table support
|
|
69
|
-
const header = columns.map((c) => c.header).join(
|
|
70
|
-
const separator = columns.map(() =>
|
|
71
|
-
const body = rows.map((row) => row.map((c) => String(c ??
|
|
69
|
+
const header = columns.map((c) => c.header).join(' | ')
|
|
70
|
+
const separator = columns.map(() => '---').join('-+-')
|
|
71
|
+
const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
|
|
72
72
|
|
|
73
73
|
return `<pre>${esc(header)}\n${esc(separator)}\n${esc(body)}</pre>\n\n`
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
case
|
|
76
|
+
case 'list': {
|
|
77
77
|
const ordered = p.ordered as boolean | undefined
|
|
78
78
|
const items = node.children
|
|
79
|
-
.filter((c): c is DocNode => typeof c !==
|
|
79
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
80
80
|
.map((item, i) => {
|
|
81
|
-
const prefix = ordered ? `${i + 1}.` :
|
|
81
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
82
82
|
return `${prefix} ${esc(getTextContent(item.children))}`
|
|
83
83
|
})
|
|
84
|
-
.join(
|
|
84
|
+
.join('\n')
|
|
85
85
|
return `${items}\n\n`
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
case
|
|
89
|
-
const lang = (p.language as string) ??
|
|
88
|
+
case 'code': {
|
|
89
|
+
const lang = (p.language as string) ?? ''
|
|
90
90
|
const text = esc(getTextContent(node.children))
|
|
91
91
|
if (lang) {
|
|
92
92
|
return `<pre><code class="language-${esc(lang)}">${text}</code></pre>\n\n`
|
|
@@ -94,26 +94,26 @@ function renderNode(node: DocNode): string {
|
|
|
94
94
|
return `<pre>${text}</pre>\n\n`
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
case
|
|
98
|
-
case
|
|
99
|
-
return
|
|
97
|
+
case 'divider':
|
|
98
|
+
case 'page-break':
|
|
99
|
+
return '───────────\n\n'
|
|
100
100
|
|
|
101
|
-
case
|
|
102
|
-
return
|
|
101
|
+
case 'spacer':
|
|
102
|
+
return '\n'
|
|
103
103
|
|
|
104
|
-
case
|
|
104
|
+
case 'button': {
|
|
105
105
|
const href = sanitizeHref(p.href as string)
|
|
106
106
|
const text = esc(getTextContent(node.children))
|
|
107
107
|
return `<a href="${esc(href)}">${text}</a>\n\n`
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
case
|
|
110
|
+
case 'quote': {
|
|
111
111
|
const text = esc(getTextContent(node.children))
|
|
112
112
|
return `<blockquote>${text}</blockquote>\n\n`
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
default:
|
|
116
|
-
return
|
|
116
|
+
return ''
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
package/src/renderers/text.ts
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
2
2
|
|
|
3
3
|
function resolveColumn(col: string | TableColumn): TableColumn {
|
|
4
|
-
return typeof col ===
|
|
4
|
+
return typeof col === 'string' ? { header: col } : col
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
function renderChild(child: DocChild): string {
|
|
8
|
-
if (typeof child ===
|
|
8
|
+
if (typeof child === 'string') return child
|
|
9
9
|
return renderNode(child)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function renderChildren(children: DocChild[]): string {
|
|
13
|
-
return children.map(renderChild).join(
|
|
13
|
+
return children.map(renderChild).join('')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function pad(str: string, width: number, align:
|
|
16
|
+
function pad(str: string, width: number, align: 'left' | 'center' | 'right' = 'left'): string {
|
|
17
17
|
if (str.length >= width) return str.slice(0, width)
|
|
18
18
|
const diff = width - str.length
|
|
19
|
-
if (align ===
|
|
19
|
+
if (align === 'center') {
|
|
20
20
|
const left = Math.floor(diff / 2)
|
|
21
|
-
return
|
|
21
|
+
return ' '.repeat(left) + str + ' '.repeat(diff - left)
|
|
22
22
|
}
|
|
23
|
-
if (align ===
|
|
24
|
-
return str +
|
|
23
|
+
if (align === 'right') return ' '.repeat(diff) + str
|
|
24
|
+
return str + ' '.repeat(diff)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function renderNode(node: DocNode): string {
|
|
28
28
|
const p = node.props
|
|
29
29
|
|
|
30
30
|
switch (node.type) {
|
|
31
|
-
case
|
|
31
|
+
case 'document':
|
|
32
32
|
return renderChildren(node.children)
|
|
33
33
|
|
|
34
|
-
case
|
|
34
|
+
case 'page':
|
|
35
35
|
return renderChildren(node.children)
|
|
36
36
|
|
|
37
|
-
case
|
|
38
|
-
case
|
|
39
|
-
case
|
|
37
|
+
case 'section':
|
|
38
|
+
case 'row':
|
|
39
|
+
case 'column':
|
|
40
40
|
return renderChildren(node.children)
|
|
41
41
|
|
|
42
|
-
case
|
|
42
|
+
case 'heading': {
|
|
43
43
|
const text = renderChildren(node.children)
|
|
44
44
|
const level = (p.level as number) ?? 1
|
|
45
|
-
if (level === 1) return `${text.toUpperCase()}\n${
|
|
46
|
-
if (level === 2) return `${text}\n${
|
|
45
|
+
if (level === 1) return `${text.toUpperCase()}\n${'='.repeat(text.length)}\n\n`
|
|
46
|
+
if (level === 2) return `${text}\n${'-'.repeat(text.length)}\n\n`
|
|
47
47
|
return `${text}\n\n`
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
case
|
|
50
|
+
case 'text':
|
|
51
51
|
return `${renderChildren(node.children)}\n\n`
|
|
52
52
|
|
|
53
|
-
case
|
|
53
|
+
case 'link':
|
|
54
54
|
return `${renderChildren(node.children)} (${p.href})`
|
|
55
55
|
|
|
56
|
-
case
|
|
57
|
-
const alt = (p.alt as string) ??
|
|
58
|
-
const caption = p.caption ? ` — ${p.caption}` :
|
|
56
|
+
case 'image': {
|
|
57
|
+
const alt = (p.alt as string) ?? 'Image'
|
|
58
|
+
const caption = p.caption ? ` — ${p.caption}` : ''
|
|
59
59
|
return `[${alt}${caption}]\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
|
// Calculate column widths
|
|
69
69
|
const widths = columns.map((col, i) => {
|
|
70
70
|
const headerLen = col.header.length
|
|
71
|
-
const maxDataLen = rows.reduce((max, row) => Math.max(max, String(row[i] ??
|
|
71
|
+
const maxDataLen = rows.reduce((max, row) => Math.max(max, String(row[i] ?? '').length), 0)
|
|
72
72
|
return Math.max(headerLen, maxDataLen, 3)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
75
|
// Header
|
|
76
|
-
const header = columns.map((col, i) => pad(col.header, widths[i] ?? 3, col.align)).join(
|
|
77
|
-
const separator = widths.map((w) =>
|
|
76
|
+
const header = columns.map((col, i) => pad(col.header, widths[i] ?? 3, col.align)).join(' | ')
|
|
77
|
+
const separator = widths.map((w) => '-'.repeat(w ?? 3)).join('-+-')
|
|
78
78
|
|
|
79
79
|
// Rows
|
|
80
80
|
const body = rows
|
|
81
81
|
.map((row) =>
|
|
82
|
-
columns.map((col, i) => pad(String(row[i] ??
|
|
82
|
+
columns.map((col, i) => pad(String(row[i] ?? ''), widths[i] ?? 3, col.align)).join(' | '),
|
|
83
83
|
)
|
|
84
|
-
.join(
|
|
84
|
+
.join('\n')
|
|
85
85
|
|
|
86
86
|
let result = `${header}\n${separator}\n${body}\n\n`
|
|
87
87
|
if (p.caption) result = `${p.caption}\n\n${result}`
|
|
88
88
|
return result
|
|
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} ${renderChildren(item.children)}`
|
|
98
98
|
})
|
|
99
|
-
.join(
|
|
99
|
+
.join('\n')}\n\n`
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
case
|
|
102
|
+
case 'list-item':
|
|
103
103
|
return renderChildren(node.children)
|
|
104
104
|
|
|
105
|
-
case
|
|
105
|
+
case 'code':
|
|
106
106
|
return `${renderChildren(node.children)}\n\n`
|
|
107
107
|
|
|
108
|
-
case
|
|
109
|
-
return `${
|
|
108
|
+
case 'divider':
|
|
109
|
+
return `${'─'.repeat(40)}\n\n`
|
|
110
110
|
|
|
111
|
-
case
|
|
112
|
-
return `\n${
|
|
111
|
+
case 'page-break':
|
|
112
|
+
return `\n${'═'.repeat(40)}\n\n`
|
|
113
113
|
|
|
114
|
-
case
|
|
115
|
-
return
|
|
114
|
+
case 'spacer':
|
|
115
|
+
return '\n'
|
|
116
116
|
|
|
117
|
-
case
|
|
117
|
+
case 'button':
|
|
118
118
|
return `[${renderChildren(node.children)}] → ${p.href}\n\n`
|
|
119
119
|
|
|
120
|
-
case
|
|
120
|
+
case 'quote':
|
|
121
121
|
return ` "${renderChildren(node.children)}"\n\n`
|
|
122
122
|
|
|
123
123
|
default:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { sanitizeHref } from
|
|
2
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import { sanitizeHref } from '../sanitize'
|
|
2
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* WhatsApp renderer — outputs formatted text using WhatsApp's markup.
|
|
@@ -7,32 +7,32 @@ 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
|
function renderNode(node: DocNode): string {
|
|
20
20
|
const p = node.props
|
|
21
21
|
|
|
22
22
|
switch (node.type) {
|
|
23
|
-
case
|
|
24
|
-
case
|
|
25
|
-
case
|
|
26
|
-
case
|
|
27
|
-
case
|
|
28
|
-
return node.children.map((c) => (typeof c ===
|
|
29
|
-
|
|
30
|
-
case
|
|
23
|
+
case 'document':
|
|
24
|
+
case 'page':
|
|
25
|
+
case 'section':
|
|
26
|
+
case 'row':
|
|
27
|
+
case 'column':
|
|
28
|
+
return node.children.map((c) => (typeof c === 'string' ? c : renderNode(c))).join('')
|
|
29
|
+
|
|
30
|
+
case 'heading': {
|
|
31
31
|
const text = getTextContent(node.children)
|
|
32
32
|
return `*${text}*\n\n`
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
case
|
|
35
|
+
case 'text': {
|
|
36
36
|
let text = getTextContent(node.children)
|
|
37
37
|
if (p.bold) text = `*${text}*`
|
|
38
38
|
if (p.italic) text = `_${text}_`
|
|
@@ -40,64 +40,64 @@ function renderNode(node: DocNode): string {
|
|
|
40
40
|
return `${text}\n\n`
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
case
|
|
43
|
+
case 'link': {
|
|
44
44
|
const href = sanitizeHref(p.href as string)
|
|
45
45
|
const text = getTextContent(node.children)
|
|
46
46
|
return `${text}: ${href}\n\n`
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
case
|
|
49
|
+
case 'image':
|
|
50
50
|
// WhatsApp doesn't support inline images in text
|
|
51
|
-
return
|
|
51
|
+
return ''
|
|
52
52
|
|
|
53
|
-
case
|
|
53
|
+
case 'table': {
|
|
54
54
|
const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
|
|
55
55
|
const rows = (p.rows ?? []) as (string | number)[][]
|
|
56
56
|
|
|
57
|
-
const header = columns.map((c) => `*${c.header}*`).join(
|
|
58
|
-
const body = rows.map((row) => row.map((c) => String(c ??
|
|
57
|
+
const header = columns.map((c) => `*${c.header}*`).join(' | ')
|
|
58
|
+
const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
|
|
59
59
|
|
|
60
60
|
let result = `${header}\n${body}\n\n`
|
|
61
61
|
if (p.caption) result = `_${p.caption}_\n${result}`
|
|
62
62
|
return result
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
case
|
|
65
|
+
case 'list': {
|
|
66
66
|
const ordered = p.ordered as boolean | undefined
|
|
67
67
|
return `${node.children
|
|
68
|
-
.filter((c): c is DocNode => typeof c !==
|
|
68
|
+
.filter((c): c is DocNode => typeof c !== 'string')
|
|
69
69
|
.map((item, i) => {
|
|
70
|
-
const prefix = ordered ? `${i + 1}.` :
|
|
70
|
+
const prefix = ordered ? `${i + 1}.` : '•'
|
|
71
71
|
return `${prefix} ${getTextContent(item.children)}`
|
|
72
72
|
})
|
|
73
|
-
.join(
|
|
73
|
+
.join('\n')}\n\n`
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
case
|
|
76
|
+
case 'code': {
|
|
77
77
|
const text = getTextContent(node.children)
|
|
78
78
|
return `\`\`\`${text}\`\`\`\n\n`
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
case
|
|
82
|
-
case
|
|
83
|
-
return
|
|
81
|
+
case 'divider':
|
|
82
|
+
case 'page-break':
|
|
83
|
+
return '───────────\n\n'
|
|
84
84
|
|
|
85
|
-
case
|
|
86
|
-
return
|
|
85
|
+
case 'spacer':
|
|
86
|
+
return '\n'
|
|
87
87
|
|
|
88
|
-
case
|
|
88
|
+
case 'button': {
|
|
89
89
|
const href = sanitizeHref(p.href as string)
|
|
90
90
|
const text = getTextContent(node.children)
|
|
91
91
|
return `*${text}*: ${href}\n\n`
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
case
|
|
94
|
+
case 'quote': {
|
|
95
95
|
const text = getTextContent(node.children)
|
|
96
96
|
return `> ${text}\n\n`
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
default:
|
|
100
|
-
return
|
|
100
|
+
return ''
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
package/src/renderers/xlsx.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from
|
|
1
|
+
import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* XLSX renderer — lazy-loads ExcelJS on first use.
|
|
@@ -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 ExtractedSheet {
|
|
@@ -26,18 +26,18 @@ interface ExtractedSheet {
|
|
|
26
26
|
function extractSheets(node: DocNode): ExtractedSheet[] {
|
|
27
27
|
const sheets: ExtractedSheet[] = []
|
|
28
28
|
let currentSheet: ExtractedSheet = {
|
|
29
|
-
name:
|
|
29
|
+
name: 'Sheet 1',
|
|
30
30
|
headings: [],
|
|
31
31
|
tables: [],
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function walk(n: DocNode): void {
|
|
35
35
|
switch (n.type) {
|
|
36
|
-
case
|
|
36
|
+
case 'document':
|
|
37
37
|
walkChildren(n)
|
|
38
38
|
break
|
|
39
39
|
|
|
40
|
-
case
|
|
40
|
+
case 'page':
|
|
41
41
|
pushCurrentSheet()
|
|
42
42
|
currentSheet = {
|
|
43
43
|
name: `Sheet ${sheets.length + 1}`,
|
|
@@ -47,11 +47,11 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
|
|
|
47
47
|
walkChildren(n)
|
|
48
48
|
break
|
|
49
49
|
|
|
50
|
-
case
|
|
50
|
+
case 'heading':
|
|
51
51
|
addHeading(n)
|
|
52
52
|
break
|
|
53
53
|
|
|
54
|
-
case
|
|
54
|
+
case 'table':
|
|
55
55
|
currentSheet.tables.push(n)
|
|
56
56
|
break
|
|
57
57
|
|
|
@@ -62,7 +62,7 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
|
|
|
62
62
|
|
|
63
63
|
function walkChildren(n: DocNode): void {
|
|
64
64
|
for (const child of n.children) {
|
|
65
|
-
if (typeof child !==
|
|
65
|
+
if (typeof child !== 'string') walk(child)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -88,8 +88,8 @@ function extractSheets(node: DocNode): ExtractedSheet[] {
|
|
|
88
88
|
|
|
89
89
|
/** Parse a cell value, handling currencies, percentages, and plain numbers. */
|
|
90
90
|
function parseCellValue(value: string | number | undefined): string | number {
|
|
91
|
-
if (value == null) return
|
|
92
|
-
if (typeof value ===
|
|
91
|
+
if (value == null) return ''
|
|
92
|
+
if (typeof value === 'number') return value
|
|
93
93
|
|
|
94
94
|
const trimmed = value.trim()
|
|
95
95
|
|
|
@@ -101,11 +101,11 @@ function parseCellValue(value: string | number | undefined): string | number {
|
|
|
101
101
|
// Currency: "$1,234.56", "$1234", "-$500"
|
|
102
102
|
const currencyMatch = trimmed.match(/^-?\$[\d,]+(\.\d+)?$/)
|
|
103
103
|
if (currencyMatch) {
|
|
104
|
-
return Number.parseFloat(trimmed.replace(/[$,]/g,
|
|
104
|
+
return Number.parseFloat(trimmed.replace(/[$,]/g, ''))
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Plain number: "1,234.56", "1234", "-500.5"
|
|
108
|
-
const plainNum = Number(trimmed.replace(/,/g,
|
|
108
|
+
const plainNum = Number(trimmed.replace(/,/g, ''))
|
|
109
109
|
if (!Number.isNaN(plainNum) && /^-?[\d,]+(\.\d+)?$/.test(trimmed)) {
|
|
110
110
|
return plainNum
|
|
111
111
|
}
|
|
@@ -115,23 +115,23 @@ function parseCellValue(value: string | number | undefined): string | number {
|
|
|
115
115
|
|
|
116
116
|
/** Get ExcelJS number format string for a value. */
|
|
117
117
|
function getCellFormat(originalValue: string | number | undefined): string | undefined {
|
|
118
|
-
if (typeof originalValue !==
|
|
118
|
+
if (typeof originalValue !== 'string') return undefined
|
|
119
119
|
const trimmed = originalValue.trim()
|
|
120
120
|
|
|
121
|
-
if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return
|
|
122
|
-
if (/^-?\$/.test(trimmed)) return
|
|
121
|
+
if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return '0.00%'
|
|
122
|
+
if (/^-?\$/.test(trimmed)) return '$#,##0.00'
|
|
123
123
|
return undefined
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/** Map alignment string to ExcelJS horizontal alignment. */
|
|
127
|
-
function mapAlignment(align?: string):
|
|
128
|
-
if (align ===
|
|
127
|
+
function mapAlignment(align?: string): 'left' | 'center' | 'right' | undefined {
|
|
128
|
+
if (align === 'left' || align === 'center' || align === 'right') return align
|
|
129
129
|
return undefined
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/** Thin border style for ExcelJS. */
|
|
133
|
-
function thinBorder(): { style:
|
|
134
|
-
return { style:
|
|
133
|
+
function thinBorder(): { style: 'thin'; color: { argb: string } } {
|
|
134
|
+
return { style: 'thin', color: { argb: 'FFDDDDDD' } }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** Apply header styling to a cell. */
|
|
@@ -150,16 +150,16 @@ function styleHeaderCell(
|
|
|
150
150
|
cell.value = col.header
|
|
151
151
|
cell.font = {
|
|
152
152
|
bold: true,
|
|
153
|
-
color: { argb: hs?.color?.replace(
|
|
153
|
+
color: { argb: hs?.color?.replace('#', 'FF') ?? 'FF000000' },
|
|
154
154
|
}
|
|
155
155
|
if (hs?.background) {
|
|
156
156
|
cell.fill = {
|
|
157
|
-
type:
|
|
158
|
-
pattern:
|
|
159
|
-
fgColor: { argb: hs.background.replace(
|
|
157
|
+
type: 'pattern',
|
|
158
|
+
pattern: 'solid',
|
|
159
|
+
fgColor: { argb: hs.background.replace('#', 'FF') },
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
cell.alignment = { horizontal: mapAlignment(col.align) ??
|
|
162
|
+
cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
|
|
163
163
|
if (bordered) {
|
|
164
164
|
cell.border = {
|
|
165
165
|
top: thinBorder(),
|
|
@@ -188,12 +188,12 @@ function styleDataCell(
|
|
|
188
188
|
cell.value = parseCellValue(rawValue)
|
|
189
189
|
const fmt = getCellFormat(rawValue)
|
|
190
190
|
if (fmt) cell.numFmt = fmt
|
|
191
|
-
cell.alignment = { horizontal: mapAlignment(col.align) ??
|
|
191
|
+
cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }
|
|
192
192
|
if (striped && isOddRow) {
|
|
193
193
|
cell.fill = {
|
|
194
|
-
type:
|
|
195
|
-
pattern:
|
|
196
|
-
fgColor: { argb:
|
|
194
|
+
type: 'pattern',
|
|
195
|
+
pattern: 'solid',
|
|
196
|
+
fgColor: { argb: 'FFF9F9F9' },
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
if (bordered) {
|
|
@@ -270,7 +270,7 @@ function autoFitColumns(ws: {
|
|
|
270
270
|
for (const col of ws.columns) {
|
|
271
271
|
let maxLen = 10
|
|
272
272
|
col.eachCell?.({ includeEmpty: false }, (cell) => {
|
|
273
|
-
const len = String(cell.value ??
|
|
273
|
+
const len = String(cell.value ?? '').length
|
|
274
274
|
if (len > maxLen) maxLen = len
|
|
275
275
|
})
|
|
276
276
|
col.width = Math.min(maxLen + 2, 50)
|
|
@@ -281,7 +281,7 @@ export const xlsxRenderer: DocumentRenderer = {
|
|
|
281
281
|
async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
|
|
282
282
|
let ExcelJS: any
|
|
283
283
|
try {
|
|
284
|
-
ExcelJS = await import(
|
|
284
|
+
ExcelJS = await import('exceljs')
|
|
285
285
|
} catch {
|
|
286
286
|
throw new Error(
|
|
287
287
|
'[@pyreon/document] XLSX renderer requires "exceljs" package. Install it: bun add exceljs',
|
|
@@ -289,13 +289,13 @@ export const xlsxRenderer: DocumentRenderer = {
|
|
|
289
289
|
}
|
|
290
290
|
const workbook = new ExcelJS.default.Workbook()
|
|
291
291
|
|
|
292
|
-
workbook.creator = (node.props.author as string) ??
|
|
293
|
-
workbook.title = (node.props.title as string) ??
|
|
292
|
+
workbook.creator = (node.props.author as string) ?? ''
|
|
293
|
+
workbook.title = (node.props.title as string) ?? ''
|
|
294
294
|
|
|
295
295
|
const sheets = extractSheets(node)
|
|
296
296
|
|
|
297
297
|
if (sheets.length === 0) {
|
|
298
|
-
workbook.addWorksheet(
|
|
298
|
+
workbook.addWorksheet('Sheet 1')
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
for (const sheet of sheets) {
|