@beyondwork/docx-react-component 1.0.35 → 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.
- package/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +84 -1
- package/src/core/commands/index.ts +19 -2
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +337 -50
- package/src/io/export/serialize-numbering.ts +115 -24
- package/src/io/export/serialize-tables.ts +13 -11
- package/src/io/export/table-properties-xml.ts +35 -16
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +5 -0
- package/src/io/ooxml/parse-footnotes.ts +2 -1
- package/src/io/ooxml/parse-headers-footers.ts +2 -1
- package/src/io/ooxml/parse-main-document.ts +21 -1
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +11 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-navigation.ts +1 -305
- package/src/runtime/document-runtime.ts +178 -16
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +188 -0
- package/src/runtime/layout/inert-layout-facet.ts +45 -0
- package/src/runtime/layout/layout-engine-instance.ts +618 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +433 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +788 -0
- package/src/runtime/layout/public-facet.ts +705 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/session-capabilities.ts +7 -4
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/ui/WordReviewEditor.tsx +15 -0
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +235 -0
- package/src/ui/headless/scoped-chrome-policy.ts +164 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
- package/src/ui-tailwind/theme/editor-theme.css +40 -14
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
|
@@ -47,12 +47,14 @@ import type {
|
|
|
47
47
|
RuntimeContextAnalyticsQuery,
|
|
48
48
|
RuntimeContextAnalyticsSnapshot,
|
|
49
49
|
RuntimeRenderSnapshot,
|
|
50
|
+
ScopeTagTouch,
|
|
50
51
|
SelectionSnapshot,
|
|
51
52
|
SnapshotRefreshHints,
|
|
52
53
|
SuggestionsSnapshot,
|
|
53
54
|
SurfaceBlockSnapshot,
|
|
54
55
|
SurfaceInlineSegment,
|
|
55
56
|
StoryTextStreamSnapshot,
|
|
57
|
+
TextCommandAck,
|
|
56
58
|
TocSnapshot,
|
|
57
59
|
StyleCatalogSnapshot,
|
|
58
60
|
TocRefreshOptions,
|
|
@@ -121,6 +123,12 @@ import {
|
|
|
121
123
|
createDocumentNavigationSnapshot,
|
|
122
124
|
findPageForOffset,
|
|
123
125
|
} from "./document-navigation.ts";
|
|
126
|
+
import {
|
|
127
|
+
createLayoutEngine,
|
|
128
|
+
createLayoutFacet,
|
|
129
|
+
type LayoutEngineInstance,
|
|
130
|
+
type WordReviewEditorLayoutFacet,
|
|
131
|
+
} from "./layout/index.ts";
|
|
124
132
|
import {
|
|
125
133
|
createDocumentOutlineSnapshot,
|
|
126
134
|
createDocumentSectionSnapshots,
|
|
@@ -150,6 +158,7 @@ import {
|
|
|
150
158
|
resolveActiveSection,
|
|
151
159
|
} from "./document-layout.ts";
|
|
152
160
|
import { normalizeHeaderFooterTarget } from "./story-context.ts";
|
|
161
|
+
import { computeAdjustedRange as computeAdjustedRangeImpl } from "./text-ack-range.ts";
|
|
153
162
|
import {
|
|
154
163
|
getStoryBlocks,
|
|
155
164
|
replaceStoryBlocks,
|
|
@@ -213,7 +222,7 @@ export interface DocumentRuntime {
|
|
|
213
222
|
getCanonicalDocument(): CanonicalDocumentEnvelope;
|
|
214
223
|
getSourcePackage(): EditorSessionState["sourcePackage"] | undefined;
|
|
215
224
|
replaceText(text: string, target?: EditorAnchorProjection): void;
|
|
216
|
-
applyActiveStoryTextCommand(command: ActiveStoryTextCommand):
|
|
225
|
+
applyActiveStoryTextCommand(command: ActiveStoryTextCommand): TextCommandAck;
|
|
217
226
|
dispatch(command: EditorCommand): void;
|
|
218
227
|
emitBlockedCommand(command: string, reasons: WorkflowBlockedCommandReason[]): void;
|
|
219
228
|
undo(): void;
|
|
@@ -243,6 +252,12 @@ export interface DocumentRuntime {
|
|
|
243
252
|
setZoom(level: ZoomLevel): void;
|
|
244
253
|
getPageLayoutSnapshot(): PageLayoutSnapshot | null;
|
|
245
254
|
getDocumentNavigationSnapshot(): DocumentNavigationSnapshot;
|
|
255
|
+
/**
|
|
256
|
+
* Runtime-owned layout facet. Provides graph-aware queries, fragment
|
|
257
|
+
* resolution, formatting inspection, and layout events. Prefer this over
|
|
258
|
+
* the opaque snapshot methods for new integration code.
|
|
259
|
+
*/
|
|
260
|
+
readonly layout: WordReviewEditorLayoutFacet;
|
|
246
261
|
getCurrentLocation(): DocumentLocationSnapshot | null;
|
|
247
262
|
getLocationForSelection(selection: SelectionSnapshot): DocumentLocationSnapshot | null;
|
|
248
263
|
getLocationForAnchor(
|
|
@@ -385,6 +400,23 @@ export function createDocumentRuntime(
|
|
|
385
400
|
fatalError: options.fatalError as never,
|
|
386
401
|
});
|
|
387
402
|
storySelections.set(storyTargetKey(MAIN_STORY_TARGET), state.selection);
|
|
403
|
+
|
|
404
|
+
// Runtime-owned paginated layout engine (Phase 1+ of the layout facet work).
|
|
405
|
+
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
406
|
+
// (content, styles, subParts). It is the single internal source of truth
|
|
407
|
+
// for page composition, story resolution, and layout invalidation.
|
|
408
|
+
const layoutEngine: LayoutEngineInstance = createLayoutEngine();
|
|
409
|
+
const layoutFacet: WordReviewEditorLayoutFacet = createLayoutFacet({
|
|
410
|
+
engine: layoutEngine,
|
|
411
|
+
getQueryInput: () => ({
|
|
412
|
+
document: state.document,
|
|
413
|
+
viewState: {
|
|
414
|
+
activeStory,
|
|
415
|
+
workspaceMode: viewState.workspaceMode,
|
|
416
|
+
zoomLevel: viewState.zoomLevel,
|
|
417
|
+
},
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
388
420
|
let cachedSurface:
|
|
389
421
|
| {
|
|
390
422
|
revisionToken: string;
|
|
@@ -738,7 +770,7 @@ export function createDocumentRuntime(
|
|
|
738
770
|
});
|
|
739
771
|
}
|
|
740
772
|
|
|
741
|
-
if (viewState.documentMode === "viewing") {
|
|
773
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
|
|
742
774
|
reasons.push({
|
|
743
775
|
code: "document_viewing_mode",
|
|
744
776
|
message: "Document is in viewing mode.",
|
|
@@ -822,8 +854,8 @@ export function createDocumentRuntime(
|
|
|
822
854
|
function getEffectiveDocumentMode(
|
|
823
855
|
selection: EditorState["selection"],
|
|
824
856
|
): DocumentMode {
|
|
825
|
-
if (viewState.documentMode === "viewing") {
|
|
826
|
-
return
|
|
857
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") {
|
|
858
|
+
return viewState.documentMode;
|
|
827
859
|
}
|
|
828
860
|
const matchingScope = getMatchingWorkflowScope(selection);
|
|
829
861
|
if (matchingScope?.mode === "suggest") {
|
|
@@ -1529,13 +1561,13 @@ export function createDocumentRuntime(
|
|
|
1529
1561
|
}
|
|
1530
1562
|
|
|
1531
1563
|
if (command.type === "history.undo") {
|
|
1532
|
-
if (viewState.documentMode === "viewing") return;
|
|
1564
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") return;
|
|
1533
1565
|
applyHistory("undo");
|
|
1534
1566
|
return;
|
|
1535
1567
|
}
|
|
1536
1568
|
|
|
1537
1569
|
if (command.type === "history.redo") {
|
|
1538
|
-
if (viewState.documentMode === "viewing") return;
|
|
1570
|
+
if (viewState.documentMode === "viewing" || viewState.documentMode === "commenting") return;
|
|
1539
1571
|
applyHistory("redo");
|
|
1540
1572
|
return;
|
|
1541
1573
|
}
|
|
@@ -1604,9 +1636,21 @@ export function createDocumentRuntime(
|
|
|
1604
1636
|
},
|
|
1605
1637
|
applyActiveStoryTextCommand(command) {
|
|
1606
1638
|
try {
|
|
1607
|
-
applyTextCommandInActiveStory(command);
|
|
1639
|
+
return applyTextCommandInActiveStory(command);
|
|
1608
1640
|
} catch (error) {
|
|
1609
|
-
|
|
1641
|
+
const runtimeError = toRuntimeError(error);
|
|
1642
|
+
emitError(runtimeError);
|
|
1643
|
+
return {
|
|
1644
|
+
kind: "rejected",
|
|
1645
|
+
opId: (command.origin as { opId?: string } | undefined)?.opId,
|
|
1646
|
+
newRevisionToken: "",
|
|
1647
|
+
blockedReasons: [
|
|
1648
|
+
{
|
|
1649
|
+
code: runtimeError.code ?? "runtime_error",
|
|
1650
|
+
message: runtimeError.message,
|
|
1651
|
+
},
|
|
1652
|
+
],
|
|
1653
|
+
};
|
|
1610
1654
|
}
|
|
1611
1655
|
},
|
|
1612
1656
|
addComment(params) {
|
|
@@ -1813,6 +1857,7 @@ export function createDocumentRuntime(
|
|
|
1813
1857
|
getDocumentNavigationSnapshot() {
|
|
1814
1858
|
return getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
1815
1859
|
},
|
|
1860
|
+
layout: layoutFacet,
|
|
1816
1861
|
getCurrentLocation() {
|
|
1817
1862
|
const navigation = getCachedDocumentNavigationSnapshot(state, activeStory);
|
|
1818
1863
|
return createCurrentLocation({
|
|
@@ -2272,6 +2317,25 @@ export function createDocumentRuntime(
|
|
|
2272
2317
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
2273
2318
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
2274
2319
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
2320
|
+
|
|
2321
|
+
// Signal a bounded content-edit invalidation to the layout engine so the
|
|
2322
|
+
// next layout query can splice rather than rebuild the full graph. The
|
|
2323
|
+
// engine analyzes the reason against its cached graph and falls back to
|
|
2324
|
+
// a full rebuild when the edit crosses section boundaries or reaches a
|
|
2325
|
+
// page the engine cannot safely resume from.
|
|
2326
|
+
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
2327
|
+
let minFrom = Infinity;
|
|
2328
|
+
let maxTo = -Infinity;
|
|
2329
|
+
for (const step of transaction.mapping.steps) {
|
|
2330
|
+
if (step.from < minFrom) minFrom = step.from;
|
|
2331
|
+
const end = step.from + step.insertSize;
|
|
2332
|
+
if (end > maxTo) maxTo = end;
|
|
2333
|
+
}
|
|
2334
|
+
if (minFrom < maxTo) {
|
|
2335
|
+
layoutEngine.invalidate({ kind: "content-edit", from: minFrom, to: maxTo });
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2275
2339
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2276
2340
|
notify(previous, state, transaction);
|
|
2277
2341
|
}
|
|
@@ -2455,24 +2519,31 @@ export function createDocumentRuntime(
|
|
|
2455
2519
|
selection?: EditorState["selection"];
|
|
2456
2520
|
blockedCommandName?: string;
|
|
2457
2521
|
} = {},
|
|
2458
|
-
):
|
|
2522
|
+
): TextCommandAck {
|
|
2523
|
+
const opId = (command.origin as { opId?: string } | undefined)?.opId;
|
|
2459
2524
|
const selection = options.selection ?? state.selection;
|
|
2460
2525
|
if (
|
|
2461
2526
|
activeStory.kind !== "main" &&
|
|
2462
2527
|
getEffectiveDocumentMode(selection) === "suggesting" &&
|
|
2463
2528
|
command.type === "paragraph.split"
|
|
2464
2529
|
) {
|
|
2530
|
+
const message = `"${command.type}" is not supported in suggesting mode for this story.`;
|
|
2465
2531
|
emit({
|
|
2466
2532
|
type: "command_blocked",
|
|
2467
2533
|
documentId: state.documentId,
|
|
2468
2534
|
command: options.blockedCommandName ?? command.type,
|
|
2469
2535
|
reasons: [{
|
|
2470
2536
|
code: "suggesting_unsupported",
|
|
2471
|
-
message
|
|
2537
|
+
message,
|
|
2472
2538
|
storyTarget: activeStory,
|
|
2473
2539
|
}],
|
|
2474
2540
|
});
|
|
2475
|
-
return
|
|
2541
|
+
return {
|
|
2542
|
+
kind: "rejected",
|
|
2543
|
+
opId,
|
|
2544
|
+
newRevisionToken: "",
|
|
2545
|
+
blockedReasons: [{ code: "suggesting_unsupported", message }],
|
|
2546
|
+
};
|
|
2476
2547
|
}
|
|
2477
2548
|
const blockedReasons = evaluateWorkflowBlockedReasons(selection, command.type);
|
|
2478
2549
|
if (blockedReasons.length > 0) {
|
|
@@ -2482,7 +2553,12 @@ export function createDocumentRuntime(
|
|
|
2482
2553
|
command: options.blockedCommandName ?? command.type,
|
|
2483
2554
|
reasons: blockedReasons,
|
|
2484
2555
|
});
|
|
2485
|
-
return
|
|
2556
|
+
return {
|
|
2557
|
+
kind: "rejected",
|
|
2558
|
+
opId,
|
|
2559
|
+
newRevisionToken: "",
|
|
2560
|
+
blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
|
|
2561
|
+
};
|
|
2486
2562
|
}
|
|
2487
2563
|
|
|
2488
2564
|
const timestamp = command.origin?.timestamp ?? clock();
|
|
@@ -2499,8 +2575,15 @@ export function createDocumentRuntime(
|
|
|
2499
2575
|
};
|
|
2500
2576
|
|
|
2501
2577
|
if (activeStory.kind === "main") {
|
|
2502
|
-
|
|
2503
|
-
|
|
2578
|
+
const mainTransaction = executeEditorCommand(baseState, command, context);
|
|
2579
|
+
commit(mainTransaction);
|
|
2580
|
+
return classifyAck({
|
|
2581
|
+
command,
|
|
2582
|
+
opId,
|
|
2583
|
+
priorState: baseState,
|
|
2584
|
+
transaction: mainTransaction,
|
|
2585
|
+
newRevisionToken: state.revisionToken,
|
|
2586
|
+
});
|
|
2504
2587
|
}
|
|
2505
2588
|
|
|
2506
2589
|
const localState = createEditorState({
|
|
@@ -2531,7 +2614,11 @@ export function createDocumentRuntime(
|
|
|
2531
2614
|
historyBoundary: "skip",
|
|
2532
2615
|
markDirty: false,
|
|
2533
2616
|
});
|
|
2534
|
-
return
|
|
2617
|
+
return {
|
|
2618
|
+
kind: "equivalent",
|
|
2619
|
+
opId,
|
|
2620
|
+
newRevisionToken: state.revisionToken,
|
|
2621
|
+
};
|
|
2535
2622
|
}
|
|
2536
2623
|
|
|
2537
2624
|
const nextDocument = replaceStoryBlocks(
|
|
@@ -2561,12 +2648,87 @@ export function createDocumentRuntime(
|
|
|
2561
2648
|
context,
|
|
2562
2649
|
);
|
|
2563
2650
|
|
|
2564
|
-
|
|
2651
|
+
const mergedTransaction: EditorTransaction = {
|
|
2565
2652
|
...fullTransaction,
|
|
2566
2653
|
effects: mergeTransactionEffects(fullTransaction.effects, localTransaction.effects),
|
|
2654
|
+
};
|
|
2655
|
+
commit(mergedTransaction);
|
|
2656
|
+
return classifyAck({
|
|
2657
|
+
command,
|
|
2658
|
+
opId,
|
|
2659
|
+
priorState: baseState,
|
|
2660
|
+
transaction: mergedTransaction,
|
|
2661
|
+
newRevisionToken: state.revisionToken,
|
|
2567
2662
|
});
|
|
2568
2663
|
}
|
|
2569
2664
|
|
|
2665
|
+
function classifyAck(params: {
|
|
2666
|
+
command: ActiveStoryTextCommand;
|
|
2667
|
+
opId: string | undefined;
|
|
2668
|
+
priorState: EditorState;
|
|
2669
|
+
transaction: EditorTransaction;
|
|
2670
|
+
newRevisionToken: string;
|
|
2671
|
+
}): TextCommandAck {
|
|
2672
|
+
const { opId, priorState, transaction, newRevisionToken } = params;
|
|
2673
|
+
const meta = transaction.mapping.metadata ?? {};
|
|
2674
|
+
const touches: readonly ScopeTagTouch[] =
|
|
2675
|
+
(meta.scopeTagTouches as readonly ScopeTagTouch[] | undefined) ?? [];
|
|
2676
|
+
|
|
2677
|
+
if (meta.invalidatesStructures) {
|
|
2678
|
+
return {
|
|
2679
|
+
kind: "structural-divergence",
|
|
2680
|
+
opId,
|
|
2681
|
+
newRevisionToken,
|
|
2682
|
+
scopeTagTouches: touches,
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// A real touch means the runtime actually changed a tag anchor — not
|
|
2687
|
+
// merely "a text edit happened and might conceivably have touched one".
|
|
2688
|
+
// The coarse `affectsComments` / `affectsRevisions` flags today are set
|
|
2689
|
+
// unconditionally by the text-transaction pipeline, so we cannot trust
|
|
2690
|
+
// them to distinguish equivalent from adjusted. `scopeTagTouches` is the
|
|
2691
|
+
// fine-grained truth that the predicted lane needs.
|
|
2692
|
+
const touchedForAdjusted = touches.some(
|
|
2693
|
+
(t) =>
|
|
2694
|
+
t.behavior === "extended" ||
|
|
2695
|
+
t.behavior === "trimmed" ||
|
|
2696
|
+
t.behavior === "split" ||
|
|
2697
|
+
t.behavior === "detached",
|
|
2698
|
+
);
|
|
2699
|
+
|
|
2700
|
+
if (touchedForAdjusted) {
|
|
2701
|
+
const adjustedRange = computeAdjustedRange(priorState, transaction);
|
|
2702
|
+
return {
|
|
2703
|
+
kind: "adjusted",
|
|
2704
|
+
opId,
|
|
2705
|
+
newRevisionToken,
|
|
2706
|
+
adjustedRange,
|
|
2707
|
+
scopeTagTouches: touches,
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
return {
|
|
2712
|
+
kind: "equivalent",
|
|
2713
|
+
opId,
|
|
2714
|
+
newRevisionToken,
|
|
2715
|
+
scopeTagTouches: touches,
|
|
2716
|
+
};
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function computeAdjustedRange(
|
|
2720
|
+
prior: EditorState,
|
|
2721
|
+
transaction: EditorTransaction,
|
|
2722
|
+
): { fromRuntime: number; toRuntime: number } {
|
|
2723
|
+
return computeAdjustedRangeImpl(
|
|
2724
|
+
{ from: prior.selection.anchor, to: prior.selection.head },
|
|
2725
|
+
transaction.mapping.steps.map((step) => ({
|
|
2726
|
+
from: step.from,
|
|
2727
|
+
insertSize: step.insertSize,
|
|
2728
|
+
})),
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2570
2732
|
function mergeTransactionEffects(
|
|
2571
2733
|
base: EditorTransaction["effects"],
|
|
2572
2734
|
local: EditorTransaction["effects"],
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocxFontLoader — best-effort registration of the document's declared font
|
|
3
|
+
* families with the browser's FontFace registry.
|
|
4
|
+
*
|
|
5
|
+
* Scope:
|
|
6
|
+
* - For each font family the document uses, resolve whether the browser
|
|
7
|
+
* can already render it.
|
|
8
|
+
* - If the package ships embedded font binary data, register each face via
|
|
9
|
+
* `new FontFace(family, data).load()` then `document.fonts.add(face)`.
|
|
10
|
+
* - Wait on `document.fonts.ready` to know when layout-affecting fonts are
|
|
11
|
+
* available, so the Canvas backend is measuring against real metrics.
|
|
12
|
+
*
|
|
13
|
+
* Non-goals:
|
|
14
|
+
* - This loader does not attempt to fetch fonts from external CDNs.
|
|
15
|
+
* - It does not attempt style-matching with Panose; that belongs in a
|
|
16
|
+
* font-substitution pass if we ever need it.
|
|
17
|
+
* - SSR and gRPC never run this loader.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface FontLoaderInput {
|
|
21
|
+
/** Font family names the document actively uses. */
|
|
22
|
+
families: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* Optional embedded font payloads keyed by family name (uppercase-insensitive).
|
|
25
|
+
* Each entry holds binary data for regular / bold / italic / bold-italic
|
|
26
|
+
* variants. Callers may omit any variant; the loader will register only
|
|
27
|
+
* what is provided.
|
|
28
|
+
*/
|
|
29
|
+
embeddedFontBytes?: Map<string, EmbeddedFontBytes>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EmbeddedFontBytes {
|
|
33
|
+
regular?: ArrayBuffer;
|
|
34
|
+
bold?: ArrayBuffer;
|
|
35
|
+
italic?: ArrayBuffer;
|
|
36
|
+
boldItalic?: ArrayBuffer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DocxFontLoader {
|
|
40
|
+
whenReady(): Promise<void>;
|
|
41
|
+
isSupported(): boolean;
|
|
42
|
+
/** Which families are currently registered or detected as available. */
|
|
43
|
+
getRegisteredFamilies(): readonly string[];
|
|
44
|
+
/** Force re-resolution of the ready promise (e.g. after adding more fonts). */
|
|
45
|
+
refresh(input: FontLoaderInput): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createDocxFontLoader(initial: FontLoaderInput): DocxFontLoader {
|
|
49
|
+
const supported =
|
|
50
|
+
typeof document !== "undefined" &&
|
|
51
|
+
typeof (globalThis as { FontFace?: unknown }).FontFace !== "undefined" &&
|
|
52
|
+
// Guard against jsdom which exposes FontFace but not document.fonts
|
|
53
|
+
Boolean((document as Document & { fonts?: FontFaceSet }).fonts);
|
|
54
|
+
|
|
55
|
+
let current: FontLoaderInput = initial;
|
|
56
|
+
let readyPromise: Promise<void>;
|
|
57
|
+
const registered = new Set<string>();
|
|
58
|
+
|
|
59
|
+
function run(input: FontLoaderInput): Promise<void> {
|
|
60
|
+
if (!supported) return Promise.resolve();
|
|
61
|
+
const fontSet = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
62
|
+
if (!fontSet) return Promise.resolve();
|
|
63
|
+
|
|
64
|
+
const pending: Array<Promise<unknown>> = [];
|
|
65
|
+
|
|
66
|
+
if (input.embeddedFontBytes) {
|
|
67
|
+
for (const [familyRaw, variants] of input.embeddedFontBytes) {
|
|
68
|
+
const family = familyRaw.trim();
|
|
69
|
+
if (!family) continue;
|
|
70
|
+
|
|
71
|
+
for (const [descriptor, data] of variantsOf(variants)) {
|
|
72
|
+
try {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const FontFaceCtor = (globalThis as any).FontFace as {
|
|
75
|
+
new (family: string, source: BufferSource, descriptors?: FontFaceDescriptors): FontFace;
|
|
76
|
+
};
|
|
77
|
+
const face = new FontFaceCtor(family, data, descriptor);
|
|
78
|
+
pending.push(
|
|
79
|
+
face.load().then((loaded) => {
|
|
80
|
+
fontSet.add(loaded);
|
|
81
|
+
registered.add(family);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
// Single-face failures should not fail the whole batch.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mark declared families as registered if the browser already resolves
|
|
92
|
+
// them (e.g. system fonts like Calibri, Arial).
|
|
93
|
+
for (const family of input.families) {
|
|
94
|
+
try {
|
|
95
|
+
const probe = `12px "${family.replace(/"/g, "'")}", serif`;
|
|
96
|
+
if (fontSet.check(probe)) {
|
|
97
|
+
registered.add(family);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// ignore
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Promise.all(pending)
|
|
105
|
+
.then(() => fontSet.ready)
|
|
106
|
+
.then(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
readyPromise = run(current);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
whenReady() {
|
|
113
|
+
return readyPromise;
|
|
114
|
+
},
|
|
115
|
+
isSupported() {
|
|
116
|
+
return supported;
|
|
117
|
+
},
|
|
118
|
+
getRegisteredFamilies() {
|
|
119
|
+
return Array.from(registered);
|
|
120
|
+
},
|
|
121
|
+
refresh(input: FontLoaderInput) {
|
|
122
|
+
current = input;
|
|
123
|
+
readyPromise = run(current);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function* variantsOf(
|
|
129
|
+
variants: EmbeddedFontBytes,
|
|
130
|
+
): IterableIterator<[FontFaceDescriptors, ArrayBuffer]> {
|
|
131
|
+
if (variants.regular) {
|
|
132
|
+
yield [{ weight: "400", style: "normal" }, variants.regular];
|
|
133
|
+
}
|
|
134
|
+
if (variants.bold) {
|
|
135
|
+
yield [{ weight: "700", style: "normal" }, variants.bold];
|
|
136
|
+
}
|
|
137
|
+
if (variants.italic) {
|
|
138
|
+
yield [{ weight: "400", style: "italic" }, variants.italic];
|
|
139
|
+
}
|
|
140
|
+
if (variants.boldItalic) {
|
|
141
|
+
yield [{ weight: "700", style: "italic" }, variants.boldItalic];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-owned paginated layout engine — internal API surface.
|
|
3
|
+
*
|
|
4
|
+
* Runtime consumption (live, wired):
|
|
5
|
+
* buildPageStack, computePageStack — shipped façade
|
|
6
|
+
* createLayoutEngine — stateful engine owned by DocumentRuntime
|
|
7
|
+
* resolvePageStories — per-page header/footer variant selection
|
|
8
|
+
* buildPageGraph — RuntimePageGraph construction
|
|
9
|
+
* analyzeInvalidation / computeFieldDirtiness — invalidation + field dirtiness
|
|
10
|
+
* createPageFragmentMapper — offset ↔ fragment ↔ region mapping
|
|
11
|
+
*
|
|
12
|
+
* Measurement:
|
|
13
|
+
* LayoutMeasurementProvider + empirical and canvas backends
|
|
14
|
+
* DocxFontLoader — FontFace registration for canvas fidelity
|
|
15
|
+
*
|
|
16
|
+
* Nothing in this module is part of the package's public API.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Façade (backward-compatible entry point)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
buildPageStack,
|
|
25
|
+
buildPageStackFrom,
|
|
26
|
+
computePageStack,
|
|
27
|
+
requiresFullRecompute,
|
|
28
|
+
type LayoutInvalidationReason,
|
|
29
|
+
type PageStackResult,
|
|
30
|
+
} from "./paginated-layout-engine.ts";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Stateful engine (Phase 1)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
createLayoutEngine,
|
|
38
|
+
findActivePageNode,
|
|
39
|
+
type LayoutEngineInstance,
|
|
40
|
+
type LayoutEngineViewState,
|
|
41
|
+
type LayoutEngineQueryInput,
|
|
42
|
+
type LayoutEngineEvent,
|
|
43
|
+
type CreateLayoutEngineOptions,
|
|
44
|
+
} from "./layout-engine-instance.ts";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Formatting resolution
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
resolveBlockFormatting,
|
|
52
|
+
resolveTextWidth,
|
|
53
|
+
resolveCharsPerLine,
|
|
54
|
+
calculateParagraphHeight,
|
|
55
|
+
resolveNumberingPrefixLength,
|
|
56
|
+
type ResolvedParagraphFormatting,
|
|
57
|
+
type ResolvedTableRowFormatting,
|
|
58
|
+
type LayoutTabStop,
|
|
59
|
+
} from "./resolved-formatting-state.ts";
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
buildResolvedFormattingState,
|
|
63
|
+
collectUsedFontFamilies,
|
|
64
|
+
populateNumberingInstances,
|
|
65
|
+
getResolvedParagraph,
|
|
66
|
+
getResolvedRun,
|
|
67
|
+
type ResolvedFormattingState,
|
|
68
|
+
type ResolvedRunFormatting,
|
|
69
|
+
type ResolvedFontCatalog,
|
|
70
|
+
type ResolvedFontDescriptor,
|
|
71
|
+
type ResolvedTabSettings,
|
|
72
|
+
type ResolvedNumberingGeometry,
|
|
73
|
+
type ResolvedDocumentSettings,
|
|
74
|
+
type NumberingInstanceMetadata,
|
|
75
|
+
type RunId,
|
|
76
|
+
} from "./resolved-formatting-document.ts";
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Page story resolution + graph + fragment mapping
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
resolvePageStories,
|
|
84
|
+
resolveDisplayPageNumber,
|
|
85
|
+
resolveTotalPageCount,
|
|
86
|
+
type ResolvedPageStories,
|
|
87
|
+
} from "./page-story-resolver.ts";
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
buildPageGraph,
|
|
91
|
+
spliceGraph,
|
|
92
|
+
findPageNodeForOffset,
|
|
93
|
+
findPagesForSection,
|
|
94
|
+
findPageForStoryTarget,
|
|
95
|
+
toDocumentPageSnapshots,
|
|
96
|
+
type RuntimePageGraph,
|
|
97
|
+
type RuntimePageNode,
|
|
98
|
+
type RuntimePageRegions,
|
|
99
|
+
type RuntimePageRegion,
|
|
100
|
+
type RuntimeBlockFragment,
|
|
101
|
+
type RuntimeLineBox,
|
|
102
|
+
type RuntimeNoteAllocation,
|
|
103
|
+
type RuntimePageAnchor,
|
|
104
|
+
type BuildPageGraphInput,
|
|
105
|
+
} from "./page-graph.ts";
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
createPageFragmentMapper,
|
|
109
|
+
rebuildMapper,
|
|
110
|
+
// Deprecated alias retained for one release after §6 E.5 rename:
|
|
111
|
+
updateMapperIncremental,
|
|
112
|
+
type PageFragmentMapper,
|
|
113
|
+
type PageFragmentLocation,
|
|
114
|
+
type PageSpan,
|
|
115
|
+
} from "./page-fragment-mapper.ts";
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
derivePageLayoutSnapshotFromGraph,
|
|
119
|
+
deriveDocumentPageSnapshots,
|
|
120
|
+
deriveActivePageIndex,
|
|
121
|
+
deriveActiveSectionIndex,
|
|
122
|
+
deriveActivePage,
|
|
123
|
+
} from "./page-layout-snapshot-adapter.ts";
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Invalidation + field dirtiness
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
analyzeInvalidation,
|
|
131
|
+
computeFieldDirtiness,
|
|
132
|
+
type InvalidationResult,
|
|
133
|
+
} from "./layout-invalidation.ts";
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Measurement provider
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
createMeasurementProvider,
|
|
141
|
+
createEmpiricalMeasurementProvider,
|
|
142
|
+
type LayoutMeasurementProvider,
|
|
143
|
+
type MeasurementFidelity,
|
|
144
|
+
type MeasurementBackendPreference,
|
|
145
|
+
type CreateMeasurementProviderOptions,
|
|
146
|
+
type MeasureLineFragmentsInput,
|
|
147
|
+
type MeasuredLineFragments,
|
|
148
|
+
type MeasureInlineObjectInput,
|
|
149
|
+
type MeasuredInlineObject,
|
|
150
|
+
type MeasureTableBlockInput,
|
|
151
|
+
type MeasuredTableBlock,
|
|
152
|
+
} from "./layout-measurement-provider.ts";
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
createDocxFontLoader,
|
|
156
|
+
type DocxFontLoader,
|
|
157
|
+
type FontLoaderInput,
|
|
158
|
+
type EmbeddedFontBytes,
|
|
159
|
+
} from "./docx-font-loader.ts";
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Public facet (Phase 7)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export { createInertLayoutFacet } from "./inert-layout-facet.ts";
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
createLayoutFacet,
|
|
169
|
+
type WordReviewEditorLayoutFacet,
|
|
170
|
+
type PublicPageNode,
|
|
171
|
+
type PublicPageRegions,
|
|
172
|
+
type PublicPageRegion,
|
|
173
|
+
type PublicBlockFragment,
|
|
174
|
+
type PublicLineBox,
|
|
175
|
+
type PublicNoteAllocation,
|
|
176
|
+
type PublicPageAnchor,
|
|
177
|
+
type PublicPageSpan,
|
|
178
|
+
type PublicSectionNode,
|
|
179
|
+
type PublicResolvedPageStories,
|
|
180
|
+
type PublicResolvedParagraphFormatting,
|
|
181
|
+
type PublicResolvedRunFormatting,
|
|
182
|
+
type PublicBlockMeasurement,
|
|
183
|
+
type PublicMeasurementFidelity,
|
|
184
|
+
type PublicFieldDirtinessReport,
|
|
185
|
+
type LayoutFacetEvent,
|
|
186
|
+
type LayoutFacetInvalidationReason,
|
|
187
|
+
type CreateLayoutFacetInput,
|
|
188
|
+
} from "./public-facet.ts";
|