feat: proper DOCX support

This commit is contained in:
Philipinho
2026-08-11 01:45:01 +01:00
parent 0a5206f19e
commit c17369411d
3 changed files with 42 additions and 8 deletions
@@ -1,4 +1,4 @@
import { HeadingLevel, ShadingType } from 'docx';
import { FootnoteReferenceRun, HeadingLevel, Paragraph, ShadingType } from 'docx';
import { Node } from 'prosemirror-model';
import {
DocxSerializerAsync,
@@ -169,15 +169,26 @@ export const defaultAsyncNodes: NodeSerializerAsync = {
state.closeBlock(node, { pageBreakBefore: true });
},
footnoteReference(state, node) {
state.addRunOptions({ superScript: true });
state.text(`[${node.attrs?.referenceNumber ?? ''}]`);
const number =
Number(node.attrs?.referenceNumber) || state.$footnoteCounter + 1;
state.$footnoteCounter = Math.max(state.$footnoteCounter, number);
// seed an empty body so the reference stays valid even if the trailing
// footnotes list is missing; the footnotes node overwrites it with content
if (!state.footnotes[number]) {
state.footnotes[number] = { children: [new Paragraph('')] };
}
state.current.push(new FootnoteReferenceRun(number));
},
async footnotes(state, node) {
await state.renderList(node, 'numbered');
},
async footnote(state, node) {
await state.renderListItem(node);
for (let i = 0; i < node.childCount; i += 1) {
const item = node.child(i);
const number =
Number(String(item.attrs?.id ?? '').replace('fn:', '')) || i + 1;
await state.footnoteDefinition(item, number);
}
},
// items are consumed by the footnotes handler above
footnote() {},
// No usable static export representation: skip without failing.
subpages() {},
transclusionReference() {},
@@ -824,6 +824,29 @@ export class DocxSerializerStateAsync {
this.current.push(new FootnoteReferenceRun(this.$footnoteCounter));
}
// Fills the footnote body for an already-referenced footnote number from a
// node holding block content (Docmost keeps footnote text in a trailing
// list, separate from the inline reference).
async footnoteDefinition(node: Node, number: number) {
const { current, children, nextRunOpts, nextParentParagraphOpts } = this;
this.current = [];
this.children = [];
delete this.nextRunOpts;
delete this.nextParentParagraphOpts;
await this.renderContent(node);
this.footnotes[number] = {
children: this.children.filter(
(child): child is Paragraph => child instanceof Paragraph,
),
};
this.current = current;
this.children = children;
this.nextRunOpts = nextRunOpts;
this.nextParentParagraphOpts = nextParentParagraphOpts;
}
closeBlock(node: Node, props?: IParagraphOptions) {
const paragraph = new Paragraph({
children: this.current,