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 { Node } from 'prosemirror-model';
import { import {
DocxSerializerAsync, DocxSerializerAsync,
@@ -169,15 +169,26 @@ export const defaultAsyncNodes: NodeSerializerAsync = {
state.closeBlock(node, { pageBreakBefore: true }); state.closeBlock(node, { pageBreakBefore: true });
}, },
footnoteReference(state, node) { footnoteReference(state, node) {
state.addRunOptions({ superScript: true }); const number =
state.text(`[${node.attrs?.referenceNumber ?? ''}]`); 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) { async footnotes(state, node) {
await state.renderList(node, 'numbered'); for (let i = 0; i < node.childCount; i += 1) {
}, const item = node.child(i);
async footnote(state, node) { const number =
await state.renderListItem(node); 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. // No usable static export representation: skip without failing.
subpages() {}, subpages() {},
transclusionReference() {}, transclusionReference() {},
@@ -824,6 +824,29 @@ export class DocxSerializerStateAsync {
this.current.push(new FootnoteReferenceRun(this.$footnoteCounter)); 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) { closeBlock(node: Node, props?: IParagraphOptions) {
const paragraph = new Paragraph({ const paragraph = new Paragraph({
children: this.current, children: this.current,