@beyondwork/docx-react-component 1.0.101 → 1.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/core/commands/formatting-commands.ts +8 -7
  3. package/src/core/commands/paragraph-layout-commands.ts +11 -10
  4. package/src/core/commands/section-layout-commands.ts +7 -6
  5. package/src/core/commands/style-commands.ts +3 -2
  6. package/src/io/export/build-app-properties-xml.ts +24 -0
  7. package/src/io/normalize/normalize-text.ts +6 -5
  8. package/src/io/ooxml/docprops.ts +298 -0
  9. package/src/io/ooxml/parse-anchor.ts +15 -15
  10. package/src/io/ooxml/parse-drawing.ts +5 -5
  11. package/src/io/ooxml/parse-fields.ts +16 -15
  12. package/src/io/ooxml/parse-font-table.ts +2 -1
  13. package/src/io/ooxml/parse-footnotes.ts +3 -2
  14. package/src/io/ooxml/parse-headers-footers.ts +7 -6
  15. package/src/io/ooxml/parse-main-document.ts +41 -40
  16. package/src/io/ooxml/parse-numbering.ts +3 -2
  17. package/src/io/ooxml/parse-object.ts +6 -6
  18. package/src/io/ooxml/parse-paragraph-formatting.ts +12 -11
  19. package/src/io/ooxml/parse-picture.ts +16 -16
  20. package/src/io/ooxml/parse-run-formatting.ts +11 -10
  21. package/src/io/ooxml/parse-settings.ts +2 -1
  22. package/src/io/ooxml/parse-shapes.ts +18 -17
  23. package/src/io/ooxml/parse-styles.ts +16 -16
  24. package/src/io/ooxml/parse-theme.ts +5 -4
  25. package/src/model/canonical-document.ts +920 -815
  26. package/src/runtime/formatting/document-lookup.ts +3 -2
  27. package/src/runtime/formatting/formatting-context.ts +66 -25
  28. package/src/runtime/formatting/index.ts +18 -0
  29. package/src/runtime/formatting/layout-inputs.ts +256 -0
  30. package/src/runtime/formatting/numbering/geometry.ts +13 -12
  31. package/src/runtime/formatting/style-cascade.ts +2 -1
  32. package/src/runtime/formatting/table-style-resolver.ts +8 -7
  33. package/src/runtime/surface-projection.ts +31 -36
  34. package/src/session/export/stateful-export-pipeline.ts +9 -4
  35. package/src/session/export/stateful-export.ts +22 -6
  36. package/src/session/import/canonical-assembly.ts +2 -3
  37. package/src/session/import/loader-types.ts +3 -1
  38. package/src/session/import/loader.ts +12 -0
  39. package/src/session/import/normalize.ts +2 -1
  40. package/src/session/import/source-package-evidence.ts +1016 -0
  41. package/src/session/shared/session-utils.ts +9 -0
@@ -26,6 +26,7 @@ import type {
26
26
  TableNode,
27
27
  TextMark,
28
28
  TextNode,
29
+ Mutable,
29
30
  } from "../../model/canonical-document.ts";
30
31
 
31
32
  export interface FoundParagraph {
@@ -165,7 +166,7 @@ export function canonicalMarksToRunFormatting(
165
166
  marks: readonly TextMark[] | undefined,
166
167
  ): CanonicalRunFormatting | undefined {
167
168
  if (!marks || marks.length === 0) return undefined;
168
- const direct: CanonicalRunFormatting = {};
169
+ const direct: Mutable<CanonicalRunFormatting> = {};
169
170
  for (const mark of marks) {
170
171
  switch (mark.type) {
171
172
  case "bold":
@@ -190,7 +191,7 @@ export function canonicalMarksToRunFormatting(
190
191
  direct.smallCaps = true;
191
192
  break;
192
193
  case "fontFamily":
193
- direct.fontFamily = mark.val;
194
+ (direct as Mutable<typeof direct>).fontFamily = mark.val;
194
195
  direct.fontFamilyAscii = mark.val;
195
196
  break;
196
197
  case "fontSize":
@@ -44,6 +44,7 @@ import type {
44
44
  RevisionRecord,
45
45
  TableNode,
46
46
  TableStyleConditionalRegion,
47
+ Mutable,
47
48
  } from "../../model/canonical-document.ts";
48
49
  import type { FieldPageGraph, FieldResolver, ResolvedField } from "./field/resolver.ts";
49
50
  import { createFieldResolver } from "./field/resolver.ts";
@@ -68,6 +69,13 @@ import type {
68
69
  EffectiveParagraphFormatting,
69
70
  EffectiveRunFormatting,
70
71
  } from "./formatting-types.ts";
72
+ import {
73
+ collectFieldLayoutInputs,
74
+ toFieldLayoutInput,
75
+ toNumberingLayoutInput,
76
+ type FieldLayoutInput,
77
+ type NumberingLayoutInput,
78
+ } from "./layout-inputs.ts";
71
79
 
72
80
  /**
73
81
  * Direct-text-mark direct formatting. Mirrors (and replaces) the
@@ -242,6 +250,16 @@ export interface FormattingContext {
242
250
  options?: { advance?: boolean; emitGeometry?: boolean },
243
251
  ): NumberingResolution | null;
244
252
 
253
+ /**
254
+ * PE2 layout-ready numbering input. Normalizes the existing prefix result
255
+ * into marker/text-column/tab-stop fields L04 can measure without
256
+ * reinterpreting numbering semantics.
257
+ */
258
+ resolveNumberingLayoutInput(
259
+ para: ParagraphNode,
260
+ options?: { advance?: boolean; emitGeometry?: boolean },
261
+ ): NumberingLayoutInput | undefined;
262
+
245
263
  /**
246
264
  * Per-paragraph numbering-marker rPr cascade (ECMA-376 §17.9).
247
265
  * Mirrors `resolveNumberingMarkerRunFormatting` with the layer's
@@ -318,6 +336,12 @@ export interface FormattingContext {
318
336
  * context has no page-graph (so the resolver was never built). */
319
337
  resolveField(entry: FieldRegistryEntry): ResolvedField | undefined;
320
338
 
339
+ /** Resolve one registered field into the PE2 field-layout input shape. */
340
+ resolveFieldLayoutInput(entry: FieldRegistryEntry): FieldLayoutInput;
341
+
342
+ /** Resolve every field/TOC registry entry into PE2 field-layout inputs. */
343
+ collectFieldLayoutInputs(): readonly FieldLayoutInput[];
344
+
321
345
  /** Resolve the effective font family for a run following
322
346
  * ECMA-376 §17.3.2.26 precedence + theme-minor fallback. */
323
347
  resolveFontFamily(input: RunResolveInput, themeMinorFont?: string): string | undefined;
@@ -489,6 +513,13 @@ class FormattingContextImpl implements FormattingContext {
489
513
  return this.numbering.resolveDetailed(effectiveNumbering, para);
490
514
  }
491
515
 
516
+ resolveNumberingLayoutInput(
517
+ para: ParagraphNode,
518
+ options: { advance?: boolean; emitGeometry?: boolean } = {},
519
+ ): NumberingLayoutInput | undefined {
520
+ return toNumberingLayoutInput(this.resolveParagraphNumbering(para, options));
521
+ }
522
+
492
523
  resolveNumberingMarkerRunFormatting(
493
524
  paragraphStyleId: string | undefined,
494
525
  levelRunProperties: CanonicalRunFormatting | undefined,
@@ -599,6 +630,16 @@ class FormattingContextImpl implements FormattingContext {
599
630
  return this.field.resolve(entry);
600
631
  }
601
632
 
633
+ resolveFieldLayoutInput(entry: FieldRegistryEntry): FieldLayoutInput {
634
+ return toFieldLayoutInput(entry, this.resolveField(entry));
635
+ }
636
+
637
+ collectFieldLayoutInputs(): readonly FieldLayoutInput[] {
638
+ return collectFieldLayoutInputs(this.doc.fieldRegistry, (entry) =>
639
+ this.resolveField(entry),
640
+ );
641
+ }
642
+
602
643
  resolveFontFamily(
603
644
  input: RunResolveInput,
604
645
  themeMinorFont?: string,
@@ -635,7 +676,7 @@ class FormattingContextImpl implements FormattingContext {
635
676
  // Walk each tier in priority-ascending order, recording which tier
636
677
  // last set each field. Highest-priority writer wins — same order
637
678
  // as `resolveEffectiveRunFormatting` but with provenance tracked.
638
- const properties: { [K in keyof CanonicalRunFormatting]?: RunResolvedProperty } = {};
679
+ const properties: { -readonly [K in keyof CanonicalRunFormatting]?: RunResolvedProperty } = {};
639
680
  const applyTier = (
640
681
  tierRecord: CanonicalRunFormatting | undefined,
641
682
  source: RunResolvedProperty["source"],
@@ -755,25 +796,25 @@ function buildDirectRunFormattingFromProjected(
755
796
  projected: ProjectedRunMarks | undefined,
756
797
  ): CanonicalRunFormatting | undefined {
757
798
  if (!projected) return undefined;
758
- const direct: CanonicalRunFormatting = {};
799
+ const direct: Mutable<CanonicalRunFormatting> = {};
759
800
  const marks = projected.marks;
760
801
  if (marks) {
761
- if (marks.includes("bold")) direct.bold = true;
762
- if (marks.includes("italic")) direct.italic = true;
763
- if (marks.includes("underline")) direct.underline = "single";
764
- if (marks.includes("strikethrough")) direct.strikethrough = true;
765
- if (marks.includes("doubleStrikethrough")) direct.doubleStrikethrough = true;
766
- if (marks.includes("vanish")) direct.vanish = true;
767
- if (marks.includes("allCaps")) direct.allCaps = true;
802
+ if (marks.includes("bold")) (direct as Mutable<typeof direct>).bold = true;
803
+ if (marks.includes("italic")) (direct as Mutable<typeof direct>).italic = true;
804
+ if (marks.includes("underline")) (direct as Mutable<typeof direct>).underline = "single";
805
+ if (marks.includes("strikethrough")) (direct as Mutable<typeof direct>).strikethrough = true;
806
+ if (marks.includes("doubleStrikethrough")) (direct as Mutable<typeof direct>).doubleStrikethrough = true;
807
+ if (marks.includes("vanish")) (direct as Mutable<typeof direct>).vanish = true;
808
+ if (marks.includes("allCaps")) (direct as Mutable<typeof direct>).allCaps = true;
768
809
  if (marks.includes("smallCaps")) direct.smallCaps = true;
769
810
  }
770
811
  const markAttrs = projected.markAttrs;
771
812
  if (markAttrs) {
772
813
  if (markAttrs.fontFamily) {
773
- direct.fontFamily = markAttrs.fontFamily;
814
+ (direct as Mutable<typeof direct>).fontFamily = markAttrs.fontFamily;
774
815
  direct.fontFamilyAscii = markAttrs.fontFamily;
775
816
  }
776
- if (typeof markAttrs.fontSize === "number") {
817
+ if (typeof (markAttrs as Mutable<typeof markAttrs>).fontSize === "number") {
777
818
  direct.fontSizeHalfPoints = markAttrs.fontSize;
778
819
  }
779
820
  if (markAttrs.textColor) {
@@ -789,20 +830,20 @@ function buildDirectRunFormattingFromProjected(
789
830
  function extractDirectParagraphFormatting(
790
831
  para: ParagraphNode,
791
832
  ): CanonicalParagraphFormatting {
792
- const direct: CanonicalParagraphFormatting = {};
793
- if (para.alignment !== undefined) direct.alignment = para.alignment;
794
- if (para.spacing !== undefined) direct.spacing = para.spacing;
795
- if (para.contextualSpacing !== undefined) direct.contextualSpacing = para.contextualSpacing;
796
- if (para.indentation !== undefined) direct.indentation = para.indentation;
797
- if (para.tabStops !== undefined) direct.tabStops = para.tabStops;
798
- if (para.keepNext !== undefined) direct.keepNext = para.keepNext;
799
- if (para.keepLines !== undefined) direct.keepLines = para.keepLines;
800
- if (para.outlineLevel !== undefined) direct.outlineLevel = para.outlineLevel;
801
- if (para.pageBreakBefore !== undefined) direct.pageBreakBefore = para.pageBreakBefore;
802
- if (para.widowControl !== undefined) direct.widowControl = para.widowControl;
803
- if (para.borders !== undefined) direct.borders = para.borders;
804
- if (para.shading !== undefined) direct.shading = para.shading;
805
- if (para.bidi !== undefined) direct.bidi = para.bidi;
833
+ const direct: Mutable<CanonicalParagraphFormatting> = {};
834
+ if (para.alignment !== undefined) (direct as Mutable<typeof direct>).alignment = para.alignment;
835
+ if (para.spacing !== undefined) (direct as Mutable<typeof direct>).spacing = para.spacing;
836
+ if (para.contextualSpacing !== undefined) (direct as Mutable<typeof direct>).contextualSpacing = para.contextualSpacing;
837
+ if (para.indentation !== undefined) (direct as Mutable<typeof direct>).indentation = para.indentation;
838
+ if (para.tabStops !== undefined) (direct as Mutable<typeof direct>).tabStops = para.tabStops;
839
+ if (para.keepNext !== undefined) (direct as Mutable<typeof direct>).keepNext = para.keepNext;
840
+ if (para.keepLines !== undefined) (direct as Mutable<typeof direct>).keepLines = para.keepLines;
841
+ if (para.outlineLevel !== undefined) (direct as Mutable<typeof direct>).outlineLevel = para.outlineLevel;
842
+ if (para.pageBreakBefore !== undefined) (direct as Mutable<typeof direct>).pageBreakBefore = para.pageBreakBefore;
843
+ if (para.widowControl !== undefined) (direct as Mutable<typeof direct>).widowControl = para.widowControl;
844
+ if (para.borders !== undefined) (direct as Mutable<typeof direct>).borders = para.borders;
845
+ if (para.shading !== undefined) (direct as Mutable<typeof direct>).shading = para.shading;
846
+ if (para.bidi !== undefined) (direct as Mutable<typeof direct>).bidi = para.bidi;
806
847
  if (para.suppressLineNumbers !== undefined) direct.suppressLineNumbers = para.suppressLineNumbers;
807
848
  return direct;
808
849
  }
@@ -103,6 +103,24 @@ export { formatPageNumber } from "./field/page-number-format.ts";
103
103
 
104
104
  export { rebuildFieldRegistry } from "./field/registry.ts";
105
105
 
106
+ // ── PE2 layout-ready formatting inputs ───────────────────────────────────
107
+ export {
108
+ buildEffectiveLayoutFormatting,
109
+ buildRevisionLayoutPosture,
110
+ collectFieldLayoutInputs,
111
+ createStructuralHash,
112
+ normalizeNumberingMarkerSuffix,
113
+ toFieldLayoutInput,
114
+ toLayoutTabStops,
115
+ toNumberingLayoutInput,
116
+ type EffectiveLayoutFormatting,
117
+ type FieldLayoutInput,
118
+ type LayoutMarkerSuffix,
119
+ type LayoutTabStopInput,
120
+ type NumberingLayoutInput,
121
+ type RevisionLayoutPosture,
122
+ } from "./layout-inputs.ts";
123
+
106
124
  // ── Debug projector (Slice 3) ─────────────────────────────────────────────
107
125
  export {
108
126
  buildFormattingDebugEntry,
@@ -0,0 +1,256 @@
1
+ import type {
2
+ CanonicalParagraphFormatting,
3
+ CanonicalRunFormatting,
4
+ FieldFamily,
5
+ FieldRefreshStatus,
6
+ FieldRegistry,
7
+ FieldRegistryEntry,
8
+ TabStop,
9
+ } from "../../model/canonical-document.ts";
10
+ import type { ResolvedField } from "./field/resolver.ts";
11
+ import type {
12
+ EffectiveParagraphFormatting,
13
+ EffectiveRunFormatting,
14
+ EffectiveTableFormatting,
15
+ RevisionDisplayFlags,
16
+ } from "./formatting-types.ts";
17
+ import type { NumberingPrefixResult } from "./numbering/prefix.ts";
18
+
19
+ export type LayoutMarkerSuffix = "tab" | "space" | "none";
20
+
21
+ export interface LayoutTabStopInput {
22
+ readonly positionTwips: number;
23
+ readonly align: TabStop["align"];
24
+ readonly leader?: TabStop["leader"];
25
+ readonly source: "paragraph" | "numbering" | "toc";
26
+ }
27
+
28
+ export interface NumberingLayoutInput {
29
+ readonly markerText: string | null;
30
+ readonly markerRunFormatting?: CanonicalRunFormatting;
31
+ readonly markerSuffix: LayoutMarkerSuffix;
32
+ readonly markerLaneStartTwips?: number;
33
+ readonly markerLaneWidthTwips?: number;
34
+ readonly textColumnStartTwips?: number;
35
+ readonly hangingTwips?: number;
36
+ readonly associatedTabStops: readonly LayoutTabStopInput[];
37
+ readonly level: number;
38
+ readonly format: string;
39
+ readonly startAt: number;
40
+ readonly isLegalNumbering?: boolean;
41
+ readonly pictureBulletMediaId?: string;
42
+ }
43
+
44
+ export interface FieldLayoutInput {
45
+ readonly fieldId: string;
46
+ readonly fieldIndex: number;
47
+ readonly family: FieldFamily;
48
+ readonly instruction: string;
49
+ readonly cachedText: string;
50
+ readonly switches?: FieldRegistryEntry["switches"];
51
+ readonly targetBookmark?: string;
52
+ readonly refreshState: FieldRefreshStatus | "unresolved";
53
+ readonly resolvedText?: string;
54
+ readonly asHyperlink?: boolean;
55
+ readonly tocRegionId?: string;
56
+ readonly tocEntryCount?: number;
57
+ }
58
+
59
+ export interface RevisionLayoutPosture {
60
+ readonly mode: "clean" | "simple" | "all" | "original" | "no-markup";
61
+ readonly affectsMeasuredText: boolean;
62
+ readonly hiddenRevisionIds: readonly string[];
63
+ readonly visibleRevisionIds: readonly string[];
64
+ }
65
+
66
+ export interface EffectiveLayoutFormatting {
67
+ readonly paragraph?: EffectiveParagraphFormatting;
68
+ readonly runs: readonly EffectiveRunFormatting[];
69
+ readonly table?: EffectiveTableFormatting;
70
+ readonly cell?: {
71
+ readonly paragraph: CanonicalParagraphFormatting;
72
+ readonly run: CanonicalRunFormatting;
73
+ };
74
+ readonly numbering?: NumberingLayoutInput;
75
+ readonly tabs: readonly LayoutTabStopInput[];
76
+ readonly compatFlags: readonly string[];
77
+ readonly revisionPosture?: RevisionLayoutPosture;
78
+ readonly structuralHash: string;
79
+ }
80
+
81
+ export function normalizeNumberingMarkerSuffix(
82
+ suffix: NumberingPrefixResult["suffix"] | undefined,
83
+ ): LayoutMarkerSuffix {
84
+ if (suffix === "tab" || suffix === "space") return suffix;
85
+ return "none";
86
+ }
87
+
88
+ export function toLayoutTabStops(
89
+ tabStops: readonly TabStop[] | undefined,
90
+ source: LayoutTabStopInput["source"],
91
+ ): readonly LayoutTabStopInput[] {
92
+ if (!tabStops || tabStops.length === 0) return [];
93
+ return tabStops
94
+ .map((tabStop) => ({
95
+ positionTwips: tabStop.position,
96
+ align: tabStop.align,
97
+ ...(tabStop.leader ? { leader: tabStop.leader } : {}),
98
+ source,
99
+ }))
100
+ .sort((a, b) => a.positionTwips - b.positionTwips);
101
+ }
102
+
103
+ export function toNumberingLayoutInput(
104
+ numbering: NumberingPrefixResult | null | undefined,
105
+ ): NumberingLayoutInput | undefined {
106
+ if (!numbering) return undefined;
107
+ const markerLane = numbering.geometry.markerLane;
108
+ const textColumn = numbering.geometry.textColumn;
109
+ const hangingTwips =
110
+ textColumn?.hanging ??
111
+ numbering.geometry.indentation?.hanging ??
112
+ (typeof numbering.geometry.indentation?.firstLine === "number" &&
113
+ numbering.geometry.indentation.firstLine < 0
114
+ ? Math.abs(numbering.geometry.indentation.firstLine)
115
+ : undefined);
116
+
117
+ return {
118
+ markerText: numbering.text,
119
+ ...(numbering.markerRunProperties
120
+ ? { markerRunFormatting: numbering.markerRunProperties }
121
+ : {}),
122
+ markerSuffix: normalizeNumberingMarkerSuffix(numbering.suffix),
123
+ ...(markerLane ? { markerLaneStartTwips: markerLane.start } : {}),
124
+ ...(markerLane ? { markerLaneWidthTwips: markerLane.width } : {}),
125
+ ...(textColumn ? { textColumnStartTwips: textColumn.start } : {}),
126
+ ...(hangingTwips !== undefined ? { hangingTwips } : {}),
127
+ associatedTabStops: toLayoutTabStops(numbering.geometry.tabStops, "numbering"),
128
+ level: numbering.level,
129
+ format: numbering.format,
130
+ startAt: numbering.startAt,
131
+ ...(numbering.isLegalNumbering ? { isLegalNumbering: true } : {}),
132
+ ...(numbering.picBulletMediaId
133
+ ? { pictureBulletMediaId: numbering.picBulletMediaId }
134
+ : {}),
135
+ };
136
+ }
137
+
138
+ export function toFieldLayoutInput(
139
+ entry: FieldRegistryEntry,
140
+ resolved?: ResolvedField,
141
+ ): FieldLayoutInput {
142
+ return {
143
+ fieldId: `field-${entry.fieldIndex}`,
144
+ fieldIndex: entry.fieldIndex,
145
+ family: entry.fieldFamily,
146
+ instruction: entry.instruction,
147
+ cachedText: entry.displayText,
148
+ ...(entry.switches ? { switches: entry.switches } : {}),
149
+ ...(entry.fieldTarget ? { targetBookmark: entry.fieldTarget } : {}),
150
+ refreshState: resolved?.refreshStatus ?? entry.refreshStatus ?? "unresolved",
151
+ ...(resolved?.displayText !== undefined ? { resolvedText: resolved.displayText } : {}),
152
+ ...(resolved?.asHyperlink ? { asHyperlink: true } : {}),
153
+ };
154
+ }
155
+
156
+ export function collectFieldLayoutInputs(
157
+ registry: FieldRegistry | undefined,
158
+ resolve?: (entry: FieldRegistryEntry) => ResolvedField | undefined,
159
+ ): readonly FieldLayoutInput[] {
160
+ if (!registry) return [];
161
+ const inputs: FieldLayoutInput[] = [];
162
+ const entries = [...registry.supported, ...registry.preserveOnly];
163
+ for (const entry of entries) {
164
+ inputs.push(toFieldLayoutInput(entry, resolve?.(entry)));
165
+ }
166
+ for (const region of registry.tocRegions ?? []) {
167
+ const source = registry.supported.find(
168
+ (entry) => entry.fieldIndex === region.sourceFieldIndex,
169
+ );
170
+ if (!source) continue;
171
+ inputs.push({
172
+ ...toFieldLayoutInput(source, resolve?.(source)),
173
+ fieldId: `toc-${region.tocId}`,
174
+ family: "TOC",
175
+ instruction: region.instruction.raw,
176
+ cachedText: region.cachedEntries.map((entry) => entry.displayText).join("\n"),
177
+ refreshState: region.status === "current" ? "current" : "stale",
178
+ tocRegionId: region.tocId,
179
+ tocEntryCount: region.cachedEntries.length + region.generatedEntries.length,
180
+ });
181
+ }
182
+ return inputs;
183
+ }
184
+
185
+ export function buildRevisionLayoutPosture(
186
+ mode: RevisionLayoutPosture["mode"],
187
+ displays: readonly RevisionDisplayFlags[] = [],
188
+ ): RevisionLayoutPosture {
189
+ const hiddenRevisionIds = displays
190
+ .filter((display) => display.hidden === true)
191
+ .map((display) => display.revisionId);
192
+ const visibleRevisionIds = displays
193
+ .filter((display) => display.hidden !== true)
194
+ .map((display) => display.revisionId);
195
+ return {
196
+ mode,
197
+ affectsMeasuredText: hiddenRevisionIds.length > 0,
198
+ hiddenRevisionIds,
199
+ visibleRevisionIds,
200
+ };
201
+ }
202
+
203
+ export function buildEffectiveLayoutFormatting(input: {
204
+ readonly paragraph?: EffectiveParagraphFormatting;
205
+ readonly runs?: readonly EffectiveRunFormatting[];
206
+ readonly table?: EffectiveTableFormatting;
207
+ readonly cell?: {
208
+ readonly paragraph: CanonicalParagraphFormatting;
209
+ readonly run: CanonicalRunFormatting;
210
+ };
211
+ readonly numbering?: NumberingLayoutInput;
212
+ readonly tabs?: readonly LayoutTabStopInput[];
213
+ readonly compatFlags?: readonly string[];
214
+ readonly revisionPosture?: RevisionLayoutPosture;
215
+ }): EffectiveLayoutFormatting {
216
+ const withoutHash = {
217
+ ...(input.paragraph ? { paragraph: input.paragraph } : {}),
218
+ runs: input.runs ?? [],
219
+ ...(input.table ? { table: input.table } : {}),
220
+ ...(input.cell ? { cell: input.cell } : {}),
221
+ ...(input.numbering ? { numbering: input.numbering } : {}),
222
+ tabs: input.tabs ?? [],
223
+ compatFlags: input.compatFlags ?? [],
224
+ ...(input.revisionPosture ? { revisionPosture: input.revisionPosture } : {}),
225
+ };
226
+ return {
227
+ ...withoutHash,
228
+ structuralHash: createStructuralHash(withoutHash),
229
+ };
230
+ }
231
+
232
+ export function createStructuralHash(value: unknown): string {
233
+ const stable = stableStringify(value);
234
+ let hash = 0x811c9dc5;
235
+ for (let i = 0; i < stable.length; i += 1) {
236
+ hash ^= stable.charCodeAt(i);
237
+ hash = Math.imul(hash, 0x01000193);
238
+ }
239
+ return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
240
+ }
241
+
242
+ function stableStringify(value: unknown): string {
243
+ if (value === null || typeof value !== "object") {
244
+ return JSON.stringify(value);
245
+ }
246
+ if (Array.isArray(value)) {
247
+ return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
248
+ }
249
+ const record = value as Record<string, unknown>;
250
+ const keys = Object.keys(record)
251
+ .filter((key) => record[key] !== undefined)
252
+ .sort();
253
+ return `{${keys
254
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
255
+ .join(",")}}`;
256
+ }
@@ -8,6 +8,7 @@ import type {
8
8
  ParagraphNode,
9
9
  ParagraphSpacing,
10
10
  TabStop,
11
+ Mutable,
11
12
  } from "../../../model/canonical-document.ts";
12
13
 
13
14
  export const DEFAULT_NUMBERING_START_AT = 1;
@@ -52,7 +53,7 @@ export interface ResolvedNumberingDefinitionSet {
52
53
  }
53
54
 
54
55
  export function resolveNumberingDefinitionSet(
55
- catalog: NumberingCatalog,
56
+ catalog: Mutable<NumberingCatalog>,
56
57
  numbering: ParagraphNode["numbering"] | undefined,
57
58
  paragraph?: Pick<ParagraphNode, "spacing" | "indentation" | "tabStops">,
58
59
  ): ResolvedNumberingDefinitionSet | null {
@@ -145,7 +146,7 @@ function mergeLevelDefinition(
145
146
  };
146
147
  }
147
148
 
148
- function withDefaultStartAt(level: NumberingLevelDefinition): NumberingLevelDefinition {
149
+ function withDefaultStartAt(level: Mutable<NumberingLevelDefinition>): NumberingLevelDefinition {
149
150
  return {
150
151
  ...level,
151
152
  startAt: level.startAt ?? DEFAULT_NUMBERING_START_AT,
@@ -156,7 +157,7 @@ function withDefaultStartAt(level: NumberingLevelDefinition): NumberingLevelDefi
156
157
  }
157
158
 
158
159
  function cloneLevelParagraphGeometry(
159
- geometry: NumberingLevelParagraphGeometry,
160
+ geometry: Mutable<NumberingLevelParagraphGeometry>,
160
161
  ): NumberingLevelParagraphGeometry {
161
162
  return {
162
163
  ...(geometry.justification ? { justification: geometry.justification } : {}),
@@ -244,7 +245,7 @@ function mergeParagraphSpacing(
244
245
  return undefined;
245
246
  }
246
247
 
247
- const spacing: ParagraphSpacing = {
248
+ const spacing: Mutable<ParagraphSpacing> = {
248
249
  ...(base ?? {}),
249
250
  ...(override ?? {}),
250
251
  };
@@ -260,24 +261,24 @@ function mergeParagraphIndentation(
260
261
  return undefined;
261
262
  }
262
263
 
263
- const out: ParagraphIndentation = {};
264
+ const out: Mutable<ParagraphIndentation> = {};
264
265
 
265
266
  const left = override?.left ?? base?.left;
266
- if (left !== undefined) out.left = left;
267
+ if (left !== undefined) (out as Mutable<typeof out>).left = left;
267
268
 
268
269
  const right = override?.right ?? base?.right;
269
- if (right !== undefined) out.right = right;
270
+ if (right !== undefined) (out as Mutable<typeof out>).right = right;
270
271
 
271
272
  // ECMA-376 §17.3.1.12: firstLine and hanging are mutually exclusive on a single
272
273
  // w:ind element. When override specifies either, it wins exclusively.
273
274
  const overrideTouchesFLorH =
274
275
  override?.firstLine !== undefined || override?.hanging !== undefined;
275
276
  if (overrideTouchesFLorH) {
276
- if (override?.hanging !== undefined) out.hanging = override.hanging;
277
- else if (override?.firstLine !== undefined) out.firstLine = override.firstLine;
277
+ if (override?.hanging !== undefined) (out as Mutable<typeof out>).hanging = override.hanging;
278
+ else if (override?.firstLine !== undefined) (out as Mutable<typeof out>).firstLine = override.firstLine;
278
279
  } else {
279
- if (base?.hanging !== undefined) out.hanging = base.hanging;
280
- else if (base?.firstLine !== undefined) out.firstLine = base.firstLine;
280
+ if (base?.hanging !== undefined) (out as Mutable<typeof out>).hanging = base.hanging;
281
+ else if (base?.firstLine !== undefined) (out as Mutable<typeof out>).firstLine = base.firstLine;
281
282
  }
282
283
 
283
284
  return Object.keys(out).length > 0 ? out : undefined;
@@ -333,7 +334,7 @@ function deriveTextColumn(
333
334
  };
334
335
  }
335
336
 
336
- function resolveHangingWidth(indentation: ParagraphIndentation): number | undefined {
337
+ function resolveHangingWidth(indentation: Mutable<ParagraphIndentation>): number | undefined {
337
338
  if (indentation.hanging !== undefined) {
338
339
  return indentation.hanging;
339
340
  }
@@ -28,6 +28,7 @@ import type {
28
28
  StylesCatalog,
29
29
  TableStyleConditionalRegion,
30
30
  TableStyleDefinition,
31
+ Mutable,
31
32
  } from "../../model/canonical-document.ts";
32
33
  import {
33
34
  resolveCharacterStyleChain,
@@ -51,7 +52,7 @@ export function mergeParagraph(
51
52
  over: CanonicalParagraphFormatting | undefined,
52
53
  ): CanonicalParagraphFormatting | undefined {
53
54
  if (!base && !over) return undefined;
54
- const merged: CanonicalParagraphFormatting = { ...(base ?? {}), ...(over ?? {}) };
55
+ const merged: Mutable<CanonicalParagraphFormatting> = { ...(base ?? {}), ...(over ?? {}) };
55
56
  if (base?.spacing || over?.spacing) {
56
57
  merged.spacing = { ...(base?.spacing ?? {}), ...(over?.spacing ?? {}) };
57
58
  }
@@ -10,6 +10,7 @@ import type {
10
10
  TableStyleDefinition,
11
11
  TableStyleFormatting,
12
12
  TableWidth,
13
+ Mutable,
13
14
  } from "../../model/canonical-document.ts";
14
15
 
15
16
  export interface ResolvedTableCellStyle {
@@ -58,7 +59,7 @@ export interface ResolvedTableStyleResolution {
58
59
  }>;
59
60
  }
60
61
 
61
- const DEFAULT_EFFECTIVE_TABLE_LOOK: TableLook = {
62
+ const DEFAULT_EFFECTIVE_TABLE_LOOK: Mutable<TableLook> = {
62
63
  val: "04A0",
63
64
  firstRow: true,
64
65
  lastRow: false,
@@ -80,7 +81,7 @@ const CONDITIONAL_REGION_ORDER: TableStyleConditionalRegion[] = [
80
81
  ];
81
82
 
82
83
  export function resolveTableStyleResolution(
83
- table: TableNode,
84
+ table: Mutable<TableNode>,
84
85
  tableStyles: Record<string, TableStyleDefinition>,
85
86
  ): ResolvedTableStyleResolution {
86
87
  const resolvedStyle = table.styleId ? resolveTableStyleDefinition(table.styleId, tableStyles) : {};
@@ -266,7 +267,7 @@ function decodeTableLookMask(val: string | undefined): TableLook | undefined {
266
267
  function getActiveRowRegions(
267
268
  rowIndex: number,
268
269
  rowCount: number,
269
- effectiveTblLook: TableLook,
270
+ effectiveTblLook: Mutable<TableLook>,
270
271
  ): TableStyleConditionalRegion[] {
271
272
  const active = new Set<TableStyleConditionalRegion>();
272
273
 
@@ -294,7 +295,7 @@ function getActiveCellRegions(
294
295
  startColumn: number,
295
296
  endColumn: number,
296
297
  columnCount: number,
297
- effectiveTblLook: TableLook,
298
+ effectiveTblLook: Mutable<TableLook>,
298
299
  ): TableStyleConditionalRegion[] {
299
300
  const active = new Set<TableStyleConditionalRegion>(rowRegions);
300
301
 
@@ -343,7 +344,7 @@ function mergeWholeStyleFormatting(
343
344
  return undefined;
344
345
  }
345
346
 
346
- const merged: TableStyleFormatting = {};
347
+ const merged: Mutable<TableStyleFormatting> = {};
347
348
  const table = mergeTableFormatting(base?.table, override?.table);
348
349
  const row = mergeRowFormatting(base?.row, override?.row);
349
350
  const cell = mergeCellFormatting(base?.cell, override?.cell);
@@ -429,14 +430,14 @@ function mergeBorderMap<T extends TableBorders | TableCellBorders>(
429
430
  }
430
431
 
431
432
  const sides = ["top", "left", "bottom", "right", "insideH", "insideV"] as const;
432
- const merged = {} as T;
433
+ const merged = {} as Mutable<T>;
433
434
  for (const side of sides) {
434
435
  const spec = mergePlainObject(base?.[side], override?.[side]) as BorderSpec | undefined;
435
436
  if (spec) {
436
437
  merged[side] = spec as T[typeof side];
437
438
  }
438
439
  }
439
- return Object.keys(merged).length > 0 ? merged : undefined;
440
+ return Object.keys(merged).length > 0 ? (merged as T) : undefined;
440
441
  }
441
442
 
442
443
  function mergePlainObject<T extends object>(base: T | undefined, override: T | undefined): T | undefined {