diff --git a/packages/static-renderer/package.json b/packages/static-renderer/package.json index d3978a7a89..16491e5eb8 100644 --- a/packages/static-renderer/package.json +++ b/packages/static-renderer/package.json @@ -87,7 +87,8 @@ "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "docx": "^9.1.1" }, "repository": { "type": "git", diff --git a/packages/static-renderer/src/json/docx/docx.example.ts b/packages/static-renderer/src/json/docx/docx.example.ts new file mode 100644 index 0000000000..ac1b642f95 --- /dev/null +++ b/packages/static-renderer/src/json/docx/docx.example.ts @@ -0,0 +1,121 @@ +import { Document, FileChild, Packer, Paragraph, Table, TableCell, TableRow, TextRun, XmlComponent } from 'docx' + +import { renderDocxChildrenToDocxElement, renderJSONContentToDocxElement } from './docx.js' + +const table = new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph('Hello')], + }), + new TableCell({ + children: [], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [], + }), + new TableCell({ + children: [new Paragraph('World')], + }), + ], + }), + ], +}) + +const pmDoc = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello World, This should be in BOLD ', + marks: [ + { + type: 'bold', + }, + ], + }, + { + type: 'mention', + attrs: { + id: 'A mention? How random', + }, + }, + { + type: 'text', + text: " Italic & Bold, aren't we fancy", + marks: [ + { + type: 'italic', + }, + { + type: 'bold', + }, + ], + }, + ], + }, + ], + attrs: {}, +} + +const render = renderJSONContentToDocxElement({ + nodeMapping: { + doc({ children }) { + return new Document({ + sections: [ + { + children: children as FileChild[], + }, + ], + }) as unknown as XmlComponent + }, + paragraph({ node, renderInlineContent }) { + return new Paragraph({ + children: renderInlineContent({ content: node.content }), + }) + }, + mention({ node }) { + return new TextRun({ + text: node.attrs.id, + }) + }, + text({ node }) { + console.log('text', node) + return (node as any).text + }, + }, + mergeMapping: { + bold() { + return { type: 'run', properties: { bold: true } } + }, + italic() { + return { type: 'run', properties: { italics: true } } + }, + underline() { + return { type: 'run', properties: { underline: { type: 'single' } } } + }, + highlight() { + return { type: 'run', properties: { highlight: 'yellow' } } + }, + strikethrough() { + return { type: 'run', properties: { strike: true } } + }, + }, + markMapping: {}, +}) + +const doc = render({ content: pmDoc }) + +// console.log(doc) + +const file = Bun.file('./example.docx') + +Bun.write(file, await Packer.toBuffer(doc)) diff --git a/packages/static-renderer/src/json/docx/docx.ts b/packages/static-renderer/src/json/docx/docx.ts new file mode 100644 index 0000000000..1291b25b87 --- /dev/null +++ b/packages/static-renderer/src/json/docx/docx.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import type { MarkType, NodeType } from '@tiptap/core' +import { FileChild, IRunPropertiesOptions, TextRun, XmlComponent } from 'docx' + +import { MarkProps, NodeProps, TiptapStaticRenderer, TiptapStaticRendererOptions } from '../renderer.js' + +type MergeTypes = { type: 'run'; properties: IRunPropertiesOptions } + +export type TDocxElement = XmlComponent | FileChild + +export function renderJSONContentToDocxElement< + /** + * A mark type is either a JSON representation of a mark or a Prosemirror mark instance + */ + TMarkType extends { type: any } = MarkType, + /** + * A node type is either a JSON representation of a node or a Prosemirror node instance + */ + TNodeType extends { + content?: { forEach: (cb: (node: TNodeType) => void) => void } + marks?: readonly TMarkType[] + type: string | { name: string } + } = NodeType, +>( + options: TiptapStaticRendererOptions< + TDocxElement, + TMarkType, + TNodeType, + // @ts-ignore I can't get the types to work here, but we want to add some properties to node renderers + ( + ctx: NodeProps & { + renderInlineContent: (ctx: { content: undefined | NodeType | NodeType[] }) => TDocxElement[] + }, + ) => TDocxElement + > & { + mergeMapping: Record) => MergeTypes> + }, +) { + options.mergeMapping = options.mergeMapping || {} + + return TiptapStaticRenderer( + ctx => { + return ctx.component({ + renderInlineContent({ content }: { content: undefined | NodeType | NodeType[] }): TDocxElement[] { + if (!content || !Array.isArray(content) || content.length === 0) { + return [] + } + return ([] as NodeType[]).concat(content).map(node => { + if (node.type === 'text' && !node.marks?.length) { + return new TextRun((node as any).text) + } + if (node.type === 'text') { + const runOpts = (node.marks || []).reduce((acc, mark) => { + const merge = options.mergeMapping[typeof mark.type === 'string' ? mark.type : mark.type.name] + if (merge) { + const addOptions = merge({ mark, node, parent: ctx.props.node } as any) + if (addOptions.type === 'run') { + return { ...acc, ...addOptions.properties } + } + throw new Error(`Only type: 'run' is supported for mergeMapping`) + } + return acc + }, {} as IRunPropertiesOptions) + + return new TextRun({ + text: (node as any).text, + ...runOpts, + }) + } + return (ctx.props as NodeProps).renderElement({ content: node, parent: ctx.props.node }) + }) + }, + ...ctx.props, + } as any) + }, + { + ...options, + markMapping: { + ...options.markMapping, + ...Object.keys(options.mergeMapping).reduce( + (acc, key) => ({ + ...acc, + // Just return the children to passthrough + [key]: ({ children }: any) => children, + }), + {}, + ), + }, + } as any, + ) +} + +export function renderDocxChildrenToDocxElement( + children: undefined | TDocxElement | TDocxElement[], +): (FileChild | XmlComponent)[] { + if (!children) { + return [] + } + return ([] as (FileChild | XmlComponent)[]).concat(children) +} diff --git a/packages/static-renderer/src/json/docx/index.ts b/packages/static-renderer/src/json/docx/index.ts new file mode 100644 index 0000000000..a2d214722e --- /dev/null +++ b/packages/static-renderer/src/json/docx/index.ts @@ -0,0 +1 @@ +export * from './docx.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e98093cb5..c5140bc27d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -807,6 +807,9 @@ importers: '@types/react-dom': specifier: ^18.2.6 version: 18.3.5(@types/react@18.3.18) + docx: + specifier: ^9.1.1 + version: 9.1.1 react: specifier: ^17.0.0 || ^18.0.0 || ^19.0.0 version: 18.3.1 @@ -3396,6 +3399,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + docx@9.1.1: + resolution: {integrity: sha512-jz941pdz4+gMljZ1pV+95FwuWEouKi4u1Elhv3ptqeytGOSyX+u131hRYg4wgqLU+x2CbGsz9eTYgo2uMMz65Q==} + engines: {node: '>=10'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -3983,6 +3990,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -4063,6 +4073,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immutable@5.0.3: resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} @@ -4279,6 +4292,9 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -4382,6 +4398,9 @@ packages: resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} engines: {'0': node >=0.6.0} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4405,6 +4424,9 @@ packages: engines: {node: '>=16'} hasBin: true + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4612,6 +4634,9 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4654,6 +4679,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.0.9: + resolution: {integrity: sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -4814,6 +4844,9 @@ packages: package-manager-detector@0.2.8: resolution: {integrity: sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5001,6 +5034,9 @@ packages: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -5133,6 +5169,9 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -5290,6 +5329,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -5324,6 +5366,9 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5449,6 +5494,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -6074,10 +6122,17 @@ packages: utf-8-validate: optional: true + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml@1.0.1: + resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} + y-prosemirror@1.2.15: resolution: {integrity: sha512-XDdrytq2M5bIy3qusQvfRclLu2eWZYPA+BbGWAb9FFWEhOB5FCrnzez2vsA+gvAd0FJTAcr89mjJ5g45r0j7TQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -9029,6 +9084,16 @@ snapshots: dependencies: esutils: 2.0.3 + docx@9.1.1: + dependencies: + '@types/node': 22.10.3 + hash.js: 1.1.7 + jszip: 3.10.1 + nanoid: 5.0.9 + xml: 1.0.1 + xml-js: 1.6.11 + optional: true + dom-accessibility-api@0.5.16: {} dom-serializer@1.4.1: @@ -9787,6 +9852,12 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + optional: true + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -9885,6 +9956,9 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: + optional: true + immutable@5.0.3: {} import-fresh@3.3.0: @@ -10088,6 +10162,9 @@ snapshots: is-windows@1.0.2: {} + isarray@1.0.0: + optional: true + isarray@2.0.5: {} isbinaryfile@5.0.4: {} @@ -10172,6 +10249,14 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + optional: true + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10191,6 +10276,11 @@ snapshots: dependencies: isomorphic.js: 0.2.5 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + optional: true + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -10399,6 +10489,9 @@ snapshots: mimic-function@5.0.1: {} + minimalistic-assert@1.0.1: + optional: true + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -10438,6 +10531,9 @@ snapshots: nanoid@3.3.8: {} + nanoid@5.0.9: + optional: true + natural-compare@1.4.0: {} neo-async@2.6.2: {} @@ -10605,6 +10701,9 @@ snapshots: package-manager-detector@0.2.8: {} + pako@1.0.11: + optional: true + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -10754,6 +10853,9 @@ snapshots: prismjs@1.29.0: {} + process-nextick-args@2.0.1: + optional: true + property-information@6.5.0: {} prosemirror-changeset@2.2.1: @@ -10933,6 +11035,17 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + optional: true + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -11120,6 +11233,9 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.0 + sax@1.4.1: + optional: true + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -11163,6 +11279,9 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + setimmediate@1.0.5: + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11331,6 +11450,11 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + optional: true + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -11985,8 +12109,16 @@ snapshots: ws@8.18.0: {} + xml-js@1.6.11: + dependencies: + sax: 1.4.1 + optional: true + xml-name-validator@4.0.0: {} + xml@1.0.1: + optional: true + y-prosemirror@1.2.15(prosemirror-model@1.24.1)(prosemirror-state@1.4.3)(prosemirror-view@1.37.1)(y-protocols@1.0.6(yjs@13.6.21))(yjs@13.6.21): dependencies: lib0: 0.2.99