@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
package/src/render.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DocNode, DocumentRenderer, OutputFormat, RenderOptions, RenderResult } from "./types"
1
+ import type { DocNode, DocumentRenderer, OutputFormat, RenderOptions, RenderResult } from './types'
2
2
 
3
3
  // ─── Renderer Registry ──────────────────────────────────────────────────────
4
4
 
@@ -37,44 +37,44 @@ export function unregisterRenderer(format: string): void {
37
37
 
38
38
  // Built-in renderers are registered lazily — only loaded when first used.
39
39
 
40
- registerRenderer("html", () => import("./renderers/html").then((m) => m.htmlRenderer))
40
+ registerRenderer('html', () => import('./renderers/html').then((m) => m.htmlRenderer))
41
41
 
42
- registerRenderer("email", () => import("./renderers/email").then((m) => m.emailRenderer))
42
+ registerRenderer('email', () => import('./renderers/email').then((m) => m.emailRenderer))
43
43
 
44
- registerRenderer("md", () => import("./renderers/markdown").then((m) => m.markdownRenderer))
44
+ registerRenderer('md', () => import('./renderers/markdown').then((m) => m.markdownRenderer))
45
45
 
46
- registerRenderer("text", () => import("./renderers/text").then((m) => m.textRenderer))
46
+ registerRenderer('text', () => import('./renderers/text').then((m) => m.textRenderer))
47
47
 
48
- registerRenderer("csv", () => import("./renderers/csv").then((m) => m.csvRenderer))
48
+ registerRenderer('csv', () => import('./renderers/csv').then((m) => m.csvRenderer))
49
49
 
50
- registerRenderer("pdf", () => import("./renderers/pdf").then((m) => m.pdfRenderer))
50
+ registerRenderer('pdf', () => import('./renderers/pdf').then((m) => m.pdfRenderer))
51
51
 
52
- registerRenderer("docx", () => import("./renderers/docx").then((m) => m.docxRenderer))
52
+ registerRenderer('docx', () => import('./renderers/docx').then((m) => m.docxRenderer))
53
53
 
54
- registerRenderer("xlsx", () => import("./renderers/xlsx").then((m) => m.xlsxRenderer))
54
+ registerRenderer('xlsx', () => import('./renderers/xlsx').then((m) => m.xlsxRenderer))
55
55
 
56
- registerRenderer("pptx", () => import("./renderers/pptx").then((m) => m.pptxRenderer))
56
+ registerRenderer('pptx', () => import('./renderers/pptx').then((m) => m.pptxRenderer))
57
57
 
58
- registerRenderer("slack", () => import("./renderers/slack").then((m) => m.slackRenderer))
58
+ registerRenderer('slack', () => import('./renderers/slack').then((m) => m.slackRenderer))
59
59
 
60
- registerRenderer("svg", () => import("./renderers/svg").then((m) => m.svgRenderer))
60
+ registerRenderer('svg', () => import('./renderers/svg').then((m) => m.svgRenderer))
61
61
 
62
- registerRenderer("teams", () => import("./renderers/teams").then((m) => m.teamsRenderer))
62
+ registerRenderer('teams', () => import('./renderers/teams').then((m) => m.teamsRenderer))
63
63
 
64
- registerRenderer("discord", () => import("./renderers/discord").then((m) => m.discordRenderer))
64
+ registerRenderer('discord', () => import('./renderers/discord').then((m) => m.discordRenderer))
65
65
 
66
- registerRenderer("telegram", () => import("./renderers/telegram").then((m) => m.telegramRenderer))
66
+ registerRenderer('telegram', () => import('./renderers/telegram').then((m) => m.telegramRenderer))
67
67
 
68
- registerRenderer("notion", () => import("./renderers/notion").then((m) => m.notionRenderer))
68
+ registerRenderer('notion', () => import('./renderers/notion').then((m) => m.notionRenderer))
69
69
 
70
- registerRenderer("confluence", () =>
71
- import("./renderers/confluence").then((m) => m.confluenceRenderer),
70
+ registerRenderer('confluence', () =>
71
+ import('./renderers/confluence').then((m) => m.confluenceRenderer),
72
72
  )
73
73
 
74
- registerRenderer("whatsapp", () => import("./renderers/whatsapp").then((m) => m.whatsappRenderer))
74
+ registerRenderer('whatsapp', () => import('./renderers/whatsapp').then((m) => m.whatsappRenderer))
75
75
 
76
- registerRenderer("google-chat", () =>
77
- import("./renderers/google-chat").then((m) => m.googleChatRenderer),
76
+ registerRenderer('google-chat', () =>
77
+ import('./renderers/google-chat').then((m) => m.googleChatRenderer),
78
78
  )
79
79
 
80
80
  // ─── Render Function ────────────────────────────────────────────────────────
@@ -83,11 +83,11 @@ async function resolveRenderer(format: string): Promise<DocumentRenderer> {
83
83
  const entry = renderers.get(format)
84
84
  if (!entry) {
85
85
  throw new Error(
86
- `[@pyreon/document] No renderer registered for format '${format}'. Available: ${[...renderers.keys()].join(", ")}`,
86
+ `[@pyreon/document] No renderer registered for format '${format}'. Available: ${[...renderers.keys()].join(', ')}`,
87
87
  )
88
88
  }
89
89
 
90
- if (typeof entry === "function") {
90
+ if (typeof entry === 'function') {
91
91
  const renderer = await entry()
92
92
  // Cache the resolved renderer so we don't re-import
93
93
  renderers.set(format, renderer)
@@ -124,26 +124,26 @@ export async function render(
124
124
  export function _resetRenderers(): void {
125
125
  renderers.clear()
126
126
  // Re-register built-in lazy loaders
127
- registerRenderer("html", () => import("./renderers/html").then((m) => m.htmlRenderer))
128
- registerRenderer("email", () => import("./renderers/email").then((m) => m.emailRenderer))
129
- registerRenderer("md", () => import("./renderers/markdown").then((m) => m.markdownRenderer))
130
- registerRenderer("text", () => import("./renderers/text").then((m) => m.textRenderer))
131
- registerRenderer("csv", () => import("./renderers/csv").then((m) => m.csvRenderer))
132
- registerRenderer("pdf", () => import("./renderers/pdf").then((m) => m.pdfRenderer))
133
- registerRenderer("docx", () => import("./renderers/docx").then((m) => m.docxRenderer))
134
- registerRenderer("xlsx", () => import("./renderers/xlsx").then((m) => m.xlsxRenderer))
135
- registerRenderer("pptx", () => import("./renderers/pptx").then((m) => m.pptxRenderer))
136
- registerRenderer("slack", () => import("./renderers/slack").then((m) => m.slackRenderer))
137
- registerRenderer("svg", () => import("./renderers/svg").then((m) => m.svgRenderer))
138
- registerRenderer("teams", () => import("./renderers/teams").then((m) => m.teamsRenderer))
139
- registerRenderer("discord", () => import("./renderers/discord").then((m) => m.discordRenderer))
140
- registerRenderer("telegram", () => import("./renderers/telegram").then((m) => m.telegramRenderer))
141
- registerRenderer("notion", () => import("./renderers/notion").then((m) => m.notionRenderer))
142
- registerRenderer("confluence", () =>
143
- import("./renderers/confluence").then((m) => m.confluenceRenderer),
127
+ registerRenderer('html', () => import('./renderers/html').then((m) => m.htmlRenderer))
128
+ registerRenderer('email', () => import('./renderers/email').then((m) => m.emailRenderer))
129
+ registerRenderer('md', () => import('./renderers/markdown').then((m) => m.markdownRenderer))
130
+ registerRenderer('text', () => import('./renderers/text').then((m) => m.textRenderer))
131
+ registerRenderer('csv', () => import('./renderers/csv').then((m) => m.csvRenderer))
132
+ registerRenderer('pdf', () => import('./renderers/pdf').then((m) => m.pdfRenderer))
133
+ registerRenderer('docx', () => import('./renderers/docx').then((m) => m.docxRenderer))
134
+ registerRenderer('xlsx', () => import('./renderers/xlsx').then((m) => m.xlsxRenderer))
135
+ registerRenderer('pptx', () => import('./renderers/pptx').then((m) => m.pptxRenderer))
136
+ registerRenderer('slack', () => import('./renderers/slack').then((m) => m.slackRenderer))
137
+ registerRenderer('svg', () => import('./renderers/svg').then((m) => m.svgRenderer))
138
+ registerRenderer('teams', () => import('./renderers/teams').then((m) => m.teamsRenderer))
139
+ registerRenderer('discord', () => import('./renderers/discord').then((m) => m.discordRenderer))
140
+ registerRenderer('telegram', () => import('./renderers/telegram').then((m) => m.telegramRenderer))
141
+ registerRenderer('notion', () => import('./renderers/notion').then((m) => m.notionRenderer))
142
+ registerRenderer('confluence', () =>
143
+ import('./renderers/confluence').then((m) => m.confluenceRenderer),
144
144
  )
145
- registerRenderer("whatsapp", () => import("./renderers/whatsapp").then((m) => m.whatsappRenderer))
146
- registerRenderer("google-chat", () =>
147
- import("./renderers/google-chat").then((m) => m.googleChatRenderer),
145
+ registerRenderer('whatsapp', () => import('./renderers/whatsapp').then((m) => m.whatsappRenderer))
146
+ registerRenderer('google-chat', () =>
147
+ import('./renderers/google-chat').then((m) => m.googleChatRenderer),
148
148
  )
149
149
  }
@@ -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
  * Atlassian Document Format (ADF) renderer — for Jira and Confluence.
@@ -8,13 +8,13 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
8
8
  */
9
9
 
10
10
  function resolveColumn(col: string | TableColumn): TableColumn {
11
- return typeof col === "string" ? { header: col } : col
11
+ return typeof col === 'string' ? { header: col } : col
12
12
  }
13
13
 
14
14
  function getTextContent(children: DocChild[]): string {
15
15
  return children
16
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
17
- .join("")
16
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
17
+ .join('')
18
18
  }
19
19
 
20
20
  interface AdfNode {
@@ -25,8 +25,8 @@ interface AdfNode {
25
25
  attrs?: Record<string, unknown>
26
26
  }
27
27
 
28
- function textNode(text: string, marks?: AdfNode["marks"]): AdfNode {
29
- return { type: "text", text, ...(marks && marks.length > 0 ? { marks } : {}) }
28
+ function textNode(text: string, marks?: AdfNode['marks']): AdfNode {
29
+ return { type: 'text', text, ...(marks && marks.length > 0 ? { marks } : {}) }
30
30
  }
31
31
 
32
32
  function nodeToAdf(node: DocNode): AdfNode[] {
@@ -34,65 +34,65 @@ function nodeToAdf(node: DocNode): AdfNode[] {
34
34
  const result: AdfNode[] = []
35
35
 
36
36
  switch (node.type) {
37
- case "document":
38
- case "page":
39
- case "section":
40
- case "row":
41
- case "column":
37
+ case 'document':
38
+ case 'page':
39
+ case 'section':
40
+ case 'row':
41
+ case 'column':
42
42
  for (const child of node.children) {
43
- if (typeof child !== "string") {
43
+ if (typeof child !== 'string') {
44
44
  result.push(...nodeToAdf(child))
45
45
  }
46
46
  }
47
47
  break
48
48
 
49
- case "heading": {
49
+ case 'heading': {
50
50
  const level = Math.min(Math.max((p.level as number) ?? 1, 1), 6)
51
51
  const text = getTextContent(node.children)
52
52
  result.push({
53
- type: "heading",
53
+ type: 'heading',
54
54
  attrs: { level },
55
- content: [textNode(text, [{ type: "strong" }])],
55
+ content: [textNode(text, [{ type: 'strong' }])],
56
56
  })
57
57
  break
58
58
  }
59
59
 
60
- case "text": {
60
+ case 'text': {
61
61
  const text = getTextContent(node.children)
62
- const marks: AdfNode["marks"] = []
63
- if (p.bold) marks.push({ type: "strong" })
64
- if (p.italic) marks.push({ type: "em" })
65
- if (p.underline) marks.push({ type: "underline" })
66
- if (p.strikethrough) marks.push({ type: "strike" })
67
- if (p.color) marks.push({ type: "textColor", attrs: { color: p.color as string } })
62
+ const marks: AdfNode['marks'] = []
63
+ if (p.bold) marks.push({ type: 'strong' })
64
+ if (p.italic) marks.push({ type: 'em' })
65
+ if (p.underline) marks.push({ type: 'underline' })
66
+ if (p.strikethrough) marks.push({ type: 'strike' })
67
+ if (p.color) marks.push({ type: 'textColor', attrs: { color: p.color as string } })
68
68
  result.push({
69
- type: "paragraph",
69
+ type: 'paragraph',
70
70
  content: [textNode(text, marks)],
71
71
  })
72
72
  break
73
73
  }
74
74
 
75
- case "link": {
75
+ case 'link': {
76
76
  const href = sanitizeHref(p.href as string)
77
77
  const text = getTextContent(node.children)
78
78
  result.push({
79
- type: "paragraph",
80
- content: [textNode(text, [{ type: "link", attrs: { href } }])],
79
+ type: 'paragraph',
80
+ content: [textNode(text, [{ type: 'link', attrs: { href } }])],
81
81
  })
82
82
  break
83
83
  }
84
84
 
85
- case "image": {
85
+ case 'image': {
86
86
  const src = sanitizeImageSrc(p.src as string)
87
- if (src.startsWith("http")) {
87
+ if (src.startsWith('http')) {
88
88
  result.push({
89
- type: "mediaSingle",
90
- attrs: { layout: "center" },
89
+ type: 'mediaSingle',
90
+ attrs: { layout: 'center' },
91
91
  content: [
92
92
  {
93
- type: "media",
93
+ type: 'media',
94
94
  attrs: {
95
- type: "external",
95
+ type: 'external',
96
96
  url: src,
97
97
  width: (p.width as number) ?? undefined,
98
98
  height: (p.height as number) ?? undefined,
@@ -104,54 +104,54 @@ function nodeToAdf(node: DocNode): AdfNode[] {
104
104
  break
105
105
  }
106
106
 
107
- case "table": {
107
+ case 'table': {
108
108
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
109
109
  const rows = (p.rows ?? []) as (string | number)[][]
110
110
 
111
111
  const headerRow: AdfNode = {
112
- type: "tableRow",
112
+ type: 'tableRow',
113
113
  content: columns.map((col) => ({
114
- type: "tableHeader",
114
+ type: 'tableHeader',
115
115
  content: [
116
116
  {
117
- type: "paragraph",
118
- content: [textNode(col.header, [{ type: "strong" }])],
117
+ type: 'paragraph',
118
+ content: [textNode(col.header, [{ type: 'strong' }])],
119
119
  },
120
120
  ],
121
121
  })),
122
122
  }
123
123
 
124
124
  const dataRows = rows.map((row) => ({
125
- type: "tableRow" as const,
125
+ type: 'tableRow' as const,
126
126
  content: columns.map((_, i) => ({
127
- type: "tableCell" as const,
127
+ type: 'tableCell' as const,
128
128
  content: [
129
129
  {
130
- type: "paragraph" as const,
131
- content: [textNode(String(row[i] ?? ""))],
130
+ type: 'paragraph' as const,
131
+ content: [textNode(String(row[i] ?? ''))],
132
132
  },
133
133
  ],
134
134
  })),
135
135
  }))
136
136
 
137
137
  result.push({
138
- type: "table",
139
- attrs: { isNumberColumnEnabled: false, layout: "default" },
138
+ type: 'table',
139
+ attrs: { isNumberColumnEnabled: false, layout: 'default' },
140
140
  content: [headerRow, ...dataRows],
141
141
  })
142
142
  break
143
143
  }
144
144
 
145
- case "list": {
145
+ case 'list': {
146
146
  const ordered = p.ordered as boolean | undefined
147
- const type = ordered ? "orderedList" : "bulletList"
147
+ const type = ordered ? 'orderedList' : 'bulletList'
148
148
  const items = node.children
149
- .filter((c): c is DocNode => typeof c !== "string")
149
+ .filter((c): c is DocNode => typeof c !== 'string')
150
150
  .map((item) => ({
151
- type: "listItem" as const,
151
+ type: 'listItem' as const,
152
152
  content: [
153
153
  {
154
- type: "paragraph" as const,
154
+ type: 'paragraph' as const,
155
155
  content: [textNode(getTextContent(item.children))],
156
156
  },
157
157
  ],
@@ -160,41 +160,41 @@ function nodeToAdf(node: DocNode): AdfNode[] {
160
160
  break
161
161
  }
162
162
 
163
- case "code": {
163
+ case 'code': {
164
164
  const text = getTextContent(node.children)
165
165
  const lang = (p.language as string) ?? null
166
166
  result.push({
167
- type: "codeBlock",
167
+ type: 'codeBlock',
168
168
  attrs: { language: lang },
169
169
  content: [textNode(text)],
170
170
  })
171
171
  break
172
172
  }
173
173
 
174
- case "divider":
175
- case "page-break":
176
- result.push({ type: "rule" })
174
+ case 'divider':
175
+ case 'page-break':
176
+ result.push({ type: 'rule' })
177
177
  break
178
178
 
179
- case "spacer":
180
- result.push({ type: "paragraph", content: [] })
179
+ case 'spacer':
180
+ result.push({ type: 'paragraph', content: [] })
181
181
  break
182
182
 
183
- case "button": {
183
+ case 'button': {
184
184
  const href = sanitizeHref(p.href as string)
185
185
  const text = getTextContent(node.children)
186
186
  result.push({
187
- type: "paragraph",
188
- content: [textNode(text, [{ type: "link", attrs: { href } }, { type: "strong" }])],
187
+ type: 'paragraph',
188
+ content: [textNode(text, [{ type: 'link', attrs: { href } }, { type: 'strong' }])],
189
189
  })
190
190
  break
191
191
  }
192
192
 
193
- case "quote": {
193
+ case 'quote': {
194
194
  const text = getTextContent(node.children)
195
195
  result.push({
196
- type: "blockquote",
197
- content: [{ type: "paragraph", content: [textNode(text)] }],
196
+ type: 'blockquote',
197
+ content: [{ type: 'paragraph', content: [textNode(text)] }],
198
198
  })
199
199
  break
200
200
  }
@@ -208,7 +208,7 @@ export const confluenceRenderer: DocumentRenderer = {
208
208
  const content = nodeToAdf(node)
209
209
  const adf = {
210
210
  version: 1,
211
- type: "doc",
211
+ type: 'doc',
212
212
  content,
213
213
  }
214
214
  return JSON.stringify(adf, null, 2)
@@ -1,11 +1,11 @@
1
- import type { DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import type { DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
2
2
 
3
3
  function resolveColumn(col: string | TableColumn): TableColumn {
4
- return typeof col === "string" ? { header: col } : col
4
+ return typeof col === 'string' ? { header: col } : col
5
5
  }
6
6
 
7
7
  function escapeCsv(value: string): string {
8
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
8
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
9
9
  return `"${value.replace(/"/g, '""')}"`
10
10
  }
11
11
  return value
@@ -13,11 +13,11 @@ function escapeCsv(value: string): string {
13
13
 
14
14
  function findTables(node: DocNode): DocNode[] {
15
15
  const tables: DocNode[] = []
16
- if (node.type === "table") {
16
+ if (node.type === 'table') {
17
17
  tables.push(node)
18
18
  }
19
19
  for (const child of node.children) {
20
- if (typeof child !== "string") {
20
+ if (typeof child !== 'string') {
21
21
  tables.push(...findTables(child))
22
22
  }
23
23
  }
@@ -36,14 +36,14 @@ function tableToCsv(node: DocNode): string {
36
36
  }
37
37
 
38
38
  // Header
39
- lines.push(columns.map((c) => escapeCsv(c.header)).join(","))
39
+ lines.push(columns.map((c) => escapeCsv(c.header)).join(','))
40
40
 
41
41
  // Rows
42
42
  for (const row of rows) {
43
- lines.push(row.map((cell) => escapeCsv(String(cell ?? ""))).join(","))
43
+ lines.push(row.map((cell) => escapeCsv(String(cell ?? ''))).join(','))
44
44
  }
45
45
 
46
- return lines.join("\n")
46
+ return lines.join('\n')
47
47
  }
48
48
 
49
49
  export const csvRenderer: DocumentRenderer = {
@@ -51,10 +51,10 @@ export const csvRenderer: DocumentRenderer = {
51
51
  const tables = findTables(node)
52
52
 
53
53
  if (tables.length === 0) {
54
- return "# No tables found in document\n"
54
+ return '# No tables found in document\n'
55
55
  }
56
56
 
57
57
  // If multiple tables, separate with blank lines
58
- return `${tables.map(tableToCsv).join("\n\n")}\n`
58
+ return `${tables.map(tableToCsv).join('\n\n')}\n`
59
59
  },
60
60
  }
@@ -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
  * Discord renderer — outputs embed JSON for Discord webhooks/bots.
@@ -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 DiscordField {
@@ -24,16 +24,16 @@ interface DiscordField {
24
24
 
25
25
  /** Extract the first h1 title and first HTTP image from the tree. */
26
26
  function extractMeta(node: DocNode): { title?: string; imageUrl?: string } {
27
- if (node.type === "heading") {
27
+ if (node.type === 'heading') {
28
28
  const level = (node.props.level as number) ?? 1
29
29
  if (level === 1) return { title: getTextContent(node.children) }
30
30
  }
31
- if (node.type === "image") {
31
+ if (node.type === 'image') {
32
32
  const src = sanitizeImageSrc(node.props.src as string)
33
- if (src.startsWith("http")) return { imageUrl: src }
33
+ if (src.startsWith('http')) return { imageUrl: src }
34
34
  }
35
35
  for (const child of node.children) {
36
- if (typeof child !== "string") {
36
+ if (typeof child !== 'string') {
37
37
  const result = extractMeta(child)
38
38
  if (result.title || result.imageUrl) return result
39
39
  }
@@ -46,17 +46,17 @@ function nodeToMarkdown(
46
46
  meta: { title?: string },
47
47
  ): { content: string; fields: DiscordField[] } {
48
48
  const p = node.props
49
- let content = ""
49
+ let content = ''
50
50
  const fields: DiscordField[] = []
51
51
 
52
52
  switch (node.type) {
53
- case "document":
54
- case "page":
55
- case "section":
56
- case "row":
57
- case "column":
53
+ case 'document':
54
+ case 'page':
55
+ case 'section':
56
+ case 'row':
57
+ case 'column':
58
58
  for (const child of node.children) {
59
- if (typeof child !== "string") {
59
+ if (typeof child !== 'string') {
60
60
  const result = nodeToMarkdown(child, meta)
61
61
  content += result.content
62
62
  fields.push(...result.fields)
@@ -64,7 +64,7 @@ function nodeToMarkdown(
64
64
  }
65
65
  break
66
66
 
67
- case "heading": {
67
+ case 'heading': {
68
68
  const text = getTextContent(node.children)
69
69
  const level = (p.level as number) ?? 1
70
70
  // Skip the first h1 — it's used as embed title
@@ -75,7 +75,7 @@ function nodeToMarkdown(
75
75
  break
76
76
  }
77
77
 
78
- case "text": {
78
+ case 'text': {
79
79
  let text = getTextContent(node.children)
80
80
  if (p.bold) text = `**${text}**`
81
81
  if (p.italic) text = `*${text}*`
@@ -84,18 +84,18 @@ function nodeToMarkdown(
84
84
  break
85
85
  }
86
86
 
87
- case "link": {
87
+ case 'link': {
88
88
  const href = sanitizeHref(p.href as string)
89
89
  const text = getTextContent(node.children)
90
90
  content += `[${text}](${href})\n\n`
91
91
  break
92
92
  }
93
93
 
94
- case "image":
94
+ case 'image':
95
95
  // Image handled via extractMeta — embedded as embed.image
96
96
  break
97
97
 
98
- case "table": {
98
+ case 'table': {
99
99
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
100
100
  const rows = (p.rows ?? []) as (string | number)[][]
101
101
 
@@ -103,56 +103,56 @@ function nodeToMarkdown(
103
103
  if (columns.length <= 3 && rows.length <= 10) {
104
104
  for (const col of columns) {
105
105
  const colIdx = columns.indexOf(col)
106
- const values = rows.map((row) => String(row[colIdx] ?? "")).join("\n")
106
+ const values = rows.map((row) => String(row[colIdx] ?? '')).join('\n')
107
107
  fields.push({
108
108
  name: col.header,
109
- value: values || "-",
109
+ value: values || '-',
110
110
  inline: true,
111
111
  })
112
112
  }
113
113
  } else {
114
114
  // Fallback to code block for large tables
115
- const header = columns.map((c) => c.header).join(" | ")
116
- const separator = columns.map(() => "---").join(" | ")
117
- const body = rows.map((row) => row.map((c) => String(c ?? "")).join(" | ")).join("\n")
115
+ const header = columns.map((c) => c.header).join(' | ')
116
+ const separator = columns.map(() => '---').join(' | ')
117
+ const body = rows.map((row) => row.map((c) => String(c ?? '')).join(' | ')).join('\n')
118
118
  content += `\`\`\`\n${header}\n${separator}\n${body}\n\`\`\`\n\n`
119
119
  }
120
120
  break
121
121
  }
122
122
 
123
- case "list": {
123
+ case 'list': {
124
124
  const ordered = p.ordered as boolean | undefined
125
125
  const items = node.children
126
- .filter((c): c is DocNode => typeof c !== "string")
126
+ .filter((c): c is DocNode => typeof c !== 'string')
127
127
  .map((item, i) => {
128
- const prefix = ordered ? `${i + 1}.` : ""
128
+ const prefix = ordered ? `${i + 1}.` : ''
129
129
  return `${prefix} ${getTextContent(item.children)}`
130
130
  })
131
- .join("\n")
131
+ .join('\n')
132
132
  content += `${items}\n\n`
133
133
  break
134
134
  }
135
135
 
136
- case "code": {
137
- const lang = (p.language as string) ?? ""
136
+ case 'code': {
137
+ const lang = (p.language as string) ?? ''
138
138
  const text = getTextContent(node.children)
139
139
  content += `\`\`\`${lang}\n${text}\n\`\`\`\n\n`
140
140
  break
141
141
  }
142
142
 
143
- case "divider":
144
- case "page-break":
145
- content += "───────────\n\n"
143
+ case 'divider':
144
+ case 'page-break':
145
+ content += '───────────\n\n'
146
146
  break
147
147
 
148
- case "button": {
148
+ case 'button': {
149
149
  const href = sanitizeHref(p.href as string)
150
150
  const text = getTextContent(node.children)
151
151
  content += `[**${text}**](${href})\n\n`
152
152
  break
153
153
  }
154
154
 
155
- case "quote": {
155
+ case 'quote': {
156
156
  const text = getTextContent(node.children)
157
157
  content += `> ${text}\n\n`
158
158
  break