@beyondwork/docx-react-component 1.0.36 → 1.0.37

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 (64) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +83 -0
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/selection/mapping.ts +6 -0
  7. package/src/io/docx-session.ts +24 -9
  8. package/src/io/export/build-app-properties-xml.ts +88 -0
  9. package/src/io/export/serialize-comments.ts +6 -1
  10. package/src/io/export/serialize-footnotes.ts +10 -9
  11. package/src/io/export/serialize-headers-footers.ts +11 -10
  12. package/src/io/export/serialize-main-document.ts +337 -50
  13. package/src/io/export/serialize-numbering.ts +115 -24
  14. package/src/io/export/serialize-tables.ts +13 -11
  15. package/src/io/export/table-properties-xml.ts +35 -16
  16. package/src/io/export/twip.ts +66 -0
  17. package/src/io/normalize/normalize-text.ts +5 -0
  18. package/src/io/ooxml/parse-footnotes.ts +2 -1
  19. package/src/io/ooxml/parse-headers-footers.ts +2 -1
  20. package/src/io/ooxml/parse-main-document.ts +21 -1
  21. package/src/legal/bookmarks.ts +78 -0
  22. package/src/model/canonical-document.ts +11 -0
  23. package/src/review/store/scope-tag-diff.ts +130 -0
  24. package/src/runtime/document-navigation.ts +1 -305
  25. package/src/runtime/document-runtime.ts +173 -11
  26. package/src/runtime/layout/docx-font-loader.ts +143 -0
  27. package/src/runtime/layout/index.ts +188 -0
  28. package/src/runtime/layout/inert-layout-facet.ts +45 -0
  29. package/src/runtime/layout/layout-engine-instance.ts +618 -0
  30. package/src/runtime/layout/layout-invalidation.ts +257 -0
  31. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  32. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  33. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  34. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  35. package/src/runtime/layout/page-graph.ts +433 -0
  36. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  37. package/src/runtime/layout/page-story-resolver.ts +195 -0
  38. package/src/runtime/layout/paginated-layout-engine.ts +788 -0
  39. package/src/runtime/layout/public-facet.ts +705 -0
  40. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  41. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  42. package/src/runtime/scope-tag-registry.ts +95 -0
  43. package/src/runtime/surface-projection.ts +1 -0
  44. package/src/runtime/text-ack-range.ts +49 -0
  45. package/src/ui/WordReviewEditor.tsx +15 -0
  46. package/src/ui/editor-runtime-boundary.ts +10 -1
  47. package/src/ui/editor-surface-controller.tsx +3 -0
  48. package/src/ui/headless/chrome-registry.ts +235 -0
  49. package/src/ui/headless/scoped-chrome-policy.ts +164 -0
  50. package/src/ui/headless/selection-tool-context.ts +2 -0
  51. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  52. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
  53. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
  54. package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
  55. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
  56. package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
  57. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  58. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  59. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
  61. package/src/ui-tailwind/theme/editor-theme.css +40 -14
  62. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  63. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
  64. package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Document-level ResolvedFormattingState.
3
+ *
4
+ * Per the architecture spec (runtime-owned-paginated-layout-engine.md §1), the
5
+ * layout engine needs a single inspectable surface that aggregates every
6
+ * formatting input required for page composition:
7
+ *
8
+ * paragraphs, runs, fonts, tabs, numbering geometry, document settings
9
+ *
10
+ * This module builds that aggregate once per document snapshot and hands it to
11
+ * the engine. Per-block lookups become O(1) map reads instead of re-deriving
12
+ * the same resolution from multiple sources.
13
+ *
14
+ * Important:
15
+ * - This state is derived entirely from the canonical envelope; it never
16
+ * reads DOM or PM state.
17
+ * - It is read-only once constructed. Mutations produce a new instance.
18
+ * - The canonical model here carries limited style/run metadata; the
19
+ * resolver therefore synthesizes per-segment `ResolvedRunFormatting` from
20
+ * the surface snapshot's `markAttrs` plus theme minor/major fonts.
21
+ */
22
+
23
+ import type {
24
+ EditorSurfaceSnapshot,
25
+ SurfaceBlockSnapshot,
26
+ SurfaceInlineSegment,
27
+ } from "../../api/public-types";
28
+ import type {
29
+ CanonicalDocumentEnvelope,
30
+ } from "../../core/state/editor-state.ts";
31
+ import type {
32
+ DocumentSettings,
33
+ NumberingInstance,
34
+ ResolvedTheme,
35
+ SubPartsCatalog,
36
+ } from "../../model/canonical-document.ts";
37
+ import {
38
+ resolveBlockFormatting,
39
+ type ResolvedParagraphFormatting,
40
+ } from "./resolved-formatting-state.ts";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** Stable run id: `${blockId}:${segmentId}`. */
47
+ export type RunId = string;
48
+
49
+ export interface ResolvedRunFormatting {
50
+ fontFamily?: string;
51
+ /** Resolved font size in half-points. */
52
+ fontSizeHalfPoints?: number;
53
+ bold: boolean;
54
+ italic: boolean;
55
+ underline: boolean;
56
+ strikethrough: boolean;
57
+ color?: string;
58
+ highlight?: string;
59
+ verticalAlign: "baseline" | "superscript" | "subscript";
60
+ }
61
+
62
+ export interface ResolvedFontDescriptor {
63
+ family: string;
64
+ /** Which heading tier, if any, this family represents in the theme. */
65
+ themeRole?: "major" | "minor";
66
+ }
67
+
68
+ export interface ResolvedFontCatalog {
69
+ families: Map<string, ResolvedFontDescriptor>;
70
+ /** Theme minor (body) font, if the package declares one. */
71
+ minorFont?: string;
72
+ /** Theme major (heading) font, if the package declares one. */
73
+ majorFont?: string;
74
+ }
75
+
76
+ export interface ResolvedTabSettings {
77
+ /** Document default tab interval in twips. */
78
+ defaultTabInterval: number;
79
+ }
80
+
81
+ export interface NumberingInstanceMetadata {
82
+ instanceId: string;
83
+ abstractId: string | undefined;
84
+ maxLevels: number;
85
+ }
86
+
87
+ export interface ResolvedNumberingGeometry {
88
+ instances: Map<string, NumberingInstanceMetadata>;
89
+ }
90
+
91
+ export interface ResolvedDocumentSettings {
92
+ /** Whether odd/even header/footer rendering is active. */
93
+ evenAndOddHeaders: boolean;
94
+ /** Widow/orphan default (always true in the current model). */
95
+ widowControlDefault: boolean;
96
+ }
97
+
98
+ export interface ResolvedFormattingState {
99
+ paragraphs: Map<string, ResolvedParagraphFormatting>;
100
+ runs: Map<RunId, ResolvedRunFormatting>;
101
+ fonts: ResolvedFontCatalog;
102
+ tabs: ResolvedTabSettings;
103
+ numbering: ResolvedNumberingGeometry;
104
+ settings: ResolvedDocumentSettings;
105
+ revision: number;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Builder
110
+ // ---------------------------------------------------------------------------
111
+
112
+ let revisionCounter = 0;
113
+
114
+ export function buildResolvedFormattingState(
115
+ document: CanonicalDocumentEnvelope,
116
+ mainSurface: EditorSurfaceSnapshot,
117
+ ): ResolvedFormattingState {
118
+ revisionCounter += 1;
119
+
120
+ const paragraphs = new Map<string, ResolvedParagraphFormatting>();
121
+ const runs = new Map<RunId, ResolvedRunFormatting>();
122
+ const usedFamilies = new Set<string>();
123
+
124
+ const theme = document.subParts?.resolvedTheme;
125
+ collectFormatting(mainSurface.blocks, paragraphs, runs, usedFamilies, theme);
126
+
127
+ return {
128
+ paragraphs,
129
+ runs,
130
+ fonts: buildFontCatalog(usedFamilies, document.subParts),
131
+ tabs: buildTabSettings(),
132
+ numbering: buildNumberingGeometry(document.subParts),
133
+ settings: buildDocumentSettings(document.subParts?.settings),
134
+ revision: revisionCounter,
135
+ };
136
+ }
137
+
138
+ function collectFormatting(
139
+ blocks: readonly SurfaceBlockSnapshot[],
140
+ paragraphs: Map<string, ResolvedParagraphFormatting>,
141
+ runs: Map<RunId, ResolvedRunFormatting>,
142
+ usedFamilies: Set<string>,
143
+ theme: ResolvedTheme | undefined,
144
+ ): void {
145
+ for (const block of blocks) {
146
+ switch (block.kind) {
147
+ case "paragraph": {
148
+ const formatting = resolveBlockFormatting(block);
149
+ if (formatting) {
150
+ paragraphs.set(block.blockId, formatting);
151
+ }
152
+ for (const segment of block.segments) {
153
+ if (segment.kind !== "text") continue;
154
+ const runId = `${block.blockId}:${segment.segmentId}`;
155
+ const resolved = resolveRunFormattingFromSegment(segment, theme);
156
+ runs.set(runId, resolved);
157
+ if (resolved.fontFamily) {
158
+ usedFamilies.add(resolved.fontFamily);
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ case "table":
164
+ for (const row of block.rows) {
165
+ for (const cell of row.cells) {
166
+ collectFormatting(cell.content, paragraphs, runs, usedFamilies, theme);
167
+ }
168
+ }
169
+ break;
170
+ case "sdt_block":
171
+ collectFormatting(block.children, paragraphs, runs, usedFamilies, theme);
172
+ break;
173
+ }
174
+ }
175
+ }
176
+
177
+ function resolveRunFormattingFromSegment(
178
+ segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
179
+ theme: ResolvedTheme | undefined,
180
+ ): ResolvedRunFormatting {
181
+ const markAttrs = segment.markAttrs ?? {};
182
+ const markSet = new Set(segment.marks ?? []);
183
+ const explicitFamily =
184
+ typeof markAttrs.fontFamily === "string" ? markAttrs.fontFamily : undefined;
185
+ // When an explicit family isn't present, fall back to theme minor font as
186
+ // per OOXML default (body text uses the minor font from the theme).
187
+ const fontFamily = explicitFamily ?? theme?.minorFont;
188
+
189
+ return {
190
+ ...(fontFamily ? { fontFamily } : {}),
191
+ ...(typeof markAttrs.fontSize === "number"
192
+ ? { fontSizeHalfPoints: markAttrs.fontSize }
193
+ : {}),
194
+ bold: markSet.has("bold"),
195
+ italic: markSet.has("italic"),
196
+ underline: markSet.has("underline"),
197
+ strikethrough: markSet.has("strikethrough") || markSet.has("doubleStrikethrough"),
198
+ ...(typeof markAttrs.textColor === "string" ? { color: markAttrs.textColor } : {}),
199
+ ...(typeof markAttrs.backgroundColor === "string"
200
+ ? { highlight: markAttrs.backgroundColor }
201
+ : {}),
202
+ verticalAlign: markSet.has("superscript")
203
+ ? "superscript"
204
+ : markSet.has("subscript")
205
+ ? "subscript"
206
+ : "baseline",
207
+ };
208
+ }
209
+
210
+ function buildFontCatalog(
211
+ usedFamilies: ReadonlySet<string>,
212
+ subParts: SubPartsCatalog | undefined,
213
+ ): ResolvedFontCatalog {
214
+ const families = new Map<string, ResolvedFontDescriptor>();
215
+ const theme = subParts?.resolvedTheme;
216
+ for (const family of usedFamilies) {
217
+ const normalized = family.toLowerCase();
218
+ const isMinor =
219
+ theme?.minorFont !== undefined && family === theme.minorFont;
220
+ const isMajor =
221
+ theme?.majorFont !== undefined && family === theme.majorFont;
222
+ families.set(normalized, {
223
+ family,
224
+ ...(isMinor ? { themeRole: "minor" as const } : {}),
225
+ ...(isMajor ? { themeRole: "major" as const } : {}),
226
+ });
227
+ }
228
+ if (theme?.minorFont && !families.has(theme.minorFont.toLowerCase())) {
229
+ families.set(theme.minorFont.toLowerCase(), {
230
+ family: theme.minorFont,
231
+ themeRole: "minor",
232
+ });
233
+ }
234
+ if (theme?.majorFont && !families.has(theme.majorFont.toLowerCase())) {
235
+ families.set(theme.majorFont.toLowerCase(), {
236
+ family: theme.majorFont,
237
+ themeRole: "major",
238
+ });
239
+ }
240
+ return {
241
+ families,
242
+ ...(theme?.minorFont ? { minorFont: theme.minorFont } : {}),
243
+ ...(theme?.majorFont ? { majorFont: theme.majorFont } : {}),
244
+ };
245
+ }
246
+
247
+ function buildTabSettings(): ResolvedTabSettings {
248
+ // Current DocumentSettings model does not carry `defaultTabStop`.
249
+ // Word's default is 720 twips (0.5"); retain that until settings exposes one.
250
+ return {
251
+ defaultTabInterval: 720,
252
+ };
253
+ }
254
+
255
+ function buildNumberingGeometry(
256
+ subParts: SubPartsCatalog | undefined,
257
+ ): ResolvedNumberingGeometry {
258
+ // Numbering is imported separately into runtime state (not on subParts);
259
+ // for layout we only need instance metadata, which is sufficient to dirty
260
+ // layout when a numbering instance changes.
261
+ const instances = new Map<string, NumberingInstanceMetadata>();
262
+ // `subParts` presence check keeps the parameter used for future expansion.
263
+ if (!subParts) return { instances };
264
+ return { instances };
265
+ }
266
+
267
+ function buildDocumentSettings(
268
+ settings: DocumentSettings | undefined,
269
+ ): ResolvedDocumentSettings {
270
+ return {
271
+ evenAndOddHeaders: Boolean(settings?.evenAndOddHeaders),
272
+ widowControlDefault: true,
273
+ };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Helper: cheap lookup
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export function getResolvedParagraph(
281
+ state: ResolvedFormattingState,
282
+ blockId: string,
283
+ ): ResolvedParagraphFormatting | undefined {
284
+ return state.paragraphs.get(blockId);
285
+ }
286
+
287
+ export function getResolvedRun(
288
+ state: ResolvedFormattingState,
289
+ runId: RunId,
290
+ ): ResolvedRunFormatting | undefined {
291
+ return state.runs.get(runId);
292
+ }
293
+
294
+ /** Collect the set of font families actually used anywhere in the document. */
295
+ export function collectUsedFontFamilies(
296
+ state: ResolvedFormattingState,
297
+ ): readonly string[] {
298
+ const out = new Set<string>();
299
+ for (const descriptor of state.fonts.families.values()) {
300
+ out.add(descriptor.family);
301
+ }
302
+ return Array.from(out);
303
+ }
304
+
305
+ /** Accumulate `NumberingInstance` metadata from runtime numbering state. */
306
+ export function populateNumberingInstances(
307
+ state: ResolvedFormattingState,
308
+ instances: ReadonlyArray<NumberingInstance>,
309
+ ): void {
310
+ for (const instance of instances) {
311
+ state.numbering.instances.set(instance.numberingInstanceId, {
312
+ instanceId: instance.numberingInstanceId,
313
+ abstractId: instance.abstractNumberingId,
314
+ maxLevels: instance.overrides?.length ?? 0,
315
+ });
316
+ }
317
+ }