@pyreon/document 0.0.1

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/lib/analysis/index.js.html +5406 -0
  3. package/lib/chunk-ErZ26oRB.js +48 -0
  4. package/lib/confluence-Va8e7RxQ.js +192 -0
  5. package/lib/confluence-Va8e7RxQ.js.map +1 -0
  6. package/lib/csv-2c38ub-Y.js +32 -0
  7. package/lib/csv-2c38ub-Y.js.map +1 -0
  8. package/lib/discord-DAoUZqvE.js +134 -0
  9. package/lib/discord-DAoUZqvE.js.map +1 -0
  10. package/lib/dist-BsqdI2nY.js +20179 -0
  11. package/lib/dist-BsqdI2nY.js.map +1 -0
  12. package/lib/docx-CorFwEH9.js +450 -0
  13. package/lib/docx-CorFwEH9.js.map +1 -0
  14. package/lib/email-Bn_Brjdp.js +131 -0
  15. package/lib/email-Bn_Brjdp.js.map +1 -0
  16. package/lib/exceljs-BoIDUUaw.js +34377 -0
  17. package/lib/exceljs-BoIDUUaw.js.map +1 -0
  18. package/lib/google-chat-B6I017I1.js +125 -0
  19. package/lib/google-chat-B6I017I1.js.map +1 -0
  20. package/lib/html-De_iS_f0.js +151 -0
  21. package/lib/html-De_iS_f0.js.map +1 -0
  22. package/lib/index.js +619 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/markdown-BYC_3C9i.js +75 -0
  25. package/lib/markdown-BYC_3C9i.js.map +1 -0
  26. package/lib/notion-DHaQHO6P.js +187 -0
  27. package/lib/notion-DHaQHO6P.js.map +1 -0
  28. package/lib/pdf-CDPc5Itc.js +419 -0
  29. package/lib/pdf-CDPc5Itc.js.map +1 -0
  30. package/lib/pdfmake-DnmLxK4Q.js +55511 -0
  31. package/lib/pdfmake-DnmLxK4Q.js.map +1 -0
  32. package/lib/pptx-DKQU6bjq.js +252 -0
  33. package/lib/pptx-DKQU6bjq.js.map +1 -0
  34. package/lib/pptxgen.es-COcgXsyx.js +5697 -0
  35. package/lib/pptxgen.es-COcgXsyx.js.map +1 -0
  36. package/lib/slack-CJRJgkag.js +139 -0
  37. package/lib/slack-CJRJgkag.js.map +1 -0
  38. package/lib/svg-BM8biZmL.js +187 -0
  39. package/lib/svg-BM8biZmL.js.map +1 -0
  40. package/lib/teams-S99tonRG.js +176 -0
  41. package/lib/teams-S99tonRG.js.map +1 -0
  42. package/lib/telegram-CbEO_2PN.js +77 -0
  43. package/lib/telegram-CbEO_2PN.js.map +1 -0
  44. package/lib/text-B5U8ucRr.js +75 -0
  45. package/lib/text-B5U8ucRr.js.map +1 -0
  46. package/lib/types/index.d.ts +528 -0
  47. package/lib/types/index.d.ts.map +1 -0
  48. package/lib/vfs_fonts-Df1kkZ4Y.js +19 -0
  49. package/lib/vfs_fonts-Df1kkZ4Y.js.map +1 -0
  50. package/lib/whatsapp-DJ2D1jGG.js +64 -0
  51. package/lib/whatsapp-DJ2D1jGG.js.map +1 -0
  52. package/lib/xlsx-D47x-gZ5.js +199 -0
  53. package/lib/xlsx-D47x-gZ5.js.map +1 -0
  54. package/package.json +62 -0
  55. package/src/builder.ts +266 -0
  56. package/src/download.ts +76 -0
  57. package/src/env.d.ts +17 -0
  58. package/src/index.ts +98 -0
  59. package/src/nodes.ts +315 -0
  60. package/src/render.ts +222 -0
  61. package/src/renderers/confluence.ts +231 -0
  62. package/src/renderers/csv.ts +67 -0
  63. package/src/renderers/discord.ts +192 -0
  64. package/src/renderers/docx.ts +612 -0
  65. package/src/renderers/email.ts +230 -0
  66. package/src/renderers/google-chat.ts +211 -0
  67. package/src/renderers/html.ts +225 -0
  68. package/src/renderers/markdown.ts +144 -0
  69. package/src/renderers/notion.ts +264 -0
  70. package/src/renderers/pdf.ts +427 -0
  71. package/src/renderers/pptx.ts +353 -0
  72. package/src/renderers/slack.ts +192 -0
  73. package/src/renderers/svg.ts +254 -0
  74. package/src/renderers/teams.ts +234 -0
  75. package/src/renderers/telegram.ts +137 -0
  76. package/src/renderers/text.ts +154 -0
  77. package/src/renderers/whatsapp.ts +121 -0
  78. package/src/renderers/xlsx.ts +342 -0
  79. package/src/tests/document.test.ts +2920 -0
  80. package/src/types.ts +291 -0
@@ -0,0 +1,64 @@
1
+ //#region src/renderers/whatsapp.ts
2
+ /**
3
+ * WhatsApp renderer — outputs formatted text using WhatsApp's markup.
4
+ * WhatsApp supports: *bold*, _italic_, ~strikethrough~, ```code```, > quote
5
+ */
6
+ function resolveColumn(col) {
7
+ return typeof col === "string" ? { header: col } : col;
8
+ }
9
+ function getTextContent(children) {
10
+ return children.map((c) => typeof c === "string" ? c : getTextContent(c.children)).join("");
11
+ }
12
+ function renderNode(node) {
13
+ const p = node.props;
14
+ switch (node.type) {
15
+ case "document":
16
+ case "page":
17
+ case "section":
18
+ case "row":
19
+ case "column": return node.children.map((c) => typeof c === "string" ? c : renderNode(c)).join("");
20
+ case "heading": return `*${getTextContent(node.children)}*\n\n`;
21
+ case "text": {
22
+ let text = getTextContent(node.children);
23
+ if (p.bold) text = `*${text}*`;
24
+ if (p.italic) text = `_${text}_`;
25
+ if (p.strikethrough) text = `~${text}~`;
26
+ return `${text}\n\n`;
27
+ }
28
+ case "link": {
29
+ const href = p.href;
30
+ return `${getTextContent(node.children)}: ${href}\n\n`;
31
+ }
32
+ case "image": return "";
33
+ case "table": {
34
+ const columns = (p.columns ?? []).map(resolveColumn);
35
+ const rows = p.rows ?? [];
36
+ let result = `${columns.map((c) => `*${c.header}*`).join(" | ")}\n${rows.map((row) => row.map((c) => String(c ?? "")).join(" | ")).join("\n")}\n\n`;
37
+ if (p.caption) result = `_${p.caption}_\n${result}`;
38
+ return result;
39
+ }
40
+ case "list": {
41
+ const ordered = p.ordered;
42
+ return `${node.children.filter((c) => typeof c !== "string").map((item, i) => {
43
+ return `${ordered ? `${i + 1}.` : "•"} ${getTextContent(item.children)}`;
44
+ }).join("\n")}\n\n`;
45
+ }
46
+ case "code": return `\`\`\`${getTextContent(node.children)}\`\`\`\n\n`;
47
+ case "divider":
48
+ case "page-break": return "───────────\n\n";
49
+ case "spacer": return "\n";
50
+ case "button": {
51
+ const href = p.href;
52
+ return `*${getTextContent(node.children)}*: ${href}\n\n`;
53
+ }
54
+ case "quote": return `> ${getTextContent(node.children)}\n\n`;
55
+ default: return "";
56
+ }
57
+ }
58
+ const whatsappRenderer = { async render(node, _options) {
59
+ return renderNode(node).trim();
60
+ } };
61
+
62
+ //#endregion
63
+ export { whatsappRenderer };
64
+ //# sourceMappingURL=whatsapp-DJ2D1jGG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whatsapp-DJ2D1jGG.js","names":[],"sources":["../src/renderers/whatsapp.ts"],"sourcesContent":["import type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * WhatsApp renderer — outputs formatted text using WhatsApp's markup.\n * WhatsApp supports: *bold*, _italic_, ~strikethrough~, ```code```, > quote\n */\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction getTextContent(children: DocChild[]): string {\n return children\n .map((c) =>\n typeof c === 'string' ? c : getTextContent((c as DocNode).children),\n )\n .join('')\n}\n\nfunction renderNode(node: DocNode): string {\n const p = node.props\n\n switch (node.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n return node.children\n .map((c) => (typeof c === 'string' ? c : renderNode(c)))\n .join('')\n\n case 'heading': {\n const text = getTextContent(node.children)\n return `*${text}*\\n\\n`\n }\n\n case 'text': {\n let text = getTextContent(node.children)\n if (p.bold) text = `*${text}*`\n if (p.italic) text = `_${text}_`\n if (p.strikethrough) text = `~${text}~`\n return `${text}\\n\\n`\n }\n\n case 'link': {\n const href = p.href as string\n const text = getTextContent(node.children)\n return `${text}: ${href}\\n\\n`\n }\n\n case 'image':\n // WhatsApp doesn't support inline images in text\n return ''\n\n case 'table': {\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(\n resolveColumn,\n )\n const rows = (p.rows ?? []) as (string | number)[][]\n\n const header = columns.map((c) => `*${c.header}*`).join(' | ')\n const body = rows\n .map((row) => row.map((c) => String(c ?? '')).join(' | '))\n .join('\\n')\n\n let result = `${header}\\n${body}\\n\\n`\n if (p.caption) result = `_${p.caption}_\\n${result}`\n return result\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n return `${node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item, i) => {\n const prefix = ordered ? `${i + 1}.` : '•'\n return `${prefix} ${getTextContent(item.children)}`\n })\n .join('\\n')}\\n\\n`\n }\n\n case 'code': {\n const text = getTextContent(node.children)\n return `\\`\\`\\`${text}\\`\\`\\`\\n\\n`\n }\n\n case 'divider':\n case 'page-break':\n return '───────────\\n\\n'\n\n case 'spacer':\n return '\\n'\n\n case 'button': {\n const href = p.href as string\n const text = getTextContent(node.children)\n return `*${text}*: ${href}\\n\\n`\n }\n\n case 'quote': {\n const text = getTextContent(node.children)\n return `> ${text}\\n\\n`\n }\n\n default:\n return ''\n }\n}\n\nexport const whatsappRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n return renderNode(node).trim()\n },\n}\n"],"mappings":";;;;;AAaA,SAAS,cAAc,KAAwC;AAC7D,QAAO,OAAO,QAAQ,WAAW,EAAE,QAAQ,KAAK,GAAG;;AAGrD,SAAS,eAAe,UAA8B;AACpD,QAAO,SACJ,KAAK,MACJ,OAAO,MAAM,WAAW,IAAI,eAAgB,EAAc,SAAS,CACpE,CACA,KAAK,GAAG;;AAGb,SAAS,WAAW,MAAuB;CACzC,MAAM,IAAI,KAAK;AAEf,SAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO,KAAK,SACT,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,WAAW,EAAE,CAAE,CACvD,KAAK,GAAG;EAEb,KAAK,UAEH,QAAO,IADM,eAAe,KAAK,SAAS,CAC1B;EAGlB,KAAK,QAAQ;GACX,IAAI,OAAO,eAAe,KAAK,SAAS;AACxC,OAAI,EAAE,KAAM,QAAO,IAAI,KAAK;AAC5B,OAAI,EAAE,OAAQ,QAAO,IAAI,KAAK;AAC9B,OAAI,EAAE,cAAe,QAAO,IAAI,KAAK;AACrC,UAAO,GAAG,KAAK;;EAGjB,KAAK,QAAQ;GACX,MAAM,OAAO,EAAE;AAEf,UAAO,GADM,eAAe,KAAK,SAAS,CAC3B,IAAI,KAAK;;EAG1B,KAAK,QAEH,QAAO;EAET,KAAK,SAAS;GACZ,MAAM,WAAY,EAAE,WAAW,EAAE,EAA+B,IAC9D,cACD;GACD,MAAM,OAAQ,EAAE,QAAQ,EAAE;GAO1B,IAAI,SAAS,GALE,QAAQ,KAAK,MAAM,IAAI,EAAE,OAAO,GAAG,CAAC,KAAK,MAAM,CAKvC,IAJV,KACV,KAAK,QAAQ,IAAI,KAAK,MAAM,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,CACzD,KAAK,KAAK,CAEmB;AAChC,OAAI,EAAE,QAAS,UAAS,IAAI,EAAE,QAAQ,KAAK;AAC3C,UAAO;;EAGT,KAAK,QAAQ;GACX,MAAM,UAAU,EAAE;AAClB,UAAO,GAAG,KAAK,SACZ,QAAQ,MAAoB,OAAO,MAAM,SAAS,CAClD,KAAK,MAAM,MAAM;AAEhB,WAAO,GADQ,UAAU,GAAG,IAAI,EAAE,KAAK,IACtB,GAAG,eAAe,KAAK,SAAS;KACjD,CACD,KAAK,KAAK,CAAC;;EAGhB,KAAK,OAEH,QAAO,SADM,eAAe,KAAK,SAAS,CACrB;EAGvB,KAAK;EACL,KAAK,aACH,QAAO;EAET,KAAK,SACH,QAAO;EAET,KAAK,UAAU;GACb,MAAM,OAAO,EAAE;AAEf,UAAO,IADM,eAAe,KAAK,SAAS,CAC1B,KAAK,KAAK;;EAG5B,KAAK,QAEH,QAAO,KADM,eAAe,KAAK,SAAS,CACzB;EAGnB,QACE,QAAO;;;AAIb,MAAa,mBAAqC,EAChD,MAAM,OAAO,MAAe,UAA2C;AACrE,QAAO,WAAW,KAAK,CAAC,MAAM;GAEjC"}
@@ -0,0 +1,199 @@
1
+ //#region src/renderers/xlsx.ts
2
+ /**
3
+ * XLSX renderer — lazy-loads ExcelJS on first use.
4
+ * Extracts tables from the document and renders each as a worksheet.
5
+ * Non-table content (headings, text) becomes header rows.
6
+ */
7
+ function resolveColumn(col) {
8
+ return typeof col === "string" ? { header: col } : col;
9
+ }
10
+ function getTextContent(children) {
11
+ return children.map((c) => typeof c === "string" ? c : getTextContent(c.children)).join("");
12
+ }
13
+ /** Walk the tree and group content into sheets (one per page, or one global). */
14
+ function extractSheets(node) {
15
+ const sheets = [];
16
+ let currentSheet = {
17
+ name: "Sheet 1",
18
+ headings: [],
19
+ tables: []
20
+ };
21
+ function walk(n) {
22
+ switch (n.type) {
23
+ case "document":
24
+ walkChildren(n);
25
+ break;
26
+ case "page":
27
+ pushCurrentSheet();
28
+ currentSheet = {
29
+ name: `Sheet ${sheets.length + 1}`,
30
+ headings: [],
31
+ tables: []
32
+ };
33
+ walkChildren(n);
34
+ break;
35
+ case "heading":
36
+ addHeading(n);
37
+ break;
38
+ case "table":
39
+ currentSheet.tables.push(n);
40
+ break;
41
+ default: walkChildren(n);
42
+ }
43
+ }
44
+ function walkChildren(n) {
45
+ for (const child of n.children) if (typeof child !== "string") walk(child);
46
+ }
47
+ function pushCurrentSheet() {
48
+ if (currentSheet.tables.length > 0 || currentSheet.headings.length > 0) sheets.push(currentSheet);
49
+ }
50
+ function addHeading(n) {
51
+ const text = getTextContent(n.children);
52
+ currentSheet.headings.push(text);
53
+ if (currentSheet.headings.length === 1) currentSheet.name = text.slice(0, 31);
54
+ }
55
+ walk(node);
56
+ pushCurrentSheet();
57
+ return sheets;
58
+ }
59
+ /** Parse a cell value, handling currencies, percentages, and plain numbers. */
60
+ function parseCellValue(value) {
61
+ if (value == null) return "";
62
+ if (typeof value === "number") return value;
63
+ const trimmed = value.trim();
64
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return Number.parseFloat(trimmed) / 100;
65
+ if (trimmed.match(/^-?\$[\d,]+(\.\d+)?$/)) return Number.parseFloat(trimmed.replace(/[$,]/g, ""));
66
+ const plainNum = Number(trimmed.replace(/,/g, ""));
67
+ if (!Number.isNaN(plainNum) && /^-?[\d,]+(\.\d+)?$/.test(trimmed)) return plainNum;
68
+ return value;
69
+ }
70
+ /** Get ExcelJS number format string for a value. */
71
+ function getCellFormat(originalValue) {
72
+ if (typeof originalValue !== "string") return void 0;
73
+ const trimmed = originalValue.trim();
74
+ if (/^-?\d+(\.\d+)?%$/.test(trimmed)) return "0.00%";
75
+ if (/^-?\$/.test(trimmed)) return "$#,##0.00";
76
+ }
77
+ /** Map alignment string to ExcelJS horizontal alignment. */
78
+ function mapAlignment(align) {
79
+ if (align === "left" || align === "center" || align === "right") return align;
80
+ }
81
+ /** Thin border style for ExcelJS. */
82
+ function thinBorder() {
83
+ return {
84
+ style: "thin",
85
+ color: { argb: "FFDDDDDD" }
86
+ };
87
+ }
88
+ /** Apply header styling to a cell. */
89
+ function styleHeaderCell(cell, col, hs, bordered) {
90
+ cell.value = col.header;
91
+ cell.font = {
92
+ bold: true,
93
+ color: { argb: hs?.color?.replace("#", "FF") ?? "FF000000" }
94
+ };
95
+ if (hs?.background) cell.fill = {
96
+ type: "pattern",
97
+ pattern: "solid",
98
+ fgColor: { argb: hs.background.replace("#", "FF") }
99
+ };
100
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" };
101
+ if (bordered) cell.border = {
102
+ top: thinBorder(),
103
+ bottom: thinBorder(),
104
+ left: thinBorder(),
105
+ right: thinBorder()
106
+ };
107
+ }
108
+ /** Apply data cell value and styling. */
109
+ function styleDataCell(cell, rawValue, col, striped, isOddRow, bordered) {
110
+ cell.value = parseCellValue(rawValue);
111
+ const fmt = getCellFormat(rawValue);
112
+ if (fmt) cell.numFmt = fmt;
113
+ cell.alignment = { horizontal: mapAlignment(col.align) ?? "left" };
114
+ if (striped && isOddRow) cell.fill = {
115
+ type: "pattern",
116
+ pattern: "solid",
117
+ fgColor: { argb: "FFF9F9F9" }
118
+ };
119
+ if (bordered) cell.border = {
120
+ top: thinBorder(),
121
+ bottom: thinBorder(),
122
+ left: thinBorder(),
123
+ right: thinBorder()
124
+ };
125
+ }
126
+ /** Render a single table node into the worksheet starting at the given row. Returns the next row number. */
127
+ function renderTable(ws, tableNode, startRow) {
128
+ let rowNum = startRow;
129
+ const columns = (tableNode.props.columns ?? []).map(resolveColumn);
130
+ const rows = tableNode.props.rows ?? [];
131
+ const hs = tableNode.props.headerStyle;
132
+ const bordered = tableNode.props.bordered ?? false;
133
+ if (tableNode.props.caption) {
134
+ const captionCell = ws.getRow(rowNum).getCell(1);
135
+ captionCell.value = tableNode.props.caption;
136
+ captionCell.font = {
137
+ italic: true,
138
+ size: 10
139
+ };
140
+ rowNum++;
141
+ }
142
+ const headerRow = ws.getRow(rowNum);
143
+ for (let i = 0; i < columns.length; i++) {
144
+ const col = columns[i];
145
+ if (!col) continue;
146
+ styleHeaderCell(headerRow.getCell(i + 1), col, hs, bordered);
147
+ }
148
+ rowNum++;
149
+ for (let r = 0; r < rows.length; r++) {
150
+ const dataRow = ws.getRow(rowNum);
151
+ for (let c = 0; c < columns.length; c++) {
152
+ const col = columns[c];
153
+ if (!col) continue;
154
+ styleDataCell(dataRow.getCell(c + 1), rows[r]?.[c], col, tableNode.props.striped ?? false, r % 2 === 1, bordered);
155
+ }
156
+ rowNum++;
157
+ }
158
+ return rowNum + 1;
159
+ }
160
+ /** Auto-fit column widths based on content. */
161
+ function autoFitColumns(ws) {
162
+ for (const col of ws.columns) {
163
+ let maxLen = 10;
164
+ col.eachCell?.({ includeEmpty: false }, (cell) => {
165
+ const len = String(cell.value ?? "").length;
166
+ if (len > maxLen) maxLen = len;
167
+ });
168
+ col.width = Math.min(maxLen + 2, 50);
169
+ }
170
+ }
171
+ const xlsxRenderer = { async render(node, _options) {
172
+ const workbook = new (await (import("./exceljs-BoIDUUaw.js"))).default.Workbook();
173
+ workbook.creator = node.props.author ?? "";
174
+ workbook.title = node.props.title ?? "";
175
+ const sheets = extractSheets(node);
176
+ if (sheets.length === 0) workbook.addWorksheet("Sheet 1");
177
+ for (const sheet of sheets) {
178
+ const ws = workbook.addWorksheet(sheet.name);
179
+ let rowNum = 1;
180
+ for (const heading of sheet.headings) {
181
+ const row = ws.getRow(rowNum);
182
+ row.getCell(1).value = heading;
183
+ row.getCell(1).font = {
184
+ bold: true,
185
+ size: 14
186
+ };
187
+ rowNum++;
188
+ }
189
+ if (sheet.headings.length > 0) rowNum++;
190
+ for (const tableNode of sheet.tables) rowNum = renderTable(ws, tableNode, rowNum);
191
+ autoFitColumns(ws);
192
+ }
193
+ const buffer = await workbook.xlsx.writeBuffer();
194
+ return new Uint8Array(buffer);
195
+ } };
196
+
197
+ //#endregion
198
+ export { xlsxRenderer };
199
+ //# sourceMappingURL=xlsx-D47x-gZ5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xlsx-D47x-gZ5.js","names":[],"sources":["../src/renderers/xlsx.ts"],"sourcesContent":["import type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * XLSX renderer — lazy-loads ExcelJS on first use.\n * Extracts tables from the document and renders each as a worksheet.\n * Non-table content (headings, text) becomes header rows.\n */\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction getTextContent(children: DocChild[]): string {\n return children\n .map((c) =>\n typeof c === 'string' ? c : getTextContent((c as DocNode).children),\n )\n .join('')\n}\n\ninterface ExtractedSheet {\n name: string\n headings: string[]\n tables: DocNode[]\n}\n\n/** Walk the tree and group content into sheets (one per page, or one global). */\nfunction extractSheets(node: DocNode): ExtractedSheet[] {\n const sheets: ExtractedSheet[] = []\n let currentSheet: ExtractedSheet = {\n name: 'Sheet 1',\n headings: [],\n tables: [],\n }\n\n function walk(n: DocNode): void {\n switch (n.type) {\n case 'document':\n walkChildren(n)\n break\n\n case 'page':\n pushCurrentSheet()\n currentSheet = {\n name: `Sheet ${sheets.length + 1}`,\n headings: [],\n tables: [],\n }\n walkChildren(n)\n break\n\n case 'heading':\n addHeading(n)\n break\n\n case 'table':\n currentSheet.tables.push(n)\n break\n\n default:\n walkChildren(n)\n }\n }\n\n function walkChildren(n: DocNode): void {\n for (const child of n.children) {\n if (typeof child !== 'string') walk(child)\n }\n }\n\n function pushCurrentSheet(): void {\n if (currentSheet.tables.length > 0 || currentSheet.headings.length > 0) {\n sheets.push(currentSheet)\n }\n }\n\n function addHeading(n: DocNode): void {\n const text = getTextContent(n.children)\n currentSheet.headings.push(text)\n if (currentSheet.headings.length === 1) {\n currentSheet.name = text.slice(0, 31) // Excel sheet name max 31 chars\n }\n }\n\n walk(node)\n pushCurrentSheet()\n\n return sheets\n}\n\n/** Parse a cell value, handling currencies, percentages, and plain numbers. */\nfunction parseCellValue(value: string | number | undefined): string | number {\n if (value == null) return ''\n if (typeof value === 'number') return value\n\n const trimmed = value.trim()\n\n // Percentage: \"45%\" or \"12.5%\"\n if (/^-?\\d+(\\.\\d+)?%$/.test(trimmed)) {\n return Number.parseFloat(trimmed) / 100\n }\n\n // Currency: \"$1,234.56\", \"$1234\", \"-$500\"\n const currencyMatch = trimmed.match(/^-?\\$[\\d,]+(\\.\\d+)?$/)\n if (currencyMatch) {\n return Number.parseFloat(trimmed.replace(/[$,]/g, ''))\n }\n\n // Plain number: \"1,234.56\", \"1234\", \"-500.5\"\n const plainNum = Number(trimmed.replace(/,/g, ''))\n if (!Number.isNaN(plainNum) && /^-?[\\d,]+(\\.\\d+)?$/.test(trimmed)) {\n return plainNum\n }\n\n return value\n}\n\n/** Get ExcelJS number format string for a value. */\nfunction getCellFormat(\n originalValue: string | number | undefined,\n): string | undefined {\n if (typeof originalValue !== 'string') return undefined\n const trimmed = originalValue.trim()\n\n if (/^-?\\d+(\\.\\d+)?%$/.test(trimmed)) return '0.00%'\n if (/^-?\\$/.test(trimmed)) return '$#,##0.00'\n return undefined\n}\n\n/** Map alignment string to ExcelJS horizontal alignment. */\nfunction mapAlignment(align?: string): 'left' | 'center' | 'right' | undefined {\n if (align === 'left' || align === 'center' || align === 'right') return align\n return undefined\n}\n\n/** Thin border style for ExcelJS. */\nfunction thinBorder(): { style: 'thin'; color: { argb: string } } {\n return { style: 'thin', color: { argb: 'FFDDDDDD' } }\n}\n\n/** Apply header styling to a cell. */\nfunction styleHeaderCell(\n cell: {\n font: unknown\n fill: unknown\n alignment: unknown\n border: unknown\n value: unknown\n },\n col: TableColumn,\n hs: { background?: string; color?: string } | undefined,\n bordered: boolean,\n): void {\n cell.value = col.header\n cell.font = {\n bold: true,\n color: { argb: hs?.color?.replace('#', 'FF') ?? 'FF000000' },\n }\n if (hs?.background) {\n cell.fill = {\n type: 'pattern',\n pattern: 'solid',\n fgColor: { argb: hs.background.replace('#', 'FF') },\n }\n }\n cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }\n if (bordered) {\n cell.border = {\n top: thinBorder(),\n bottom: thinBorder(),\n left: thinBorder(),\n right: thinBorder(),\n }\n }\n}\n\n/** Apply data cell value and styling. */\nfunction styleDataCell(\n cell: {\n value: unknown\n numFmt: unknown\n alignment: unknown\n fill: unknown\n border: unknown\n },\n rawValue: string | number | undefined,\n col: TableColumn,\n striped: boolean,\n isOddRow: boolean,\n bordered: boolean,\n): void {\n cell.value = parseCellValue(rawValue)\n const fmt = getCellFormat(rawValue)\n if (fmt) cell.numFmt = fmt\n cell.alignment = { horizontal: mapAlignment(col.align) ?? 'left' }\n if (striped && isOddRow) {\n cell.fill = {\n type: 'pattern',\n pattern: 'solid',\n fgColor: { argb: 'FFF9F9F9' },\n }\n }\n if (bordered) {\n cell.border = {\n top: thinBorder(),\n bottom: thinBorder(),\n left: thinBorder(),\n right: thinBorder(),\n }\n }\n}\n\n/** Render a single table node into the worksheet starting at the given row. Returns the next row number. */\nfunction renderTable(\n ws: {\n getRow: (n: number) => { getCell: (n: number) => Record<string, unknown> }\n columns: unknown[]\n },\n tableNode: DocNode,\n startRow: number,\n): number {\n let rowNum = startRow\n const columns = (\n (tableNode.props.columns ?? []) as (string | TableColumn)[]\n ).map(resolveColumn)\n const rows = (tableNode.props.rows ?? []) as (string | number)[][]\n const hs = tableNode.props.headerStyle as\n | { background?: string; color?: string }\n | undefined\n const bordered = (tableNode.props.bordered as boolean) ?? false\n\n // Caption\n if (tableNode.props.caption) {\n const captionRow = ws.getRow(rowNum)\n const captionCell = captionRow.getCell(1)\n captionCell.value = tableNode.props.caption as string\n captionCell.font = { italic: true, size: 10 }\n rowNum++\n }\n\n // Header row\n const headerRow = ws.getRow(rowNum)\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n if (!col) continue\n styleHeaderCell(headerRow.getCell(i + 1) as any, col, hs, bordered)\n }\n rowNum++\n\n // Data rows\n for (let r = 0; r < rows.length; r++) {\n const dataRow = ws.getRow(rowNum)\n for (let c = 0; c < columns.length; c++) {\n const col = columns[c]\n if (!col) continue\n styleDataCell(\n dataRow.getCell(c + 1) as any,\n rows[r]?.[c],\n col,\n (tableNode.props.striped as boolean) ?? false,\n r % 2 === 1,\n bordered,\n )\n }\n rowNum++\n }\n\n return rowNum + 1 // gap after table\n}\n\n/** Auto-fit column widths based on content. */\nfunction autoFitColumns(ws: {\n columns: {\n width: number\n eachCell?: (\n opts: { includeEmpty: boolean },\n cb: (cell: { value: unknown }) => void,\n ) => void\n }[]\n}): void {\n for (const col of ws.columns) {\n let maxLen = 10\n col.eachCell?.({ includeEmpty: false }, (cell) => {\n const len = String(cell.value ?? '').length\n if (len > maxLen) maxLen = len\n })\n col.width = Math.min(maxLen + 2, 50)\n }\n}\n\nexport const xlsxRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n const ExcelJS = await import('exceljs')\n const workbook = new ExcelJS.default.Workbook()\n\n workbook.creator = (node.props.author as string) ?? ''\n workbook.title = (node.props.title as string) ?? ''\n\n const sheets = extractSheets(node)\n\n if (sheets.length === 0) {\n workbook.addWorksheet('Sheet 1')\n }\n\n for (const sheet of sheets) {\n const ws = workbook.addWorksheet(sheet.name)\n\n let rowNum = 1\n\n // Add headings as title rows\n for (const heading of sheet.headings) {\n const row = ws.getRow(rowNum)\n row.getCell(1).value = heading\n row.getCell(1).font = { bold: true, size: 14 }\n rowNum++\n }\n\n if (sheet.headings.length > 0) rowNum++ // gap after headings\n\n // Add tables\n for (const tableNode of sheet.tables) {\n rowNum = renderTable(\n ws as unknown as Parameters<typeof renderTable>[0],\n tableNode,\n rowNum,\n )\n }\n\n // Auto-fit columns (approximate)\n autoFitColumns(ws as unknown as Parameters<typeof autoFitColumns>[0])\n }\n\n const buffer = await workbook.xlsx.writeBuffer()\n return new Uint8Array(buffer as ArrayBuffer)\n },\n}\n"],"mappings":";;;;;;AAcA,SAAS,cAAc,KAAwC;AAC7D,QAAO,OAAO,QAAQ,WAAW,EAAE,QAAQ,KAAK,GAAG;;AAGrD,SAAS,eAAe,UAA8B;AACpD,QAAO,SACJ,KAAK,MACJ,OAAO,MAAM,WAAW,IAAI,eAAgB,EAAc,SAAS,CACpE,CACA,KAAK,GAAG;;;AAUb,SAAS,cAAc,MAAiC;CACtD,MAAM,SAA2B,EAAE;CACnC,IAAI,eAA+B;EACjC,MAAM;EACN,UAAU,EAAE;EACZ,QAAQ,EAAE;EACX;CAED,SAAS,KAAK,GAAkB;AAC9B,UAAQ,EAAE,MAAV;GACE,KAAK;AACH,iBAAa,EAAE;AACf;GAEF,KAAK;AACH,sBAAkB;AAClB,mBAAe;KACb,MAAM,SAAS,OAAO,SAAS;KAC/B,UAAU,EAAE;KACZ,QAAQ,EAAE;KACX;AACD,iBAAa,EAAE;AACf;GAEF,KAAK;AACH,eAAW,EAAE;AACb;GAEF,KAAK;AACH,iBAAa,OAAO,KAAK,EAAE;AAC3B;GAEF,QACE,cAAa,EAAE;;;CAIrB,SAAS,aAAa,GAAkB;AACtC,OAAK,MAAM,SAAS,EAAE,SACpB,KAAI,OAAO,UAAU,SAAU,MAAK,MAAM;;CAI9C,SAAS,mBAAyB;AAChC,MAAI,aAAa,OAAO,SAAS,KAAK,aAAa,SAAS,SAAS,EACnE,QAAO,KAAK,aAAa;;CAI7B,SAAS,WAAW,GAAkB;EACpC,MAAM,OAAO,eAAe,EAAE,SAAS;AACvC,eAAa,SAAS,KAAK,KAAK;AAChC,MAAI,aAAa,SAAS,WAAW,EACnC,cAAa,OAAO,KAAK,MAAM,GAAG,GAAG;;AAIzC,MAAK,KAAK;AACV,mBAAkB;AAElB,QAAO;;;AAIT,SAAS,eAAe,OAAqD;AAC3E,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;CAEtC,MAAM,UAAU,MAAM,MAAM;AAG5B,KAAI,mBAAmB,KAAK,QAAQ,CAClC,QAAO,OAAO,WAAW,QAAQ,GAAG;AAKtC,KADsB,QAAQ,MAAM,uBAAuB,CAEzD,QAAO,OAAO,WAAW,QAAQ,QAAQ,SAAS,GAAG,CAAC;CAIxD,MAAM,WAAW,OAAO,QAAQ,QAAQ,MAAM,GAAG,CAAC;AAClD,KAAI,CAAC,OAAO,MAAM,SAAS,IAAI,qBAAqB,KAAK,QAAQ,CAC/D,QAAO;AAGT,QAAO;;;AAIT,SAAS,cACP,eACoB;AACpB,KAAI,OAAO,kBAAkB,SAAU,QAAO;CAC9C,MAAM,UAAU,cAAc,MAAM;AAEpC,KAAI,mBAAmB,KAAK,QAAQ,CAAE,QAAO;AAC7C,KAAI,QAAQ,KAAK,QAAQ,CAAE,QAAO;;;AAKpC,SAAS,aAAa,OAAyD;AAC7E,KAAI,UAAU,UAAU,UAAU,YAAY,UAAU,QAAS,QAAO;;;AAK1E,SAAS,aAAyD;AAChE,QAAO;EAAE,OAAO;EAAQ,OAAO,EAAE,MAAM,YAAY;EAAE;;;AAIvD,SAAS,gBACP,MAOA,KACA,IACA,UACM;AACN,MAAK,QAAQ,IAAI;AACjB,MAAK,OAAO;EACV,MAAM;EACN,OAAO,EAAE,MAAM,IAAI,OAAO,QAAQ,KAAK,KAAK,IAAI,YAAY;EAC7D;AACD,KAAI,IAAI,WACN,MAAK,OAAO;EACV,MAAM;EACN,SAAS;EACT,SAAS,EAAE,MAAM,GAAG,WAAW,QAAQ,KAAK,KAAK,EAAE;EACpD;AAEH,MAAK,YAAY,EAAE,YAAY,aAAa,IAAI,MAAM,IAAI,QAAQ;AAClE,KAAI,SACF,MAAK,SAAS;EACZ,KAAK,YAAY;EACjB,QAAQ,YAAY;EACpB,MAAM,YAAY;EAClB,OAAO,YAAY;EACpB;;;AAKL,SAAS,cACP,MAOA,UACA,KACA,SACA,UACA,UACM;AACN,MAAK,QAAQ,eAAe,SAAS;CACrC,MAAM,MAAM,cAAc,SAAS;AACnC,KAAI,IAAK,MAAK,SAAS;AACvB,MAAK,YAAY,EAAE,YAAY,aAAa,IAAI,MAAM,IAAI,QAAQ;AAClE,KAAI,WAAW,SACb,MAAK,OAAO;EACV,MAAM;EACN,SAAS;EACT,SAAS,EAAE,MAAM,YAAY;EAC9B;AAEH,KAAI,SACF,MAAK,SAAS;EACZ,KAAK,YAAY;EACjB,QAAQ,YAAY;EACpB,MAAM,YAAY;EAClB,OAAO,YAAY;EACpB;;;AAKL,SAAS,YACP,IAIA,WACA,UACQ;CACR,IAAI,SAAS;CACb,MAAM,WACH,UAAU,MAAM,WAAW,EAAE,EAC9B,IAAI,cAAc;CACpB,MAAM,OAAQ,UAAU,MAAM,QAAQ,EAAE;CACxC,MAAM,KAAK,UAAU,MAAM;CAG3B,MAAM,WAAY,UAAU,MAAM,YAAwB;AAG1D,KAAI,UAAU,MAAM,SAAS;EAE3B,MAAM,cADa,GAAG,OAAO,OAAO,CACL,QAAQ,EAAE;AACzC,cAAY,QAAQ,UAAU,MAAM;AACpC,cAAY,OAAO;GAAE,QAAQ;GAAM,MAAM;GAAI;AAC7C;;CAIF,MAAM,YAAY,GAAG,OAAO,OAAO;AACnC,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,MAAM,QAAQ;AACpB,MAAI,CAAC,IAAK;AACV,kBAAgB,UAAU,QAAQ,IAAI,EAAE,EAAS,KAAK,IAAI,SAAS;;AAErE;AAGA,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,UAAU,GAAG,OAAO,OAAO;AACjC,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,MAAM,QAAQ;AACpB,OAAI,CAAC,IAAK;AACV,iBACE,QAAQ,QAAQ,IAAI,EAAE,EACtB,KAAK,KAAK,IACV,KACC,UAAU,MAAM,WAAuB,OACxC,IAAI,MAAM,GACV,SACD;;AAEH;;AAGF,QAAO,SAAS;;;AAIlB,SAAS,eAAe,IAQf;AACP,MAAK,MAAM,OAAO,GAAG,SAAS;EAC5B,IAAI,SAAS;AACb,MAAI,WAAW,EAAE,cAAc,OAAO,GAAG,SAAS;GAChD,MAAM,MAAM,OAAO,KAAK,SAAS,GAAG,CAAC;AACrC,OAAI,MAAM,OAAQ,UAAS;IAC3B;AACF,MAAI,QAAQ,KAAK,IAAI,SAAS,GAAG,GAAG;;;AAIxC,MAAa,eAAiC,EAC5C,MAAM,OAAO,MAAe,UAA+C;CAEzE,MAAM,WAAW,KADD,OAAM,OAAO,2BACA,QAAQ,UAAU;AAE/C,UAAS,UAAW,KAAK,MAAM,UAAqB;AACpD,UAAS,QAAS,KAAK,MAAM,SAAoB;CAEjD,MAAM,SAAS,cAAc,KAAK;AAElC,KAAI,OAAO,WAAW,EACpB,UAAS,aAAa,UAAU;AAGlC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,KAAK,SAAS,aAAa,MAAM,KAAK;EAE5C,IAAI,SAAS;AAGb,OAAK,MAAM,WAAW,MAAM,UAAU;GACpC,MAAM,MAAM,GAAG,OAAO,OAAO;AAC7B,OAAI,QAAQ,EAAE,CAAC,QAAQ;AACvB,OAAI,QAAQ,EAAE,CAAC,OAAO;IAAE,MAAM;IAAM,MAAM;IAAI;AAC9C;;AAGF,MAAI,MAAM,SAAS,SAAS,EAAG;AAG/B,OAAK,MAAM,aAAa,MAAM,OAC5B,UAAS,YACP,IACA,WACA,OACD;AAIH,iBAAe,GAAsD;;CAGvE,MAAM,SAAS,MAAM,SAAS,KAAK,aAAa;AAChD,QAAO,IAAI,WAAW,OAAsB;GAE/C"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@pyreon/document",
3
+ "version": "0.0.1",
4
+ "description": "Universal document rendering for Pyreon — one template, every output format (HTML, PDF, DOCX, email, XLSX, Markdown, and more)",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/pyreon/fundamentals.git",
9
+ "directory": "packages/document"
10
+ },
11
+ "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/document#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/pyreon/fundamentals/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "type": "module",
25
+ "main": "./lib/index.js",
26
+ "module": "./lib/index.js",
27
+ "types": "./lib/types/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "import": "./lib/index.js",
32
+ "types": "./lib/types/index.d.ts"
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "scripts": {
37
+ "build": "vl_rolldown_build",
38
+ "dev": "vl_rolldown_build-watch",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "peerDependencies": {
43
+ "@pyreon/core": ">=0.5.0 <1.0.0",
44
+ "@pyreon/reactivity": ">=0.5.0 <1.0.0"
45
+ },
46
+ "optionalDependencies": {
47
+ "docx": "^9.6.0",
48
+ "exceljs": "^4.4.0",
49
+ "pdfmake": "^0.3.7",
50
+ "pptxgenjs": "^4.0.1"
51
+ },
52
+ "devDependencies": {
53
+ "@happy-dom/global-registrator": "^20.8.3",
54
+ "@pyreon/core": ">=0.5.0 <1.0.0",
55
+ "@pyreon/reactivity": ">=0.5.0 <1.0.0",
56
+ "@vitus-labs/tools-lint": "^1.11.0",
57
+ "docx": "^9.6.0",
58
+ "exceljs": "^4.4.0",
59
+ "pdfmake": "^0.3.7",
60
+ "pptxgenjs": "^4.0.1"
61
+ }
62
+ }
package/src/builder.ts ADDED
@@ -0,0 +1,266 @@
1
+ import { download } from './download'
2
+ import {
3
+ Button,
4
+ Code,
5
+ Divider,
6
+ Document,
7
+ Heading,
8
+ Image,
9
+ Link,
10
+ List,
11
+ ListItem,
12
+ Page,
13
+ PageBreak,
14
+ Quote,
15
+ Spacer,
16
+ Table,
17
+ Text,
18
+ } from './nodes'
19
+ import { render } from './render'
20
+ import type {
21
+ ButtonProps,
22
+ CodeProps,
23
+ DividerProps,
24
+ DocNode,
25
+ DocumentBuilder,
26
+ DocumentProps,
27
+ HeadingProps,
28
+ ImageProps,
29
+ LinkProps,
30
+ ListProps,
31
+ QuoteProps,
32
+ RenderOptions,
33
+ TableProps,
34
+ TextProps,
35
+ } from './types'
36
+
37
+ /**
38
+ * Create a document using the builder pattern — no JSX needed.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * const doc = createDocument({ title: 'Report' })
43
+ * .heading('Sales Report')
44
+ * .text('Q4 performance summary.')
45
+ * .table({ columns: ['Region', 'Revenue'], rows: [['US', '$1M']] })
46
+ *
47
+ * await doc.toPdf()
48
+ * await doc.download('report.pdf')
49
+ * ```
50
+ */
51
+ export function createDocument(props: DocumentProps = {}): DocumentBuilder {
52
+ const sections: DocNode[] = []
53
+
54
+ function getNode(): DocNode {
55
+ return Document({ ...props, children: [Page({ children: sections })] })
56
+ }
57
+
58
+ const builder: DocumentBuilder = {
59
+ heading(text: string, p?: Omit<HeadingProps, 'children'>) {
60
+ sections.push(Heading({ ...p, children: text }))
61
+ return builder
62
+ },
63
+
64
+ text(text: string, p?: Omit<TextProps, 'children'>) {
65
+ sections.push(Text({ ...p, children: text }))
66
+ return builder
67
+ },
68
+
69
+ paragraph(text: string, p?: Omit<TextProps, 'children'>) {
70
+ return builder.text(text, p)
71
+ },
72
+
73
+ image(src: string, p?: Omit<ImageProps, 'src'>) {
74
+ sections.push(Image({ src, ...p }))
75
+ return builder
76
+ },
77
+
78
+ table(p: TableProps) {
79
+ sections.push(Table(p))
80
+ return builder
81
+ },
82
+
83
+ list(items: string[], p?: Omit<ListProps, 'children'>) {
84
+ sections.push(
85
+ List({
86
+ ...p,
87
+ children: items.map((item) => ListItem({ children: item })),
88
+ }),
89
+ )
90
+ return builder
91
+ },
92
+
93
+ code(text: string, p?: Omit<CodeProps, 'children'>) {
94
+ sections.push(Code({ ...p, children: text }))
95
+ return builder
96
+ },
97
+
98
+ divider(p?: DividerProps) {
99
+ sections.push(Divider(p))
100
+ return builder
101
+ },
102
+
103
+ spacer(height: number) {
104
+ sections.push(Spacer({ height }))
105
+ return builder
106
+ },
107
+
108
+ quote(text: string, p?: Omit<QuoteProps, 'children'>) {
109
+ sections.push(Quote({ ...p, children: text }))
110
+ return builder
111
+ },
112
+
113
+ button(text: string, p: Omit<ButtonProps, 'children'>) {
114
+ sections.push(Button({ ...p, children: text }))
115
+ return builder
116
+ },
117
+
118
+ link(text: string, p: Omit<LinkProps, 'children'>) {
119
+ sections.push(Link({ ...p, children: text }))
120
+ return builder
121
+ },
122
+
123
+ pageBreak() {
124
+ sections.push(PageBreak())
125
+ return builder
126
+ },
127
+
128
+ chart(
129
+ instance: unknown,
130
+ p?: { width?: number; height?: number; caption?: string },
131
+ ) {
132
+ // Try to get data URL from chart instance
133
+ const inst = instance as { getDataURL?: (opts: unknown) => string }
134
+ if (inst?.getDataURL) {
135
+ const dataUrl = inst.getDataURL({ type: 'png', pixelRatio: 2 })
136
+ sections.push(
137
+ Image({
138
+ src: dataUrl,
139
+ width: p?.width,
140
+ height: p?.height,
141
+ caption: p?.caption,
142
+ }),
143
+ )
144
+ } else {
145
+ sections.push(
146
+ Text({
147
+ children: '[Chart]',
148
+ italic: true,
149
+ color: '#999',
150
+ } as TextProps & { children: string }),
151
+ )
152
+ }
153
+ return builder
154
+ },
155
+
156
+ flow(
157
+ instance: unknown,
158
+ p?: { width?: number; height?: number; caption?: string },
159
+ ) {
160
+ // Try to get SVG from flow instance
161
+ const inst = instance as { toSVG?: () => string }
162
+ if (inst?.toSVG) {
163
+ const svg = inst.toSVG()
164
+ sections.push(
165
+ Image({
166
+ src: `data:image/svg+xml,${encodeURIComponent(svg)}`,
167
+ width: p?.width,
168
+ height: p?.height,
169
+ caption: p?.caption,
170
+ }),
171
+ )
172
+ } else {
173
+ sections.push(
174
+ Text({
175
+ children: '[Flow Diagram]',
176
+ italic: true,
177
+ color: '#999',
178
+ } as TextProps & { children: string }),
179
+ )
180
+ }
181
+ return builder
182
+ },
183
+
184
+ build() {
185
+ return getNode()
186
+ },
187
+
188
+ async toHtml(options?: RenderOptions) {
189
+ return render(getNode(), 'html', options) as Promise<string>
190
+ },
191
+
192
+ async toPdf(options?: RenderOptions) {
193
+ return render(getNode(), 'pdf', options) as Promise<Uint8Array>
194
+ },
195
+
196
+ async toDocx(options?: RenderOptions) {
197
+ return render(getNode(), 'docx', options) as Promise<Uint8Array>
198
+ },
199
+
200
+ async toEmail(options?: RenderOptions) {
201
+ return render(getNode(), 'email', options) as Promise<string>
202
+ },
203
+
204
+ async toPptx(options?: RenderOptions) {
205
+ return render(getNode(), 'pptx', options) as Promise<Uint8Array>
206
+ },
207
+
208
+ async toXlsx(options?: RenderOptions) {
209
+ return render(getNode(), 'xlsx', options) as Promise<Uint8Array>
210
+ },
211
+
212
+ async toMarkdown(options?: RenderOptions) {
213
+ return render(getNode(), 'md', options) as Promise<string>
214
+ },
215
+
216
+ async toText(options?: RenderOptions) {
217
+ return render(getNode(), 'text', options) as Promise<string>
218
+ },
219
+
220
+ async toCsv(options?: RenderOptions) {
221
+ return render(getNode(), 'csv', options) as Promise<string>
222
+ },
223
+
224
+ async toSlack(options?: RenderOptions) {
225
+ return render(getNode(), 'slack', options) as Promise<string>
226
+ },
227
+
228
+ async toSvg(options?: RenderOptions) {
229
+ return render(getNode(), 'svg', options) as Promise<string>
230
+ },
231
+
232
+ async toTeams(options?: RenderOptions) {
233
+ return render(getNode(), 'teams', options) as Promise<string>
234
+ },
235
+
236
+ async toDiscord(options?: RenderOptions) {
237
+ return render(getNode(), 'discord', options) as Promise<string>
238
+ },
239
+
240
+ async toTelegram(options?: RenderOptions) {
241
+ return render(getNode(), 'telegram', options) as Promise<string>
242
+ },
243
+
244
+ async toNotion(options?: RenderOptions) {
245
+ return render(getNode(), 'notion', options) as Promise<string>
246
+ },
247
+
248
+ async toConfluence(options?: RenderOptions) {
249
+ return render(getNode(), 'confluence', options) as Promise<string>
250
+ },
251
+
252
+ async toWhatsApp(options?: RenderOptions) {
253
+ return render(getNode(), 'whatsapp', options) as Promise<string>
254
+ },
255
+
256
+ async toGoogleChat(options?: RenderOptions) {
257
+ return render(getNode(), 'google-chat', options) as Promise<string>
258
+ },
259
+
260
+ async download(filename: string, options?: RenderOptions) {
261
+ return download(getNode(), filename, options)
262
+ },
263
+ }
264
+
265
+ return builder
266
+ }