Skip to content

Commit

Permalink
feat(docx): add export to docx support using the static renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
nperez0111 committed Jan 17, 2025
1 parent 4c995e4 commit ccf6ce3
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/static-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
121 changes: 121 additions & 0 deletions packages/static-renderer/src/json/docx/docx.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Document, FileChild, Packer, Paragraph, Table, TableCell, TableRow, TextRun, XmlComponent } from 'docx'

import { renderDocxChildrenToDocxElement, renderJSONContentToDocxElement } from './docx.js'

Check failure on line 3 in packages/static-renderer/src/json/docx/docx.example.ts

View workflow job for this annotation

GitHub Actions / build (20)

'renderDocxChildrenToDocxElement' is defined but never used

const table = new Table({

Check failure on line 5 in packages/static-renderer/src/json/docx/docx.example.ts

View workflow job for this annotation

GitHub Actions / build (20)

'table' is assigned a value but never used
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')

Check failure on line 119 in packages/static-renderer/src/json/docx/docx.example.ts

View workflow job for this annotation

GitHub Actions / build (20)

'Bun' is not defined

Bun.write(file, await Packer.toBuffer(doc))

Check failure on line 121 in packages/static-renderer/src/json/docx/docx.example.ts

View workflow job for this annotation

GitHub Actions / build (20)

'Bun' is not defined
101 changes: 101 additions & 0 deletions packages/static-renderer/src/json/docx/docx.ts
Original file line number Diff line number Diff line change
@@ -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<TNodeType, TDocxElement | TDocxElement[]> & {
renderInlineContent: (ctx: { content: undefined | NodeType | NodeType[] }) => TDocxElement[]
},
) => TDocxElement
> & {
mergeMapping: Record<string, (ctx: MarkProps<TMarkType, MergeTypes, TNodeType>) => 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)
}
1 change: 1 addition & 0 deletions packages/static-renderer/src/json/docx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './docx.js'
Loading

0 comments on commit ccf6ce3

Please sign in to comment.