/** * docx Phase 4 — cascade resolver (READ side): effective formatting for * runs and paragraphs, and numbering (list) resolution. * * OWNERSHIP (DESIGN-P4.md): this file + resolver.test.ts belong to the * RESOLVER agent. Allowed imports: ./internal.js, ../xml, ../mce. * Never import ./index.js or another feature module. * * THE CASCADE (ECMA-376 §17.7.2, validated against real Word via corpus * fixtures — when spec and Office disagree, Office wins): * * docDefaults (styles.xml w:docDefaults) * → style chain (w:pStyle / w:rStyle, following w:basedOn recursively, * root-first application, with a cycle guard — a basedOn loop or a * chain deeper than 20 stops resolution at that point, matching * Word's tolerance rather than throwing) * → numbering-level paragraph properties (the w:lvl/w:pPr of the * paragraph's resolved numbering level — indents mostly) * → direct formatting (w:pPr / w:rPr on the element itself) * * TOGGLE PROPERTIES (§17.7.3 — the correctness centerpiece): w:b, w:i, * w:caps, w:smallCaps, w:strike, w:dstrike, w:vanish (and bCs/iCs, which * we do not surface) do NOT simply override across the style hierarchy: * 1. If DIRECT formatting specifies the property, that value wins. * 2. Otherwise the effective value is the XOR (parity) of every style * LEVEL that turns the property on — a toggle does not inherit by * override even within one basedOn chain, so a property set true in a * style AND in the style it is basedOn cancels to off; the same * parity spans the paragraph-style and character-style chains * together (italic in both = NOT italic). An explicit false/0/off in * a style is the "off" contribution and does not flip the parity — * only direct formatting can force a toggle back off. * 3. Exception: when docDefaults sets the property true, it applies * regardless of the style XOR (only direct formatting can turn it * back off). * A numbering level's w:rPr, when the paragraph resolves to one, sits * between the style layers and direct formatting (CONTRACTS.md §7.9): a * toggle it specifies replaces the style-XOR result but still loses to * direct formatting and to the docDefaults-true exception. * Non-toggle properties (u, color, sz, highlight, rFonts, jc, ind, * spacing, …) resolve by plain nearest-wins override in cascade order. * * THEME FONTS: `w:rFonts/@w:asciiTheme` is an indirection into the theme * part's a:fontScheme (majorAscii/majorHAnsi → majorFont latin typeface, * minorAscii/minorHAnsi → minorFont). Reading the theme part requires an * async zip inflate, while the resolver functions are synchronous, so the * indirection is resolved through a per-document cache populated by * {@link loadThemeFonts}; until it is loaded (or when the document has no * theme part) a theme-indirected font reads as null. Theme COLORS * (w:color/@w:themeColor without a literal @w:val) always read as null * (CONTRACTS.md §7.7) — Word writes the literal RGB alongside the theme * reference, so real files resolve through @w:val. * * All resolver functions are PURE READS: they never mutate any DOM, never * mark anything dirty, and are safe to call in tight loops (styles.xml * and numbering.xml are eagerly loaded at open — see DocumentInternal). */ import { type DocumentInternal } from './internal.js'; import type { XmlElement } from '../xml/index.js'; /** Horizontal alignment, normalized: w:jc start→'left', end→'right', both→'justify'. */ export type ParagraphAlignment = 'left' | 'center' | 'right' | 'justify' | 'distribute'; /** Line-spacing rule of w:spacing (w:lineRule; 'auto' lines are 240ths). */ export interface LineSpacing { /** * For rule 'auto': the line height in 240ths of a line (240 = single, * 360 = 1.5, 480 = double). For 'exact'/'atLeast': twips. */ readonly value: number; readonly rule: 'auto' | 'exact' | 'atLeast'; } /** * Fully resolved run formatting — what Word actually renders, after * docDefaults, the style chains, numbering rPr, and direct formatting. */ export interface EffectiveRunFormat { /** Toggle properties (§17.7.3 XOR semantics). */ readonly bold: boolean; readonly italic: boolean; readonly caps: boolean; readonly smallCaps: boolean; readonly strike: boolean; readonly doubleStrike: boolean; /** w:vanish — hidden text. */ readonly hidden: boolean; /** w:u/@w:val ('single', 'double', 'wave', …); null when not underlined ('none' → null). */ readonly underline: string | null; /** Effective font (w:rFonts/@w:ascii); null when nothing in the cascade sets one. */ readonly font: string | null; /** Font size in points (w:sz half-points ÷ 2); null when unset anywhere. */ readonly sizePt: number | null; /** Text color as 'RRGGBB' uppercase; null for 'auto' or unset. */ readonly color: string | null; /** Highlight color name (w:highlight/@w:val, e.g. 'yellow'); null when none. */ readonly highlight: string | null; /** The run's character style id (w:rStyle), when any. */ readonly styleId: string | null; /** Display name (w:style/w:name) of that character style, when any. */ readonly styleName: string | null; } /** * Fully resolved paragraph formatting after docDefaults, the paragraph * style chain, the numbering level's pPr, and direct formatting. */ export interface EffectiveParagraphFormat { /** Normalized w:jc; null when nothing in the cascade sets alignment (Word renders left). */ readonly alignment: ParagraphAlignment | null; /** w:ind/@w:left (w:start) in twips; null when unset. */ readonly indentLeftTwips: number | null; /** w:ind/@w:right (w:end) in twips; null when unset. */ readonly indentRightTwips: number | null; /** * First-line indent in twips: positive = w:firstLine, negative = * w:hanging (a hanging indent of 360 reads as -360); null when unset. */ readonly indentFirstLineTwips: number | null; /** w:spacing/@w:before in twips; null when unset. */ readonly spacingBeforeTwips: number | null; /** w:spacing/@w:after in twips; null when unset. */ readonly spacingAfterTwips: number | null; /** w:spacing line + lineRule; null when unset. */ readonly lineSpacing: LineSpacing | null; /** The paragraph's style id (w:pStyle val, or the default paragraph style's id). */ readonly styleId: string | null; /** Display name (w:style/w:name/@w:val) of that style, e.g. 'heading 1'. */ readonly styleName: string | null; } /** Numbering formats surfaced by {@link listInfo} (ST_NumberFormat subset + passthrough). */ export type ListNumberFormat = 'bullet' | 'decimal' | 'lowerLetter' | 'upperLetter' | 'lowerRoman' | 'upperRoman' | 'none' | (string & {}); /** * The resolved list membership of a paragraph — enough to answer "is * this a bullet or '3.a.' and what does it render as". */ export interface ListInfo { /** The w:num numId the paragraph resolves to (via direct numPr or its style chain). */ readonly numId: string; /** 0-based list level (w:ilvl; 0 when unspecified). */ readonly level: number; /** Resolved w:numFmt of the level ('bullet', 'decimal', …). */ readonly format: ListNumberFormat; /** * Resolved w:lvlText verbatim — '' + bullet char for bullets (e.g. * ''), placeholder text for numbers (e.g. '%1.' or '%1.%2.'). */ readonly text: string; /** Resolved w:start of the level (honoring w:lvlOverride/w:startOverride); 1 when unspecified. */ readonly start: number; /** Convenience: format === 'bullet'. */ readonly isBullet: boolean; } /** * Load the theme part's major/minor latin font names into the resolver's * per-document cache so `w:rFonts/@w:asciiTheme` indirection resolves to * literal font names (e.g. 'Calibri Light'). Idempotent, read-only (never * dirties any part — the loaded theme still raw-copies at save), and * degrade-only: a missing, dangling, or unparseable theme part caches * "no theme" and theme-indirected fonts keep reading as null. * * Call it once after `Document.open()` (integration wires this into the * open path); every resolver call afterwards is synchronous. * * @example * await loadThemeFonts(doc._internal()); * effectiveRunFormat(doc._internal(), run.element).font; // 'Calibri Light' */ export declare function loadThemeFonts(di: DocumentInternal): Promise; /** * Resolve the effective formatting of one `w:r` element through the full * cascade. The containing paragraph is found by walking up the parent * chain (a detached run resolves as if in an unstyled paragraph). * * @example * const fmt = effectiveRunFormat(di, run.element); * fmt.bold; // true — even when bold comes from Heading1, not the run * fmt.sizePt; // 11 */ export declare function effectiveRunFormat(di: DocumentInternal, run: XmlElement): EffectiveRunFormat; /** * Resolve the effective paragraph formatting of one `w:p` element * through docDefaults → style chain → numbering-level pPr → direct pPr. * * @example * const fmt = effectiveParagraphFormat(di, para.element); * fmt.alignment; // 'center' * fmt.styleName; // 'heading 1' * fmt.indentLeftTwips; // 720 */ export declare function effectiveParagraphFormat(di: DocumentInternal, paragraph: XmlElement): EffectiveParagraphFormat; /** * Resolve which list a paragraph belongs to, or null when it is not * numbered. Follows direct w:pPr/w:numPr and numbering inherited through * the paragraph style chain; honors w:num → w:abstractNum indirection * (incl. single-level w:numStyleLink) and w:lvlOverride/w:startOverride. * A numPr with numId 0 (Word's "remove numbering") and dangling * numId/abstractNumId/level references resolve to null. * * @example * const li = listInfo(di, para.element); * li?.isBullet; // false * li?.format; // 'decimal' * li?.text; // '%1.' */ export declare function listInfo(di: DocumentInternal, paragraph: XmlElement): ListInfo | null; //# sourceMappingURL=resolver.d.ts.map