@pyreon/document 0.7.0 → 0.9.0

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 (76) hide show
  1. package/README.md +68 -0
  2. package/lib/analysis/index.js.html +1 -1
  3. package/lib/{confluence-Va8e7RxQ.js → confluence-Bd3ua1Ut.js} +6 -4
  4. package/lib/confluence-Bd3ua1Ut.js.map +1 -0
  5. package/lib/{csv-2c38ub-Y.js → csv-COrS4qdy.js} +1 -1
  6. package/lib/{csv-2c38ub-Y.js.map → csv-COrS4qdy.js.map} +1 -1
  7. package/lib/{discord-DAoUZqvE.js → discord-BLUnkEh9.js} +6 -4
  8. package/lib/discord-BLUnkEh9.js.map +1 -0
  9. package/lib/{docx-CorFwEH9.js → docx-BEBOihjl.js} +27 -26
  10. package/lib/docx-BEBOihjl.js.map +1 -0
  11. package/lib/{email-Bn_Brjdp.js → email-D0bbfWq4.js} +15 -13
  12. package/lib/email-D0bbfWq4.js.map +1 -0
  13. package/lib/{google-chat-B6I017I1.js → google-chat-CkKCBUWC.js} +6 -4
  14. package/lib/google-chat-CkKCBUWC.js.map +1 -0
  15. package/lib/{html-De_iS_f0.js → html-B5biprN2.js} +15 -13
  16. package/lib/html-B5biprN2.js.map +1 -0
  17. package/lib/index.js +44 -42
  18. package/lib/index.js.map +1 -1
  19. package/lib/{markdown-BYC_3C9i.js → markdown-CdtlFGC0.js} +6 -4
  20. package/lib/markdown-CdtlFGC0.js.map +1 -0
  21. package/lib/{notion-DHaQHO6P.js → notion-iG2C5bEY.js} +6 -4
  22. package/lib/notion-iG2C5bEY.js.map +1 -0
  23. package/lib/{pdf-CDPc5Itc.js → pdf-DIUQUEdj.js} +1 -1
  24. package/lib/{pdf-CDPc5Itc.js.map → pdf-DIUQUEdj.js.map} +1 -1
  25. package/lib/{pptx-DKQU6bjq.js → pptx-Dd33oL3_.js} +13 -11
  26. package/lib/pptx-Dd33oL3_.js.map +1 -0
  27. package/lib/sanitize-O_3j1mNJ.js +73 -0
  28. package/lib/sanitize-O_3j1mNJ.js.map +1 -0
  29. package/lib/{slack-CJRJgkag.js → slack-BI3EQwYm.js} +6 -4
  30. package/lib/slack-BI3EQwYm.js.map +1 -0
  31. package/lib/{svg-BM8biZmL.js → svg-BKxumy-p.js} +14 -12
  32. package/lib/svg-BKxumy-p.js.map +1 -0
  33. package/lib/{teams-S99tonRG.js → teams-Cwz9lce0.js} +6 -4
  34. package/lib/teams-Cwz9lce0.js.map +1 -0
  35. package/lib/{telegram-CbEO_2PN.js → telegram-gYFqyMXb.js} +5 -3
  36. package/lib/telegram-gYFqyMXb.js.map +1 -0
  37. package/lib/{text-B5U8ucRr.js → text-l1XNXBOC.js} +1 -1
  38. package/lib/{text-B5U8ucRr.js.map → text-l1XNXBOC.js.map} +1 -1
  39. package/lib/types/index.d.ts.map +1 -1
  40. package/lib/{whatsapp-DJ2D1jGG.js → whatsapp-CjSGoOKx.js} +5 -3
  41. package/lib/whatsapp-CjSGoOKx.js.map +1 -0
  42. package/lib/{xlsx-D47x-gZ5.js → xlsx-Bb5TWyXQ.js} +1 -1
  43. package/lib/{xlsx-D47x-gZ5.js.map → xlsx-Bb5TWyXQ.js.map} +1 -1
  44. package/package.json +3 -3
  45. package/src/builder.ts +6 -6
  46. package/src/download.ts +6 -0
  47. package/src/nodes.ts +5 -0
  48. package/src/renderers/confluence.ts +4 -3
  49. package/src/renderers/discord.ts +4 -3
  50. package/src/renderers/docx.ts +44 -32
  51. package/src/renderers/email.ts +15 -12
  52. package/src/renderers/google-chat.ts +4 -3
  53. package/src/renderers/html.ts +20 -12
  54. package/src/renderers/markdown.ts +4 -3
  55. package/src/renderers/notion.ts +4 -3
  56. package/src/renderers/pptx.ts +11 -10
  57. package/src/renderers/slack.ts +4 -3
  58. package/src/renderers/svg.ts +12 -11
  59. package/src/renderers/teams.ts +4 -3
  60. package/src/renderers/telegram.ts +3 -2
  61. package/src/renderers/whatsapp.ts +3 -2
  62. package/src/sanitize.ts +88 -0
  63. package/lib/confluence-Va8e7RxQ.js.map +0 -1
  64. package/lib/discord-DAoUZqvE.js.map +0 -1
  65. package/lib/docx-CorFwEH9.js.map +0 -1
  66. package/lib/email-Bn_Brjdp.js.map +0 -1
  67. package/lib/google-chat-B6I017I1.js.map +0 -1
  68. package/lib/html-De_iS_f0.js.map +0 -1
  69. package/lib/markdown-BYC_3C9i.js.map +0 -1
  70. package/lib/notion-DHaQHO6P.js.map +0 -1
  71. package/lib/pptx-DKQU6bjq.js.map +0 -1
  72. package/lib/slack-CJRJgkag.js.map +0 -1
  73. package/lib/svg-BM8biZmL.js.map +0 -1
  74. package/lib/teams-S99tonRG.js.map +0 -1
  75. package/lib/telegram-CbEO_2PN.js.map +0 -1
  76. package/lib/whatsapp-DJ2D1jGG.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"pdf-CDPc5Itc.js","names":[],"sources":["../src/renderers/pdf.ts"],"sourcesContent":["import type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * PDF renderer — lazy-loads pdfmake on first use.\n * pdfmake handles pagination, tables, text wrapping, and font embedding.\n *\n * @example\n * ```ts\n * import { render, Document, Page, Heading } from '@pyreon/document'\n *\n * const doc = Document({\n * title: 'Report',\n * children: Page({ children: Heading({ children: 'Hello' }) }),\n * })\n * const pdf = await render(doc, 'pdf') // → Uint8Array\n * ```\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\ntype PdfContent = Record<string, unknown> | string | PdfContent[]\n\n/** pdfmake expects page sizes as `{ width, height }` objects. */\nconst PAGE_SIZES: Record<string, { width: number; height: number }> = {\n A3: { width: 841.89, height: 1190.55 },\n A4: { width: 595.28, height: 841.89 },\n A5: { width: 419.53, height: 595.28 },\n letter: { width: 612, height: 792 },\n legal: { width: 612, height: 1008 },\n tabloid: { width: 792, height: 1224 },\n}\n\n/**\n * Resolve an image `src` for pdfmake.\n *\n * - `data:` URIs are passed through directly (pdfmake supports base64).\n * - `http(s)://` URLs cannot be resolved at render time in the browser.\n * A placeholder text node is returned instead.\n * - Relative / absolute paths (e.g. `/logo.png`) cannot be resolved without\n * a server-side fetch, so they are skipped with a placeholder.\n */\nfunction resolveImageSrc(\n src: string,\n): { image: string } | { text: string; italics: true; color: string } {\n if (src.startsWith('data:')) {\n return { image: src }\n }\n if (src.startsWith('http://') || src.startsWith('https://')) {\n return { text: `[Image: ${src}]`, italics: true, color: '#999999' }\n }\n // Local path — cannot resolve in browser\n return { text: `[Image: ${src}]`, italics: true, color: '#999999' }\n}\n\nfunction nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {\n const p = node.props\n\n switch (node.type) {\n case 'document':\n case 'page':\n return node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n\n case 'section': {\n const content = node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n .flat()\n\n if (p.direction === 'row') {\n return {\n columns: node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((child) => ({\n stack: [nodeToContent(child)].flat().filter(Boolean),\n width:\n child.props.width === '*' || !child.props.width\n ? '*'\n : child.props.width,\n })),\n columnGap: (p.gap as number) ?? 0,\n }\n }\n\n return content\n }\n\n case 'row': {\n return {\n columns: node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((child) => ({\n stack: [nodeToContent(child)].flat().filter(Boolean),\n width: child.props.width ?? '*',\n })),\n columnGap: (p.gap as number) ?? 0,\n }\n }\n\n case 'column':\n return node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n .flat()\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const sizes: Record<number, number> = {\n 1: 24,\n 2: 20,\n 3: 18,\n 4: 16,\n 5: 14,\n 6: 12,\n }\n return {\n text: getTextContent(node.children),\n fontSize: sizes[level] ?? 18,\n bold: true,\n color: (p.color as string) ?? '#000000',\n alignment: (p.align as string) ?? 'left',\n margin: [0, level === 1 ? 0 : 8, 0, 8],\n }\n }\n\n case 'text':\n return {\n text: getTextContent(node.children),\n fontSize: (p.size as number) ?? 12,\n color: (p.color as string) ?? '#333333',\n bold: p.bold ?? false,\n italics: p.italic ?? false,\n decoration: p.underline\n ? 'underline'\n : p.strikethrough\n ? 'lineThrough'\n : undefined,\n alignment: (p.align as string) ?? 'left',\n lineHeight: (p.lineHeight as number) ?? 1.4,\n margin: [0, 0, 0, 8],\n }\n\n case 'link':\n return {\n text: getTextContent(node.children),\n link: p.href as string,\n color: (p.color as string) ?? '#4f46e5',\n decoration: 'underline',\n }\n\n case 'image': {\n const src = p.src as string\n const resolved = resolveImageSrc(src)\n\n if ('image' in resolved) {\n const result: Record<string, unknown> = {\n image: resolved.image,\n fit: [p.width ?? 500, p.height ?? 400],\n margin: [0, 0, 0, 8],\n }\n if (p.align === 'center') result.alignment = 'center'\n if (p.align === 'right') result.alignment = 'right'\n return result\n }\n\n // Placeholder for non-resolvable images\n return { ...resolved, margin: [0, 0, 0, 8] }\n }\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 const hs = p.headerStyle as\n | { background?: string; color?: string }\n | undefined\n\n const headerRow = columns.map((col) => ({\n text: col.header,\n bold: true,\n fillColor: hs?.background ?? '#f5f5f5',\n color: hs?.color ?? '#000000',\n alignment: col.align ?? 'left',\n }))\n\n const dataRows = rows.map((row, rowIdx) =>\n columns.map((col, colIdx) => ({\n text: String(row[colIdx] ?? ''),\n alignment: col.align ?? 'left',\n fillColor: p.striped && rowIdx % 2 === 1 ? '#f9f9f9' : undefined,\n })),\n )\n\n const widths = columns.map((col) => {\n if (!col.width) return '*'\n if (typeof col.width === 'string' && col.width.endsWith('%')) {\n return col.width\n }\n return col.width\n })\n\n return {\n table: {\n headerRows: 1,\n widths,\n body: [headerRow, ...dataRows],\n },\n layout: p.bordered ? undefined : 'lightHorizontalLines',\n unbreakable: p.keepTogether ?? false,\n margin: [0, 0, 0, 12],\n }\n }\n\n case 'list': {\n const items = node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item) => getTextContent(item.children))\n\n return p.ordered\n ? { ol: items, margin: [0, 0, 0, 8] }\n : { ul: items, margin: [0, 0, 0, 8] }\n }\n\n case 'list-item':\n return getTextContent(node.children)\n\n case 'code':\n return {\n text: getTextContent(node.children),\n font: 'Courier',\n fontSize: 10,\n background: '#f5f5f5',\n margin: [0, 0, 0, 8],\n }\n\n case 'page-break':\n return { text: '', pageBreak: 'after' }\n\n case 'divider':\n return {\n canvas: [\n {\n type: 'line',\n x1: 0,\n y1: 0,\n x2: 515,\n y2: 0,\n lineWidth: (p.thickness as number) ?? 1,\n lineColor: (p.color as string) ?? '#dddddd',\n },\n ],\n margin: [0, 8, 0, 8],\n }\n\n case 'spacer':\n return { text: '', margin: [0, (p.height as number) ?? 12, 0, 0] }\n\n case 'button':\n return {\n text: getTextContent(node.children),\n link: p.href as string,\n bold: true,\n color: (p.color as string) ?? '#ffffff',\n background: (p.background as string) ?? '#4f46e5',\n margin: [0, 8, 0, 8],\n }\n\n case 'quote':\n return {\n table: {\n widths: [4, '*'],\n body: [\n [\n { text: '', fillColor: (p.borderColor as string) ?? '#dddddd' },\n {\n text: getTextContent(node.children),\n italics: true,\n color: '#555555',\n margin: [8, 4, 0, 4],\n },\n ],\n ],\n },\n layout: 'noBorders',\n margin: [0, 4, 0, 8],\n }\n\n default:\n return null\n }\n}\n\nfunction resolveMargin(\n margin:\n | number\n | [number, number]\n | [number, number, number, number]\n | undefined,\n): [number, number, number, number] {\n if (margin == null) return [40, 40, 40, 40]\n if (typeof margin === 'number') return [margin, margin, margin, margin]\n if (margin.length === 2) return [margin[1], margin[0], margin[1], margin[0]]\n return margin\n}\n\n/**\n * Render header/footer DocNodes into pdfmake content for page headers/footers.\n *\n * pdfmake header/footer functions receive `(currentPage, pageCount, pageSize)`\n * and must return a content object. We flatten the DocNode into static content.\n */\nfunction renderHeaderFooter(node: DocNode | undefined): PdfContent | undefined {\n if (!node) return undefined\n const content = nodeToContent(node)\n if (content == null) return undefined\n if (Array.isArray(content)) return { stack: content, margin: [40, 10, 40, 0] }\n if (typeof content === 'object')\n return { ...content, margin: [40, 10, 40, 0] }\n return { text: content, margin: [40, 10, 40, 0] }\n}\n\nexport const pdfRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n // Lazy-load pdfmake — handle ESM/CJS interop\n const pdfMakeModule = await import('pdfmake/build/pdfmake')\n const pdfFontsModule = await import('pdfmake/build/vfs_fonts')\n\n // Resolve the actual exports (handle .default for ESM wrappers).\n // pdfmake's default export is a singleton instance of browser_extensions_pdfmake.\n // ESM interop may wrap it in an extra .default layer.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete\n let pdfMake: any = pdfMakeModule.default ?? pdfMakeModule\n if (pdfMake.default && typeof pdfMake.default.createPdf === 'function') {\n pdfMake = pdfMake.default\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete\n const pdfFonts: any = pdfFontsModule.default ?? pdfFontsModule\n\n // Assign virtual filesystem for fonts\n if (pdfMake.vfs == null) {\n pdfMake.vfs = pdfFonts.pdfMake?.vfs ?? pdfFonts.vfs\n }\n\n // Find page config\n const pageNode = node.children.find(\n (c): c is DocNode => typeof c !== 'string' && c.type === 'page',\n )\n const pageSize = (pageNode?.props.size as string) ?? 'A4'\n const pageOrientation =\n (pageNode?.props.orientation as string) ?? 'portrait'\n const pageMargin = resolveMargin(\n pageNode?.props.margin as\n | number\n | [number, number]\n | [number, number, number, number]\n | undefined,\n )\n\n const content = [nodeToContent(node)].flat().filter(Boolean) as PdfContent[]\n\n // Build header/footer from PageProps if present\n const headerFn = renderHeaderFooter(\n pageNode?.props.header as DocNode | undefined,\n )\n const footerFn = renderHeaderFooter(\n pageNode?.props.footer as DocNode | undefined,\n )\n\n const docDefinition: Record<string, unknown> = {\n pageSize: PAGE_SIZES[pageSize] ?? PAGE_SIZES.A4,\n pageOrientation,\n pageMargins: pageMargin,\n info: {\n title: (node.props.title as string) ?? '',\n author: (node.props.author as string) ?? '',\n subject: (node.props.subject as string) ?? '',\n keywords: (node.props.keywords as string[])?.join(', ') ?? '',\n },\n content,\n defaultStyle: {\n fontSize: 12,\n lineHeight: 1.4,\n },\n // Keep sections together — break before a heading if it would be\n // orphaned at the bottom of a page.\n pageBreakBefore: (\n currentNode: { headlineLevel?: number },\n helpers: { getFollowingNodesOnPage?: () => unknown[] },\n ) => {\n if (currentNode.headlineLevel && helpers.getFollowingNodesOnPage) {\n const following = helpers.getFollowingNodesOnPage()\n if (following.length === 0) return true\n }\n return false\n },\n }\n\n if (headerFn) docDefinition.header = headerFn\n if (footerFn) docDefinition.footer = footerFn\n\n try {\n const pdf = pdfMake.createPdf(docDefinition)\n const buffer = await pdf.getBuffer()\n return new Uint8Array(buffer)\n } catch (err) {\n throw new Error(`[@pyreon/document] PDF generation failed: ${err}`)\n }\n },\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pdf-DIUQUEdj.js","names":[],"sources":["../src/renderers/pdf.ts"],"sourcesContent":["import type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * PDF renderer — lazy-loads pdfmake on first use.\n * pdfmake handles pagination, tables, text wrapping, and font embedding.\n *\n * @example\n * ```ts\n * import { render, Document, Page, Heading } from '@pyreon/document'\n *\n * const doc = Document({\n * title: 'Report',\n * children: Page({ children: Heading({ children: 'Hello' }) }),\n * })\n * const pdf = await render(doc, 'pdf') // → Uint8Array\n * ```\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\ntype PdfContent = Record<string, unknown> | string | PdfContent[]\n\n/** pdfmake expects page sizes as `{ width, height }` objects. */\nconst PAGE_SIZES: Record<string, { width: number; height: number }> = {\n A3: { width: 841.89, height: 1190.55 },\n A4: { width: 595.28, height: 841.89 },\n A5: { width: 419.53, height: 595.28 },\n letter: { width: 612, height: 792 },\n legal: { width: 612, height: 1008 },\n tabloid: { width: 792, height: 1224 },\n}\n\n/**\n * Resolve an image `src` for pdfmake.\n *\n * - `data:` URIs are passed through directly (pdfmake supports base64).\n * - `http(s)://` URLs cannot be resolved at render time in the browser.\n * A placeholder text node is returned instead.\n * - Relative / absolute paths (e.g. `/logo.png`) cannot be resolved without\n * a server-side fetch, so they are skipped with a placeholder.\n */\nfunction resolveImageSrc(\n src: string,\n): { image: string } | { text: string; italics: true; color: string } {\n if (src.startsWith('data:')) {\n return { image: src }\n }\n if (src.startsWith('http://') || src.startsWith('https://')) {\n return { text: `[Image: ${src}]`, italics: true, color: '#999999' }\n }\n // Local path — cannot resolve in browser\n return { text: `[Image: ${src}]`, italics: true, color: '#999999' }\n}\n\nfunction nodeToContent(node: DocNode): PdfContent | PdfContent[] | null {\n const p = node.props\n\n switch (node.type) {\n case 'document':\n case 'page':\n return node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n\n case 'section': {\n const content = node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n .flat()\n\n if (p.direction === 'row') {\n return {\n columns: node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((child) => ({\n stack: [nodeToContent(child)].flat().filter(Boolean),\n width:\n child.props.width === '*' || !child.props.width\n ? '*'\n : child.props.width,\n })),\n columnGap: (p.gap as number) ?? 0,\n }\n }\n\n return content\n }\n\n case 'row': {\n return {\n columns: node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((child) => ({\n stack: [nodeToContent(child)].flat().filter(Boolean),\n width: child.props.width ?? '*',\n })),\n columnGap: (p.gap as number) ?? 0,\n }\n }\n\n case 'column':\n return node.children\n .map((c) => (typeof c === 'string' ? c : nodeToContent(c)))\n .filter((c): c is PdfContent => c != null)\n .flat()\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const sizes: Record<number, number> = {\n 1: 24,\n 2: 20,\n 3: 18,\n 4: 16,\n 5: 14,\n 6: 12,\n }\n return {\n text: getTextContent(node.children),\n fontSize: sizes[level] ?? 18,\n bold: true,\n color: (p.color as string) ?? '#000000',\n alignment: (p.align as string) ?? 'left',\n margin: [0, level === 1 ? 0 : 8, 0, 8],\n }\n }\n\n case 'text':\n return {\n text: getTextContent(node.children),\n fontSize: (p.size as number) ?? 12,\n color: (p.color as string) ?? '#333333',\n bold: p.bold ?? false,\n italics: p.italic ?? false,\n decoration: p.underline\n ? 'underline'\n : p.strikethrough\n ? 'lineThrough'\n : undefined,\n alignment: (p.align as string) ?? 'left',\n lineHeight: (p.lineHeight as number) ?? 1.4,\n margin: [0, 0, 0, 8],\n }\n\n case 'link':\n return {\n text: getTextContent(node.children),\n link: p.href as string,\n color: (p.color as string) ?? '#4f46e5',\n decoration: 'underline',\n }\n\n case 'image': {\n const src = p.src as string\n const resolved = resolveImageSrc(src)\n\n if ('image' in resolved) {\n const result: Record<string, unknown> = {\n image: resolved.image,\n fit: [p.width ?? 500, p.height ?? 400],\n margin: [0, 0, 0, 8],\n }\n if (p.align === 'center') result.alignment = 'center'\n if (p.align === 'right') result.alignment = 'right'\n return result\n }\n\n // Placeholder for non-resolvable images\n return { ...resolved, margin: [0, 0, 0, 8] }\n }\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 const hs = p.headerStyle as\n | { background?: string; color?: string }\n | undefined\n\n const headerRow = columns.map((col) => ({\n text: col.header,\n bold: true,\n fillColor: hs?.background ?? '#f5f5f5',\n color: hs?.color ?? '#000000',\n alignment: col.align ?? 'left',\n }))\n\n const dataRows = rows.map((row, rowIdx) =>\n columns.map((col, colIdx) => ({\n text: String(row[colIdx] ?? ''),\n alignment: col.align ?? 'left',\n fillColor: p.striped && rowIdx % 2 === 1 ? '#f9f9f9' : undefined,\n })),\n )\n\n const widths = columns.map((col) => {\n if (!col.width) return '*'\n if (typeof col.width === 'string' && col.width.endsWith('%')) {\n return col.width\n }\n return col.width\n })\n\n return {\n table: {\n headerRows: 1,\n widths,\n body: [headerRow, ...dataRows],\n },\n layout: p.bordered ? undefined : 'lightHorizontalLines',\n unbreakable: p.keepTogether ?? false,\n margin: [0, 0, 0, 12],\n }\n }\n\n case 'list': {\n const items = node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item) => getTextContent(item.children))\n\n return p.ordered\n ? { ol: items, margin: [0, 0, 0, 8] }\n : { ul: items, margin: [0, 0, 0, 8] }\n }\n\n case 'list-item':\n return getTextContent(node.children)\n\n case 'code':\n return {\n text: getTextContent(node.children),\n font: 'Courier',\n fontSize: 10,\n background: '#f5f5f5',\n margin: [0, 0, 0, 8],\n }\n\n case 'page-break':\n return { text: '', pageBreak: 'after' }\n\n case 'divider':\n return {\n canvas: [\n {\n type: 'line',\n x1: 0,\n y1: 0,\n x2: 515,\n y2: 0,\n lineWidth: (p.thickness as number) ?? 1,\n lineColor: (p.color as string) ?? '#dddddd',\n },\n ],\n margin: [0, 8, 0, 8],\n }\n\n case 'spacer':\n return { text: '', margin: [0, (p.height as number) ?? 12, 0, 0] }\n\n case 'button':\n return {\n text: getTextContent(node.children),\n link: p.href as string,\n bold: true,\n color: (p.color as string) ?? '#ffffff',\n background: (p.background as string) ?? '#4f46e5',\n margin: [0, 8, 0, 8],\n }\n\n case 'quote':\n return {\n table: {\n widths: [4, '*'],\n body: [\n [\n { text: '', fillColor: (p.borderColor as string) ?? '#dddddd' },\n {\n text: getTextContent(node.children),\n italics: true,\n color: '#555555',\n margin: [8, 4, 0, 4],\n },\n ],\n ],\n },\n layout: 'noBorders',\n margin: [0, 4, 0, 8],\n }\n\n default:\n return null\n }\n}\n\nfunction resolveMargin(\n margin:\n | number\n | [number, number]\n | [number, number, number, number]\n | undefined,\n): [number, number, number, number] {\n if (margin == null) return [40, 40, 40, 40]\n if (typeof margin === 'number') return [margin, margin, margin, margin]\n if (margin.length === 2) return [margin[1], margin[0], margin[1], margin[0]]\n return margin\n}\n\n/**\n * Render header/footer DocNodes into pdfmake content for page headers/footers.\n *\n * pdfmake header/footer functions receive `(currentPage, pageCount, pageSize)`\n * and must return a content object. We flatten the DocNode into static content.\n */\nfunction renderHeaderFooter(node: DocNode | undefined): PdfContent | undefined {\n if (!node) return undefined\n const content = nodeToContent(node)\n if (content == null) return undefined\n if (Array.isArray(content)) return { stack: content, margin: [40, 10, 40, 0] }\n if (typeof content === 'object')\n return { ...content, margin: [40, 10, 40, 0] }\n return { text: content, margin: [40, 10, 40, 0] }\n}\n\nexport const pdfRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n // Lazy-load pdfmake — handle ESM/CJS interop\n const pdfMakeModule = await import('pdfmake/build/pdfmake')\n const pdfFontsModule = await import('pdfmake/build/vfs_fonts')\n\n // Resolve the actual exports (handle .default for ESM wrappers).\n // pdfmake's default export is a singleton instance of browser_extensions_pdfmake.\n // ESM interop may wrap it in an extra .default layer.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete\n let pdfMake: any = pdfMakeModule.default ?? pdfMakeModule\n if (pdfMake.default && typeof pdfMake.default.createPdf === 'function') {\n pdfMake = pdfMake.default\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- pdfmake types are incomplete\n const pdfFonts: any = pdfFontsModule.default ?? pdfFontsModule\n\n // Assign virtual filesystem for fonts\n if (pdfMake.vfs == null) {\n pdfMake.vfs = pdfFonts.pdfMake?.vfs ?? pdfFonts.vfs\n }\n\n // Find page config\n const pageNode = node.children.find(\n (c): c is DocNode => typeof c !== 'string' && c.type === 'page',\n )\n const pageSize = (pageNode?.props.size as string) ?? 'A4'\n const pageOrientation =\n (pageNode?.props.orientation as string) ?? 'portrait'\n const pageMargin = resolveMargin(\n pageNode?.props.margin as\n | number\n | [number, number]\n | [number, number, number, number]\n | undefined,\n )\n\n const content = [nodeToContent(node)].flat().filter(Boolean) as PdfContent[]\n\n // Build header/footer from PageProps if present\n const headerFn = renderHeaderFooter(\n pageNode?.props.header as DocNode | undefined,\n )\n const footerFn = renderHeaderFooter(\n pageNode?.props.footer as DocNode | undefined,\n )\n\n const docDefinition: Record<string, unknown> = {\n pageSize: PAGE_SIZES[pageSize] ?? PAGE_SIZES.A4,\n pageOrientation,\n pageMargins: pageMargin,\n info: {\n title: (node.props.title as string) ?? '',\n author: (node.props.author as string) ?? '',\n subject: (node.props.subject as string) ?? '',\n keywords: (node.props.keywords as string[])?.join(', ') ?? '',\n },\n content,\n defaultStyle: {\n fontSize: 12,\n lineHeight: 1.4,\n },\n // Keep sections together — break before a heading if it would be\n // orphaned at the bottom of a page.\n pageBreakBefore: (\n currentNode: { headlineLevel?: number },\n helpers: { getFollowingNodesOnPage?: () => unknown[] },\n ) => {\n if (currentNode.headlineLevel && helpers.getFollowingNodesOnPage) {\n const following = helpers.getFollowingNodesOnPage()\n if (following.length === 0) return true\n }\n return false\n },\n }\n\n if (headerFn) docDefinition.header = headerFn\n if (footerFn) docDefinition.footer = footerFn\n\n try {\n const pdf = pdfMake.createPdf(docDefinition)\n const buffer = await pdf.getBuffer()\n return new Uint8Array(buffer)\n } catch (err) {\n throw new Error(`[@pyreon/document] PDF generation failed: ${err}`)\n }\n },\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { a as sanitizeXmlColor, n as sanitizeHref, r as sanitizeImageSrc } from "./sanitize-O_3j1mNJ.js";
2
+
1
3
  //#region src/renderers/pptx.ts
2
4
  /**
3
5
  * PPTX renderer — lazy-loads pptxgenjs on first use.
@@ -46,7 +48,7 @@ function processNode(node, ctx) {
46
48
  h: .6,
47
49
  fontSize,
48
50
  bold: true,
49
- color: (p.color ?? "#000000").replace("#", ""),
51
+ color: sanitizeXmlColor(p.color ?? "#000000"),
50
52
  align: p.align ?? "left"
51
53
  });
52
54
  ctx.y += .7;
@@ -64,14 +66,14 @@ function processNode(node, ctx) {
64
66
  italic: p.italic ?? false,
65
67
  underline: p.underline ? { style: "sng" } : void 0,
66
68
  strike: p.strikethrough ? "sngStrike" : void 0,
67
- color: (p.color ?? "#333333").replace("#", ""),
69
+ color: sanitizeXmlColor(p.color ?? "#333333"),
68
70
  align: p.align ?? "left"
69
71
  });
70
72
  ctx.y += .5;
71
73
  break;
72
74
  }
73
75
  case "image": {
74
- const src = p.src;
76
+ const src = sanitizeImageSrc(p.src);
75
77
  const w = Math.min((p.width ?? 400) / 96, CONTENT_WIDTH);
76
78
  const h = (p.height ?? 300) / 96;
77
79
  if (src.startsWith("data:")) {
@@ -94,8 +96,8 @@ function processNode(node, ctx) {
94
96
  text: col.header,
95
97
  options: {
96
98
  bold: true,
97
- fill: { color: (hs?.background ?? "#f5f5f5").replace("#", "") },
98
- color: (hs?.color ?? "#000000").replace("#", ""),
99
+ fill: { color: sanitizeXmlColor(hs?.background ?? "#f5f5f5") },
100
+ color: sanitizeXmlColor(hs?.color ?? "#000000"),
99
101
  align: col.align ?? "left",
100
102
  fontSize: 12
101
103
  }
@@ -179,7 +181,7 @@ function processNode(node, ctx) {
179
181
  fontSize: 13,
180
182
  color: "4F46E5",
181
183
  underline: { style: "sng" },
182
- hyperlink: { url: p.href }
184
+ hyperlink: { url: sanitizeHref(p.href) }
183
185
  });
184
186
  ctx.y += .5;
185
187
  break;
@@ -191,10 +193,10 @@ function processNode(node, ctx) {
191
193
  h: .5,
192
194
  fontSize: 14,
193
195
  bold: true,
194
- color: (p.color ?? "#ffffff").replace("#", ""),
195
- fill: { color: (p.background ?? "#4f46e5").replace("#", "") },
196
+ color: sanitizeXmlColor(p.color ?? "#ffffff"),
197
+ fill: { color: sanitizeXmlColor(p.background ?? "#4f46e5") },
196
198
  align: "center",
197
- hyperlink: { url: p.href }
199
+ hyperlink: { url: sanitizeHref(p.href) }
198
200
  });
199
201
  ctx.y += .6;
200
202
  break;
@@ -207,7 +209,7 @@ function processNode(node, ctx) {
207
209
  y: ctx.y,
208
210
  w: CONTENT_WIDTH,
209
211
  h: .02,
210
- fill: { color: (p.color ?? "#DDDDDD").replace("#", "") }
212
+ fill: { color: sanitizeXmlColor(p.color ?? "#DDDDDD") }
211
213
  });
212
214
  ctx.y += .2;
213
215
  break;
@@ -249,4 +251,4 @@ const pptxRenderer = { async render(node, _options) {
249
251
 
250
252
  //#endregion
251
253
  export { pptxRenderer };
252
- //# sourceMappingURL=pptx-DKQU6bjq.js.map
254
+ //# sourceMappingURL=pptx-Dd33oL3_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pptx-Dd33oL3_.js","names":[],"sources":["../src/renderers/pptx.ts"],"sourcesContent":["import { sanitizeHref, sanitizeImageSrc, sanitizeXmlColor } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * PPTX renderer — lazy-loads pptxgenjs on first use.\n * Each `<Page>` becomes a slide. Document nodes map to PPTX elements.\n *\n * @example\n * ```ts\n * import { render, Document, Page, Heading, Text } from '@pyreon/document'\n *\n * const doc = Document({\n * title: 'Presentation',\n * children: [\n * Page({ children: [Heading({ children: 'Slide 1' }), Text({ children: 'Hello' })] }),\n * Page({ children: [Heading({ children: 'Slide 2' })] }),\n * ],\n * })\n * const pptx = await render(doc, 'pptx') // → Uint8Array\n * ```\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\n/** Vertical position tracker for placing elements on a slide. */\ninterface SlideContext {\n slide: PptxSlide\n y: number\n}\n\n// Duck-typed pptxgenjs interfaces to avoid hard dependency on types\ninterface PptxSlide {\n addText(text: string | PptxTextProps[], opts?: Record<string, unknown>): void\n addImage(opts: Record<string, unknown>): void\n addTable(rows: unknown[][], opts?: Record<string, unknown>): void\n}\n\ninterface PptxTextProps {\n text: string\n options?: Record<string, unknown>\n}\n\ninterface PptxGen {\n addSlide(): PptxSlide\n write(outputType: string): Promise<unknown>\n title: string\n author: string\n subject: string\n}\n\nconst HEADING_SIZES: Record<number, number> = {\n 1: 28,\n 2: 24,\n 3: 20,\n 4: 18,\n 5: 16,\n 6: 14,\n}\n\nconst SLIDE_WIDTH = 10 // inches\nconst CONTENT_MARGIN = 0.5\nconst CONTENT_WIDTH = SLIDE_WIDTH - CONTENT_MARGIN * 2\n\nfunction processNode(node: DocNode, ctx: SlideContext): void {\n const p = node.props\n\n switch (node.type) {\n case 'heading': {\n const level = (p.level as number) ?? 1\n const fontSize = HEADING_SIZES[level] ?? 20\n ctx.slide.addText(getTextContent(node.children), {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: 0.6,\n fontSize,\n bold: true,\n color: sanitizeXmlColor((p.color as string) ?? '#000000'),\n align: (p.align as string) ?? 'left',\n })\n ctx.y += 0.7\n break\n }\n\n case 'text': {\n const text = getTextContent(node.children)\n ctx.slide.addText(text, {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: 0.4,\n fontSize: (p.size as number) ?? 14,\n bold: p.bold ?? false,\n italic: p.italic ?? false,\n underline: p.underline ? { style: 'sng' } : undefined,\n strike: p.strikethrough ? 'sngStrike' : undefined,\n color: sanitizeXmlColor((p.color as string) ?? '#333333'),\n align: (p.align as string) ?? 'left',\n })\n ctx.y += 0.5\n break\n }\n\n case 'image': {\n const src = sanitizeImageSrc(p.src as string)\n const w = Math.min(((p.width as number) ?? 400) / 96, CONTENT_WIDTH)\n const h = ((p.height as number) ?? 300) / 96\n\n if (src.startsWith('data:')) {\n ctx.slide.addImage({\n data: src,\n x: CONTENT_MARGIN,\n y: ctx.y,\n w,\n h,\n })\n ctx.y += h + 0.2\n }\n // HTTP URLs and local paths are not supported — skip silently\n break\n }\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 const hs = p.headerStyle as\n | { background?: string; color?: string }\n | undefined\n\n const headerRow = columns.map((col) => ({\n text: col.header,\n options: {\n bold: true,\n fill: { color: sanitizeXmlColor(hs?.background ?? '#f5f5f5') },\n color: sanitizeXmlColor(hs?.color ?? '#000000'),\n align: col.align ?? 'left',\n fontSize: 12,\n },\n }))\n\n const dataRows = rows.map((row, rowIdx) =>\n columns.map((col, colIdx) => ({\n text: String(row[colIdx] ?? ''),\n options: {\n align: col.align ?? 'left',\n fontSize: 11,\n fill:\n p.striped && rowIdx % 2 === 1 ? { color: 'F9F9F9' } : undefined,\n },\n })),\n )\n\n const allRows = [headerRow, ...dataRows]\n const rowHeight = 0.35\n const tableHeight = allRows.length * rowHeight\n\n ctx.slide.addTable(allRows, {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n border: { pt: 0.5, color: 'DDDDDD' },\n rowH: rowHeight,\n })\n ctx.y += tableHeight + 0.2\n break\n }\n\n case 'list': {\n const items = node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item) => getTextContent(item.children))\n\n const isOrdered = p.ordered as boolean\n const listText = items.map((item, i) => ({\n text: isOrdered ? `${i + 1}. ${item}\\n` : `\\u2022 ${item}\\n`,\n options: { fontSize: 13, bullet: false },\n }))\n\n ctx.slide.addText(listText, {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: items.length * 0.35,\n })\n ctx.y += items.length * 0.35 + 0.1\n break\n }\n\n case 'code': {\n const text = getTextContent(node.children)\n ctx.slide.addText(text, {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: 0.5,\n fontSize: 10,\n fontFace: 'Courier New',\n fill: { color: 'F5F5F5' },\n color: '333333',\n })\n ctx.y += 0.6\n break\n }\n\n case 'quote': {\n const text = getTextContent(node.children)\n ctx.slide.addText(text, {\n x: CONTENT_MARGIN + 0.3,\n y: ctx.y,\n w: CONTENT_WIDTH - 0.3,\n h: 0.5,\n fontSize: 13,\n italic: true,\n color: '555555',\n })\n ctx.y += 0.6\n break\n }\n\n case 'link': {\n ctx.slide.addText(getTextContent(node.children), {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: 0.4,\n fontSize: 13,\n color: '4F46E5',\n underline: { style: 'sng' },\n hyperlink: { url: sanitizeHref(p.href as string) },\n })\n ctx.y += 0.5\n break\n }\n\n case 'button': {\n ctx.slide.addText(getTextContent(node.children), {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: 3,\n h: 0.5,\n fontSize: 14,\n bold: true,\n color: sanitizeXmlColor((p.color as string) ?? '#ffffff'),\n fill: {\n color: sanitizeXmlColor((p.background as string) ?? '#4f46e5'),\n },\n align: 'center',\n hyperlink: { url: sanitizeHref(p.href as string) },\n })\n ctx.y += 0.6\n break\n }\n\n case 'spacer': {\n ctx.y += ((p.height as number) ?? 12) / 72\n break\n }\n\n case 'divider': {\n // Render as a thin line using a text element with top border\n ctx.slide.addText('', {\n x: CONTENT_MARGIN,\n y: ctx.y,\n w: CONTENT_WIDTH,\n h: 0.02,\n fill: { color: sanitizeXmlColor((p.color as string) ?? '#DDDDDD') },\n })\n ctx.y += 0.2\n break\n }\n\n // Container types — recurse into children\n case 'section':\n case 'row':\n case 'column':\n for (const child of node.children) {\n if (typeof child !== 'string') {\n processNode(child, ctx)\n }\n }\n break\n\n default:\n break\n }\n}\n\nfunction processSlide(pageNode: DocNode, pptx: PptxGen): void {\n const slide = pptx.addSlide()\n const ctx: SlideContext = { slide, y: CONTENT_MARGIN }\n\n for (const child of pageNode.children) {\n if (typeof child !== 'string') {\n processNode(child, ctx)\n }\n }\n}\n\nexport const pptxRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n const PptxGenJS = await import('pptxgenjs')\n const PptxGenClass = PptxGenJS.default ?? PptxGenJS\n\n const pptx = new PptxGenClass() as PptxGen\n\n // Set metadata\n if (node.props.title) pptx.title = node.props.title as string\n if (node.props.author) pptx.author = node.props.author as string\n if (node.props.subject) pptx.subject = node.props.subject as string\n\n // Collect pages — each becomes a slide\n const pages: DocNode[] = []\n for (const child of node.children) {\n if (typeof child !== 'string' && child.type === 'page') {\n pages.push(child)\n }\n }\n\n // If no explicit pages, treat entire document content as one slide\n if (pages.length === 0) {\n const syntheticPage: DocNode = {\n type: 'page',\n props: {},\n children: node.children,\n }\n pages.push(syntheticPage)\n }\n\n for (const page of pages) {\n processSlide(page, pptx)\n }\n\n const output = await pptx.write('arraybuffer')\n return new Uint8Array(output as ArrayBuffer)\n },\n}\n"],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ //#region src/sanitize.ts
2
+ /**
3
+ * Shared sanitization utilities for document renderers.
4
+ * Prevents XSS via CSS injection, XML injection, and javascript: protocol attacks.
5
+ */
6
+ /**
7
+ * Sanitize a CSS value — strips characters that could break out of a CSS property.
8
+ * Blocks: semicolons, braces, angle brackets, quotes, backslashes, expressions.
9
+ */
10
+ function sanitizeCss(value) {
11
+ if (value == null) return "";
12
+ return value.replace(/[;{}()<>\\'"]/g, "").replace(/expression\s*\(/gi, "").replace(/url\s*\(/gi, "").replace(/javascript\s*:/gi, "");
13
+ }
14
+ /**
15
+ * Sanitize a color value — only allows hex colors, named colors, rgb/rgba, hsl/hsla.
16
+ * Returns the value if valid, empty string if not.
17
+ */
18
+ function sanitizeColor(value) {
19
+ if (value == null) return "";
20
+ const trimmed = value.trim();
21
+ if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed;
22
+ if (/^[a-zA-Z]{1,20}$/.test(trimmed)) return trimmed;
23
+ if (/^(rgb|hsl)a?\(\s*[\d.,\s%]+\)$/.test(trimmed)) return trimmed;
24
+ if (/^(transparent|inherit|currentColor|initial|unset)$/i.test(trimmed)) return trimmed;
25
+ return "";
26
+ }
27
+ /**
28
+ * Sanitize a color for XML attributes (DOCX/PPTX) — only hex without #.
29
+ * Returns 6-char hex string or default.
30
+ */
31
+ function sanitizeXmlColor(value, fallback = "000000") {
32
+ if (value == null) return fallback;
33
+ const hex = value.replace("#", "");
34
+ if (/^[0-9a-fA-F]{3,8}$/.test(hex)) return hex;
35
+ return fallback;
36
+ }
37
+ /**
38
+ * Sanitize a URL — blocks javascript:, data: (except images), and vbscript: protocols.
39
+ * Returns the URL if safe, empty string if not.
40
+ */
41
+ function sanitizeHref(url) {
42
+ if (url == null) return "";
43
+ const trimmed = url.trim();
44
+ const lower = trimmed.toLowerCase().replace(/\s/g, "");
45
+ if (lower.startsWith("javascript:")) return "";
46
+ if (lower.startsWith("vbscript:")) return "";
47
+ if (lower.startsWith("data:") && !lower.startsWith("data:image/")) return "";
48
+ return trimmed;
49
+ }
50
+ /**
51
+ * Sanitize an image src — allows http(s), data:image, and relative paths.
52
+ * Blocks javascript:, vbscript:, and non-image data: URIs.
53
+ */
54
+ function sanitizeImageSrc(src) {
55
+ if (src == null) return "";
56
+ const trimmed = src.trim();
57
+ const lower = trimmed.toLowerCase().replace(/\s/g, "");
58
+ if (lower.startsWith("javascript:")) return "";
59
+ if (lower.startsWith("vbscript:")) return "";
60
+ if (lower.startsWith("data:") && !lower.startsWith("data:image/")) return "";
61
+ return trimmed;
62
+ }
63
+ /**
64
+ * Sanitize a style attribute value — validates it's safe CSS.
65
+ */
66
+ function sanitizeStyle(value) {
67
+ if (value == null) return "";
68
+ return sanitizeCss(value);
69
+ }
70
+
71
+ //#endregion
72
+ export { sanitizeXmlColor as a, sanitizeStyle as i, sanitizeHref as n, sanitizeImageSrc as r, sanitizeColor as t };
73
+ //# sourceMappingURL=sanitize-O_3j1mNJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize-O_3j1mNJ.js","names":[],"sources":["../src/sanitize.ts"],"sourcesContent":["/**\n * Shared sanitization utilities for document renderers.\n * Prevents XSS via CSS injection, XML injection, and javascript: protocol attacks.\n */\n\n/**\n * Sanitize a CSS value — strips characters that could break out of a CSS property.\n * Blocks: semicolons, braces, angle brackets, quotes, backslashes, expressions.\n */\nexport function sanitizeCss(value: string | undefined): string {\n if (value == null) return ''\n // Remove anything that could break out of a CSS value\n return value\n .replace(/[;{}()<>\\\\'\"]/g, '')\n .replace(/expression\\s*\\(/gi, '')\n .replace(/url\\s*\\(/gi, '')\n .replace(/javascript\\s*:/gi, '')\n}\n\n/**\n * Sanitize a color value — only allows hex colors, named colors, rgb/rgba, hsl/hsla.\n * Returns the value if valid, empty string if not.\n */\nexport function sanitizeColor(value: string | undefined): string {\n if (value == null) return ''\n const trimmed = value.trim()\n // Hex: #fff, #ffffff, #ffffffff\n if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed\n // Named colors (common subset)\n if (/^[a-zA-Z]{1,20}$/.test(trimmed)) return trimmed\n // rgb/rgba/hsl/hsla\n if (/^(rgb|hsl)a?\\(\\s*[\\d.,\\s%]+\\)$/.test(trimmed)) return trimmed\n // transparent, inherit, currentColor\n if (/^(transparent|inherit|currentColor|initial|unset)$/i.test(trimmed))\n return trimmed\n return ''\n}\n\n/**\n * Sanitize a color for XML attributes (DOCX/PPTX) — only hex without #.\n * Returns 6-char hex string or default.\n */\nexport function sanitizeXmlColor(\n value: string | undefined,\n fallback = '000000',\n): string {\n if (value == null) return fallback\n const hex = value.replace('#', '')\n if (/^[0-9a-fA-F]{3,8}$/.test(hex)) return hex\n return fallback\n}\n\n/**\n * Sanitize a URL — blocks javascript:, data: (except images), and vbscript: protocols.\n * Returns the URL if safe, empty string if not.\n */\nexport function sanitizeHref(url: string | undefined): string {\n if (url == null) return ''\n const trimmed = url.trim()\n // Block dangerous protocols\n const lower = trimmed.toLowerCase().replace(/\\s/g, '')\n if (lower.startsWith('javascript:')) return ''\n if (lower.startsWith('vbscript:')) return ''\n if (lower.startsWith('data:') && !lower.startsWith('data:image/')) return ''\n return trimmed\n}\n\n/**\n * Sanitize an image src — allows http(s), data:image, and relative paths.\n * Blocks javascript:, vbscript:, and non-image data: URIs.\n */\nexport function sanitizeImageSrc(src: string | undefined): string {\n if (src == null) return ''\n const trimmed = src.trim()\n const lower = trimmed.toLowerCase().replace(/\\s/g, '')\n if (lower.startsWith('javascript:')) return ''\n if (lower.startsWith('vbscript:')) return ''\n if (lower.startsWith('data:') && !lower.startsWith('data:image/')) return ''\n return trimmed\n}\n\n/**\n * Sanitize a style attribute value — validates it's safe CSS.\n */\nexport function sanitizeStyle(value: string | undefined): string {\n if (value == null) return ''\n return sanitizeCss(value)\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,YAAY,OAAmC;AAC7D,KAAI,SAAS,KAAM,QAAO;AAE1B,QAAO,MACJ,QAAQ,kBAAkB,GAAG,CAC7B,QAAQ,qBAAqB,GAAG,CAChC,QAAQ,cAAc,GAAG,CACzB,QAAQ,oBAAoB,GAAG;;;;;;AAOpC,SAAgB,cAAc,OAAmC;AAC/D,KAAI,SAAS,KAAM,QAAO;CAC1B,MAAM,UAAU,MAAM,MAAM;AAE5B,KAAI,sBAAsB,KAAK,QAAQ,CAAE,QAAO;AAEhD,KAAI,mBAAmB,KAAK,QAAQ,CAAE,QAAO;AAE7C,KAAI,iCAAiC,KAAK,QAAQ,CAAE,QAAO;AAE3D,KAAI,sDAAsD,KAAK,QAAQ,CACrE,QAAO;AACT,QAAO;;;;;;AAOT,SAAgB,iBACd,OACA,WAAW,UACH;AACR,KAAI,SAAS,KAAM,QAAO;CAC1B,MAAM,MAAM,MAAM,QAAQ,KAAK,GAAG;AAClC,KAAI,qBAAqB,KAAK,IAAI,CAAE,QAAO;AAC3C,QAAO;;;;;;AAOT,SAAgB,aAAa,KAAiC;AAC5D,KAAI,OAAO,KAAM,QAAO;CACxB,MAAM,UAAU,IAAI,MAAM;CAE1B,MAAM,QAAQ,QAAQ,aAAa,CAAC,QAAQ,OAAO,GAAG;AACtD,KAAI,MAAM,WAAW,cAAc,CAAE,QAAO;AAC5C,KAAI,MAAM,WAAW,YAAY,CAAE,QAAO;AAC1C,KAAI,MAAM,WAAW,QAAQ,IAAI,CAAC,MAAM,WAAW,cAAc,CAAE,QAAO;AAC1E,QAAO;;;;;;AAOT,SAAgB,iBAAiB,KAAiC;AAChE,KAAI,OAAO,KAAM,QAAO;CACxB,MAAM,UAAU,IAAI,MAAM;CAC1B,MAAM,QAAQ,QAAQ,aAAa,CAAC,QAAQ,OAAO,GAAG;AACtD,KAAI,MAAM,WAAW,cAAc,CAAE,QAAO;AAC5C,KAAI,MAAM,WAAW,YAAY,CAAE,QAAO;AAC1C,KAAI,MAAM,WAAW,QAAQ,IAAI,CAAC,MAAM,WAAW,cAAc,CAAE,QAAO;AAC1E,QAAO;;;;;AAMT,SAAgB,cAAc,OAAmC;AAC/D,KAAI,SAAS,KAAM,QAAO;AAC1B,QAAO,YAAY,MAAM"}
@@ -1,3 +1,5 @@
1
+ import { n as sanitizeHref, r as sanitizeImageSrc } from "./sanitize-O_3j1mNJ.js";
2
+
1
3
  //#region src/renderers/slack.ts
2
4
  /**
3
5
  * Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.
@@ -50,7 +52,7 @@ function nodeToBlocks(node) {
50
52
  break;
51
53
  }
52
54
  case "link": {
53
- const href = p.href;
55
+ const href = sanitizeHref(p.href);
54
56
  const text = getTextContent(node.children);
55
57
  blocks.push({
56
58
  type: "section",
@@ -59,7 +61,7 @@ function nodeToBlocks(node) {
59
61
  break;
60
62
  }
61
63
  case "image": {
62
- const src = p.src;
64
+ const src = sanitizeImageSrc(p.src);
63
65
  if (src.startsWith("http")) blocks.push({
64
66
  type: "image",
65
67
  image_url: src,
@@ -105,7 +107,7 @@ function nodeToBlocks(node) {
105
107
  break;
106
108
  case "spacer": break;
107
109
  case "button": {
108
- const href = p.href;
110
+ const href = sanitizeHref(p.href);
109
111
  const text = getTextContent(node.children);
110
112
  blocks.push({
111
113
  type: "actions",
@@ -136,4 +138,4 @@ const slackRenderer = { async render(node, _options) {
136
138
 
137
139
  //#endregion
138
140
  export { slackRenderer };
139
- //# sourceMappingURL=slack-CJRJgkag.js.map
141
+ //# sourceMappingURL=slack-BI3EQwYm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-BI3EQwYm.js","names":[],"sources":["../src/renderers/slack.ts"],"sourcesContent":["import { sanitizeHref, sanitizeImageSrc } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.\n * Maps document nodes to Slack blocks (section, header, divider, image, etc.).\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 SlackBlock {\n type: string\n [key: string]: unknown\n}\n\nfunction mrkdwn(text: string): { type: 'mrkdwn'; text: string } {\n return { type: 'mrkdwn', text }\n}\n\nfunction plainText(text: string): { type: 'plain_text'; text: string } {\n return { type: 'plain_text', text }\n}\n\nfunction nodeToBlocks(node: DocNode): SlackBlock[] {\n const p = node.props\n const blocks: SlackBlock[] = []\n\n switch (node.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n for (const child of node.children) {\n if (typeof child !== 'string') {\n blocks.push(...nodeToBlocks(child))\n }\n }\n break\n\n case 'heading':\n blocks.push({\n type: 'header',\n text: plainText(getTextContent(node.children)),\n })\n break\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 blocks.push({\n type: 'section',\n text: mrkdwn(text),\n })\n break\n }\n\n case 'link': {\n const href = sanitizeHref(p.href as string)\n const text = getTextContent(node.children)\n blocks.push({\n type: 'section',\n text: mrkdwn(`<${href}|${text}>`),\n })\n break\n }\n\n case 'image': {\n const src = sanitizeImageSrc(p.src as string)\n // Slack only supports public URLs for images\n if (src.startsWith('http')) {\n blocks.push({\n type: 'image',\n image_url: src,\n alt_text: (p.alt as string) ?? 'Image',\n ...(p.caption ? { title: plainText(p.caption as string) } : {}),\n })\n }\n break\n }\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 // Slack doesn't have native tables — render as formatted text\n const header = columns.map((c) => `*${c.header}*`).join(' | ')\n const separator = columns.map(() => '---').join(' | ')\n const body = rows\n .map((row) => row.map((cell) => String(cell ?? '')).join(' | '))\n .join('\\n')\n\n let text = `${header}\\n${separator}\\n${body}`\n if (p.caption) text = `_${p.caption}_\\n${text}`\n\n blocks.push({\n type: 'section',\n text: mrkdwn(`\\`\\`\\`\\n${text}\\n\\`\\`\\``),\n })\n break\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n const items = 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 blocks.push({\n type: 'section',\n text: mrkdwn(items),\n })\n break\n }\n\n case 'code': {\n const text = getTextContent(node.children)\n const lang = (p.language as string) ?? ''\n blocks.push({\n type: 'section',\n text: mrkdwn(`\\`\\`\\`${lang}\\n${text}\\n\\`\\`\\``),\n })\n break\n }\n\n case 'divider':\n case 'page-break':\n blocks.push({ type: 'divider' })\n break\n\n case 'spacer':\n // No equivalent in Slack — skip\n break\n\n case 'button': {\n const href = sanitizeHref(p.href as string)\n const text = getTextContent(node.children)\n blocks.push({\n type: 'actions',\n elements: [\n {\n type: 'button',\n text: plainText(text),\n url: href,\n style: 'primary',\n },\n ],\n })\n break\n }\n\n case 'quote': {\n const text = getTextContent(node.children)\n blocks.push({\n type: 'section',\n text: mrkdwn(`> ${text}`),\n })\n break\n }\n }\n\n return blocks\n}\n\nexport const slackRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n const blocks = nodeToBlocks(node)\n return JSON.stringify({ blocks }, null, 2)\n },\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { n as sanitizeHref, r as sanitizeImageSrc, t as sanitizeColor } from "./sanitize-O_3j1mNJ.js";
2
+
1
3
  //#region src/renderers/svg.ts
2
4
  /**
3
5
  * SVG renderer — generates a standalone SVG document from the node tree.
@@ -35,7 +37,7 @@ function renderNode(node, ctx) {
35
37
  5: 16,
36
38
  6: 14
37
39
  }[level] ?? 24;
38
- const color = p.color ?? "#000000";
40
+ const color = sanitizeColor(p.color ?? "#000000");
39
41
  const text = escapeXml(getTextContent(node.children));
40
42
  ctx.y += size + 8;
41
43
  svg += `<text x="${ctx.padding}" y="${ctx.y}" font-size="${size}" font-weight="bold" fill="${color}" font-family="system-ui, -apple-system, sans-serif">${text}</text>`;
@@ -44,7 +46,7 @@ function renderNode(node, ctx) {
44
46
  }
45
47
  case "text": {
46
48
  const size = p.size ?? 14;
47
- const color = p.color ?? "#333333";
49
+ const color = sanitizeColor(p.color ?? "#333333");
48
50
  const weight = p.bold ? "bold" : "normal";
49
51
  const style = p.italic ? "italic" : "normal";
50
52
  const text = escapeXml(getTextContent(node.children));
@@ -54,9 +56,9 @@ function renderNode(node, ctx) {
54
56
  break;
55
57
  }
56
58
  case "link": {
57
- const href = p.href;
59
+ const href = sanitizeHref(p.href);
58
60
  const text = escapeXml(getTextContent(node.children));
59
- const color = p.color ?? "#4f46e5";
61
+ const color = sanitizeColor(p.color ?? "#4f46e5");
60
62
  ctx.y += 18;
61
63
  svg += `<a href="${escapeXml(href)}"><text x="${ctx.padding}" y="${ctx.y}" font-size="14" fill="${color}" text-decoration="underline" font-family="system-ui, -apple-system, sans-serif">${text}</text></a>`;
62
64
  ctx.y += 10;
@@ -65,7 +67,7 @@ function renderNode(node, ctx) {
65
67
  case "image": {
66
68
  const width = p.width ?? Math.min(contentWidth, 400);
67
69
  const height = p.height ?? 200;
68
- const src = p.src;
70
+ const src = sanitizeImageSrc(p.src);
69
71
  if (src.startsWith("data:") || src.startsWith("http")) svg += `<image x="${ctx.padding}" y="${ctx.y}" width="${width}" height="${height}" href="${escapeXml(src)}" />`;
70
72
  else {
71
73
  svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${width}" height="${height}" fill="#f0f0f0" stroke="#ddd" rx="4" />`;
@@ -86,8 +88,8 @@ function renderNode(node, ctx) {
86
88
  const striped = p.striped;
87
89
  const colWidth = contentWidth / columns.length;
88
90
  const rowHeight = 28;
89
- const headerBg = hs?.background ?? "#f5f5f5";
90
- const headerColor = hs?.color ?? "#000000";
91
+ const headerBg = sanitizeColor(hs?.background ?? "#f5f5f5");
92
+ const headerColor = sanitizeColor(hs?.color ?? "#000000");
91
93
  svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="${contentWidth}" height="${rowHeight}" fill="${headerBg}" />`;
92
94
  for (let i = 0; i < columns.length; i++) {
93
95
  const col = columns[i];
@@ -127,7 +129,7 @@ function renderNode(node, ctx) {
127
129
  break;
128
130
  }
129
131
  case "divider": {
130
- const color = p.color ?? "#ddd";
132
+ const color = sanitizeColor(p.color ?? "#ddd");
131
133
  const thickness = p.thickness ?? 1;
132
134
  ctx.y += 12;
133
135
  svg += `<line x1="${ctx.padding}" y1="${ctx.y}" x2="${ctx.padding + contentWidth}" y2="${ctx.y}" stroke="${color}" stroke-width="${thickness}" />`;
@@ -143,8 +145,8 @@ function renderNode(node, ctx) {
143
145
  ctx.y += p.height ?? 12;
144
146
  break;
145
147
  case "button": {
146
- const bg = p.background ?? "#4f46e5";
147
- const color = p.color ?? "#ffffff";
148
+ const bg = sanitizeColor(p.background ?? "#4f46e5");
149
+ const color = sanitizeColor(p.color ?? "#ffffff");
148
150
  const text = escapeXml(getTextContent(node.children));
149
151
  const btnWidth = Math.min(text.length * 10 + 48, contentWidth);
150
152
  const btnHeight = 40;
@@ -155,7 +157,7 @@ function renderNode(node, ctx) {
155
157
  break;
156
158
  }
157
159
  case "quote": {
158
- const borderColor = p.borderColor ?? "#ddd";
160
+ const borderColor = sanitizeColor(p.borderColor ?? "#ddd");
159
161
  const text = escapeXml(getTextContent(node.children));
160
162
  ctx.y += 4;
161
163
  svg += `<rect x="${ctx.padding}" y="${ctx.y}" width="4" height="20" fill="${borderColor}" />`;
@@ -184,4 +186,4 @@ ${content}
184
186
 
185
187
  //#endregion
186
188
  export { svgRenderer };
187
- //# sourceMappingURL=svg-BM8biZmL.js.map
189
+ //# sourceMappingURL=svg-BKxumy-p.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-BKxumy-p.js","names":[],"sources":["../src/renderers/svg.ts"],"sourcesContent":["import { sanitizeColor, sanitizeHref, sanitizeImageSrc } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * SVG renderer — generates a standalone SVG document from the node tree.\n * Useful for thumbnails, social cards, and preview images.\n * No external dependencies — pure SVG string generation.\n */\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\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 RenderContext {\n y: number\n width: number\n padding: number\n}\n\nfunction renderNode(node: DocNode, ctx: RenderContext): string {\n const p = node.props\n const contentWidth = ctx.width - ctx.padding * 2\n let svg = ''\n\n switch (node.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n for (const child of node.children) {\n if (typeof child !== 'string') {\n svg += renderNode(child, ctx)\n }\n }\n break\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const sizes: Record<number, number> = {\n 1: 28,\n 2: 24,\n 3: 20,\n 4: 18,\n 5: 16,\n 6: 14,\n }\n const size = sizes[level] ?? 24\n const color = sanitizeColor((p.color as string) ?? '#000000')\n const text = escapeXml(getTextContent(node.children))\n ctx.y += size + 8\n svg += `<text x=\"${ctx.padding}\" y=\"${ctx.y}\" font-size=\"${size}\" font-weight=\"bold\" fill=\"${color}\" font-family=\"system-ui, -apple-system, sans-serif\">${text}</text>`\n ctx.y += 12\n break\n }\n\n case 'text': {\n const size = (p.size as number) ?? 14\n const color = sanitizeColor((p.color as string) ?? '#333333')\n const weight = p.bold ? 'bold' : 'normal'\n const style = p.italic ? 'italic' : 'normal'\n const text = escapeXml(getTextContent(node.children))\n ctx.y += size + 4\n svg += `<text x=\"${ctx.padding}\" y=\"${ctx.y}\" font-size=\"${size}\" font-weight=\"${weight}\" font-style=\"${style}\" fill=\"${color}\" font-family=\"system-ui, -apple-system, sans-serif\">${text}</text>`\n ctx.y += 10\n break\n }\n\n case 'link': {\n const href = sanitizeHref(p.href as string)\n const text = escapeXml(getTextContent(node.children))\n const color = sanitizeColor((p.color as string) ?? '#4f46e5')\n ctx.y += 18\n svg += `<a href=\"${escapeXml(href)}\"><text x=\"${ctx.padding}\" y=\"${ctx.y}\" font-size=\"14\" fill=\"${color}\" text-decoration=\"underline\" font-family=\"system-ui, -apple-system, sans-serif\">${text}</text></a>`\n ctx.y += 10\n break\n }\n\n case 'image': {\n const width = (p.width as number) ?? Math.min(contentWidth, 400)\n const height = (p.height as number) ?? 200\n const src = sanitizeImageSrc(p.src as string)\n\n if (src.startsWith('data:') || src.startsWith('http')) {\n svg += `<image x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${width}\" height=\"${height}\" href=\"${escapeXml(src)}\" />`\n } else {\n // Placeholder rectangle for local paths\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${width}\" height=\"${height}\" fill=\"#f0f0f0\" stroke=\"#ddd\" rx=\"4\" />`\n svg += `<text x=\"${ctx.padding + width / 2}\" y=\"${ctx.y + height / 2}\" text-anchor=\"middle\" dominant-baseline=\"middle\" font-size=\"12\" fill=\"#999\" font-family=\"system-ui, sans-serif\">${escapeXml((p.alt as string) ?? 'Image')}</text>`\n }\n ctx.y += height + 8\n\n if (p.caption) {\n ctx.y += 14\n svg += `<text x=\"${ctx.padding}\" y=\"${ctx.y}\" font-size=\"12\" fill=\"#666\" font-style=\"italic\" font-family=\"system-ui, sans-serif\">${escapeXml(p.caption as string)}</text>`\n ctx.y += 8\n }\n break\n }\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 const hs = p.headerStyle as\n | { background?: string; color?: string }\n | undefined\n const striped = p.striped as boolean | undefined\n\n const colWidth = contentWidth / columns.length\n const rowHeight = 28\n const headerBg = sanitizeColor(hs?.background ?? '#f5f5f5')\n const headerColor = sanitizeColor(hs?.color ?? '#000000')\n\n // Header\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${contentWidth}\" height=\"${rowHeight}\" fill=\"${headerBg}\" />`\n for (let i = 0; i < columns.length; i++) {\n const col = columns[i]\n if (!col) continue\n svg += `<text x=\"${ctx.padding + i * colWidth + 8}\" y=\"${ctx.y + 18}\" font-size=\"12\" font-weight=\"bold\" fill=\"${headerColor}\" font-family=\"system-ui, sans-serif\">${escapeXml(col.header)}</text>`\n }\n ctx.y += rowHeight\n\n // Rows\n for (let r = 0; r < rows.length; r++) {\n if (striped && r % 2 === 1) {\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${contentWidth}\" height=\"${rowHeight}\" fill=\"#f9f9f9\" />`\n }\n for (let c = 0; c < columns.length; c++) {\n svg += `<text x=\"${ctx.padding + c * colWidth + 8}\" y=\"${ctx.y + 18}\" font-size=\"12\" fill=\"#333\" font-family=\"system-ui, sans-serif\">${escapeXml(String(rows[r]?.[c] ?? ''))}</text>`\n }\n ctx.y += rowHeight\n }\n\n // Bottom border\n svg += `<line x1=\"${ctx.padding}\" y1=\"${ctx.y}\" x2=\"${ctx.padding + contentWidth}\" y2=\"${ctx.y}\" stroke=\"#ddd\" stroke-width=\"1\" />`\n ctx.y += 12\n break\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n const items = node.children.filter(\n (c): c is DocNode => typeof c !== 'string',\n )\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item) continue\n const prefix = ordered ? `${i + 1}.` : '•'\n const text = escapeXml(getTextContent(item.children))\n ctx.y += 18\n svg += `<text x=\"${ctx.padding + 16}\" y=\"${ctx.y}\" font-size=\"13\" fill=\"#333\" font-family=\"system-ui, sans-serif\">${prefix} ${text}</text>`\n }\n ctx.y += 10\n break\n }\n\n case 'code': {\n const text = getTextContent(node.children)\n const lines = text.split('\\n')\n const codeHeight = lines.length * 18 + 16\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${contentWidth}\" height=\"${codeHeight}\" fill=\"#f5f5f5\" rx=\"4\" />`\n for (let i = 0; i < lines.length; i++) {\n svg += `<text x=\"${ctx.padding + 12}\" y=\"${ctx.y + 20 + i * 18}\" font-size=\"12\" fill=\"#333\" font-family=\"monospace\">${escapeXml(lines[i] ?? '')}</text>`\n }\n ctx.y += codeHeight + 8\n break\n }\n\n case 'divider': {\n const color = sanitizeColor((p.color as string) ?? '#ddd')\n const thickness = (p.thickness as number) ?? 1\n ctx.y += 12\n svg += `<line x1=\"${ctx.padding}\" y1=\"${ctx.y}\" x2=\"${ctx.padding + contentWidth}\" y2=\"${ctx.y}\" stroke=\"${color}\" stroke-width=\"${thickness}\" />`\n ctx.y += 12\n break\n }\n\n case 'page-break':\n ctx.y += 16\n svg += `<line x1=\"${ctx.padding}\" y1=\"${ctx.y}\" x2=\"${ctx.padding + contentWidth}\" y2=\"${ctx.y}\" stroke=\"#ccc\" stroke-width=\"2\" stroke-dasharray=\"8,4\" />`\n ctx.y += 16\n break\n\n case 'spacer':\n ctx.y += (p.height as number) ?? 12\n break\n\n case 'button': {\n const bg = sanitizeColor((p.background as string) ?? '#4f46e5')\n const color = sanitizeColor((p.color as string) ?? '#ffffff')\n const text = escapeXml(getTextContent(node.children))\n const btnWidth = Math.min(text.length * 10 + 48, contentWidth)\n const btnHeight = 40\n ctx.y += 8\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"${btnWidth}\" height=\"${btnHeight}\" fill=\"${bg}\" rx=\"4\" />`\n svg += `<text x=\"${ctx.padding + btnWidth / 2}\" y=\"${ctx.y + 25}\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"bold\" fill=\"${color}\" font-family=\"system-ui, sans-serif\">${text}</text>`\n ctx.y += btnHeight + 12\n break\n }\n\n case 'quote': {\n const borderColor = sanitizeColor((p.borderColor as string) ?? '#ddd')\n const text = escapeXml(getTextContent(node.children))\n ctx.y += 4\n svg += `<rect x=\"${ctx.padding}\" y=\"${ctx.y}\" width=\"4\" height=\"20\" fill=\"${borderColor}\" />`\n svg += `<text x=\"${ctx.padding + 16}\" y=\"${ctx.y + 15}\" font-size=\"13\" fill=\"#555\" font-style=\"italic\" font-family=\"system-ui, sans-serif\">${text}</text>`\n ctx.y += 28\n break\n }\n }\n\n return svg\n}\n\nexport const svgRenderer: DocumentRenderer = {\n async render(node: DocNode, options?: RenderOptions): Promise<string> {\n const width = 800\n const padding = 40\n const ctx: RenderContext = { y: padding, width, padding }\n\n const content = renderNode(node, ctx)\n const height = ctx.y + padding\n\n const dir = options?.direction === 'rtl' ? ' direction=\"rtl\"' : ''\n\n return `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\"${dir}>\n<rect width=\"${width}\" height=\"${height}\" fill=\"#ffffff\" />\n${content}\n</svg>`\n },\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { n as sanitizeHref, r as sanitizeImageSrc } from "./sanitize-O_3j1mNJ.js";
2
+
1
3
  //#region src/renderers/teams.ts
2
4
  /**
3
5
  * Microsoft Teams renderer — outputs Adaptive Cards JSON.
@@ -53,7 +55,7 @@ function nodeToElements(node) {
53
55
  break;
54
56
  }
55
57
  case "link": {
56
- const href = p.href;
58
+ const href = sanitizeHref(p.href);
57
59
  const text = getTextContent(node.children);
58
60
  elements.push({
59
61
  type: "TextBlock",
@@ -63,7 +65,7 @@ function nodeToElements(node) {
63
65
  break;
64
66
  }
65
67
  case "image": {
66
- const src = p.src;
68
+ const src = sanitizeImageSrc(p.src);
67
69
  if (src.startsWith("http")) elements.push({
68
70
  type: "Image",
69
71
  url: src,
@@ -139,7 +141,7 @@ function nodeToElements(node) {
139
141
  actions: [{
140
142
  type: "Action.OpenUrl",
141
143
  title: getTextContent(node.children),
142
- url: p.href,
144
+ url: sanitizeHref(p.href),
143
145
  style: "positive"
144
146
  }]
145
147
  });
@@ -173,4 +175,4 @@ const teamsRenderer = { async render(node, _options) {
173
175
 
174
176
  //#endregion
175
177
  export { teamsRenderer };
176
- //# sourceMappingURL=teams-S99tonRG.js.map
178
+ //# sourceMappingURL=teams-Cwz9lce0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams-Cwz9lce0.js","names":[],"sources":["../src/renderers/teams.ts"],"sourcesContent":["import { sanitizeHref, sanitizeImageSrc } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * Microsoft Teams renderer — outputs Adaptive Cards JSON.\n * Can be posted via Teams Webhooks, Bot Framework, or Power Automate.\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 AdaptiveElement {\n type: string\n [key: string]: unknown\n}\n\nfunction nodeToElements(node: DocNode): AdaptiveElement[] {\n const p = node.props\n const elements: AdaptiveElement[] = []\n\n switch (node.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n for (const child of node.children) {\n if (typeof child !== 'string') {\n elements.push(...nodeToElements(child))\n }\n }\n break\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const sizeMap: Record<number, string> = {\n 1: 'extraLarge',\n 2: 'large',\n 3: 'medium',\n 4: 'default',\n 5: 'small',\n 6: 'small',\n }\n elements.push({\n type: 'TextBlock',\n text: getTextContent(node.children),\n size: sizeMap[level] ?? 'large',\n weight: 'bolder',\n wrap: true,\n })\n break\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 elements.push({\n type: 'TextBlock',\n text,\n wrap: true,\n ...(p.color ? { color: 'default' } : {}),\n ...(p.size\n ? { size: (p.size as number) >= 18 ? 'large' : 'default' }\n : {}),\n })\n break\n }\n\n case 'link': {\n const href = sanitizeHref(p.href as string)\n const text = getTextContent(node.children)\n elements.push({\n type: 'TextBlock',\n text: `[${text}](${href})`,\n wrap: true,\n })\n break\n }\n\n case 'image': {\n const src = sanitizeImageSrc(p.src as string)\n if (src.startsWith('http')) {\n elements.push({\n type: 'Image',\n url: src,\n altText: (p.alt as string) ?? 'Image',\n size: 'large',\n })\n }\n break\n }\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 // Adaptive Cards have native Table support (schema 1.5+)\n const tableColumns = columns.map((col) => ({\n type: 'Column',\n width: 'stretch',\n items: [\n {\n type: 'TextBlock',\n text: `**${col.header}**`,\n weight: 'bolder',\n wrap: true,\n },\n ...rows.map((row, i) => ({\n type: 'TextBlock',\n text: String(row[columns.indexOf(col)] ?? ''),\n wrap: true,\n separator: i === 0,\n })),\n ],\n }))\n\n elements.push({\n type: 'ColumnSet',\n columns: tableColumns,\n })\n break\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n const items = 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 elements.push({\n type: 'TextBlock',\n text: items,\n wrap: true,\n })\n break\n }\n\n case 'code': {\n const text = getTextContent(node.children)\n elements.push({\n type: 'TextBlock',\n text: `\\`\\`\\`\\n${text}\\n\\`\\`\\``,\n fontType: 'monospace',\n wrap: true,\n })\n break\n }\n\n case 'divider':\n case 'page-break':\n elements.push({\n type: 'TextBlock',\n text: ' ',\n separator: true,\n })\n break\n\n case 'spacer':\n elements.push({\n type: 'TextBlock',\n text: ' ',\n spacing: 'large',\n })\n break\n\n case 'button': {\n elements.push({\n type: 'ActionSet',\n actions: [\n {\n type: 'Action.OpenUrl',\n title: getTextContent(node.children),\n url: sanitizeHref(p.href as string),\n style: 'positive',\n },\n ],\n })\n break\n }\n\n case 'quote': {\n const text = getTextContent(node.children)\n elements.push({\n type: 'Container',\n style: 'emphasis',\n items: [\n {\n type: 'TextBlock',\n text: `_${text}_`,\n wrap: true,\n isSubtle: true,\n },\n ],\n })\n break\n }\n }\n\n return elements\n}\n\nexport const teamsRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n const body = nodeToElements(node)\n const card = {\n type: 'AdaptiveCard',\n $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',\n version: '1.5',\n body,\n }\n return JSON.stringify(card, null, 2)\n },\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { n as sanitizeHref } from "./sanitize-O_3j1mNJ.js";
2
+
1
3
  //#region src/renderers/telegram.ts
2
4
  /**
3
5
  * Telegram renderer — outputs HTML using Telegram's supported subset.
@@ -31,7 +33,7 @@ function renderNode(node) {
31
33
  return `${text}\n\n`;
32
34
  }
33
35
  case "link": {
34
- const href = p.href;
36
+ const href = sanitizeHref(p.href);
35
37
  const text = esc(getTextContent(node.children));
36
38
  return `<a href="${esc(href)}">${text}</a>\n\n`;
37
39
  }
@@ -60,7 +62,7 @@ function renderNode(node) {
60
62
  case "page-break": return "───────────\n\n";
61
63
  case "spacer": return "\n";
62
64
  case "button": {
63
- const href = p.href;
65
+ const href = sanitizeHref(p.href);
64
66
  const text = esc(getTextContent(node.children));
65
67
  return `<a href="${esc(href)}">${text}</a>\n\n`;
66
68
  }
@@ -74,4 +76,4 @@ const telegramRenderer = { async render(node, _options) {
74
76
 
75
77
  //#endregion
76
78
  export { telegramRenderer };
77
- //# sourceMappingURL=telegram-CbEO_2PN.js.map
79
+ //# sourceMappingURL=telegram-gYFqyMXb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram-gYFqyMXb.js","names":[],"sources":["../src/renderers/telegram.ts"],"sourcesContent":["import { sanitizeHref } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * Telegram renderer — outputs HTML using Telegram's supported subset.\n * Telegram Bot API supports: <b>, <i>, <u>, <s>, <a>, <code>, <pre>, <blockquote>.\n * No tables, no images inline — images sent separately via sendPhoto.\n */\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction esc(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\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' ? esc(c) : renderNode(c)))\n .join('')\n\n case 'heading': {\n const text = esc(getTextContent(node.children))\n return `<b>${text}</b>\\n\\n`\n }\n\n case 'text': {\n let text = esc(getTextContent(node.children))\n if (p.bold) text = `<b>${text}</b>`\n if (p.italic) text = `<i>${text}</i>`\n if (p.underline) text = `<u>${text}</u>`\n if (p.strikethrough) text = `<s>${text}</s>`\n return `${text}\\n\\n`\n }\n\n case 'link': {\n const href = sanitizeHref(p.href as string)\n const text = esc(getTextContent(node.children))\n return `<a href=\"${esc(href)}\">${text}</a>\\n\\n`\n }\n\n case 'image':\n // Telegram doesn't support inline images in HTML\n // Images need to be sent separately via sendPhoto\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 // Render as preformatted text since Telegram has no table support\n const header = columns.map((c) => c.header).join(' | ')\n const separator = columns.map(() => '---').join('-+-')\n const body = rows\n .map((row) => row.map((c) => String(c ?? '')).join(' | '))\n .join('\\n')\n\n return `<pre>${esc(header)}\\n${esc(separator)}\\n${esc(body)}</pre>\\n\\n`\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n const items = node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item, i) => {\n const prefix = ordered ? `${i + 1}.` : '•'\n return `${prefix} ${esc(getTextContent(item.children))}`\n })\n .join('\\n')\n return `${items}\\n\\n`\n }\n\n case 'code': {\n const lang = (p.language as string) ?? ''\n const text = esc(getTextContent(node.children))\n if (lang) {\n return `<pre><code class=\"language-${esc(lang)}\">${text}</code></pre>\\n\\n`\n }\n return `<pre>${text}</pre>\\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 = sanitizeHref(p.href as string)\n const text = esc(getTextContent(node.children))\n return `<a href=\"${esc(href)}\">${text}</a>\\n\\n`\n }\n\n case 'quote': {\n const text = esc(getTextContent(node.children))\n return `<blockquote>${text}</blockquote>\\n\\n`\n }\n\n default:\n return ''\n }\n}\n\nexport const telegramRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n return renderNode(node).trim()\n },\n}\n"],"mappings":"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"}
@@ -72,4 +72,4 @@ const textRenderer = { async render(node, _options) {
72
72
 
73
73
  //#endregion
74
74
  export { textRenderer };
75
- //# sourceMappingURL=text-B5U8ucRr.js.map
75
+ //# sourceMappingURL=text-l1XNXBOC.js.map