Operand

thee, sea, us.

gram: docs

> ./packages/core/src/prosemirror/conversion/fromProseDoc.ts

/**
* ProseMirror to Document Conversion
*
* Converts a ProseMirror document back to our Document type.
* This enables round-trip editing: DOCX -> Document -> PM -> Document -> DOCX
*
* Key responsibilities:
* - Coalesce consecutive text with same marks into single Runs
* - Preserve paragraph attributes (paraId, textId, formatting)
* - Handle marks -> TextFormatting conversion
*
* This file owns the top-level orchestrator (`fromProseDoc`) plus block
* extraction and the page-break paragraph factory. Per-domain converters
* live under ./fromProseDoc/ (marks, runs, paragraph, tables, textbox).
* The deep import `@eigenpal/.../prosemirror/conversion/fromProseDoc` is
* a tsup entry consumed by the Vue adapter — the barrel re-exports
* preserve that surface.
* @packageDocumentation
* @public
*/
import type { Node as PMNode } from 'prosemirror-model';
import type {
Document,
DocumentBody,
Paragraph,
Run,
BreakContent,
BlockContent,
BlockSdt,
Watermark,
} from '../../types/document';
import { getDocumentWatermark, setDocumentWatermark } from '../../docx/watermarkApi';
import type { TextBoxAttrs } from '../extensions/nodes/TextBoxExtension';
import { shouldExportTextBoxInsideFollowingParagraph } from './textBoxAnchors';
import { sdtAttrsToProps } from './sdtAttrs';
import { convertPMParagraph } from './fromProseDoc/paragraph';
import { convertPMTable } from './fromProseDoc/tables';
import { convertPMTextBox, convertPMTextBoxRun } from './fromProseDoc/textbox';
/**
* Convert a ProseMirror document to our Document type
*/
export function fromProseDoc(pmDoc: PMNode, baseDocument?: Document): Document {
const blocks = extractBlocks(pmDoc);
// Preserve section properties (margins, headers, footers) from base document
const documentBody: DocumentBody = {
content: blocks,
finalSectionProperties: baseDocument?.package.document.finalSectionProperties,
sections: baseDocument?.package.document.sections,
comments: baseDocument?.package.document.comments,
};
// If we have a base document, preserve its package structure
const result: Document = baseDocument
? {
...baseDocument,
package: {
...baseDocument.package,
document: documentBody,
},
}
: { package: { document: documentBody } };
// Sync the watermark doc attr → `HeaderFooter.watermark` so the serializer
// and any model consumers see watermark applies/removes (incl. via undo).
// Reference comparison skips the header-map clone on the common no-change
// path — the same Watermark object rides PM attrs until explicitly changed.
const attrWatermark = (pmDoc.attrs.watermark as Watermark | null) ?? null;
const currentWatermark = getDocumentWatermark(result) ?? null;
if (attrWatermark !== currentWatermark) {
return setDocumentWatermark(result, attrWatermark);
}
return result;
}
/**
* Extract blocks (paragraphs, tables, and block-level SDTs) from a
* ProseMirror document or block-containing node.
*/
function extractBlocks(pmDoc: PMNode): BlockContent[] {
const blocks: BlockContent[] = [];
let pendingAnchoredTextBoxRuns: Run[] = [];
const flushPendingTextBoxes = (): void => {
for (const run of pendingAnchoredTextBoxRuns) {
blocks.push({
type: 'paragraph',
content: [run],
});
}
pendingAnchoredTextBoxRuns = [];
};
pmDoc.forEach((node) => {
if (node.type.name === 'paragraph') {
const paragraph = convertPMParagraph(node);
if (pendingAnchoredTextBoxRuns.length > 0) {
paragraph.content = [...pendingAnchoredTextBoxRuns, ...paragraph.content];
pendingAnchoredTextBoxRuns = [];
}
blocks.push(paragraph);
} else if (node.type.name === 'table') {
flushPendingTextBoxes();
blocks.push(convertPMTable(node));
} else if (node.type.name === 'blockSdt') {
flushPendingTextBoxes();
blocks.push(convertPMBlockSdt(node));
} else if (node.type.name === 'textBox') {
const attrs = node.attrs as TextBoxAttrs;
if (shouldExportTextBoxInsideFollowingParagraph(attrs)) {
pendingAnchoredTextBoxRuns.push(convertPMTextBoxRun(node));
} else {
flushPendingTextBoxes();
blocks.push(convertPMTextBox(node));
}
} else if (node.type.name === 'pageBreak') {
flushPendingTextBoxes();
// Convert page break node to a paragraph with a page break run
blocks.push(createPageBreakParagraph());
}
});
flushPendingTextBoxes();
return blocks;
}
/**
* Reconstruct a {@link BlockSdt} model node from a `blockSdt` PM node:
* project the attrs back to {@link SdtProperties} (the captured raw `sdtPr`
* rides along for lossless serialization) and recurse into the children.
*/
function convertPMBlockSdt(node: PMNode): BlockSdt {
return {
type: 'blockSdt',
properties: sdtAttrsToProps(node.attrs as Record<string, unknown>),
content: extractBlocks(node),
};
}
/**
* Create a paragraph containing only a page break run (for DOCX serialization)
*/
function createPageBreakParagraph(): Paragraph {
const breakContent: BreakContent = { type: 'break', breakType: 'page' };
const run: Run = { type: 'run', content: [breakContent] };
return {
type: 'paragraph',
content: [run],
};
}
/**
* Update a Document with content from a ProseMirror document
* Preserves all non-content parts of the original document
*/
export function updateDocumentContent(originalDocument: Document, pmDoc: PMNode): Document {
return fromProseDoc(pmDoc, originalDocument);
}
/**
* Convert a ProseMirror document back to an array of Paragraph/Table blocks.
* Used for converting edited header/footer PM content back to the document model.
*/
export function proseDocToBlocks(pmDoc: PMNode): BlockContent[] {
return extractBlocks(pmDoc);
}