@pyreon/document 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/{docx-uNAel545.js → docx-CcB3jYd3.js} +9 -1
- package/lib/{docx-uNAel545.js.map → docx-CcB3jYd3.js.map} +1 -1
- package/lib/{html-B5biprN2.js → html-DtsbNARB.js} +2 -2
- package/lib/html-DtsbNARB.js.map +1 -0
- package/lib/index.js +6 -6
- package/lib/{markdown-CdtlFGC0.js → markdown-BYkSLplL.js} +23 -2
- package/lib/markdown-BYkSLplL.js.map +1 -0
- package/package.json +5 -5
- package/src/renderers/docx.ts +32 -0
- package/src/renderers/html.ts +13 -1
- package/src/renderers/markdown.ts +31 -1
- package/src/tests/integration.test.ts +275 -0
- package/lib/html-B5biprN2.js.map +0 -1
- package/lib/markdown-CdtlFGC0.js.map +0 -1
|
@@ -438,7 +438,15 @@ const docxRenderer = { async render(node, _options) {
|
|
|
438
438
|
margin: pageMargins
|
|
439
439
|
};
|
|
440
440
|
else if (pageMargins) sectionProperties.page = { margin: pageMargins };
|
|
441
|
+
const docTitle = node.props.title;
|
|
442
|
+
const docAuthor = node.props.author;
|
|
443
|
+
const docSubject = node.props.subject;
|
|
444
|
+
const docKeywords = node.props.keywords;
|
|
441
445
|
const doc = new docx.Document({
|
|
446
|
+
...docTitle ? { title: docTitle } : {},
|
|
447
|
+
...docSubject ? { subject: docSubject } : {},
|
|
448
|
+
...docAuthor ? { creator: docAuthor } : {},
|
|
449
|
+
...docKeywords && docKeywords.length > 0 ? { keywords: docKeywords.join(", ") } : {},
|
|
442
450
|
numbering: numberingConfigs.length > 0 ? { config: numberingConfigs } : void 0,
|
|
443
451
|
sections: [{
|
|
444
452
|
properties: sectionProperties,
|
|
@@ -453,4 +461,4 @@ const docxRenderer = { async render(node, _options) {
|
|
|
453
461
|
|
|
454
462
|
//#endregion
|
|
455
463
|
export { docxRenderer };
|
|
456
|
-
//# sourceMappingURL=docx-
|
|
464
|
+
//# sourceMappingURL=docx-CcB3jYd3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docx-uNAel545.js","names":[],"sources":["../src/renderers/docx.ts"],"sourcesContent":["import { sanitizeHref, sanitizeXmlColor } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n PageOrientation,\n PageSize,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * DOCX renderer — lazy-loads the 'docx' npm package on first use.\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) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))\n .join('')\n}\n\n/** Parse a data URL and return the base64 data and media type, or null for external URLs. */\nfunction parseDataUrl(src: string): { data: string; mime: string } | null {\n const match = src.match(/^data:(image\\/[^;]+);base64,(.+)$/)\n if (!match) return null\n return { mime: match[1]!, data: match[2]! }\n}\n\n/** Convert page size name to DOCX page dimensions in twips (1 inch = 1440 twips). */\nfunction getPageSize(\n size?: PageSize,\n orientation?: PageOrientation,\n): { width: number; height: number } | undefined {\n if (!size) return undefined\n const sizes: Record<string, { width: number; height: number }> = {\n A4: { width: 11906, height: 16838 },\n A3: { width: 16838, height: 23811 },\n A5: { width: 8391, height: 11906 },\n letter: { width: 12240, height: 15840 },\n legal: { width: 12240, height: 20160 },\n tabloid: { width: 15840, height: 24480 },\n }\n const dims = sizes[size]\n if (!dims) return undefined\n if (orientation === 'landscape') {\n return { width: dims.height, height: dims.width }\n }\n return dims\n}\n\n/** Convert margin prop to DOCX section margin (in twips, 1pt ~= 20 twips). */\nfunction getPageMargins(\n margin?: number | [number, number] | [number, number, number, number],\n): object | undefined {\n if (margin == null) return undefined\n if (typeof margin === 'number') {\n const twips = margin * 20\n return { top: twips, right: twips, bottom: twips, left: twips }\n }\n if (margin.length === 2) {\n return {\n top: margin[0] * 20,\n right: margin[1] * 20,\n bottom: margin[0] * 20,\n left: margin[1] * 20,\n }\n }\n return {\n top: margin[0] * 20,\n right: margin[1] * 20,\n bottom: margin[2] * 20,\n left: margin[3] * 20,\n }\n}\n\n/** Map percentage column width to DOCX table column width. */\nfunction getColumnWidth(width?: number | string): { size: number; type: unknown } | undefined {\n if (width == null) return undefined\n if (typeof width === 'number') return undefined\n const match = width.match(/^(\\d+)%$/)\n if (!match) return undefined\n return { size: Number.parseInt(match[1]!, 10) * 100, type: 'pct' as unknown }\n}\n\n/** Shared context passed to per-node-type render helpers. */\ninterface DocxCtx {\n docx: typeof import('docx')\n children: unknown[]\n alignmentMap: (align?: string) => unknown\n processListItems: (n: DocNode, listRef: string, level: number, ordered: boolean) => void\n nextListId: () => string\n}\n\nfunction renderHeading(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const level = (p.level as number) ?? 1\n const headingMap: Record<number, unknown> = {\n 1: docx.HeadingLevel.HEADING_1,\n 2: docx.HeadingLevel.HEADING_2,\n 3: docx.HeadingLevel.HEADING_3,\n 4: docx.HeadingLevel.HEADING_4,\n 5: docx.HeadingLevel.HEADING_5,\n 6: docx.HeadingLevel.HEADING_6,\n }\n children.push(\n new docx.Paragraph({\n heading: (headingMap[level] ?? docx.HeadingLevel.HEADING_1) as any,\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n bold: true,\n color: sanitizeXmlColor(p.color as string),\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n }),\n )\n}\n\nfunction renderTextNode(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n ...(p.bold != null ? { bold: p.bold as boolean } : {}),\n ...(p.italic != null ? { italics: p.italic as boolean } : {}),\n ...(p.underline ? { underline: {} } : {}),\n ...(p.strikethrough != null ? { strike: p.strikethrough as boolean } : {}),\n ...(p.size != null ? { size: (p.size as number) * 2 } : {}),\n color: sanitizeXmlColor(p.color as string, '333333'),\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n spacing: { after: 120 },\n }),\n )\n}\n\nfunction renderLink(ctx: DocxCtx, n: DocNode): void {\n const { docx, children } = ctx\n const p = n.props\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ExternalHyperlink({\n link: sanitizeHref(p.href as string),\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n color: sanitizeXmlColor(p.color as string, '4f46e5'),\n underline: { type: docx.UnderlineType.SINGLE },\n }),\n ],\n }),\n ],\n }),\n )\n}\n\nfunction renderImage(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const src = p.src as string\n const parsed = parseDataUrl(src)\n\n if (parsed) {\n const imgWidth = (p.width as number) ?? 300\n const imgHeight = (p.height as number) ?? 200\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ImageRun({\n data: Buffer.from(parsed.data, 'base64'),\n transformation: { width: imgWidth, height: imgHeight },\n type: parsed.mime === 'image/png' ? 'png' : 'jpg',\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n }),\n )\n if (p.caption) {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: p.caption as string,\n italics: true,\n size: 20,\n color: '666666',\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n spacing: { after: 120 },\n }),\n )\n }\n } else {\n const alt = (p.alt as string) ?? 'Image'\n const caption = p.caption ? ` — ${p.caption}` : ''\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: `[${alt}${caption}]`,\n italics: true,\n color: '999999',\n }),\n ],\n }),\n )\n }\n}\n\nfunction renderDocxTable(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n const hs = p.headerStyle as { background?: string; color?: string } | undefined\n const bordered = p.bordered as boolean | undefined\n const borderStyle = bordered\n ? { style: docx.BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }\n : undefined\n const cellBorders = borderStyle\n ? {\n top: borderStyle,\n bottom: borderStyle,\n left: borderStyle,\n right: borderStyle,\n }\n : undefined\n\n const headerRow = new docx.TableRow({\n tableHeader: true,\n children: columns.map(\n (col) =>\n new docx.TableCell({\n children: [\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: col.header,\n bold: true,\n color: sanitizeXmlColor(hs?.color),\n }),\n ],\n alignment: alignmentMap(col.align) as any,\n }),\n ],\n ...(hs?.background\n ? {\n shading: {\n fill: sanitizeXmlColor(hs.background),\n type: docx.ShadingType.SOLID,\n },\n }\n : {}),\n ...(cellBorders != null ? { borders: cellBorders } : {}),\n width: getColumnWidth(col.width as string | undefined) as any,\n }),\n ),\n })\n\n const dataRows = rows.map(\n (row, rowIdx) =>\n new docx.TableRow({\n children: columns.map(\n (col, colIdx) =>\n new docx.TableCell({\n children: [\n new docx.Paragraph({\n children: [new docx.TextRun({ text: String(row[colIdx] ?? '') })],\n alignment: alignmentMap(col.align) as any,\n }),\n ],\n ...(p.striped && rowIdx % 2 === 1\n ? { shading: { fill: 'F9F9F9', type: docx.ShadingType.SOLID } }\n : {}),\n ...(cellBorders != null ? { borders: cellBorders } : {}),\n width: getColumnWidth(col.width as string | undefined) as any,\n }),\n ),\n }),\n )\n\n if (p.caption) {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: p.caption as string,\n italics: true,\n size: 20,\n }),\n ],\n spacing: { after: 60 },\n }),\n )\n }\n\n children.push(\n new docx.Table({\n rows: [headerRow, ...dataRows],\n width: { size: 100, type: docx.WidthType.PERCENTAGE },\n }),\n )\n children.push(new docx.Paragraph({ text: '', spacing: { after: 120 } }))\n}\n\nfunction renderList(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, processListItems, nextListId } = ctx\n const ordered = n.props.ordered as boolean | undefined\n const listRef = nextListId()\n processListItems(n, listRef, 0, ordered ?? false)\n children.push(new docx.Paragraph({ text: '', spacing: { after: 60 } }))\n}\n\nfunction renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {\n const { docx, children } = ctx\n const p = n.props\n const text = getTextContent(n.children)\n if (n.type === 'button') {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ExternalHyperlink({\n link: sanitizeHref(p.href as string),\n children: [\n new docx.TextRun({\n text,\n bold: true,\n color: '4F46E5',\n underline: { type: docx.UnderlineType.SINGLE },\n }),\n ],\n }),\n ],\n spacing: { after: 120 },\n }),\n )\n } else {\n children.push(\n new docx.Paragraph({\n children: [new docx.TextRun({ text, italics: true, color: '555555' })],\n indent: { left: 720 },\n border: {\n left: {\n style: docx.BorderStyle.SINGLE,\n size: 6,\n color: sanitizeXmlColor(p.borderColor as string, 'DDDDDD'),\n },\n },\n spacing: { after: 120 },\n }),\n )\n }\n}\n\nexport const docxRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n let docx: typeof import('docx')\n try {\n docx = await import('docx')\n } catch {\n throw new Error(\n '[@pyreon/document] DOCX renderer requires \"docx\" package. Install it: bun add docx',\n )\n }\n const children: unknown[] = []\n let listCounter = 0\n\n function alignmentMap(align?: string): unknown {\n if (!align) return undefined\n const map: Record<string, unknown> = {\n left: docx.AlignmentType.LEFT,\n center: docx.AlignmentType.CENTER,\n right: docx.AlignmentType.RIGHT,\n justify: docx.AlignmentType.JUSTIFIED,\n }\n return map[align]\n }\n\n function processListItems(n: DocNode, listRef: string, level: number, ordered: boolean): void {\n const items = n.children.filter((c): c is DocNode => typeof c !== 'string')\n for (const item of items) {\n const nestedList = item.children.find(\n (c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'list',\n )\n const textChildren = item.children.filter(\n (c) => typeof c === 'string' || (c as DocNode).type !== 'list',\n )\n children.push(\n new docx.Paragraph({\n children: [new docx.TextRun({ text: getTextContent(textChildren) })],\n ...(ordered ? { numbering: { reference: listRef, level } } : { bullet: { level } }),\n }),\n )\n if (nestedList) {\n const nestedOrdered = (nestedList as DocNode).props.ordered as boolean | undefined\n processListItems(nestedList as DocNode, listRef, level + 1, nestedOrdered ?? false)\n }\n }\n }\n\n const ctx: DocxCtx = {\n docx,\n children,\n alignmentMap,\n processListItems,\n nextListId: () => `list-${listCounter++}`,\n }\n\n function processNode(n: DocNode): void {\n switch (n.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n for (const child of n.children) {\n if (typeof child !== 'string') processNode(child)\n else children.push(new docx.Paragraph({ text: child }))\n }\n break\n case 'heading':\n renderHeading(ctx, n)\n break\n case 'text':\n renderTextNode(ctx, n)\n break\n case 'link':\n renderLink(ctx, n)\n break\n case 'image':\n renderImage(ctx, n)\n break\n case 'table':\n renderDocxTable(ctx, n)\n break\n case 'list':\n renderList(ctx, n)\n break\n case 'code':\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n font: 'Courier New',\n size: 20,\n }),\n ],\n shading: { fill: 'F5F5F5', type: docx.ShadingType.SOLID },\n spacing: { after: 120 },\n }),\n )\n break\n case 'divider':\n children.push(\n new docx.Paragraph({\n border: {\n bottom: {\n style: docx.BorderStyle.SINGLE,\n size: (n.props.thickness as number | undefined) ?? 1,\n color: sanitizeXmlColor(n.props.color as string, 'DDDDDD'),\n },\n },\n spacing: { before: 120, after: 120 },\n }),\n )\n break\n case 'spacer':\n children.push(\n new docx.Paragraph({\n text: '',\n spacing: { after: (n.props.height as number) * 20 },\n }),\n )\n break\n case 'button':\n case 'quote':\n renderButtonOrQuote(ctx, n)\n break\n }\n }\n\n processNode(node)\n\n // Build numbering configs for all lists\n const numberingConfigs: unknown[] = []\n for (let i = 0; i < listCounter; i++) {\n numberingConfigs.push({\n reference: `list-${i}`,\n levels: Array.from({ length: 9 }, (_, level) => ({\n level,\n format: docx.LevelFormat.DECIMAL,\n text: `%${level + 1}.`,\n alignment: docx.AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } },\n },\n })),\n })\n }\n\n // Extract page properties from first page node\n const pageNode =\n node.type === 'document'\n ? (node.children.find(\n (c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'page',\n ) as DocNode | undefined)\n : node.type === 'page'\n ? node\n : undefined\n\n const pageProps = pageNode?.props ?? {}\n const pageDims = getPageSize(\n pageProps.size as PageSize | undefined,\n pageProps.orientation as PageOrientation | undefined,\n )\n const pageMargins = getPageMargins(\n pageProps.margin as number | [number, number] | [number, number, number, number] | undefined,\n )\n\n function buildHeaderFooter(contentNode: DocNode | undefined): unknown[] | undefined {\n if (!contentNode) return undefined\n const text = getTextContent(contentNode.children)\n if (!text) return undefined\n return [\n new docx.Paragraph({\n children: [new docx.TextRun({ text, size: 18, color: '999999' })],\n alignment: docx.AlignmentType.CENTER,\n }),\n ]\n }\n\n const headerContent = buildHeaderFooter(pageProps.header as DocNode | undefined)\n const footerContent = buildHeaderFooter(pageProps.footer as DocNode | undefined)\n\n const sectionProperties: Record<string, unknown> = {}\n if (pageDims) {\n sectionProperties.page = { size: pageDims, margin: pageMargins }\n } else if (pageMargins) {\n sectionProperties.page = { margin: pageMargins }\n }\n\n const doc = new docx.Document({\n numbering: (numberingConfigs.length > 0 ? { config: numberingConfigs } : undefined) as any,\n sections: [\n {\n properties: sectionProperties,\n ...(headerContent\n ? {\n headers: {\n default: new docx.Header({ children: headerContent as any }),\n },\n }\n : {}),\n ...(footerContent\n ? {\n footers: {\n default: new docx.Footer({ children: footerContent as any }),\n },\n }\n : {}),\n children: children as any,\n },\n ],\n })\n\n const buffer = await docx.Packer.toBuffer(doc)\n return new Uint8Array(buffer)\n },\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"docx-CcB3jYd3.js","names":[],"sources":["../src/renderers/docx.ts"],"sourcesContent":["import { sanitizeHref, sanitizeXmlColor } from '../sanitize'\nimport type {\n DocChild,\n DocNode,\n DocumentRenderer,\n PageOrientation,\n PageSize,\n RenderOptions,\n TableColumn,\n} from '../types'\n\n/**\n * DOCX renderer — lazy-loads the 'docx' npm package on first use.\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) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))\n .join('')\n}\n\n/** Parse a data URL and return the base64 data and media type, or null for external URLs. */\nfunction parseDataUrl(src: string): { data: string; mime: string } | null {\n const match = src.match(/^data:(image\\/[^;]+);base64,(.+)$/)\n if (!match) return null\n return { mime: match[1]!, data: match[2]! }\n}\n\n/** Convert page size name to DOCX page dimensions in twips (1 inch = 1440 twips). */\nfunction getPageSize(\n size?: PageSize,\n orientation?: PageOrientation,\n): { width: number; height: number } | undefined {\n if (!size) return undefined\n const sizes: Record<string, { width: number; height: number }> = {\n A4: { width: 11906, height: 16838 },\n A3: { width: 16838, height: 23811 },\n A5: { width: 8391, height: 11906 },\n letter: { width: 12240, height: 15840 },\n legal: { width: 12240, height: 20160 },\n tabloid: { width: 15840, height: 24480 },\n }\n const dims = sizes[size]\n if (!dims) return undefined\n if (orientation === 'landscape') {\n return { width: dims.height, height: dims.width }\n }\n return dims\n}\n\n/** Convert margin prop to DOCX section margin (in twips, 1pt ~= 20 twips). */\nfunction getPageMargins(\n margin?: number | [number, number] | [number, number, number, number],\n): object | undefined {\n if (margin == null) return undefined\n if (typeof margin === 'number') {\n const twips = margin * 20\n return { top: twips, right: twips, bottom: twips, left: twips }\n }\n if (margin.length === 2) {\n return {\n top: margin[0] * 20,\n right: margin[1] * 20,\n bottom: margin[0] * 20,\n left: margin[1] * 20,\n }\n }\n return {\n top: margin[0] * 20,\n right: margin[1] * 20,\n bottom: margin[2] * 20,\n left: margin[3] * 20,\n }\n}\n\n/** Map percentage column width to DOCX table column width. */\nfunction getColumnWidth(width?: number | string): { size: number; type: unknown } | undefined {\n if (width == null) return undefined\n if (typeof width === 'number') return undefined\n const match = width.match(/^(\\d+)%$/)\n if (!match) return undefined\n return { size: Number.parseInt(match[1]!, 10) * 100, type: 'pct' as unknown }\n}\n\n/** Shared context passed to per-node-type render helpers. */\ninterface DocxCtx {\n docx: typeof import('docx')\n children: unknown[]\n alignmentMap: (align?: string) => unknown\n processListItems: (n: DocNode, listRef: string, level: number, ordered: boolean) => void\n nextListId: () => string\n}\n\nfunction renderHeading(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const level = (p.level as number) ?? 1\n const headingMap: Record<number, unknown> = {\n 1: docx.HeadingLevel.HEADING_1,\n 2: docx.HeadingLevel.HEADING_2,\n 3: docx.HeadingLevel.HEADING_3,\n 4: docx.HeadingLevel.HEADING_4,\n 5: docx.HeadingLevel.HEADING_5,\n 6: docx.HeadingLevel.HEADING_6,\n }\n children.push(\n new docx.Paragraph({\n heading: (headingMap[level] ?? docx.HeadingLevel.HEADING_1) as any,\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n bold: true,\n color: sanitizeXmlColor(p.color as string),\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n }),\n )\n}\n\nfunction renderTextNode(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n ...(p.bold != null ? { bold: p.bold as boolean } : {}),\n ...(p.italic != null ? { italics: p.italic as boolean } : {}),\n ...(p.underline ? { underline: {} } : {}),\n ...(p.strikethrough != null ? { strike: p.strikethrough as boolean } : {}),\n ...(p.size != null ? { size: (p.size as number) * 2 } : {}),\n color: sanitizeXmlColor(p.color as string, '333333'),\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n spacing: { after: 120 },\n }),\n )\n}\n\nfunction renderLink(ctx: DocxCtx, n: DocNode): void {\n const { docx, children } = ctx\n const p = n.props\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ExternalHyperlink({\n link: sanitizeHref(p.href as string),\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n color: sanitizeXmlColor(p.color as string, '4f46e5'),\n underline: { type: docx.UnderlineType.SINGLE },\n }),\n ],\n }),\n ],\n }),\n )\n}\n\nfunction renderImage(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const src = p.src as string\n const parsed = parseDataUrl(src)\n\n if (parsed) {\n const imgWidth = (p.width as number) ?? 300\n const imgHeight = (p.height as number) ?? 200\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ImageRun({\n data: Buffer.from(parsed.data, 'base64'),\n transformation: { width: imgWidth, height: imgHeight },\n type: parsed.mime === 'image/png' ? 'png' : 'jpg',\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n }),\n )\n if (p.caption) {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: p.caption as string,\n italics: true,\n size: 20,\n color: '666666',\n }),\n ],\n alignment: alignmentMap(p.align as string) as any,\n spacing: { after: 120 },\n }),\n )\n }\n } else {\n const alt = (p.alt as string) ?? 'Image'\n const caption = p.caption ? ` — ${p.caption}` : ''\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: `[${alt}${caption}]`,\n italics: true,\n color: '999999',\n }),\n ],\n }),\n )\n }\n}\n\nfunction renderDocxTable(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, alignmentMap } = ctx\n const p = n.props\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n const hs = p.headerStyle as { background?: string; color?: string } | undefined\n const bordered = p.bordered as boolean | undefined\n const borderStyle = bordered\n ? { style: docx.BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }\n : undefined\n const cellBorders = borderStyle\n ? {\n top: borderStyle,\n bottom: borderStyle,\n left: borderStyle,\n right: borderStyle,\n }\n : undefined\n\n const headerRow = new docx.TableRow({\n tableHeader: true,\n children: columns.map(\n (col) =>\n new docx.TableCell({\n children: [\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: col.header,\n bold: true,\n color: sanitizeXmlColor(hs?.color),\n }),\n ],\n alignment: alignmentMap(col.align) as any,\n }),\n ],\n ...(hs?.background\n ? {\n shading: {\n fill: sanitizeXmlColor(hs.background),\n type: docx.ShadingType.SOLID,\n },\n }\n : {}),\n ...(cellBorders != null ? { borders: cellBorders } : {}),\n width: getColumnWidth(col.width as string | undefined) as any,\n }),\n ),\n })\n\n const dataRows = rows.map(\n (row, rowIdx) =>\n new docx.TableRow({\n children: columns.map(\n (col, colIdx) =>\n new docx.TableCell({\n children: [\n new docx.Paragraph({\n children: [new docx.TextRun({ text: String(row[colIdx] ?? '') })],\n alignment: alignmentMap(col.align) as any,\n }),\n ],\n ...(p.striped && rowIdx % 2 === 1\n ? { shading: { fill: 'F9F9F9', type: docx.ShadingType.SOLID } }\n : {}),\n ...(cellBorders != null ? { borders: cellBorders } : {}),\n width: getColumnWidth(col.width as string | undefined) as any,\n }),\n ),\n }),\n )\n\n if (p.caption) {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: p.caption as string,\n italics: true,\n size: 20,\n }),\n ],\n spacing: { after: 60 },\n }),\n )\n }\n\n children.push(\n new docx.Table({\n rows: [headerRow, ...dataRows],\n width: { size: 100, type: docx.WidthType.PERCENTAGE },\n }),\n )\n children.push(new docx.Paragraph({ text: '', spacing: { after: 120 } }))\n}\n\nfunction renderList(ctx: DocxCtx, n: DocNode): void {\n const { docx, children, processListItems, nextListId } = ctx\n const ordered = n.props.ordered as boolean | undefined\n const listRef = nextListId()\n processListItems(n, listRef, 0, ordered ?? false)\n children.push(new docx.Paragraph({ text: '', spacing: { after: 60 } }))\n}\n\nfunction renderButtonOrQuote(ctx: DocxCtx, n: DocNode): void {\n const { docx, children } = ctx\n const p = n.props\n const text = getTextContent(n.children)\n if (n.type === 'button') {\n children.push(\n new docx.Paragraph({\n children: [\n new docx.ExternalHyperlink({\n link: sanitizeHref(p.href as string),\n children: [\n new docx.TextRun({\n text,\n bold: true,\n color: '4F46E5',\n underline: { type: docx.UnderlineType.SINGLE },\n }),\n ],\n }),\n ],\n spacing: { after: 120 },\n }),\n )\n } else {\n children.push(\n new docx.Paragraph({\n children: [new docx.TextRun({ text, italics: true, color: '555555' })],\n indent: { left: 720 },\n border: {\n left: {\n style: docx.BorderStyle.SINGLE,\n size: 6,\n color: sanitizeXmlColor(p.borderColor as string, 'DDDDDD'),\n },\n },\n spacing: { after: 120 },\n }),\n )\n }\n}\n\nexport const docxRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {\n let docx: typeof import('docx')\n try {\n docx = await import('docx')\n } catch {\n throw new Error(\n '[@pyreon/document] DOCX renderer requires \"docx\" package. Install it: bun add docx',\n )\n }\n const children: unknown[] = []\n let listCounter = 0\n\n function alignmentMap(align?: string): unknown {\n if (!align) return undefined\n const map: Record<string, unknown> = {\n left: docx.AlignmentType.LEFT,\n center: docx.AlignmentType.CENTER,\n right: docx.AlignmentType.RIGHT,\n justify: docx.AlignmentType.JUSTIFIED,\n }\n return map[align]\n }\n\n function processListItems(n: DocNode, listRef: string, level: number, ordered: boolean): void {\n const items = n.children.filter((c): c is DocNode => typeof c !== 'string')\n for (const item of items) {\n const nestedList = item.children.find(\n (c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'list',\n )\n const textChildren = item.children.filter(\n (c) => typeof c === 'string' || (c as DocNode).type !== 'list',\n )\n children.push(\n new docx.Paragraph({\n children: [new docx.TextRun({ text: getTextContent(textChildren) })],\n ...(ordered ? { numbering: { reference: listRef, level } } : { bullet: { level } }),\n }),\n )\n if (nestedList) {\n const nestedOrdered = (nestedList as DocNode).props.ordered as boolean | undefined\n processListItems(nestedList as DocNode, listRef, level + 1, nestedOrdered ?? false)\n }\n }\n }\n\n const ctx: DocxCtx = {\n docx,\n children,\n alignmentMap,\n processListItems,\n nextListId: () => `list-${listCounter++}`,\n }\n\n function processNode(n: DocNode): void {\n switch (n.type) {\n case 'document':\n case 'page':\n case 'section':\n case 'row':\n case 'column':\n for (const child of n.children) {\n if (typeof child !== 'string') processNode(child)\n else children.push(new docx.Paragraph({ text: child }))\n }\n break\n case 'heading':\n renderHeading(ctx, n)\n break\n case 'text':\n renderTextNode(ctx, n)\n break\n case 'link':\n renderLink(ctx, n)\n break\n case 'image':\n renderImage(ctx, n)\n break\n case 'table':\n renderDocxTable(ctx, n)\n break\n case 'list':\n renderList(ctx, n)\n break\n case 'code':\n children.push(\n new docx.Paragraph({\n children: [\n new docx.TextRun({\n text: getTextContent(n.children),\n font: 'Courier New',\n size: 20,\n }),\n ],\n shading: { fill: 'F5F5F5', type: docx.ShadingType.SOLID },\n spacing: { after: 120 },\n }),\n )\n break\n case 'divider':\n children.push(\n new docx.Paragraph({\n border: {\n bottom: {\n style: docx.BorderStyle.SINGLE,\n size: (n.props.thickness as number | undefined) ?? 1,\n color: sanitizeXmlColor(n.props.color as string, 'DDDDDD'),\n },\n },\n spacing: { before: 120, after: 120 },\n }),\n )\n break\n case 'spacer':\n children.push(\n new docx.Paragraph({\n text: '',\n spacing: { after: (n.props.height as number) * 20 },\n }),\n )\n break\n case 'button':\n case 'quote':\n renderButtonOrQuote(ctx, n)\n break\n }\n }\n\n processNode(node)\n\n // Build numbering configs for all lists\n const numberingConfigs: unknown[] = []\n for (let i = 0; i < listCounter; i++) {\n numberingConfigs.push({\n reference: `list-${i}`,\n levels: Array.from({ length: 9 }, (_, level) => ({\n level,\n format: docx.LevelFormat.DECIMAL,\n text: `%${level + 1}.`,\n alignment: docx.AlignmentType.LEFT,\n style: {\n paragraph: { indent: { left: 720 * (level + 1), hanging: 360 } },\n },\n })),\n })\n }\n\n // Extract page properties from first page node\n const pageNode =\n node.type === 'document'\n ? (node.children.find(\n (c): c is DocNode => typeof c !== 'string' && (c as DocNode).type === 'page',\n ) as DocNode | undefined)\n : node.type === 'page'\n ? node\n : undefined\n\n const pageProps = pageNode?.props ?? {}\n const pageDims = getPageSize(\n pageProps.size as PageSize | undefined,\n pageProps.orientation as PageOrientation | undefined,\n )\n const pageMargins = getPageMargins(\n pageProps.margin as number | [number, number] | [number, number, number, number] | undefined,\n )\n\n function buildHeaderFooter(contentNode: DocNode | undefined): unknown[] | undefined {\n if (!contentNode) return undefined\n const text = getTextContent(contentNode.children)\n if (!text) return undefined\n return [\n new docx.Paragraph({\n children: [new docx.TextRun({ text, size: 18, color: '999999' })],\n alignment: docx.AlignmentType.CENTER,\n }),\n ]\n }\n\n const headerContent = buildHeaderFooter(pageProps.header as DocNode | undefined)\n const footerContent = buildHeaderFooter(pageProps.footer as DocNode | undefined)\n\n const sectionProperties: Record<string, unknown> = {}\n if (pageDims) {\n sectionProperties.page = { size: pageDims, margin: pageMargins }\n } else if (pageMargins) {\n sectionProperties.page = { margin: pageMargins }\n }\n\n // Document metadata — `node` is the root document DocNode and\n // its props were populated by extractDocumentTree from\n // DocDocument's `_documentProps` (which now supports both\n // plain strings and reactive accessors via PR #197 D1).\n //\n // The `docx` library writes these to the .docx file's\n // `docProps/core.xml` metadata sheet (the OOXML CoreProperties\n // schema), visible in Word's File > Properties > Summary tab,\n // LibreOffice's Properties dialog, etc. Verified end-to-end by\n // unzipping a generated .docx and inspecting the XML — see\n // `src/tests/integration.test.ts > \"DOCX core.xml contains\n // dc:title / dc:creator / dc:subject / cp:keywords\"`.\n //\n // The DocNode's `author` prop maps to DOCX's `creator` field\n // (the OOXML term for the original author). The `keywords`\n // prop is `string[]` on the DocNode but DOCX wants a single\n // comma-separated string, so we join.\n //\n // `description` is intentionally NOT passed even though the\n // docx library accepts it — `DocumentProps` doesn't expose a\n // `description` field, so wiring it here would be dead code.\n // If a future PR widens `DocumentProps` with `description`,\n // add the corresponding line here.\n const docTitle = node.props.title as string | undefined\n const docAuthor = node.props.author as string | undefined\n const docSubject = node.props.subject as string | undefined\n const docKeywords = node.props.keywords as string[] | undefined\n\n const doc = new docx.Document({\n ...(docTitle ? { title: docTitle } : {}),\n ...(docSubject ? { subject: docSubject } : {}),\n ...(docAuthor ? { creator: docAuthor } : {}),\n ...(docKeywords && docKeywords.length > 0 ? { keywords: docKeywords.join(', ') } : {}),\n numbering: (numberingConfigs.length > 0 ? { config: numberingConfigs } : undefined) as any,\n sections: [\n {\n properties: sectionProperties,\n ...(headerContent\n ? {\n headers: {\n default: new docx.Header({ children: headerContent as any }),\n },\n }\n : {}),\n ...(footerContent\n ? {\n footers: {\n default: new docx.Footer({ children: footerContent as any }),\n },\n }\n : {}),\n children: children as any,\n },\n ],\n })\n\n const buffer = await docx.Packer.toBuffer(doc)\n return new Uint8Array(buffer)\n },\n}\n"],"mappings":";;;;;;AAeA,SAAS,cAAc,KAAwC;AAC7D,QAAO,OAAO,QAAQ,WAAW,EAAE,QAAQ,KAAK,GAAG;;AAGrD,SAAS,eAAe,UAA8B;AACpD,QAAO,SACJ,KAAK,MAAO,OAAO,MAAM,WAAW,IAAI,eAAgB,EAAc,SAAS,CAAE,CACjF,KAAK,GAAG;;;AAIb,SAAS,aAAa,KAAoD;CACxE,MAAM,QAAQ,IAAI,MAAM,oCAAoC;AAC5D,KAAI,CAAC,MAAO,QAAO;AACnB,QAAO;EAAE,MAAM,MAAM;EAAK,MAAM,MAAM;EAAK;;;AAI7C,SAAS,YACP,MACA,aAC+C;AAC/C,KAAI,CAAC,KAAM,QAAO;CASlB,MAAM,OAR2D;EAC/D,IAAI;GAAE,OAAO;GAAO,QAAQ;GAAO;EACnC,IAAI;GAAE,OAAO;GAAO,QAAQ;GAAO;EACnC,IAAI;GAAE,OAAO;GAAM,QAAQ;GAAO;EAClC,QAAQ;GAAE,OAAO;GAAO,QAAQ;GAAO;EACvC,OAAO;GAAE,OAAO;GAAO,QAAQ;GAAO;EACtC,SAAS;GAAE,OAAO;GAAO,QAAQ;GAAO;EACzC,CACkB;AACnB,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,gBAAgB,YAClB,QAAO;EAAE,OAAO,KAAK;EAAQ,QAAQ,KAAK;EAAO;AAEnD,QAAO;;;AAIT,SAAS,eACP,QACoB;AACpB,KAAI,UAAU,KAAM,QAAO;AAC3B,KAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,QAAQ,SAAS;AACvB,SAAO;GAAE,KAAK;GAAO,OAAO;GAAO,QAAQ;GAAO,MAAM;GAAO;;AAEjE,KAAI,OAAO,WAAW,EACpB,QAAO;EACL,KAAK,OAAO,KAAK;EACjB,OAAO,OAAO,KAAK;EACnB,QAAQ,OAAO,KAAK;EACpB,MAAM,OAAO,KAAK;EACnB;AAEH,QAAO;EACL,KAAK,OAAO,KAAK;EACjB,OAAO,OAAO,KAAK;EACnB,QAAQ,OAAO,KAAK;EACpB,MAAM,OAAO,KAAK;EACnB;;;AAIH,SAAS,eAAe,OAAsE;AAC5F,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;CACtC,MAAM,QAAQ,MAAM,MAAM,WAAW;AACrC,KAAI,CAAC,MAAO,QAAO;AACnB,QAAO;EAAE,MAAM,OAAO,SAAS,MAAM,IAAK,GAAG,GAAG;EAAK,MAAM;EAAkB;;AAY/E,SAAS,cAAc,KAAc,GAAkB;CACrD,MAAM,EAAE,MAAM,UAAU,iBAAiB;CACzC,MAAM,IAAI,EAAE;CACZ,MAAM,QAAS,EAAE,SAAoB;CACrC,MAAM,aAAsC;EAC1C,GAAG,KAAK,aAAa;EACrB,GAAG,KAAK,aAAa;EACrB,GAAG,KAAK,aAAa;EACrB,GAAG,KAAK,aAAa;EACrB,GAAG,KAAK,aAAa;EACrB,GAAG,KAAK,aAAa;EACtB;AACD,UAAS,KACP,IAAI,KAAK,UAAU;EACjB,SAAU,WAAW,UAAU,KAAK,aAAa;EACjD,UAAU,CACR,IAAI,KAAK,QAAQ;GACf,MAAM,eAAe,EAAE,SAAS;GAChC,MAAM;GACN,OAAO,iBAAiB,EAAE,MAAgB;GAC3C,CAAC,CACH;EACD,WAAW,aAAa,EAAE,MAAgB;EAC3C,CAAC,CACH;;AAGH,SAAS,eAAe,KAAc,GAAkB;CACtD,MAAM,EAAE,MAAM,UAAU,iBAAiB;CACzC,MAAM,IAAI,EAAE;AACZ,UAAS,KACP,IAAI,KAAK,UAAU;EACjB,UAAU,CACR,IAAI,KAAK,QAAQ;GACf,MAAM,eAAe,EAAE,SAAS;GAChC,GAAI,EAAE,QAAQ,OAAO,EAAE,MAAM,EAAE,MAAiB,GAAG,EAAE;GACrD,GAAI,EAAE,UAAU,OAAO,EAAE,SAAS,EAAE,QAAmB,GAAG,EAAE;GAC5D,GAAI,EAAE,YAAY,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE;GACxC,GAAI,EAAE,iBAAiB,OAAO,EAAE,QAAQ,EAAE,eAA0B,GAAG,EAAE;GACzE,GAAI,EAAE,QAAQ,OAAO,EAAE,MAAO,EAAE,OAAkB,GAAG,GAAG,EAAE;GAC1D,OAAO,iBAAiB,EAAE,OAAiB,SAAS;GACrD,CAAC,CACH;EACD,WAAW,aAAa,EAAE,MAAgB;EAC1C,SAAS,EAAE,OAAO,KAAK;EACxB,CAAC,CACH;;AAGH,SAAS,WAAW,KAAc,GAAkB;CAClD,MAAM,EAAE,MAAM,aAAa;CAC3B,MAAM,IAAI,EAAE;AACZ,UAAS,KACP,IAAI,KAAK,UAAU,EACjB,UAAU,CACR,IAAI,KAAK,kBAAkB;EACzB,MAAM,aAAa,EAAE,KAAe;EACpC,UAAU,CACR,IAAI,KAAK,QAAQ;GACf,MAAM,eAAe,EAAE,SAAS;GAChC,OAAO,iBAAiB,EAAE,OAAiB,SAAS;GACpD,WAAW,EAAE,MAAM,KAAK,cAAc,QAAQ;GAC/C,CAAC,CACH;EACF,CAAC,CACH,EACF,CAAC,CACH;;AAGH,SAAS,YAAY,KAAc,GAAkB;CACnD,MAAM,EAAE,MAAM,UAAU,iBAAiB;CACzC,MAAM,IAAI,EAAE;CACZ,MAAM,MAAM,EAAE;CACd,MAAM,SAAS,aAAa,IAAI;AAEhC,KAAI,QAAQ;EACV,MAAM,WAAY,EAAE,SAAoB;EACxC,MAAM,YAAa,EAAE,UAAqB;AAC1C,WAAS,KACP,IAAI,KAAK,UAAU;GACjB,UAAU,CACR,IAAI,KAAK,SAAS;IAChB,MAAM,OAAO,KAAK,OAAO,MAAM,SAAS;IACxC,gBAAgB;KAAE,OAAO;KAAU,QAAQ;KAAW;IACtD,MAAM,OAAO,SAAS,cAAc,QAAQ;IAC7C,CAAC,CACH;GACD,WAAW,aAAa,EAAE,MAAgB;GAC3C,CAAC,CACH;AACD,MAAI,EAAE,QACJ,UAAS,KACP,IAAI,KAAK,UAAU;GACjB,UAAU,CACR,IAAI,KAAK,QAAQ;IACf,MAAM,EAAE;IACR,SAAS;IACT,MAAM;IACN,OAAO;IACR,CAAC,CACH;GACD,WAAW,aAAa,EAAE,MAAgB;GAC1C,SAAS,EAAE,OAAO,KAAK;GACxB,CAAC,CACH;QAEE;EACL,MAAM,MAAO,EAAE,OAAkB;EACjC,MAAM,UAAU,EAAE,UAAU,MAAM,EAAE,YAAY;AAChD,WAAS,KACP,IAAI,KAAK,UAAU,EACjB,UAAU,CACR,IAAI,KAAK,QAAQ;GACf,MAAM,IAAI,MAAM,QAAQ;GACxB,SAAS;GACT,OAAO;GACR,CAAC,CACH,EACF,CAAC,CACH;;;AAIL,SAAS,gBAAgB,KAAc,GAAkB;CACvD,MAAM,EAAE,MAAM,UAAU,iBAAiB;CACzC,MAAM,IAAI,EAAE;CACZ,MAAM,WAAY,EAAE,WAAW,EAAE,EAA+B,IAAI,cAAc;CAClF,MAAM,OAAQ,EAAE,QAAQ,EAAE;CAC1B,MAAM,KAAK,EAAE;CAEb,MAAM,cADW,EAAE,WAEf;EAAE,OAAO,KAAK,YAAY;EAAQ,MAAM;EAAG,OAAO;EAAU,GAC5D;CACJ,MAAM,cAAc,cAChB;EACE,KAAK;EACL,QAAQ;EACR,MAAM;EACN,OAAO;EACR,GACD;CAEJ,MAAM,YAAY,IAAI,KAAK,SAAS;EAClC,aAAa;EACb,UAAU,QAAQ,KACf,QACC,IAAI,KAAK,UAAU;GACjB,UAAU,CACR,IAAI,KAAK,UAAU;IACjB,UAAU,CACR,IAAI,KAAK,QAAQ;KACf,MAAM,IAAI;KACV,MAAM;KACN,OAAO,iBAAiB,IAAI,MAAM;KACnC,CAAC,CACH;IACD,WAAW,aAAa,IAAI,MAAM;IACnC,CAAC,CACH;GACD,GAAI,IAAI,aACJ,EACE,SAAS;IACP,MAAM,iBAAiB,GAAG,WAAW;IACrC,MAAM,KAAK,YAAY;IACxB,EACF,GACD,EAAE;GACN,GAAI,eAAe,OAAO,EAAE,SAAS,aAAa,GAAG,EAAE;GACvD,OAAO,eAAe,IAAI,MAA4B;GACvD,CAAC,CACL;EACF,CAAC;CAEF,MAAM,WAAW,KAAK,KACnB,KAAK,WACJ,IAAI,KAAK,SAAS,EAChB,UAAU,QAAQ,KACf,KAAK,WACJ,IAAI,KAAK,UAAU;EACjB,UAAU,CACR,IAAI,KAAK,UAAU;GACjB,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,MAAM,OAAO,IAAI,WAAW,GAAG,EAAE,CAAC,CAAC;GACjE,WAAW,aAAa,IAAI,MAAM;GACnC,CAAC,CACH;EACD,GAAI,EAAE,WAAW,SAAS,MAAM,IAC5B,EAAE,SAAS;GAAE,MAAM;GAAU,MAAM,KAAK,YAAY;GAAO,EAAE,GAC7D,EAAE;EACN,GAAI,eAAe,OAAO,EAAE,SAAS,aAAa,GAAG,EAAE;EACvD,OAAO,eAAe,IAAI,MAA4B;EACvD,CAAC,CACL,EACF,CAAC,CACL;AAED,KAAI,EAAE,QACJ,UAAS,KACP,IAAI,KAAK,UAAU;EACjB,UAAU,CACR,IAAI,KAAK,QAAQ;GACf,MAAM,EAAE;GACR,SAAS;GACT,MAAM;GACP,CAAC,CACH;EACD,SAAS,EAAE,OAAO,IAAI;EACvB,CAAC,CACH;AAGH,UAAS,KACP,IAAI,KAAK,MAAM;EACb,MAAM,CAAC,WAAW,GAAG,SAAS;EAC9B,OAAO;GAAE,MAAM;GAAK,MAAM,KAAK,UAAU;GAAY;EACtD,CAAC,CACH;AACD,UAAS,KAAK,IAAI,KAAK,UAAU;EAAE,MAAM;EAAI,SAAS,EAAE,OAAO,KAAK;EAAE,CAAC,CAAC;;AAG1E,SAAS,WAAW,KAAc,GAAkB;CAClD,MAAM,EAAE,MAAM,UAAU,kBAAkB,eAAe;CACzD,MAAM,UAAU,EAAE,MAAM;AAExB,kBAAiB,GADD,YAAY,EACC,GAAG,WAAW,MAAM;AACjD,UAAS,KAAK,IAAI,KAAK,UAAU;EAAE,MAAM;EAAI,SAAS,EAAE,OAAO,IAAI;EAAE,CAAC,CAAC;;AAGzE,SAAS,oBAAoB,KAAc,GAAkB;CAC3D,MAAM,EAAE,MAAM,aAAa;CAC3B,MAAM,IAAI,EAAE;CACZ,MAAM,OAAO,eAAe,EAAE,SAAS;AACvC,KAAI,EAAE,SAAS,SACb,UAAS,KACP,IAAI,KAAK,UAAU;EACjB,UAAU,CACR,IAAI,KAAK,kBAAkB;GACzB,MAAM,aAAa,EAAE,KAAe;GACpC,UAAU,CACR,IAAI,KAAK,QAAQ;IACf;IACA,MAAM;IACN,OAAO;IACP,WAAW,EAAE,MAAM,KAAK,cAAc,QAAQ;IAC/C,CAAC,CACH;GACF,CAAC,CACH;EACD,SAAS,EAAE,OAAO,KAAK;EACxB,CAAC,CACH;KAED,UAAS,KACP,IAAI,KAAK,UAAU;EACjB,UAAU,CAAC,IAAI,KAAK,QAAQ;GAAE;GAAM,SAAS;GAAM,OAAO;GAAU,CAAC,CAAC;EACtE,QAAQ,EAAE,MAAM,KAAK;EACrB,QAAQ,EACN,MAAM;GACJ,OAAO,KAAK,YAAY;GACxB,MAAM;GACN,OAAO,iBAAiB,EAAE,aAAuB,SAAS;GAC3D,EACF;EACD,SAAS,EAAE,OAAO,KAAK;EACxB,CAAC,CACH;;AAIL,MAAa,eAAiC,EAC5C,MAAM,OAAO,MAAe,UAA+C;CACzE,IAAI;AACJ,KAAI;AACF,SAAO,MAAM,OAAO;SACd;AACN,QAAM,IAAI,MACR,uFACD;;CAEH,MAAM,WAAsB,EAAE;CAC9B,IAAI,cAAc;CAElB,SAAS,aAAa,OAAyB;AAC7C,MAAI,CAAC,MAAO,QAAO;AAOnB,SANqC;GACnC,MAAM,KAAK,cAAc;GACzB,QAAQ,KAAK,cAAc;GAC3B,OAAO,KAAK,cAAc;GAC1B,SAAS,KAAK,cAAc;GAC7B,CACU;;CAGb,SAAS,iBAAiB,GAAY,SAAiB,OAAe,SAAwB;EAC5F,MAAM,QAAQ,EAAE,SAAS,QAAQ,MAAoB,OAAO,MAAM,SAAS;AAC3E,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,aAAa,KAAK,SAAS,MAC9B,MAAoB,OAAO,MAAM,YAAa,EAAc,SAAS,OACvE;GACD,MAAM,eAAe,KAAK,SAAS,QAChC,MAAM,OAAO,MAAM,YAAa,EAAc,SAAS,OACzD;AACD,YAAS,KACP,IAAI,KAAK,UAAU;IACjB,UAAU,CAAC,IAAI,KAAK,QAAQ,EAAE,MAAM,eAAe,aAAa,EAAE,CAAC,CAAC;IACpE,GAAI,UAAU,EAAE,WAAW;KAAE,WAAW;KAAS;KAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE;IACnF,CAAC,CACH;AACD,OAAI,YAAY;IACd,MAAM,gBAAiB,WAAuB,MAAM;AACpD,qBAAiB,YAAuB,SAAS,QAAQ,GAAG,iBAAiB,MAAM;;;;CAKzF,MAAM,MAAe;EACnB;EACA;EACA;EACA;EACA,kBAAkB,QAAQ;EAC3B;CAED,SAAS,YAAY,GAAkB;AACrC,UAAQ,EAAE,MAAV;GACE,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;AACH,SAAK,MAAM,SAAS,EAAE,SACpB,KAAI,OAAO,UAAU,SAAU,aAAY,MAAM;QAC5C,UAAS,KAAK,IAAI,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAEzD;GACF,KAAK;AACH,kBAAc,KAAK,EAAE;AACrB;GACF,KAAK;AACH,mBAAe,KAAK,EAAE;AACtB;GACF,KAAK;AACH,eAAW,KAAK,EAAE;AAClB;GACF,KAAK;AACH,gBAAY,KAAK,EAAE;AACnB;GACF,KAAK;AACH,oBAAgB,KAAK,EAAE;AACvB;GACF,KAAK;AACH,eAAW,KAAK,EAAE;AAClB;GACF,KAAK;AACH,aAAS,KACP,IAAI,KAAK,UAAU;KACjB,UAAU,CACR,IAAI,KAAK,QAAQ;MACf,MAAM,eAAe,EAAE,SAAS;MAChC,MAAM;MACN,MAAM;MACP,CAAC,CACH;KACD,SAAS;MAAE,MAAM;MAAU,MAAM,KAAK,YAAY;MAAO;KACzD,SAAS,EAAE,OAAO,KAAK;KACxB,CAAC,CACH;AACD;GACF,KAAK;AACH,aAAS,KACP,IAAI,KAAK,UAAU;KACjB,QAAQ,EACN,QAAQ;MACN,OAAO,KAAK,YAAY;MACxB,MAAO,EAAE,MAAM,aAAoC;MACnD,OAAO,iBAAiB,EAAE,MAAM,OAAiB,SAAS;MAC3D,EACF;KACD,SAAS;MAAE,QAAQ;MAAK,OAAO;MAAK;KACrC,CAAC,CACH;AACD;GACF,KAAK;AACH,aAAS,KACP,IAAI,KAAK,UAAU;KACjB,MAAM;KACN,SAAS,EAAE,OAAQ,EAAE,MAAM,SAAoB,IAAI;KACpD,CAAC,CACH;AACD;GACF,KAAK;GACL,KAAK;AACH,wBAAoB,KAAK,EAAE;AAC3B;;;AAIN,aAAY,KAAK;CAGjB,MAAM,mBAA8B,EAAE;AACtC,MAAK,IAAI,IAAI,GAAG,IAAI,aAAa,IAC/B,kBAAiB,KAAK;EACpB,WAAW,QAAQ;EACnB,QAAQ,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,GAAG,WAAW;GAC/C;GACA,QAAQ,KAAK,YAAY;GACzB,MAAM,IAAI,QAAQ,EAAE;GACpB,WAAW,KAAK,cAAc;GAC9B,OAAO,EACL,WAAW,EAAE,QAAQ;IAAE,MAAM,OAAO,QAAQ;IAAI,SAAS;IAAK,EAAE,EACjE;GACF,EAAE;EACJ,CAAC;CAaJ,MAAM,aARJ,KAAK,SAAS,aACT,KAAK,SAAS,MACZ,MAAoB,OAAO,MAAM,YAAa,EAAc,SAAS,OACvE,GACD,KAAK,SAAS,SACZ,OACA,SAEoB,SAAS,EAAE;CACvC,MAAM,WAAW,YACf,UAAU,MACV,UAAU,YACX;CACD,MAAM,cAAc,eAClB,UAAU,OACX;CAED,SAAS,kBAAkB,aAAyD;AAClF,MAAI,CAAC,YAAa,QAAO;EACzB,MAAM,OAAO,eAAe,YAAY,SAAS;AACjD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,CACL,IAAI,KAAK,UAAU;GACjB,UAAU,CAAC,IAAI,KAAK,QAAQ;IAAE;IAAM,MAAM;IAAI,OAAO;IAAU,CAAC,CAAC;GACjE,WAAW,KAAK,cAAc;GAC/B,CAAC,CACH;;CAGH,MAAM,gBAAgB,kBAAkB,UAAU,OAA8B;CAChF,MAAM,gBAAgB,kBAAkB,UAAU,OAA8B;CAEhF,MAAM,oBAA6C,EAAE;AACrD,KAAI,SACF,mBAAkB,OAAO;EAAE,MAAM;EAAU,QAAQ;EAAa;UACvD,YACT,mBAAkB,OAAO,EAAE,QAAQ,aAAa;CA0BlD,MAAM,WAAW,KAAK,MAAM;CAC5B,MAAM,YAAY,KAAK,MAAM;CAC7B,MAAM,aAAa,KAAK,MAAM;CAC9B,MAAM,cAAc,KAAK,MAAM;CAE/B,MAAM,MAAM,IAAI,KAAK,SAAS;EAC5B,GAAI,WAAW,EAAE,OAAO,UAAU,GAAG,EAAE;EACvC,GAAI,aAAa,EAAE,SAAS,YAAY,GAAG,EAAE;EAC7C,GAAI,YAAY,EAAE,SAAS,WAAW,GAAG,EAAE;EAC3C,GAAI,eAAe,YAAY,SAAS,IAAI,EAAE,UAAU,YAAY,KAAK,KAAK,EAAE,GAAG,EAAE;EACrF,WAAY,iBAAiB,SAAS,IAAI,EAAE,QAAQ,kBAAkB,GAAG;EACzE,UAAU,CACR;GACE,YAAY;GACZ,GAAI,gBACA,EACE,SAAS,EACP,SAAS,IAAI,KAAK,OAAO,EAAE,UAAU,eAAsB,CAAC,EAC7D,EACF,GACD,EAAE;GACN,GAAI,gBACA,EACE,SAAS,EACP,SAAS,IAAI,KAAK,OAAO,EAAE,UAAU,eAAsB,CAAC,EAC7D,EACF,GACD,EAAE;GACI;GACX,CACF;EACF,CAAC;CAEF,MAAM,SAAS,MAAM,KAAK,OAAO,SAAS,IAAI;AAC9C,QAAO,IAAI,WAAW,OAAO;GAEhC"}
|
|
@@ -31,7 +31,7 @@ function renderChildren(children) {
|
|
|
31
31
|
function renderNode(node) {
|
|
32
32
|
const p = node.props;
|
|
33
33
|
switch (node.type) {
|
|
34
|
-
case "document": return `<!DOCTYPE html><html lang="${p.language ?? "en"}"><head><meta charset="utf-8">${p.title ? `<title>${escapeHtml(p.title)}</title>` : ""}<meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderChildren(node.children)}</body></html>`;
|
|
34
|
+
case "document": return `<!DOCTYPE html><html lang="${p.language ?? "en"}"><head><meta charset="utf-8">${p.title ? `<title>${escapeHtml(p.title)}</title>` : ""}${p.author ? `<meta name="author" content="${escapeHtml(p.author)}">` : ""}${p.subject ? `<meta name="description" content="${escapeHtml(p.subject)}">` : ""}<meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderChildren(node.children)}</body></html>`;
|
|
35
35
|
case "page": {
|
|
36
36
|
const margin = padStr(p.margin);
|
|
37
37
|
return `<div${styleStr({
|
|
@@ -150,4 +150,4 @@ const htmlRenderer = { async render(node, options) {
|
|
|
150
150
|
|
|
151
151
|
//#endregion
|
|
152
152
|
export { htmlRenderer };
|
|
153
|
-
//# sourceMappingURL=html-
|
|
153
|
+
//# sourceMappingURL=html-DtsbNARB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-DtsbNARB.js","names":[],"sources":["../src/renderers/html.ts"],"sourcesContent":["import { sanitizeColor, sanitizeHref, sanitizeImageSrc, sanitizeStyle } from '../sanitize'\nimport type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction styleStr(styles: Record<string, string | number | undefined>): string {\n const parts: string[] = []\n for (const [k, v] of Object.entries(styles)) {\n if (v != null && v !== '') {\n const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase()\n parts.push(`${prop}:${typeof v === 'number' ? `${v}px` : v}`)\n }\n }\n return parts.length > 0 ? ` style=\"${parts.join(';')}\"` : ''\n}\n\nfunction padStr(\n pad: number | [number, number] | [number, number, number, number] | undefined,\n): string | undefined {\n if (pad == null) return undefined\n if (typeof pad === 'number') return `${pad}px`\n if (pad.length === 2) return `${pad[0]}px ${pad[1]}px`\n return `${pad[0]}px ${pad[1]}px ${pad[2]}px ${pad[3]}px`\n}\n\nfunction renderChild(child: DocChild): string {\n if (typeof child === 'string') return escapeHtml(child)\n return renderNode(child)\n}\n\nfunction renderChildren(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\nfunction renderNode(node: DocNode): string {\n const p = node.props\n\n switch (node.type) {\n case 'document': {\n // Document metadata is populated from DocDocument's\n // _documentProps via extractDocumentTree (PR #197 D1).\n // Title goes in <title>, author goes in\n // <meta name=\"author\">, subject goes in <meta name=\"description\">\n // (the closest semantic HTML equivalent — DOCX's \"subject\"\n // is conceptually the same as HTML's description meta).\n const lang = (p.language as string) ?? 'en'\n const title = p.title ? `<title>${escapeHtml(p.title as string)}</title>` : ''\n const author = p.author\n ? `<meta name=\"author\" content=\"${escapeHtml(p.author as string)}\">`\n : ''\n const description = p.subject\n ? `<meta name=\"description\" content=\"${escapeHtml(p.subject as string)}\">`\n : ''\n return `<!DOCTYPE html><html lang=\"${lang}\"><head><meta charset=\"utf-8\">${title}${author}${description}<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body>${renderChildren(node.children)}</body></html>`\n }\n\n case 'page': {\n const margin = padStr(p.margin as PageMargin)\n return `<div${styleStr({ maxWidth: '800px', margin: margin ?? '0 auto', padding: margin ?? '40px' })}>${renderChildren(node.children)}</div>`\n }\n\n case 'section': {\n const dir = (p.direction as string) ?? 'column'\n return `<div${styleStr({\n display: dir === 'row' ? 'flex' : 'block',\n flexDirection: dir === 'row' ? 'row' : undefined,\n gap: p.gap as number | undefined,\n padding: padStr(p.padding as PageMargin),\n background: sanitizeColor(p.background as string | undefined),\n borderRadius: p.borderRadius as number | undefined,\n })}>${renderChildren(node.children)}</div>`\n }\n\n case 'row':\n return `<div${styleStr({ display: 'flex', gap: p.gap as number | undefined, alignItems: p.align as string | undefined })}>${renderChildren(node.children)}</div>`\n\n case 'column':\n return `<div${styleStr({ flex: p.width ? undefined : '1', width: p.width as string | undefined, textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</div>`\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const tag = `h${Math.min(Math.max(level, 1), 6)}`\n return `<${tag}${styleStr({ color: sanitizeColor(p.color as string | undefined), textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</${tag}>`\n }\n\n case 'text': {\n return `<p${styleStr({\n fontSize: p.size as number | undefined,\n color: sanitizeColor(p.color as string | undefined),\n fontWeight: p.bold ? 'bold' : undefined,\n fontStyle: p.italic ? 'italic' : undefined,\n textDecoration: p.underline ? 'underline' : p.strikethrough ? 'line-through' : undefined,\n textAlign: p.align as string | undefined,\n lineHeight: p.lineHeight as number | undefined,\n })}>${renderChildren(node.children)}</p>`\n }\n\n case 'link':\n return `<a href=\"${escapeHtml(sanitizeHref(p.href as string))}\"${styleStr({ color: sanitizeColor(p.color as string | undefined) })}>${renderChildren(node.children)}</a>`\n\n case 'image': {\n const alignStyle =\n p.align === 'center'\n ? 'display:block;margin:0 auto'\n : p.align === 'right'\n ? 'display:block;margin-left:auto'\n : ''\n const img = `<img src=\"${escapeHtml(sanitizeImageSrc(p.src as string))}\"${p.width ? ` width=\"${p.width}\"` : ''}${p.height ? ` height=\"${p.height}\"` : ''}${p.alt ? ` alt=\"${escapeHtml(p.alt as string)}\"` : ''}${alignStyle ? ` style=\"${sanitizeStyle(alignStyle)}\"` : ''} />`\n if (p.caption) {\n return `<figure${p.align === 'center' ? ' style=\"text-align:center\"' : ''}>${img}<figcaption>${escapeHtml(p.caption as string)}</figcaption></figure>`\n }\n return img\n }\n\n case 'table': {\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n const hs = p.headerStyle as\n | { background?: string; color?: string; bold?: boolean }\n | undefined\n const striped = p.striped as boolean | undefined\n const bordered = p.bordered as boolean | undefined\n const borderStyle = bordered\n ? 'border:1px solid #ddd;border-collapse:collapse;'\n : 'border-collapse:collapse;'\n\n let html = `<table style=\"width:100%;${borderStyle}\">`\n if (p.caption) html += `<caption>${escapeHtml(p.caption as string)}</caption>`\n\n html += '<thead><tr>'\n for (const col of columns) {\n const cellBorder = bordered ? 'border:1px solid #ddd;' : ''\n const bgStyle = hs?.background ? `background:${sanitizeColor(hs.background)};` : ''\n const colorStyle = hs?.color ? `color:${sanitizeColor(hs.color)};` : ''\n const fontStyle = hs?.bold !== false ? 'font-weight:bold;' : ''\n const alignStyle = col.align ? `text-align:${col.align};` : ''\n const widthStyle = col.width\n ? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`\n : ''\n html += `<th style=\"${cellBorder}${bgStyle}${colorStyle}${fontStyle}${alignStyle}${widthStyle}padding:8px\">${escapeHtml(col.header)}</th>`\n }\n html += '</tr></thead>'\n\n html += '<tbody>'\n for (let i = 0; i < rows.length; i++) {\n const rowBg = striped && i % 2 === 1 ? ' style=\"background:#f9f9f9\"' : ''\n html += `<tr${rowBg}>`\n for (let j = 0; j < columns.length; j++) {\n const cellBorder = bordered ? 'border:1px solid #ddd;' : ''\n const col = columns[j]\n const alignStyle = col?.align ? `text-align:${col.align};` : ''\n html += `<td style=\"${cellBorder}${alignStyle}padding:8px\">${escapeHtml(String(rows[i]?.[j] ?? ''))}</td>`\n }\n html += '</tr>'\n }\n html += '</tbody></table>'\n return html\n }\n\n case 'list': {\n const tag = p.ordered ? 'ol' : 'ul'\n return `<${tag}>${renderChildren(node.children)}</${tag}>`\n }\n\n case 'list-item':\n return `<li>${renderChildren(node.children)}</li>`\n\n case 'code':\n return `<pre style=\"background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto\"><code>${escapeHtml(renderChildren(node.children))}</code></pre>`\n\n case 'divider': {\n const color = sanitizeColor((p.color as string) ?? '#ddd')\n const thickness = (p.thickness as number) ?? 1\n return `<hr style=\"border:none;border-top:${thickness}px solid ${color};margin:16px 0\" />`\n }\n\n case 'page-break':\n return '<div style=\"page-break-after:always;break-after:page\"></div>'\n\n case 'spacer':\n return `<div style=\"height:${p.height}px\"></div>`\n\n case 'button': {\n const bg = sanitizeColor((p.background as string) ?? '#4f46e5')\n const color = sanitizeColor((p.color as string) ?? '#fff')\n const radius = (p.borderRadius as number) ?? 4\n const pad = padStr((p.padding ?? [12, 24]) as [number, number])\n const align = (p.align as string) ?? 'left'\n return `<div style=\"text-align:${align}\"><a href=\"${escapeHtml(sanitizeHref(p.href as string))}\" style=\"display:inline-block;background:${bg};color:${color};padding:${pad};border-radius:${radius}px;text-decoration:none;font-weight:bold\">${renderChildren(node.children)}</a></div>`\n }\n\n case 'quote': {\n const borderColor = sanitizeColor((p.borderColor as string) ?? '#ddd')\n return `<blockquote style=\"margin:0;padding:12px 20px;border-left:4px solid ${borderColor};color:#555\">${renderChildren(node.children)}</blockquote>`\n }\n\n default:\n return renderChildren(node.children)\n }\n}\n\ntype PageMargin = number | [number, number] | [number, number, number, number]\n\nexport const htmlRenderer: DocumentRenderer = {\n async render(node: DocNode, options?: RenderOptions): Promise<string> {\n let html = renderNode(node)\n if (options?.direction === 'rtl') {\n html = html.replace('<body>', '<body dir=\"rtl\" style=\"direction:rtl\">')\n }\n return html\n },\n}\n"],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -24,13 +24,13 @@ function registerRenderer(format, renderer) {
|
|
|
24
24
|
function unregisterRenderer(format) {
|
|
25
25
|
renderers.delete(format);
|
|
26
26
|
}
|
|
27
|
-
registerRenderer("html", () => import("./html-
|
|
27
|
+
registerRenderer("html", () => import("./html-DtsbNARB.js").then((m) => m.htmlRenderer));
|
|
28
28
|
registerRenderer("email", () => import("./email-D0bbfWq4.js").then((m) => m.emailRenderer));
|
|
29
|
-
registerRenderer("md", () => import("./markdown-
|
|
29
|
+
registerRenderer("md", () => import("./markdown-BYkSLplL.js").then((m) => m.markdownRenderer));
|
|
30
30
|
registerRenderer("text", () => import("./text-l1XNXBOC.js").then((m) => m.textRenderer));
|
|
31
31
|
registerRenderer("csv", () => import("./csv-COrS4qdy.js").then((m) => m.csvRenderer));
|
|
32
32
|
registerRenderer("pdf", () => import("./pdf-IuBgTb3T.js").then((m) => m.pdfRenderer));
|
|
33
|
-
registerRenderer("docx", () => import("./docx-
|
|
33
|
+
registerRenderer("docx", () => import("./docx-CcB3jYd3.js").then((m) => m.docxRenderer));
|
|
34
34
|
registerRenderer("xlsx", () => import("./xlsx-Cvu4LBNy.js").then((m) => m.xlsxRenderer));
|
|
35
35
|
registerRenderer("pptx", () => import("./pptx-DXiMiYFM.js").then((m) => m.pptxRenderer));
|
|
36
36
|
registerRenderer("slack", () => import("./slack-BI3EQwYm.js").then((m) => m.slackRenderer));
|
|
@@ -72,13 +72,13 @@ async function render(node, format, options) {
|
|
|
72
72
|
/** @internal For testing — reset renderer registry to defaults. */
|
|
73
73
|
function _resetRenderers() {
|
|
74
74
|
renderers.clear();
|
|
75
|
-
registerRenderer("html", () => import("./html-
|
|
75
|
+
registerRenderer("html", () => import("./html-DtsbNARB.js").then((m) => m.htmlRenderer));
|
|
76
76
|
registerRenderer("email", () => import("./email-D0bbfWq4.js").then((m) => m.emailRenderer));
|
|
77
|
-
registerRenderer("md", () => import("./markdown-
|
|
77
|
+
registerRenderer("md", () => import("./markdown-BYkSLplL.js").then((m) => m.markdownRenderer));
|
|
78
78
|
registerRenderer("text", () => import("./text-l1XNXBOC.js").then((m) => m.textRenderer));
|
|
79
79
|
registerRenderer("csv", () => import("./csv-COrS4qdy.js").then((m) => m.csvRenderer));
|
|
80
80
|
registerRenderer("pdf", () => import("./pdf-IuBgTb3T.js").then((m) => m.pdfRenderer));
|
|
81
|
-
registerRenderer("docx", () => import("./docx-
|
|
81
|
+
registerRenderer("docx", () => import("./docx-CcB3jYd3.js").then((m) => m.docxRenderer));
|
|
82
82
|
registerRenderer("xlsx", () => import("./xlsx-Cvu4LBNy.js").then((m) => m.xlsxRenderer));
|
|
83
83
|
registerRenderer("pptx", () => import("./pptx-DXiMiYFM.js").then((m) => m.pptxRenderer));
|
|
84
84
|
registerRenderer("slack", () => import("./slack-BI3EQwYm.js").then((m) => m.slackRenderer));
|
|
@@ -14,10 +14,31 @@ function renderChildren(children) {
|
|
|
14
14
|
function renderInline(children) {
|
|
15
15
|
return children.map(renderChild).join("");
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Escape a string for safe inclusion in YAML frontmatter as a
|
|
19
|
+
* double-quoted scalar. Backslashes and double-quotes need
|
|
20
|
+
* escaping; other characters pass through.
|
|
21
|
+
*/
|
|
22
|
+
function yamlString(value) {
|
|
23
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
24
|
+
}
|
|
17
25
|
function renderNode(node) {
|
|
18
26
|
const p = node.props;
|
|
19
27
|
switch (node.type) {
|
|
20
|
-
case "document":
|
|
28
|
+
case "document": {
|
|
29
|
+
const title = p.title;
|
|
30
|
+
const author = p.author;
|
|
31
|
+
const subject = p.subject;
|
|
32
|
+
if (title || author || subject) {
|
|
33
|
+
const lines = ["---"];
|
|
34
|
+
if (title) lines.push(`title: ${yamlString(title)}`);
|
|
35
|
+
if (author) lines.push(`author: ${yamlString(author)}`);
|
|
36
|
+
if (subject) lines.push(`description: ${yamlString(subject)}`);
|
|
37
|
+
lines.push("---", "");
|
|
38
|
+
return lines.join("\n") + renderChildren(node.children);
|
|
39
|
+
}
|
|
40
|
+
return renderChildren(node.children);
|
|
41
|
+
}
|
|
21
42
|
case "page": return renderChildren(node.children);
|
|
22
43
|
case "section": return `${renderChildren(node.children)}\n`;
|
|
23
44
|
case "row":
|
|
@@ -74,4 +95,4 @@ const markdownRenderer = { async render(node, _options) {
|
|
|
74
95
|
|
|
75
96
|
//#endregion
|
|
76
97
|
export { markdownRenderer };
|
|
77
|
-
//# sourceMappingURL=markdown-
|
|
98
|
+
//# sourceMappingURL=markdown-BYkSLplL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-BYkSLplL.js","names":[],"sources":["../src/renderers/markdown.ts"],"sourcesContent":["import { sanitizeHref, sanitizeImageSrc } from '../sanitize'\nimport type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction renderChild(child: DocChild): string {\n if (typeof child === 'string') return child\n return renderNode(child)\n}\n\nfunction renderChildren(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\nfunction renderInline(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\n/**\n * Escape a string for safe inclusion in YAML frontmatter as a\n * double-quoted scalar. Backslashes and double-quotes need\n * escaping; other characters pass through.\n */\nfunction yamlString(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`\n}\n\nfunction renderNode(node: DocNode): string {\n const p = node.props\n\n switch (node.type) {\n case 'document': {\n // Document metadata is populated from DocDocument's\n // _documentProps via extractDocumentTree (PR #197 D1).\n // Markdown has no native metadata format, but YAML\n // frontmatter is the convention used by Jekyll, Hugo,\n // Astro, MDX, Pandoc, etc. — emit it when ANY metadata\n // field is present, omit the frontmatter block entirely\n // when none are present (so plain documents stay clean).\n const title = p.title as string | undefined\n const author = p.author as string | undefined\n const subject = p.subject as string | undefined\n\n if (title || author || subject) {\n const lines: string[] = ['---']\n if (title) lines.push(`title: ${yamlString(title)}`)\n if (author) lines.push(`author: ${yamlString(author)}`)\n if (subject) lines.push(`description: ${yamlString(subject)}`)\n lines.push('---', '')\n return lines.join('\\n') + renderChildren(node.children)\n }\n\n return renderChildren(node.children)\n }\n\n case 'page':\n return renderChildren(node.children)\n\n case 'section':\n return `${renderChildren(node.children)}\\n`\n\n case 'row':\n case 'column':\n return renderChildren(node.children)\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const prefix = '#'.repeat(Math.min(Math.max(level, 1), 6))\n return `${prefix} ${renderInline(node.children)}\\n\\n`\n }\n\n case 'text': {\n let text = renderInline(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 return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})`\n\n case 'image': {\n const alt = (p.alt as string) ?? ''\n let md = `})`\n if (p.caption) md += `\\n*${p.caption}*`\n return `${md}\\n\\n`\n }\n\n case 'table': {\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n\n if (columns.length === 0) return ''\n\n // Header\n const header = `| ${columns.map((c) => c.header).join(' | ')} |`\n\n // Separator with alignment\n const separator = `| ${columns\n .map((c) => {\n const align = c.align ?? 'left'\n if (align === 'center') return ':---:'\n if (align === 'right') return '---:'\n return '---'\n })\n .join(' | ')} |`\n\n // Rows\n const body = rows\n .map((row) => `| ${row.map((cell) => String(cell ?? '')).join(' | ')} |`)\n .join('\\n')\n\n let md = `${header}\\n${separator}\\n${body}\\n\\n`\n if (p.caption) md = `*${p.caption}*\\n\\n${md}`\n return md\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} ${renderInline(item.children)}`\n })\n .join('\\n')}\\n\\n`\n }\n\n case 'list-item':\n return renderInline(node.children)\n\n case 'code': {\n const lang = (p.language as string) ?? ''\n const content = renderInline(node.children)\n return `\\`\\`\\`${lang}\\n${content}\\n\\`\\`\\`\\n\\n`\n }\n\n case 'divider':\n return '---\\n\\n'\n\n case 'page-break':\n return '---\\n\\n'\n\n case 'spacer':\n return '\\n'\n\n case 'button':\n return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})\\n\\n`\n\n case 'quote':\n return `> ${renderInline(node.children)}\\n\\n`\n\n default:\n return renderChildren(node.children)\n }\n}\n\nexport const markdownRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n return `${renderNode(node).trim()}\\n`\n },\n}\n"],"mappings":";;;AAGA,SAAS,cAAc,KAAwC;AAC7D,QAAO,OAAO,QAAQ,WAAW,EAAE,QAAQ,KAAK,GAAG;;AAGrD,SAAS,YAAY,OAAyB;AAC5C,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAO,WAAW,MAAM;;AAG1B,SAAS,eAAe,UAA8B;AACpD,QAAO,SAAS,IAAI,YAAY,CAAC,KAAK,GAAG;;AAG3C,SAAS,aAAa,UAA8B;AAClD,QAAO,SAAS,IAAI,YAAY,CAAC,KAAK,GAAG;;;;;;;AAQ3C,SAAS,WAAW,OAAuB;AACzC,QAAO,IAAI,MAAM,QAAQ,OAAO,OAAO,CAAC,QAAQ,MAAM,OAAM,CAAC;;AAG/D,SAAS,WAAW,MAAuB;CACzC,MAAM,IAAI,KAAK;AAEf,SAAQ,KAAK,MAAb;EACE,KAAK,YAAY;GAQf,MAAM,QAAQ,EAAE;GAChB,MAAM,SAAS,EAAE;GACjB,MAAM,UAAU,EAAE;AAElB,OAAI,SAAS,UAAU,SAAS;IAC9B,MAAM,QAAkB,CAAC,MAAM;AAC/B,QAAI,MAAO,OAAM,KAAK,UAAU,WAAW,MAAM,GAAG;AACpD,QAAI,OAAQ,OAAM,KAAK,WAAW,WAAW,OAAO,GAAG;AACvD,QAAI,QAAS,OAAM,KAAK,gBAAgB,WAAW,QAAQ,GAAG;AAC9D,UAAM,KAAK,OAAO,GAAG;AACrB,WAAO,MAAM,KAAK,KAAK,GAAG,eAAe,KAAK,SAAS;;AAGzD,UAAO,eAAe,KAAK,SAAS;;EAGtC,KAAK,OACH,QAAO,eAAe,KAAK,SAAS;EAEtC,KAAK,UACH,QAAO,GAAG,eAAe,KAAK,SAAS,CAAC;EAE1C,KAAK;EACL,KAAK,SACH,QAAO,eAAe,KAAK,SAAS;EAEtC,KAAK,WAAW;GACd,MAAM,QAAS,EAAE,SAAoB;AAErC,UAAO,GADQ,IAAI,OAAO,KAAK,IAAI,KAAK,IAAI,OAAO,EAAE,EAAE,EAAE,CAAC,CACzC,GAAG,aAAa,KAAK,SAAS,CAAC;;EAGlD,KAAK,QAAQ;GACX,IAAI,OAAO,aAAa,KAAK,SAAS;AACtC,OAAI,EAAE,KAAM,QAAO,KAAK,KAAK;AAC7B,OAAI,EAAE,OAAQ,QAAO,IAAI,KAAK;AAC9B,OAAI,EAAE,cAAe,QAAO,KAAK,KAAK;AACtC,UAAO,GAAG,KAAK;;EAGjB,KAAK,OACH,QAAO,IAAI,aAAa,KAAK,SAAS,CAAC,IAAI,aAAa,EAAE,KAAe,CAAC;EAE5E,KAAK,SAAS;GAEZ,IAAI,KAAK,KADI,EAAE,OAAkB,GACf,IAAI,iBAAiB,EAAE,IAAc,CAAC;AACxD,OAAI,EAAE,QAAS,OAAM,MAAM,EAAE,QAAQ;AACrC,UAAO,GAAG,GAAG;;EAGf,KAAK,SAAS;GACZ,MAAM,WAAY,EAAE,WAAW,EAAE,EAA+B,IAAI,cAAc;GAClF,MAAM,OAAQ,EAAE,QAAQ,EAAE;AAE1B,OAAI,QAAQ,WAAW,EAAG,QAAO;GAoBjC,IAAI,KAAK,GAjBM,KAAK,QAAQ,KAAK,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,IAiB1C,IAdD,KAAK,QACpB,KAAK,MAAM;IACV,MAAM,QAAQ,EAAE,SAAS;AACzB,QAAI,UAAU,SAAU,QAAO;AAC/B,QAAI,UAAU,QAAS,QAAO;AAC9B,WAAO;KACP,CACD,KAAK,MAAM,CAAC,IAOkB,IAJpB,KACV,KAAK,QAAQ,KAAK,IAAI,KAAK,SAAS,OAAO,QAAQ,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CACxE,KAAK,KAAK,CAE6B;AAC1C,OAAI,EAAE,QAAS,MAAK,IAAI,EAAE,QAAQ,OAAO;AACzC,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,aAAa,KAAK,SAAS;KAC/C,CACD,KAAK,KAAK,CAAC;;EAGhB,KAAK,YACH,QAAO,aAAa,KAAK,SAAS;EAEpC,KAAK,OAGH,QAAO,SAFO,EAAE,YAAuB,GAElB,IADL,aAAa,KAAK,SAAS,CACV;EAGnC,KAAK,UACH,QAAO;EAET,KAAK,aACH,QAAO;EAET,KAAK,SACH,QAAO;EAET,KAAK,SACH,QAAO,IAAI,aAAa,KAAK,SAAS,CAAC,IAAI,aAAa,EAAE,KAAe,CAAC;EAE5E,KAAK,QACH,QAAO,KAAK,aAAa,KAAK,SAAS,CAAC;EAE1C,QACE,QAAO,eAAe,KAAK,SAAS;;;AAI1C,MAAa,mBAAqC,EAChD,MAAM,OAAO,MAAe,UAA2C;AACrE,QAAO,GAAG,WAAW,KAAK,CAAC,MAAM,CAAC;GAErC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/document",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.12",
|
|
4
4
|
"description": "Universal document rendering for Pyreon — one template, every output format (HTML, PDF, DOCX, email, XLSX, Markdown, and more)",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/document#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@happy-dom/global-registrator": "^20.8.9",
|
|
45
|
-
"@pyreon/core": "^0.12.
|
|
46
|
-
"@pyreon/reactivity": "^0.12.
|
|
45
|
+
"@pyreon/core": "^0.12.12",
|
|
46
|
+
"@pyreon/reactivity": "^0.12.12",
|
|
47
47
|
"@types/pdfmake": "^0.3.2",
|
|
48
48
|
"@vitus-labs/tools-lint": "^1.15.5",
|
|
49
49
|
"docx": "^9.6.0",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"pptxgenjs": "^4.0.1"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@pyreon/core": "^0.12.
|
|
56
|
-
"@pyreon/reactivity": "^0.12.
|
|
55
|
+
"@pyreon/core": "^0.12.12",
|
|
56
|
+
"@pyreon/reactivity": "^0.12.12"
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"docx": "^9.6.0",
|
package/src/renderers/docx.ts
CHANGED
|
@@ -552,7 +552,39 @@ export const docxRenderer: DocumentRenderer = {
|
|
|
552
552
|
sectionProperties.page = { margin: pageMargins }
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
// Document metadata — `node` is the root document DocNode and
|
|
556
|
+
// its props were populated by extractDocumentTree from
|
|
557
|
+
// DocDocument's `_documentProps` (which now supports both
|
|
558
|
+
// plain strings and reactive accessors via PR #197 D1).
|
|
559
|
+
//
|
|
560
|
+
// The `docx` library writes these to the .docx file's
|
|
561
|
+
// `docProps/core.xml` metadata sheet (the OOXML CoreProperties
|
|
562
|
+
// schema), visible in Word's File > Properties > Summary tab,
|
|
563
|
+
// LibreOffice's Properties dialog, etc. Verified end-to-end by
|
|
564
|
+
// unzipping a generated .docx and inspecting the XML — see
|
|
565
|
+
// `src/tests/integration.test.ts > "DOCX core.xml contains
|
|
566
|
+
// dc:title / dc:creator / dc:subject / cp:keywords"`.
|
|
567
|
+
//
|
|
568
|
+
// The DocNode's `author` prop maps to DOCX's `creator` field
|
|
569
|
+
// (the OOXML term for the original author). The `keywords`
|
|
570
|
+
// prop is `string[]` on the DocNode but DOCX wants a single
|
|
571
|
+
// comma-separated string, so we join.
|
|
572
|
+
//
|
|
573
|
+
// `description` is intentionally NOT passed even though the
|
|
574
|
+
// docx library accepts it — `DocumentProps` doesn't expose a
|
|
575
|
+
// `description` field, so wiring it here would be dead code.
|
|
576
|
+
// If a future PR widens `DocumentProps` with `description`,
|
|
577
|
+
// add the corresponding line here.
|
|
578
|
+
const docTitle = node.props.title as string | undefined
|
|
579
|
+
const docAuthor = node.props.author as string | undefined
|
|
580
|
+
const docSubject = node.props.subject as string | undefined
|
|
581
|
+
const docKeywords = node.props.keywords as string[] | undefined
|
|
582
|
+
|
|
555
583
|
const doc = new docx.Document({
|
|
584
|
+
...(docTitle ? { title: docTitle } : {}),
|
|
585
|
+
...(docSubject ? { subject: docSubject } : {}),
|
|
586
|
+
...(docAuthor ? { creator: docAuthor } : {}),
|
|
587
|
+
...(docKeywords && docKeywords.length > 0 ? { keywords: docKeywords.join(', ') } : {}),
|
|
556
588
|
numbering: (numberingConfigs.length > 0 ? { config: numberingConfigs } : undefined) as any,
|
|
557
589
|
sections: [
|
|
558
590
|
{
|
package/src/renderers/html.ts
CHANGED
|
@@ -47,9 +47,21 @@ function renderNode(node: DocNode): string {
|
|
|
47
47
|
|
|
48
48
|
switch (node.type) {
|
|
49
49
|
case 'document': {
|
|
50
|
+
// Document metadata is populated from DocDocument's
|
|
51
|
+
// _documentProps via extractDocumentTree (PR #197 D1).
|
|
52
|
+
// Title goes in <title>, author goes in
|
|
53
|
+
// <meta name="author">, subject goes in <meta name="description">
|
|
54
|
+
// (the closest semantic HTML equivalent — DOCX's "subject"
|
|
55
|
+
// is conceptually the same as HTML's description meta).
|
|
50
56
|
const lang = (p.language as string) ?? 'en'
|
|
51
57
|
const title = p.title ? `<title>${escapeHtml(p.title as string)}</title>` : ''
|
|
52
|
-
|
|
58
|
+
const author = p.author
|
|
59
|
+
? `<meta name="author" content="${escapeHtml(p.author as string)}">`
|
|
60
|
+
: ''
|
|
61
|
+
const description = p.subject
|
|
62
|
+
? `<meta name="description" content="${escapeHtml(p.subject as string)}">`
|
|
63
|
+
: ''
|
|
64
|
+
return `<!DOCTYPE html><html lang="${lang}"><head><meta charset="utf-8">${title}${author}${description}<meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${renderChildren(node.children)}</body></html>`
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
case 'page': {
|
|
@@ -18,12 +18,42 @@ function renderInline(children: DocChild[]): string {
|
|
|
18
18
|
return children.map(renderChild).join('')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Escape a string for safe inclusion in YAML frontmatter as a
|
|
23
|
+
* double-quoted scalar. Backslashes and double-quotes need
|
|
24
|
+
* escaping; other characters pass through.
|
|
25
|
+
*/
|
|
26
|
+
function yamlString(value: string): string {
|
|
27
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function renderNode(node: DocNode): string {
|
|
22
31
|
const p = node.props
|
|
23
32
|
|
|
24
33
|
switch (node.type) {
|
|
25
|
-
case 'document':
|
|
34
|
+
case 'document': {
|
|
35
|
+
// Document metadata is populated from DocDocument's
|
|
36
|
+
// _documentProps via extractDocumentTree (PR #197 D1).
|
|
37
|
+
// Markdown has no native metadata format, but YAML
|
|
38
|
+
// frontmatter is the convention used by Jekyll, Hugo,
|
|
39
|
+
// Astro, MDX, Pandoc, etc. — emit it when ANY metadata
|
|
40
|
+
// field is present, omit the frontmatter block entirely
|
|
41
|
+
// when none are present (so plain documents stay clean).
|
|
42
|
+
const title = p.title as string | undefined
|
|
43
|
+
const author = p.author as string | undefined
|
|
44
|
+
const subject = p.subject as string | undefined
|
|
45
|
+
|
|
46
|
+
if (title || author || subject) {
|
|
47
|
+
const lines: string[] = ['---']
|
|
48
|
+
if (title) lines.push(`title: ${yamlString(title)}`)
|
|
49
|
+
if (author) lines.push(`author: ${yamlString(author)}`)
|
|
50
|
+
if (subject) lines.push(`description: ${yamlString(subject)}`)
|
|
51
|
+
lines.push('---', '')
|
|
52
|
+
return lines.join('\n') + renderChildren(node.children)
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
return renderChildren(node.children)
|
|
56
|
+
}
|
|
27
57
|
|
|
28
58
|
case 'page':
|
|
29
59
|
return renderChildren(node.children)
|