@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.
Files changed (50) hide show
  1. package/README.md +7 -4
  2. package/lib/confluence-Bd3ua1Ut.js.map +1 -1
  3. package/lib/csv-COrS4qdy.js.map +1 -1
  4. package/lib/discord-BLUnkEh9.js.map +1 -1
  5. package/lib/docx-uNAel545.js.map +1 -1
  6. package/lib/email-D0bbfWq4.js.map +1 -1
  7. package/lib/google-chat-CkKCBUWC.js.map +1 -1
  8. package/lib/html-B5biprN2.js.map +1 -1
  9. package/lib/index.js.map +1 -1
  10. package/lib/markdown-CdtlFGC0.js.map +1 -1
  11. package/lib/notion-iG2C5bEY.js.map +1 -1
  12. package/lib/pdf-IuBgTb3T.js.map +1 -1
  13. package/lib/pptx-DXiMiYFM.js.map +1 -1
  14. package/lib/sanitize-O_3j1mNJ.js.map +1 -1
  15. package/lib/slack-BI3EQwYm.js.map +1 -1
  16. package/lib/svg-BKxumy-p.js.map +1 -1
  17. package/lib/teams-Cwz9lce0.js.map +1 -1
  18. package/lib/telegram-gYFqyMXb.js.map +1 -1
  19. package/lib/text-l1XNXBOC.js.map +1 -1
  20. package/lib/types/index.d.ts +27 -27
  21. package/lib/whatsapp-CjSGoOKx.js.map +1 -1
  22. package/lib/xlsx-Cvu4LBNy.js.map +1 -1
  23. package/package.json +21 -21
  24. package/src/builder.ts +36 -36
  25. package/src/download.ts +32 -32
  26. package/src/index.ts +5 -10
  27. package/src/nodes.ts +45 -45
  28. package/src/render.ts +43 -43
  29. package/src/renderers/confluence.ts +63 -63
  30. package/src/renderers/csv.ts +10 -10
  31. package/src/renderers/discord.ts +37 -37
  32. package/src/renderers/docx.ts +57 -57
  33. package/src/renderers/email.ts +72 -72
  34. package/src/renderers/google-chat.ts +34 -34
  35. package/src/renderers/html.ts +76 -76
  36. package/src/renderers/markdown.ts +42 -42
  37. package/src/renderers/notion.ts +60 -60
  38. package/src/renderers/pdf.ts +78 -78
  39. package/src/renderers/pptx.ts +51 -51
  40. package/src/renderers/slack.ts +48 -48
  41. package/src/renderers/svg.ts +47 -47
  42. package/src/renderers/teams.ts +67 -67
  43. package/src/renderers/telegram.ts +39 -39
  44. package/src/renderers/text.ts +43 -43
  45. package/src/renderers/whatsapp.ts +33 -33
  46. package/src/renderers/xlsx.ts +35 -35
  47. package/src/sanitize.ts +20 -20
  48. package/src/tests/document.test.ts +1302 -1302
  49. package/src/tests/stress.test.ts +110 -110
  50. package/src/types.ts +61 -61
@@ -1,5 +1,5 @@
1
- import { sanitizeHref, sanitizeImageSrc } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
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 === "string" ? { header: col } : 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 === "string" ? c : getTextContent((c as DocNode).children)))
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 "document":
29
- case "page":
30
- case "section":
31
- case "row":
32
- case "column":
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 !== "string") {
34
+ if (typeof child !== 'string') {
35
35
  widgets.push(...nodeToWidgets(child))
36
36
  }
37
37
  }
38
38
  break
39
39
 
40
- case "heading": {
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 "text": {
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 "link": {
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 "image": {
72
+ case 'image': {
73
73
  const src = sanitizeImageSrc(p.src as string)
74
- if (src.startsWith("http")) {
74
+ if (src.startsWith('http')) {
75
75
  widgets.push({
76
76
  image: {
77
77
  imageUrl: src,
78
- altText: (p.alt as string) ?? "Image",
78
+ altText: (p.alt as string) ?? 'Image',
79
79
  },
80
80
  })
81
81
  }
82
82
  break
83
83
  }
84
84
 
85
- case "table": {
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 ?? "")).join(" | ")).join("\n")
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 "list": {
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 !== "string")
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("\n")
107
+ .join('\n')
108
108
  widgets.push({
109
109
  textParagraph: { text: items },
110
110
  })
111
111
  break
112
112
  }
113
113
 
114
- case "code": {
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 "divider":
125
- case "page-break":
124
+ case 'divider':
125
+ case 'page-break':
126
126
  widgets.push({ divider: {} })
127
127
  break
128
128
 
129
- case "spacer":
129
+ case 'spacer':
130
130
  // No direct equivalent — skip
131
131
  break
132
132
 
133
- case "button": {
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 "quote": {
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 !== "string" && c.type === "heading",
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: "document",
183
+ cardId: 'document',
184
184
  card: {
185
185
  header: title
186
186
  ? { title, subtitle: (node.props.subject as string) ?? undefined }
@@ -1,67 +1,67 @@
1
- import { sanitizeColor, sanitizeHref, sanitizeImageSrc, sanitizeStyle } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
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, "&amp;")
7
- .replace(/</g, "&lt;")
8
- .replace(/>/g, "&gt;")
9
- .replace(/"/g, "&quot;")
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
10
  }
11
11
 
12
12
  function resolveColumn(col: string | TableColumn): TableColumn {
13
- return typeof col === "string" ? { header: col } : 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, "-$1").toLowerCase()
21
- parts.push(`${prop}:${typeof v === "number" ? `${v}px` : 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 === "number") return `${pad}px`
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 === "string") return escapeHtml(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 "document": {
50
- const lang = (p.language as string) ?? "en"
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 "page": {
55
+ case 'page': {
56
56
  const margin = padStr(p.margin as PageMargin)
57
- return `<div${styleStr({ maxWidth: "800px", margin: margin ?? "0 auto", padding: margin ?? "40px" })}>${renderChildren(node.children)}</div>`
57
+ return `<div${styleStr({ maxWidth: '800px', margin: margin ?? '0 auto', padding: margin ?? '40px' })}>${renderChildren(node.children)}</div>`
58
58
  }
59
59
 
60
- case "section": {
61
- const dir = (p.direction as string) ?? "column"
60
+ case 'section': {
61
+ const dir = (p.direction as string) ?? 'column'
62
62
  return `<div${styleStr({
63
- display: dir === "row" ? "flex" : "block",
64
- flexDirection: dir === "row" ? "row" : undefined,
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 "row":
73
- return `<div${styleStr({ display: "flex", gap: p.gap as number | undefined, alignItems: p.align as string | undefined })}>${renderChildren(node.children)}</div>`
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 "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>`
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 "heading": {
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 "text": {
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 ? "bold" : undefined,
89
- fontStyle: p.italic ? "italic" : undefined,
90
- textDecoration: p.underline ? "underline" : p.strikethrough ? "line-through" : undefined,
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 "link":
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 "image": {
99
+ case 'image': {
100
100
  const alignStyle =
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)}"` : ""} />`
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 === "center" ? ' style="text-align:center"' : ""}>${img}<figcaption>${escapeHtml(p.caption as string)}</figcaption></figure>`
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 "table": {
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
- ? "border:1px solid #ddd;border-collapse:collapse;"
123
- : "border-collapse:collapse;"
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 += "<thead><tr>"
128
+ html += '<thead><tr>'
129
129
  for (const col of columns) {
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};` : ""
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 === "number" ? `${col.width}px` : 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 += "</tr></thead>"
140
+ html += '</tr></thead>'
141
141
 
142
- html += "<tbody>"
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 ? "border:1px solid #ddd;" : ""
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] ?? ""))}</td>`
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 += "</tr>"
152
+ html += '</tr>'
153
153
  }
154
- html += "</tbody></table>"
154
+ html += '</tbody></table>'
155
155
  return html
156
156
  }
157
157
 
158
- case "list": {
159
- const tag = p.ordered ? "ol" : "ul"
158
+ case 'list': {
159
+ const tag = p.ordered ? 'ol' : 'ul'
160
160
  return `<${tag}>${renderChildren(node.children)}</${tag}>`
161
161
  }
162
162
 
163
- case "list-item":
163
+ case 'list-item':
164
164
  return `<li>${renderChildren(node.children)}</li>`
165
165
 
166
- case "code":
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 "divider": {
170
- const color = sanitizeColor((p.color as string) ?? "#ddd")
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 "page-break":
175
+ case 'page-break':
176
176
  return '<div style="page-break-after:always;break-after:page"></div>'
177
177
 
178
- case "spacer":
178
+ case 'spacer':
179
179
  return `<div style="height:${p.height}px"></div>`
180
180
 
181
- case "button": {
182
- const bg = sanitizeColor((p.background as string) ?? "#4f46e5")
183
- const color = sanitizeColor((p.color as string) ?? "#fff")
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) ?? "left"
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 "quote": {
191
- const borderColor = sanitizeColor((p.borderColor as string) ?? "#ddd")
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 === "rtl") {
206
- html = html.replace("<body>", '<body dir="rtl" style="direction:rtl">')
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 "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
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 === "string" ? { header: col } : col
5
+ return typeof col === 'string' ? { header: col } : col
6
6
  }
7
7
 
8
8
  function renderChild(child: DocChild): string {
9
- if (typeof child === "string") return 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 "document":
25
+ case 'document':
26
26
  return renderChildren(node.children)
27
27
 
28
- case "page":
28
+ case 'page':
29
29
  return renderChildren(node.children)
30
30
 
31
- case "section":
31
+ case 'section':
32
32
  return `${renderChildren(node.children)}\n`
33
33
 
34
- case "row":
35
- case "column":
34
+ case 'row':
35
+ case 'column':
36
36
  return renderChildren(node.children)
37
37
 
38
- case "heading": {
38
+ case 'heading': {
39
39
  const level = (p.level as number) ?? 1
40
- const prefix = "#".repeat(Math.min(Math.max(level, 1), 6))
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 "text": {
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 "link":
52
+ case 'link':
53
53
  return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})`
54
54
 
55
- case "image": {
56
- const alt = (p.alt as string) ?? ""
55
+ case 'image': {
56
+ const alt = (p.alt as string) ?? ''
57
57
  let md = `![${alt}](${sanitizeImageSrc(p.src as string)})`
58
58
  if (p.caption) md += `\n*${p.caption}*`
59
59
  return `${md}\n\n`
60
60
  }
61
61
 
62
- case "table": {
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 ?? "left"
75
- if (align === "center") return ":---:"
76
- if (align === "right") return "---:"
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 ?? "")).join(" | ")} |`)
84
- .join("\n")
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 "list": {
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 !== "string")
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("\n")}\n\n`
99
+ .join('\n')}\n\n`
100
100
  }
101
101
 
102
- case "list-item":
102
+ case 'list-item':
103
103
  return renderInline(node.children)
104
104
 
105
- case "code": {
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 "divider":
112
- return "---\n\n"
111
+ case 'divider':
112
+ return '---\n\n'
113
113
 
114
- case "page-break":
115
- return "---\n\n"
114
+ case 'page-break':
115
+ return '---\n\n'
116
116
 
117
- case "spacer":
118
- return "\n"
117
+ case 'spacer':
118
+ return '\n'
119
119
 
120
- case "button":
120
+ case 'button':
121
121
  return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})\n\n`
122
122
 
123
- case "quote":
123
+ case 'quote':
124
124
  return `> ${renderInline(node.children)}\n\n`
125
125
 
126
126
  default: