import { FlowMode, Fragment, PageMargins, PageNumberChapterSeparator, PageNumberFormat, SourceAnchor, ResolvedLayout, ResolvedPage, ResolvedPaintItem, LayoutSourceIdentity, LayoutStoryLocator } from '../../../contracts/src/index.js'; import { PaintWorkSummary } from './page-content.js'; import { DomPainterPersistentPageInput } from './persistent-page-surface.js'; import { PageStyles } from './styles.js'; import { PaintSnapshotStructuredContentBlockEntity, PaintSnapshotStructuredContentInlineEntity } from './sdt/snapshot.js'; import { PositionValidationOptions, PositionValidationSummary } from './pm-position-validation.js'; export type { PaintSnapshotStructuredContentBlockEntity, PaintSnapshotStructuredContentInlineEntity, } from './sdt/snapshot.js'; export { applyLayoutIdentityDataset } from './utils/layout-identity.js'; /** * Layout mode for document rendering. * * `'vertical'` (page-by-page vertical layout) is the only paginated * arrangement — horizontal and book modes were deleted at painter plan P7 * (product decision 2026-07-05). The real presentation axis is `FlowMode` * (`'paginated' | 'semantic'`); this type remains for API-shape compatibility. */ export type LayoutMode = 'vertical'; export type { FlowMode } from '../../../contracts/src/index.js'; /** * Interface for position mapping from ProseMirror transactions. * Used to efficiently update DOM position attributes without full re-render. */ export interface PositionMapping { /** Transform a position from old to new document coordinates */ map(pos: number, bias?: number): number; /** Array of step maps - length indicates transaction complexity */ readonly maps: readonly unknown[]; } export type RenderedLineInfo = { el: HTMLElement; top: number; height: number; }; /** * Input to `DomPainter.paint()`. * * The painter consumes only `resolvedLayout`. All fragment, geometry, and * page-level metadata it needs is reachable from `ResolvedPaintItem.fragment` * back-pointers and `ResolvedPage` fields. */ export type DomPainterInput = { resolvedLayout: ResolvedLayout; }; export type PageDecorationPayload = { fragments: Fragment[]; /** Resolved items aligned 1:1 with `fragments`. Same length, same order. */ items: ResolvedPaintItem[]; /** Minimum Y coordinate from layout; negative when content extends above y=0. */ minY?: number; height: number; /** Optional measured content height to aid bottom alignment in footers. */ contentHeight?: number; /** Decoration band origin in page-local Y. Producer is the sole source of truth (SD-2957). */ offset: number; marginLeft?: number; contentWidth?: number; headerFooterRefId?: string; sectionType?: string; /** True while this rendered header/footer story is the active editing surface. */ isActiveHeaderFooter?: boolean; /** * When `false`, total-page-count fields (`NUMPAGES` / `SECTIONPAGES`) in this * decoration render their pre-resolved provisional text (source-cached DOCX * result, em dash when absent) instead of the current page totals — used * while pagination is still partial. Absent/`true` = exact totals (existing * caller behavior). */ pageCountFieldsExact?: boolean; box?: { x: number; y: number; width: number; height: number; }; hitRegion?: { x: number; y: number; width: number; height: number; }; }; /** * Provider function for page decorations (headers and footers). * Called for each page to generate header or footer content. * * @param {number} pageNumber - The page number (1-indexed) * @param {PageMargins} [pageMargins] - Page margin configuration * @param {ResolvedPage} [page] - Resolved page from the layout * @returns {PageDecorationPayload | null} Decoration payload containing fragments and layout info, or null if no decoration */ export type PageDecorationProvider = (pageNumber: number, pageMargins?: PageMargins, page?: ResolvedPage) => PageDecorationPayload | null; type PainterOptions = { pageStyles?: PageStyles; layoutMode?: LayoutMode; flowMode?: FlowMode; /** Gap between pages in pixels (default: 24px) */ pageGap?: number; headerProvider?: PageDecorationProvider; footerProvider?: PageDecorationProvider; /** Called with the paint snapshot after each paint cycle completes. */ onPaintSnapshot?: (snapshot: PaintSnapshot) => void; /** Render nonprinting formatting marks such as spaces, tabs, and paragraph marks. */ showFormattingMarks?: boolean; /** Built-in SDT chrome rendering mode. */ contentControlsChrome?: 'default' | 'none'; /** Per-document logical->physical font resolver (face-aware); see DomPainterOptions.resolvePhysical. */ resolvePhysical?: (cssFontFamily: string, face: { weight: '400' | '700'; style: 'normal' | 'italic'; }) => string; /** Populate PaintWorkSummary's per-page index arrays (P5 §4.6). Dark by default; see DomPainterOptions.paintWorkAttribution. */ paintWorkAttribution?: boolean; /** Story-aware position-coverage validation. Dark by default; see DomPainterOptions.positionValidation. */ positionValidation?: PositionValidationOptions; }; /** * Rendering context passed to fragment renderers containing page metadata. * Provides information about the current page position and section for dynamic content like page numbers. * * @typedef {Object} FragmentRenderContext * @property {number} pageNumber - Current page number (1-indexed) * @property {number} totalPages - Total number of pages in the document * @property {'body'|'header'|'footer'} section - Document section being rendered * @property {string} [pageNumberText] - Optional formatted page number text (e.g., "Page 1 of 10") * @property {number} [displayPageNumber] - Section-aware numeric page value before formatting * @property {number} [sectionPageCount] - Physical page count in the current section */ export type FragmentRenderContext = { pageNumber: number; totalPages: number; section: 'body' | 'header' | 'footer'; story?: LayoutStoryLocator; pageNumberText?: string; displayPageNumber?: number; pageNumberFormat?: PageNumberFormat; pageNumberChapterText?: string; pageNumberChapterSeparator?: PageNumberChapterSeparator; sectionPageCount?: number; pageIndex?: number; /** * When `false`, total-page-count tokens render their pre-resolved * provisional run text (source-cached DOCX result / em dash) instead of * `totalPages` / `sectionPageCount`. Absent/`true` = exact (default). */ pageCountFieldsExact?: boolean; }; export type PaintSnapshotLineStyle = { paddingLeftPx?: number; paddingRightPx?: number; textIndentPx?: number; marginLeftPx?: number; marginRightPx?: number; leftPx?: number; topPx?: number; widthPx?: number; heightPx?: number; display?: string; position?: string; textAlign?: string; justifyContent?: string; }; export type PaintSnapshotMarkerStyle = { text?: string; leftPx?: number; widthPx?: number; paddingRightPx?: number; display?: string; position?: string; textAlign?: string; fontWeight?: string; fontStyle?: string; color?: string; sourceAnchor?: SourceAnchor; }; export type PaintSnapshotTabStyle = { widthPx?: number; leftPx?: number; position?: string; borderBottom?: string; }; export type PaintSnapshotAnnotationEntity = { element: HTMLElement; pageIndex: number; pmStart?: number; pmEnd?: number; fieldId?: string; fieldType?: string; type?: string; layoutSourceIdentity?: LayoutSourceIdentity; }; export type PaintSnapshotImageEntity = { element: HTMLElement; pageIndex: number; kind: 'inline' | 'fragment'; pmStart?: number; pmEnd?: number; blockId?: string; sourceAnchor?: SourceAnchor; layoutSourceIdentity?: LayoutSourceIdentity; }; export type PaintSnapshotEntities = { annotations: PaintSnapshotAnnotationEntity[]; structuredContentBlocks: PaintSnapshotStructuredContentBlockEntity[]; structuredContentInlines: PaintSnapshotStructuredContentInlineEntity[]; images: PaintSnapshotImageEntity[]; }; export type PaintSnapshotLine = { index: number; inTableFragment: boolean; inTableParagraph: boolean; style: PaintSnapshotLineStyle; markers?: PaintSnapshotMarkerStyle[]; tabs?: PaintSnapshotTabStyle[]; sourceAnchor?: SourceAnchor; layoutSourceIdentity?: LayoutSourceIdentity; }; export type PaintSnapshotPage = { index: number; pageNumber?: number; lineCount: number; lines: PaintSnapshotLine[]; }; export type PaintSnapshot = { formatVersion: 1; pageCount: number; lineCount: number; markerCount: number; tabCount: number; pages: PaintSnapshotPage[]; entities: PaintSnapshotEntities; }; type PainterFragmentFailure = { readonly blockId: string; readonly code: 'painter-fragment-unavailable'; }; type PersistentPagePainterTransaction = { commit(): void; rollback(): void; readFragmentFailures(): readonly PainterFragmentFailure[]; readChangedRoots(): readonly HTMLElement[]; }; /** * DOM-based document painter that renders layout fragments to HTML elements. * One paint entry per mode (painter plan P7): the persistent shell/content * reconcile owns paginated flow, while `paint()` owns semantic flow. * * @class DomPainter * * @remarks * The DomPainter is responsible for: * - Rendering layout fragments (paragraphs, lists, images, tables, drawings) to DOM elements * - Managing page-level DOM structure and styling * - Handling headers and footers via PageDecorationProvider * - Incremental re-rendering when only specific blocks change * - Hyperlink rendering with security sanitization and accessibility */ export declare class DomPainter { private readonly options; private mount; private doc; private pageStates; private currentLayout; private changedBlocks; private readonly isSemanticFlow; private headerProvider?; private footerProvider?; private totalPages; private sectionPageCounts; private linkIdCounter; private shapeImageFillCounter; private wordArtPathCounter; private sdtLabelsRendered; /** * WeakMap storing tooltip data for hyperlink elements before DOM insertion. * Uses WeakMap to prevent memory leaks - entries are automatically garbage collected * when the corresponding element is removed from memory. * @private */ private pendingTooltips; private pageGap; private layoutVersion; private layoutEpoch; private processedLayoutVersion; /** Current transaction mapping for position updates (null if no mapping or complex transaction) */ private currentMapping; /** * Persistent paginated page surface (default persistent page geometry * plan, Unit 1): the generation-owned shell registry plus the bounded * content plane, retained across paints. Snapshot/restored by the private * persistent-page transaction like every other retained plane. */ private persistentSurface; private persistentSurfaceInvalidationHandler; /** * Provider identity may advance every render generation even when the body * page remains reusable. Refresh header/footer DOM on the next content reconcile * without discarding the retained page element or its body fragments. */ private persistentDecorationsDirty; /** * Page-window analog of `currentLayout.documentBackground`: the window path * deliberately runs with `currentLayout = null`, so the document background * scalar from `DomPainterPersistentPageInput` is retained here for * `getEffectivePageStyles()`. Reset by dense `paint()`/`resetState()`/ * `dispose()` so a stale window value can never leak across modes/mounts. */ private persistentDocumentBackground; private paintWork; /** * P5 §4.6 (review fix): per-page attribution arrays are opt-in. Counters * are O(1) when never consumed; the arrays grow per paint, and product * code never drains the summary — only the perf harness (which needs WHICH * pages for its repaint oracle) turns this on. */ private readonly paintWorkAttribution; /** * Story-aware position-coverage collector, owned per painter instance so the * live persistent surface and fresh-state oracle never mix counts. * Dark unless enabled via options; when dark, `record()` is a single branch. */ private readonly positionValidation; private paintSnapshotBuilder; private lastPaintSnapshot; private onPaintSnapshotCallback; /** * Private persistent-page transaction state. The package handle exposes this * only through a non-enumerable Symbol.for seam; it is deliberately absent * from the public DomPainterHandle contract. */ private activePersistentPageTransaction; private persistentPageIndices; /** Resolved layout for the next-gen paint pipeline. */ private resolvedLayout; private showFormattingMarks; private contentControlsChrome; constructor(options?: PainterOptions); setShowFormattingMarks(showFormattingMarks: boolean): void; setProviders(header?: PageDecorationProvider, footer?: PageDecorationProvider): void; private applyFormattingMarksClass; private invalidateRenderedContent; /** * Forget the persistent surface so its next reconcile rebuilds content * under the current render settings. Provider swaps use a * decoration-only refresh. */ private invalidateWindowSurface; /** Returns the resolved page for a given index, or null if resolved data is unavailable. */ private getResolvedPage; /** * Returns the latest painter snapshot captured during the last paint cycle. */ getPaintSnapshot(): PaintSnapshot | null; /** * Returns the stable page-root indices owned by the current surface. */ getPersistentPageIndices(): number[]; /** * Begin a rollbackable transaction around a content paint. Named for the * paginated persistent-page path it was minted for; the captured snapshot is * the painter's COMPLETE retained/index state, so the same journal serves * the semantic flow's dense `paint()` entry (the v2 host's canonical * atomic visible commit wraps both paint kinds in one transaction). * * This method is not part of DomPainterHandle. The package factory installs * a non-enumerable Symbol.for hook that the v2 routed wrapper alone reads. * The caller owns the matching DOM mutation journal; rollback here restores * every painter-owned retained/index plane to references for those restored * last-good nodes. */ beginPersistentPageTransaction(): PersistentPagePainterTransaction; private capturePersistentPagePainterState; private restorePersistentPagePainterState; private createAllPageIndices; private setPersistentPageIndices; private emitPaintSnapshot; private beginPaintSnapshot; private finalizePaintSnapshotFromBuilder; private capturePaintSnapshotLine; private collectPaintSnapshotFromDomRoot; /** Semantic continuous paint. Paginated flow has only `paintPersistentPages()`. */ paint(input: DomPainterInput, mount: HTMLElement, mapping?: PositionMapping): void; /** * The persistent paginated reconcile (default persistent page geometry * plan, Unit 1): one generation-scoped scaffold owns every page root for * the whole layout generation, and only content descendants are * virtualized. Same-scaffold calls skip shell work in O(1); a new scaffold * identity reconciles page roots by index. There is no viewport-owned shell * set and no spacer node — the scroll extent derives * from the persistent shells plus the container gap alone. */ paintPersistentPages(input: DomPainterPersistentPageInput, mount: HTMLElement): void; /** True only while the retained document-wide page-shell plane matches the live DOM. */ isPersistentPageSurfaceIntact(): boolean; /** Register the host wake-up used when foreign DOM work removes/replaces page shells. */ setPersistentSurfaceInvalidationHandler(handler?: () => void): void; /** * Hydrated content page indices of the persistent surface, ascending. * Page roots cover the whole scaffold; this is the bounded content set. */ getHydratedContentPageIndices(): number[]; /** * Painter plan §4.6 (dark observability): persistent-page paint work since the * last consume. Never invents values — fields the path cannot attribute yet * stay 0/null. */ consumePaintWorkSummary(): PaintWorkSummary; /** * Story-aware position-coverage since the last consume, drained and reset at * this documented pass boundary. Content-free and bounded; safe to serialize * into a performance report. When the collector is dark (the product * default), the summary is empty (`checked: 0`). */ consumePositionValidationSummary(): PositionValidationSummary; /** * Per-page work attribution (P5 §4.6), opt-in via `paintWorkAttribution`: * the arrays are only drained by `consumePaintWorkSummary()`, so an * always-on push would grow unboundedly on the product path where nothing * ever consumes the summary. Counters stay always-on and O(1). */ private recordPageWork; private renderColumnSeparators; private getColumnSeparatorPositions; private renderDecorationsForPage; /** * Check if a fragment is vertically anchored to the page. * Used to determine special Y positioning for page-relative anchored content * in header/footer decoration sections. */ private isPageRelativeAnchoredFragment; private isPageRelativeParagraphFrame; private isPageRelativeHorizontalAnchoredFragment; /** * Header/footer layout emits normalized anchor Y coordinates: * - headers: local to the header container origin * - footers: local to the top of the footer band (pageHeight - bottomMargin) * * Footer containers can grow upward when content overflows the reserved footer * band, so their top edge is not always the same as the footer band origin. * This helper returns the page-space origin that normalized anchor Y values * are measured from. */ private getDecorationAnchorPageOriginY; private getFooterFragmentAnchorPageOriginY; private renderDecorationSection; private resetState; dispose(): void; private getSectionPageCount; private fullRender; private patchLayout; private patchPage; /** * Updates data-pm-start/data-pm-end attributes on all elements within a fragment * using the transaction's mapping. Skips header/footer content (separate PM coordinate space). * Also skips fragments that end before the edit point (their positions don't change). */ /** * Refreshes data-pm-start/data-pm-end on a REUSED story fragment from the * fresh resolved item. Story positions are local to their story document, * so the body transaction mapping cannot update them; instead the uniform * shift between the fresh first position and the painted one is applied. * Exact for unchanged blocks (positions inside one block shift uniformly). */ private updateStoryPositionAttributes; private updatePositionAttributes; private createPageState; /** * Explicit page-content context (painter plan P3a, §4.2): the class state * `renderPage`/`patchPage` consume, rebuilt per call because totalPages, * layoutEpoch, and the transaction mapping change between paints. The deep * fragment-rendering call graph stays on the class, reached through these * bound members. */ private pageContentContext; private applySemanticPageOverrides; private getEffectivePageStyles; private renderFragment; /** * Renders a paragraph fragment with defensive error handling. * Falls back to error placeholder on rendering errors to prevent full paint failure. * * @param fragment - The paragraph fragment to render * @param context - Rendering context with page and column information * @param sdtBoundary - Optional SDT boundary overrides for multi-fragment containers * @returns HTMLElement containing the rendered fragment or error placeholder */ private renderParagraphFragment; private createErrorPlaceholder; private renderImageFragment; /** * Optionally wrap an image element in an anchor for DrawingML hyperlinks (a:hlinkClick). * * When `hyperlink` is present and its URL passes sanitization, returns an * `` wrapping `imageEl`. The existing EditorInputManager * click-delegation on `a.superdoc-link` handles both viewing-mode navigation and * editing-mode event dispatch automatically, with no extra wiring needed here. * * When `hyperlink` is absent or the URL fails sanitization the original element * is returned unchanged. * * @param imageEl - The image element (img or span wrapper) to potentially wrap. * @param hyperlink - Hyperlink metadata from the ImageBlock/ImageRun, or undefined. * @param display - CSS display value for the anchor: 'block' for fragment images, * 'inline-block' for inline runs. */ private buildImageHyperlinkAnchor; /** * SD-3521 — stamp canonical textbox interaction metadata (`data-sd-textbox-*`) * onto a painted drawing fragment. The host reads these to drive object * selection + move/resize gestures from canonical geometry (intrinsic * unrotated extent, rotation, flips, per-axis layout scale) plus kernel * capability + OCC revision, never from the rotated outer AABB. Repeated * header/footer instances share the textbox id but get a distinct * instance key (page-scoped). No-ops for non-textbox drawings. */ private applyTextboxInteractionDataset; private renderDrawingFragment; private renderDrawingContent; private createVectorShapeElement; /** * Apply fill and stroke styles to a fallback shape container */ private applyFallbackShapeStyle; /** * Shape stroke widths reach the painter in physical CSS pixels. Preset SVGs * use a normalized 100 × 100 viewBox while VML custom geometry often uses a * coordsize in the thousands. In both cases allowing the viewBox transform to * scale the stroke changes the authored line weight, sometimes by orders of * magnitude. Keep geometry scalable and paint the resolved width verbatim. */ private applyPhysicalStrokeSemantics; /** * Produce a stable 8-bit preview color for inactive header/footer graphics. * CSS color-mix() retains fractional sRGB channels and Chromium floors some * half-channel values during rasterization; Word rounds those channels. Keep * the authored color separate and expose the rounded blend as a paint hint. */ private mixResolvedHexColorWithWhite; /** Paint a DrawingML picture fill through the existing SVG geometry. */ private applyShapeImageFill; private appendShapeFillImage; private resolveShapeTileOrigin; private hasShapeTextContent; private createShapeTextElement; private createTextboxContentElement; private shouldUseWordArtTextRenderer; private createWordArtTextElement; private createWordArtEnvelopeGroup; /** * Approximates DrawingML's point-wise text-envelope transform with narrow * vector strips. Each strip maps a slice of the unwarped text block between * the authored upper and lower guide paths. Unlike a per-glyph affine * transform, this bends the glyph outlines themselves while preserving SVG * text paint (fills, outlines, glow and shadow) and browser font shaping. */ private createWordArtEnvelopeMesh; /** * DrawingML composes glow and shadow after the text outline has been * deformed by the warp. The envelope renderer repeats a clipped source * through many transformed `` elements; leaving a CSS filter on that * source clips and transforms the effect independently in every mesh strip. * Promote a shared run filter to the completed mesh so the browser paints * one effect around the final warped silhouette. Mixed run effects remain on * their source runs because collapsing them would change authored styling. */ private promoteUniformWordArtEnvelopeFilter; private measureWordArtLineMetrics; private measureWordArtLineWidth; private createWordArtReflection; private buildWordArtLines; private resolveShapeTextPartText; private getWordArtTextAnchor; private getWordArtTextX; private applyWordArtTextFormatting; /** * Create a fallback text element for shapes without SVG * @param textContent - Text content with formatting * @param textAlign - Horizontal text alignment * @param textVerticalAlign - Vertical text alignment (top, center, bottom) * @param textInsets - Text insets in pixels (top, right, bottom, left) * @param _groupScaleX - Reserved parent-group scale factor * @param _groupScaleY - Reserved parent-group scale factor */ private createFallbackTextElement; private shapeTextAllowsOverflow; private applyShapeTextFlow; private tryCreatePresetSvg; /** * Creates an SVG string from custom geometry path data (a:custGeom). * Each path in the custom geometry has its own coordinate space (w × h) which is * mapped to the shape's actual dimensions via the SVG viewBox. */ private tryCreateCustomGeometrySvg; private parseSafeSvg; private stripUnsafeSvgContent; private getEffectExtentMetrics; private applyLineEnds; private findLineEndTarget; private ensureSvgDefs; private appendLineEndMarker; private createLineEndShape; private sanitizeSvgId; private applyVectorShapeTransforms; private createShapeGroupElement; private createGroupChildContent; private createDrawingPlaceholder; /** * Create an SVG chart element from a ChartDrawing block. * Delegates to the chart-renderer module for clean separation. */ private createChartElement; private resolveTableRenderData; private createTableCellLineRenderer; /** Render drawing content nested in any canonical table, including a textbox-owned table. */ private renderDrawingContentForTable; private renderTableFragment; private renderLine; private createRunRenderContext; private defaultFragmentRenderContext; /** * Updates an existing fragment element's position and dimensions in place. * Used during incremental updates to efficiently reposition fragments without full re-render. * * @param el - The HTMLElement representing the fragment to update * @param fragment - The fragment data containing updated position and dimensions * @param section - The document section ('body', 'header', 'footer') containing this fragment. * Selects the wrapper's section-scoped identity and legacy PM attributes. */ private updateFragmentElement; private applyRenderDiagnosticFragmentFrame; /** * Applies fragment positioning, dimensions, and metadata to an HTML element. * * @param el - The HTMLElement to apply fragment properties to * @param fragment - The fragment data containing position, dimensions, and PM position information * @param section - The document section ('body', 'header', 'footer') containing this fragment. * Selects the wrapper's section-scoped identity and legacy PM attributes. */ private applyFragmentFrame; /** * Applies PM position data attributes from a legacy Fragment. * Extracted from applyFragmentFrame for use in the resolved wrapper path. * When a resolvedItem is provided, its fields take precedence over fragment fields. */ private applyFragmentPmAttributes; /** * Applies fragment wrapper positioning from a ResolvedFragmentItem. * Uses resolved data for spatial properties and delegates PM attributes to the legacy path. */ private isAnchoredMediaFragment; /** * Marks fragments whose paint coordinates are independent from body flow. * DOM consumers such as header/footer hit-testing must not infer page margins * from floating objects that happen to paint above the first flow block. */ private applyFragmentFlowClass; private shouldRenderBehindPageContent; private isHeaderWordArtWatermark; private isVmlTextWatermarkImage; private applyHeaderFooterTextWatermarkPreviewOpacity; /** * Only anchored images and drawings participate in explicit wrapper stacking. * Inline media intentionally rely on DOM order to preserve legacy paint order. */ private resolveFragmentWrapperZIndex; private applyFragmentWrapperZIndex; private applyResolvedFragmentFrame; /** * Estimates the height of a fragment when explicit height is not available. * * This method provides fallback height calculations for footer bottom-alignment * from resolved layout data, or using the fragment's height property for * tables, images, and drawings. * * @param fragment - The fragment to estimate height for * @returns Estimated height in pixels, or 0 if height cannot be determined */ private estimateFragmentHeight; }