Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./packages/core/src/prosemirror/conversion/toProseDoc.ts
/**
* Document to ProseMirror Conversion
*
* Converts our Document type (from DOCX parsing) to a ProseMirror document.
* Preserves all formatting attributes for round-trip fidelity.
*
* Style Resolution:
* When styles are provided, paragraph properties are resolved from the style chain:
* - Document defaults (docDefaults)
* - Normal style (if no explicit styleId)
* - Style chain (basedOn inheritance)
* - Inline properties (highest priority)
*
* This file owns the top-level entry points (toProseDoc, headerFooterToProseDoc,
* footnoteToProseDoc, createEmptyDoc). Per-domain converters live under
* ./toProseDoc/ (marks, runs, paragraph, tables, textbox) — symmetric to
* the fromProseDoc/ split.
*/
import type { Node as PMNode } from 'prosemirror-model';
import { schema } from '../schema';
import type { Document, BlockContent, StyleDefinitions, Theme } from '../../types/document';
import { createStyleResolver, type StyleResolver } from '../styles';
import { getDocumentWatermark } from '../../docx/watermarkApi';
import { paragraphHasPageBreak } from './toProseDoc/paragraph';
import { convertTable } from './toProseDoc/tables';
import { convertParagraphWithTextBoxes } from './toProseDoc/textbox';
import { sdtPropsToAttrs } from './sdtAttrs';
/**
* Convert a list of block-content model nodes to PM nodes.
*
* Block-level SDTs become real `blockSdt` PM nodes wrapping their
* (recursively converted) children, so content controls survive the edit
* cycle instead of being flattened. A control that wraps nothing gets a
* single empty paragraph to satisfy the `block+` content model.
*
* `includePageBreaks` mirrors the body-only behavior of emitting a
* `pageBreak` node after a paragraph carrying a rendered page break;
* header/footer content passes `false`.
*/
function convertBlocksToNodes(
blocks: BlockContent[],
styleResolver: StyleResolver | null,
theme: Theme | null,
includePageBreaks: boolean
): PMNode[] {
const nodes: PMNode[] = [];
for (const block of blocks) {
if (block.type === 'paragraph') {
nodes.push(...convertParagraphWithTextBoxes(block, styleResolver));
if (includePageBreaks && paragraphHasPageBreak(block)) {
nodes.push(schema.node('pageBreak'));
}
} else if (block.type === 'table') {
nodes.push(convertTable(block, styleResolver, theme));
} else if (block.type === 'blockSdt') {
const childNodes = convertBlocksToNodes(
block.content,
styleResolver,
theme,
includePageBreaks
);
const inner = childNodes.length > 0 ? childNodes : [schema.node('paragraph', {}, [])];
nodes.push(schema.node('blockSdt', sdtPropsToAttrs(block.properties), inner));
}
}
return nodes;
}
/**
* Options for document conversion
*/
export interface ToProseDocOptions {
/** Style definitions for resolving paragraph styles */
styles?: StyleDefinitions;
/**
* Doc-level `w:defaultTabStop` (§17.6.13) in twips, stamped onto the PM
* doc node so `toFlowBlocks` picks it up. The body entry point reads
* this from the parsed package; HF/footnote callers must pass it
* through explicitly since their input is a content array, not a full
* `Document`. Falls back to the OOXML default (720 twips) when null.
*/
defaultTabStopTwips?: number | null;
}
/**
* Convert a Document to a ProseMirror document
*
* @param document - The Document to convert
* @param options - Conversion options including style definitions
*/
export function toProseDoc(document: Document, options?: ToProseDocOptions): PMNode {
const theme = document.package.theme ?? null;
// Create style resolver if styles are provided
const styleResolver = options?.styles ? createStyleResolver(options.styles) : null;
const nodes = convertBlocksToNodes(
document.package.document.content,
styleResolver,
theme,
/* includePageBreaks */ true
);
// Ensure we have at least one paragraph.
if (nodes.length === 0) {
nodes.push(schema.node('paragraph', {}, []));
}
// Guarantee a text-cursor position after a trailing block-level content
// control. A `blockSdt` is `isolating`, so if it is the doc's last node the
// caret cannot land after it (no gapcursor) and the user can never type
// outside the control — the common "whole body wrapped in an SDT" case.
// Word likewise always keeps a body-final paragraph after such content.
if (nodes[nodes.length - 1]?.type.name === 'blockSdt') {
nodes.push(schema.node('paragraph', {}, []));
}
return schema.node(
'doc',
{
defaultTabStopTwips: document.package.settings?.defaultTabStop ?? null,
// Seed the watermark from the parsed headers so it rides PM state.
watermark: getDocumentWatermark(document) ?? null,
},
nodes
);
}
/**
* Convert HeaderFooter content (array of Paragraph/Table blocks) to a ProseMirror document.
* Used for editing headers/footers in their own ProseMirror editor and for the
* unified header/footer render pipeline. `theme` must be threaded for themeColor
* resolution in cell shading (`<w:shd w:themeFill=...>`) — without it, themed
* fills in HF tables fall back to the unresolved theme key.
*/
export function headerFooterToProseDoc(
content: BlockContent[],
options?: ToProseDocOptions & { theme?: Theme | null }
): PMNode {
const styleResolver = options?.styles ? createStyleResolver(options.styles) : null;
const theme = options?.theme ?? null;
const nodes = convertBlocksToNodes(content, styleResolver, theme, /* includePageBreaks */ false);
if (nodes.length === 0) {
nodes.push(schema.node('paragraph', {}, []));
}
return schema.node('doc', { defaultTabStopTwips: options?.defaultTabStopTwips ?? null }, nodes);
}
/**
* Convert footnote/endnote content (array of Paragraph/Table blocks) to a
* ProseMirror document. Mirrors `headerFooterToProseDoc` so footnotes flow
* through the same body pipeline (toFlowBlocks → measureBlocks →
* renderFragment) and inherit its block support — paragraph + table + image
* + textBox + fields. Pre-PR, footnoteLayout's `convertFootnoteToContent`
* re-implemented run/paragraph conversion by hand and silently dropped
* tables, images, and fields nested inside a footnote.
*/
export function footnoteToProseDoc(
content: BlockContent[],
options?: ToProseDocOptions & { theme?: Theme | null }
): PMNode {
return headerFooterToProseDoc(content, options);
}
/**
* Create an empty ProseMirror document
*/
export function createEmptyDoc(): PMNode {
return schema.node('doc', null, [schema.node('paragraph', {}, [])]);
}