@beyondwork/docx-react-component 1.0.38 → 1.0.40
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/package.json +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -42,6 +42,7 @@ import type {
|
|
|
42
42
|
LayoutEngineInstance,
|
|
43
43
|
LayoutEngineQueryInput,
|
|
44
44
|
} from "./layout-engine-instance.ts";
|
|
45
|
+
import type { LayoutMeasurementProvider } from "./layout-measurement-provider.ts";
|
|
45
46
|
import type { PageFragmentMapper } from "./page-fragment-mapper.ts";
|
|
46
47
|
import {
|
|
47
48
|
PAGE_FORMAT_CATALOG,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
type ActiveMarginPreset,
|
|
57
58
|
} from "./margin-preset-catalog.ts";
|
|
58
59
|
import {
|
|
60
|
+
attachScopeCardModel,
|
|
59
61
|
collectScopeRailSegments,
|
|
60
62
|
type ScopeRailSegment,
|
|
61
63
|
} from "../workflow-rail-segments.ts";
|
|
@@ -64,6 +66,7 @@ import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
|
|
|
64
66
|
import { createSelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
65
67
|
import { resolveTableStyleResolution } from "../table-style-resolver.ts";
|
|
66
68
|
import { buildTableRenderPlan } from "./table-render-plan.ts";
|
|
69
|
+
import { recordPerfSample } from "../../ui-tailwind/editor-surface/perf-probe.ts";
|
|
67
70
|
import type {
|
|
68
71
|
SurfaceBlockSnapshot,
|
|
69
72
|
} from "../../api/public-types";
|
|
@@ -390,12 +393,48 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
390
393
|
/** Return every scope rail segment across the document. */
|
|
391
394
|
getAllScopeRailSegments(): readonly import("../workflow-rail-segments.ts").ScopeRailSegment[];
|
|
392
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Scope-card projection consumed by the chrome overlay's card layer
|
|
398
|
+
* (scope-card-overlay P1). Joins every unique scope segment with
|
|
399
|
+
* its attached issue metadata (R2 `ISSUE_METADATA_ID`), its
|
|
400
|
+
* work-item id, and its `primaryAnchorRect` via the render kernel's
|
|
401
|
+
* anchor index. P2 fields (`suggestionGroupIds`,
|
|
402
|
+
* `reviewActionCount`, `agentPending`) are defaulted to empty /
|
|
403
|
+
* zero / false in P1 and populated by later phases.
|
|
404
|
+
*
|
|
405
|
+
* Returns an empty list when no workflow rail input has been wired
|
|
406
|
+
* or when no scopes are currently on the active story.
|
|
407
|
+
*/
|
|
408
|
+
getAllScopeCardModels(): readonly import("../../api/public-types.ts").ScopeCardModel[];
|
|
409
|
+
|
|
393
410
|
// Measurement exposure -------------------------------------------------
|
|
394
411
|
getResolvedFormatting(blockId: string): PublicResolvedParagraphFormatting | null;
|
|
395
412
|
getResolvedRunFormatting(runId: string): PublicResolvedRunFormatting | null;
|
|
396
413
|
getMeasurement(blockId: string): PublicBlockMeasurement | null;
|
|
397
414
|
getMeasurementFidelity(): PublicMeasurementFidelity;
|
|
398
415
|
whenMeasurementReady(): Promise<void>;
|
|
416
|
+
swapMeasurementProvider(provider: LayoutMeasurementProvider): void;
|
|
417
|
+
|
|
418
|
+
// Table render plan (P3e consumed by the render kernel, P4) ------------
|
|
419
|
+
/**
|
|
420
|
+
* 0-based page index where the first fragment of `blockId` lands, or
|
|
421
|
+
* `null` when the block has no fragments in the current page graph.
|
|
422
|
+
* Used by the tables facet to determine which page to request plans for
|
|
423
|
+
* without needing PM document offsets.
|
|
424
|
+
*/
|
|
425
|
+
getFirstPageIndexForBlock(blockId: string): number | null;
|
|
426
|
+
/**
|
|
427
|
+
* Build a `TableRenderPlan` for a table block on a given page. Returns
|
|
428
|
+
* `null` when the blockId does not resolve to a table in the current
|
|
429
|
+
* surface. The plan carries columnsTwips, bandClasses, verticalMerges,
|
|
430
|
+
* repeatedHeaderRows, and columnResizeHandles so chrome can render
|
|
431
|
+
* band-aware cell styling and place column-resize grips without
|
|
432
|
+
* walking canonical state.
|
|
433
|
+
*/
|
|
434
|
+
getTableRenderPlan(
|
|
435
|
+
blockId: string,
|
|
436
|
+
pageIndex: number,
|
|
437
|
+
): import("./table-render-plan.ts").TableRenderPlan | null;
|
|
399
438
|
|
|
400
439
|
// Table render plan (P3e consumed by the render kernel, P4) ------------
|
|
401
440
|
/**
|
|
@@ -447,6 +486,17 @@ export interface CreateLayoutFacetInput {
|
|
|
447
486
|
>
|
|
448
487
|
| null
|
|
449
488
|
| undefined;
|
|
489
|
+
/**
|
|
490
|
+
* Optional workflow-metadata accessor for scope-card issue resolution
|
|
491
|
+
* (R2 / scope-card-overlay P1). Returns the
|
|
492
|
+
* `WorkflowMarkupSnapshot.metadata` array so `getAllScopeCardModels`
|
|
493
|
+
* can attach issue values to their scopes. Omit when the host does
|
|
494
|
+
* not push metadata entries.
|
|
495
|
+
*/
|
|
496
|
+
getWorkflowMarkupMetadata?: () =>
|
|
497
|
+
| readonly import("../../api/public-types.ts").WorkflowMetadataMarkup[]
|
|
498
|
+
| null
|
|
499
|
+
| undefined;
|
|
450
500
|
}
|
|
451
501
|
|
|
452
502
|
export function createLayoutFacet(
|
|
@@ -659,8 +709,11 @@ export function createLayoutFacet(
|
|
|
659
709
|
hitTest(pointInRoot) {
|
|
660
710
|
const kernel = input.renderKernel?.();
|
|
661
711
|
if (!kernel) return null;
|
|
712
|
+
const t0 = typeof performance !== "undefined" ? performance.now() : 0;
|
|
662
713
|
const frame = kernel.getRenderFrame();
|
|
663
|
-
|
|
714
|
+
const result = resolveHitTest(frame, pointInRoot);
|
|
715
|
+
if (t0 > 0) recordPerfSample("chrome.hit_test", performance.now() - t0);
|
|
716
|
+
return result;
|
|
664
717
|
},
|
|
665
718
|
|
|
666
719
|
getAnchorRects(query) {
|
|
@@ -684,6 +737,21 @@ export function createLayoutFacet(
|
|
|
684
737
|
);
|
|
685
738
|
},
|
|
686
739
|
|
|
740
|
+
getAllScopeCardModels() {
|
|
741
|
+
const railInput = input.getWorkflowRailInput?.();
|
|
742
|
+
const segments = collectScopeRailSegmentsForQuery(railInput, currentGraph());
|
|
743
|
+
if (segments.length === 0) return [];
|
|
744
|
+
const kernel = input.renderKernel?.();
|
|
745
|
+
const anchorIndex = kernel?.getRenderFrame?.()?.anchorIndex ?? null;
|
|
746
|
+
const metadata = input.getWorkflowMarkupMetadata?.();
|
|
747
|
+
return attachScopeCardModel({
|
|
748
|
+
segments,
|
|
749
|
+
scopes: railInput?.scopes ?? [],
|
|
750
|
+
metadata: metadata ?? undefined,
|
|
751
|
+
anchorIndex,
|
|
752
|
+
});
|
|
753
|
+
},
|
|
754
|
+
|
|
687
755
|
getFragmentsForPage(pageIndex) {
|
|
688
756
|
const graph = currentGraph();
|
|
689
757
|
const node = graph.pages[pageIndex];
|
|
@@ -736,6 +804,18 @@ export function createLayoutFacet(
|
|
|
736
804
|
return engine.whenMeasurementReady();
|
|
737
805
|
},
|
|
738
806
|
|
|
807
|
+
getFirstPageIndexForBlock(blockId) {
|
|
808
|
+
const graph = currentGraph();
|
|
809
|
+
const fragment = graph.fragments.find((f) => f.blockId === blockId);
|
|
810
|
+
if (!fragment) return null;
|
|
811
|
+
const page = graph.pages.find((p) => p.pageId === fragment.pageId);
|
|
812
|
+
return page?.pageIndex ?? null;
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
swapMeasurementProvider(provider) {
|
|
816
|
+
engine.swapMeasurementProvider(provider);
|
|
817
|
+
},
|
|
818
|
+
|
|
739
819
|
getTableRenderPlan(blockId, pageIndex) {
|
|
740
820
|
const graph = currentGraph();
|
|
741
821
|
const fragment = graph.fragments.find((f) => f.blockId === blockId);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `PAGE` and `NUMPAGES` field values per page.
|
|
3
|
+
*
|
|
4
|
+
* These two field families (now in `SupportedFieldFamily`) compute differently
|
|
5
|
+
* from `REF` / `PAGEREF` because the resolved value depends on the *owning
|
|
6
|
+
* page* of each header/footer instance, not on a single document-level
|
|
7
|
+
* computation. A header part is shared across many pages; the same PAGE
|
|
8
|
+
* field must render "1" on page 1, "2" on page 2, and so on.
|
|
9
|
+
*
|
|
10
|
+
* The resolver is a pure function over the page graph — no DOM, no side
|
|
11
|
+
* effects. Consumers (per-page header/footer bands) call
|
|
12
|
+
* `resolvePageFieldDisplayText` at render time to substitute the cached
|
|
13
|
+
* preserve-only text that currently sits inside the paragraph inline atom.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RuntimePageGraph, RuntimePageNode } from "./page-graph.ts";
|
|
17
|
+
import type { SupportedFieldFamily } from "../../model/canonical-document.ts";
|
|
18
|
+
|
|
19
|
+
export interface PageFieldContext {
|
|
20
|
+
/** The page this header/footer copy renders on. */
|
|
21
|
+
page: RuntimePageNode;
|
|
22
|
+
/** The graph the page belongs to (for NUMPAGES resolution). */
|
|
23
|
+
graph: RuntimePageGraph;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the display text for a PAGE / NUMPAGES field on a specific page.
|
|
28
|
+
*
|
|
29
|
+
* Returns the original cached `displayText` for unsupported families so the
|
|
30
|
+
* caller can use this as a single resolution helper.
|
|
31
|
+
*/
|
|
32
|
+
export function resolvePageFieldDisplayText(
|
|
33
|
+
family: SupportedFieldFamily | string,
|
|
34
|
+
cachedDisplayText: string,
|
|
35
|
+
context: PageFieldContext,
|
|
36
|
+
): string {
|
|
37
|
+
switch (family) {
|
|
38
|
+
case "PAGE":
|
|
39
|
+
return String(context.page.stories.displayPageNumber);
|
|
40
|
+
case "NUMPAGES":
|
|
41
|
+
// Blank fillers (evenPage/oddPage section breaks) don't count toward
|
|
42
|
+
// NUMPAGES — the graph already excludes them from contentPageCount.
|
|
43
|
+
return String(context.graph.contentPageCount);
|
|
44
|
+
default:
|
|
45
|
+
return cachedDisplayText;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a lookup keyed by pageId → (fieldFamily → resolvedText). Useful when
|
|
51
|
+
* a renderer wants to bulk-resolve every page's fields before emitting DOM.
|
|
52
|
+
* Absent keys mean no override; the cached field text should be used.
|
|
53
|
+
*/
|
|
54
|
+
export function buildPageFieldResolutionTable(
|
|
55
|
+
graph: RuntimePageGraph,
|
|
56
|
+
families: ReadonlySet<string> = new Set(["PAGE", "NUMPAGES"]),
|
|
57
|
+
): Map<string, Map<string, string>> {
|
|
58
|
+
const result = new Map<string, Map<string, string>>();
|
|
59
|
+
for (const page of graph.pages) {
|
|
60
|
+
const perPage = new Map<string, string>();
|
|
61
|
+
for (const family of families) {
|
|
62
|
+
perPage.set(
|
|
63
|
+
family,
|
|
64
|
+
resolvePageFieldDisplayText(family, "", { page, graph }),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
result.set(page.pageId, perPage);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten header/footer sub-part blocks to short text previews with PAGE /
|
|
3
|
+
* NUMPAGES fields resolved for the owning page. Used by the in-flow page
|
|
4
|
+
* chrome widgets so the bands show live content ("Page 3 of 5") rather
|
|
5
|
+
* than a static "Header" / "Footer" placeholder.
|
|
6
|
+
*
|
|
7
|
+
* This is a **preview**, not a full renderer — we flatten to plain text
|
|
8
|
+
* and cap the length. A per-page band is ~32 px tall; anything more
|
|
9
|
+
* elaborate belongs to the full editing path, which is entered by double-
|
|
10
|
+
* clicking the band.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BlockNode,
|
|
15
|
+
HeaderDocument,
|
|
16
|
+
FooterDocument,
|
|
17
|
+
InlineNode,
|
|
18
|
+
} from "../../model/canonical-document.ts";
|
|
19
|
+
import type {
|
|
20
|
+
RuntimePageGraph,
|
|
21
|
+
RuntimePageNode,
|
|
22
|
+
} from "./page-graph.ts";
|
|
23
|
+
import { resolvePageFieldDisplayText } from "./resolve-page-fields.ts";
|
|
24
|
+
|
|
25
|
+
const MAX_PREVIEW_CHARS = 140;
|
|
26
|
+
|
|
27
|
+
export interface PagePreviewMaps {
|
|
28
|
+
/** pageId → short text preview of the resolved header story (if any). */
|
|
29
|
+
headerPreviewByPageId: Map<string, string>;
|
|
30
|
+
/** pageId → short text preview of the resolved footer story (if any). */
|
|
31
|
+
footerPreviewByPageId: Map<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildPagePreviewMaps(
|
|
35
|
+
graph: RuntimePageGraph,
|
|
36
|
+
subParts: {
|
|
37
|
+
headers?: ReadonlyArray<HeaderDocument>;
|
|
38
|
+
footers?: ReadonlyArray<FooterDocument>;
|
|
39
|
+
},
|
|
40
|
+
): PagePreviewMaps {
|
|
41
|
+
const headerPreviewByPageId = new Map<string, string>();
|
|
42
|
+
const footerPreviewByPageId = new Map<string, string>();
|
|
43
|
+
|
|
44
|
+
for (const page of graph.pages) {
|
|
45
|
+
if (page.isBlankFiller) continue;
|
|
46
|
+
const headerStory = page.stories.header;
|
|
47
|
+
if (headerStory && headerStory.kind === "header") {
|
|
48
|
+
const source = findSubPart(subParts.headers, headerStory.relationshipId);
|
|
49
|
+
if (source) {
|
|
50
|
+
headerPreviewByPageId.set(
|
|
51
|
+
page.pageId,
|
|
52
|
+
flattenBlocksToPreview(source.blocks, page, graph),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const footerStory = page.stories.footer;
|
|
57
|
+
if (footerStory && footerStory.kind === "footer") {
|
|
58
|
+
const source = findSubPart(subParts.footers, footerStory.relationshipId);
|
|
59
|
+
if (source) {
|
|
60
|
+
footerPreviewByPageId.set(
|
|
61
|
+
page.pageId,
|
|
62
|
+
flattenBlocksToPreview(source.blocks, page, graph),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { headerPreviewByPageId, footerPreviewByPageId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findSubPart<T extends HeaderDocument | FooterDocument>(
|
|
72
|
+
parts: ReadonlyArray<T> | undefined,
|
|
73
|
+
relationshipId: string,
|
|
74
|
+
): T | undefined {
|
|
75
|
+
if (!parts) return undefined;
|
|
76
|
+
return parts.find((p) => p.relationshipId === relationshipId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function flattenBlocksToPreview(
|
|
80
|
+
blocks: ReadonlyArray<BlockNode>,
|
|
81
|
+
page: RuntimePageNode,
|
|
82
|
+
graph: RuntimePageGraph,
|
|
83
|
+
): string {
|
|
84
|
+
const parts: string[] = [];
|
|
85
|
+
for (const block of blocks) {
|
|
86
|
+
collectBlockText(block, page, graph, parts);
|
|
87
|
+
if (joinPreview(parts).length >= MAX_PREVIEW_CHARS) break;
|
|
88
|
+
}
|
|
89
|
+
return truncate(joinPreview(parts));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectBlockText(
|
|
93
|
+
block: BlockNode,
|
|
94
|
+
page: RuntimePageNode,
|
|
95
|
+
graph: RuntimePageGraph,
|
|
96
|
+
out: string[],
|
|
97
|
+
): void {
|
|
98
|
+
switch (block.type) {
|
|
99
|
+
case "paragraph":
|
|
100
|
+
for (const child of block.children) {
|
|
101
|
+
collectInlineText(child, page, graph, out);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case "table":
|
|
105
|
+
for (const row of block.rows) {
|
|
106
|
+
for (const cell of row.cells) {
|
|
107
|
+
for (const childBlock of cell.children) {
|
|
108
|
+
collectBlockText(childBlock, page, graph, out);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "sdt":
|
|
114
|
+
for (const child of block.children) {
|
|
115
|
+
collectBlockText(child, page, graph, out);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
// opaque_block / section_break / custom_xml / alt_chunk — no preview text.
|
|
119
|
+
default:
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectInlineText(
|
|
125
|
+
inline: InlineNode,
|
|
126
|
+
page: RuntimePageNode,
|
|
127
|
+
graph: RuntimePageGraph,
|
|
128
|
+
out: string[],
|
|
129
|
+
): void {
|
|
130
|
+
switch (inline.type) {
|
|
131
|
+
case "text":
|
|
132
|
+
out.push(inline.text);
|
|
133
|
+
break;
|
|
134
|
+
case "tab":
|
|
135
|
+
out.push(" ");
|
|
136
|
+
break;
|
|
137
|
+
case "hard_break":
|
|
138
|
+
out.push(" ");
|
|
139
|
+
break;
|
|
140
|
+
case "field": {
|
|
141
|
+
const family = inline.fieldFamily ?? classifyFieldInstructionLocal(inline.instruction);
|
|
142
|
+
const cached = flattenInline(inline.children);
|
|
143
|
+
if (family === "PAGE" || family === "NUMPAGES") {
|
|
144
|
+
out.push(
|
|
145
|
+
resolvePageFieldDisplayText(family, cached, { page, graph }),
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
out.push(cached);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "hyperlink":
|
|
153
|
+
for (const child of inline.children) {
|
|
154
|
+
collectInlineText(child, page, graph, out);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
// bookmark_start/end, opaque_inline, note_ref etc — skip.
|
|
158
|
+
default:
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function flattenInline(inlines: ReadonlyArray<InlineNode> | undefined): string {
|
|
164
|
+
if (!inlines) return "";
|
|
165
|
+
let buf = "";
|
|
166
|
+
for (const inline of inlines) {
|
|
167
|
+
if (inline.type === "text") buf += inline.text;
|
|
168
|
+
else if (inline.type === "hard_break" || inline.type === "tab") buf += " ";
|
|
169
|
+
}
|
|
170
|
+
return buf;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function classifyFieldInstructionLocal(instr: string): string {
|
|
174
|
+
const match = /^\s*(\w+)/.exec(instr);
|
|
175
|
+
return match ? match[1]!.toUpperCase() : "UNKNOWN";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function joinPreview(parts: readonly string[]): string {
|
|
179
|
+
return parts.join("").replace(/\s+/g, " ").trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function truncate(s: string): string {
|
|
183
|
+
if (s.length <= MAX_PREVIEW_CHARS) return s;
|
|
184
|
+
return s.slice(0, MAX_PREVIEW_CHARS - 1).trimEnd() + "…";
|
|
185
|
+
}
|
|
@@ -151,11 +151,11 @@ export function resolveBlockFormatting(
|
|
|
151
151
|
fontSizeHalfPoints: fontInfo.fontSizeHalfPoints,
|
|
152
152
|
averageCharWidthTwips: fontInfo.avgCharWidth,
|
|
153
153
|
tabStops: resolveTabStops(block),
|
|
154
|
-
keepNext: Boolean(block.keepNext),
|
|
155
|
-
keepLines: Boolean(block.keepLines),
|
|
156
|
-
pageBreakBefore: Boolean(block.pageBreakBefore),
|
|
157
|
-
widowControl: block.widowControl !== false, // default true in Word
|
|
158
|
-
contextualSpacing: Boolean(block.contextualSpacing),
|
|
154
|
+
keepNext: Boolean(block.keepNext ?? block.resolvedParagraphFormatting?.keepNext),
|
|
155
|
+
keepLines: Boolean(block.keepLines ?? block.resolvedParagraphFormatting?.keepLines),
|
|
156
|
+
pageBreakBefore: Boolean(block.pageBreakBefore ?? block.resolvedParagraphFormatting?.pageBreakBefore),
|
|
157
|
+
widowControl: (block.widowControl ?? block.resolvedParagraphFormatting?.widowControl) !== false, // default true in Word
|
|
158
|
+
contextualSpacing: Boolean(block.contextualSpacing ?? block.resolvedParagraphFormatting?.contextualSpacing),
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -166,9 +166,10 @@ export function resolveBlockFormatting(
|
|
|
166
166
|
function resolveSpacing(
|
|
167
167
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
168
168
|
): ParagraphSpacing {
|
|
169
|
-
//
|
|
169
|
+
// Priority: numbering geometry > direct paragraph > style cascade
|
|
170
170
|
const numbering = block.resolvedNumbering?.geometry.spacing;
|
|
171
171
|
const direct = block.spacing;
|
|
172
|
+
const cascade = block.resolvedParagraphFormatting?.spacing;
|
|
172
173
|
|
|
173
174
|
const normalizeLineRule = (
|
|
174
175
|
rule: string | undefined,
|
|
@@ -177,21 +178,15 @@ function resolveSpacing(
|
|
|
177
178
|
return undefined;
|
|
178
179
|
};
|
|
179
180
|
|
|
180
|
-
if (numbering) {
|
|
181
|
+
if (numbering || direct || cascade) {
|
|
181
182
|
return {
|
|
182
|
-
before: numbering
|
|
183
|
-
after: numbering
|
|
184
|
-
line: numbering
|
|
185
|
-
lineRule:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (direct) {
|
|
190
|
-
return {
|
|
191
|
-
before: direct.before,
|
|
192
|
-
after: direct.after,
|
|
193
|
-
line: direct.line,
|
|
194
|
-
lineRule: normalizeLineRule(direct.lineRule),
|
|
183
|
+
before: numbering?.before ?? direct?.before ?? cascade?.before,
|
|
184
|
+
after: numbering?.after ?? direct?.after ?? cascade?.after,
|
|
185
|
+
line: numbering?.line ?? direct?.line ?? cascade?.line,
|
|
186
|
+
lineRule:
|
|
187
|
+
normalizeLineRule(numbering?.lineRule) ??
|
|
188
|
+
normalizeLineRule(direct?.lineRule) ??
|
|
189
|
+
normalizeLineRule(cascade?.lineRule),
|
|
195
190
|
};
|
|
196
191
|
}
|
|
197
192
|
|
|
@@ -228,16 +223,25 @@ function resolveIndentation(
|
|
|
228
223
|
};
|
|
229
224
|
}
|
|
230
225
|
|
|
231
|
-
const
|
|
232
|
-
|
|
226
|
+
const direct = block.indentation;
|
|
227
|
+
const cascade = block.resolvedParagraphFormatting?.indentation;
|
|
228
|
+
|
|
229
|
+
if (!direct && !cascade) {
|
|
233
230
|
return { left: 0, right: 0, firstLine: 0, hanging: 0 };
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
return {
|
|
237
|
-
left:
|
|
238
|
-
right:
|
|
239
|
-
firstLine:
|
|
240
|
-
hanging:
|
|
234
|
+
left: direct?.left ?? cascade?.left ?? 0,
|
|
235
|
+
right: direct?.right ?? cascade?.right ?? 0,
|
|
236
|
+
firstLine: direct?.firstLine ?? cascade?.firstLine ?? 0,
|
|
237
|
+
hanging:
|
|
238
|
+
direct?.hanging ??
|
|
239
|
+
cascade?.hanging ??
|
|
240
|
+
(typeof direct?.firstLine === "number" && direct.firstLine < 0
|
|
241
|
+
? Math.abs(direct.firstLine)
|
|
242
|
+
: typeof cascade?.firstLine === "number" && cascade.firstLine < 0
|
|
243
|
+
? Math.abs(cascade.firstLine)
|
|
244
|
+
: 0),
|
|
241
245
|
};
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -101,6 +101,13 @@ export interface BuildTableRenderPlanInput {
|
|
|
101
101
|
/** If `columnsTwips` was scaled to the canvas width, pass the scale
|
|
102
102
|
* factor here; otherwise leave undefined. */
|
|
103
103
|
columnsTwipsScale?: number;
|
|
104
|
+
/**
|
|
105
|
+
* R3: true when this plan is for a continuation page of a multi-page
|
|
106
|
+
* table (i.e. a page other than the one the table first appears on).
|
|
107
|
+
* When true, header rows from the source table are emitted in
|
|
108
|
+
* `repeatedHeaderRows` so the consumer can prepend them on render.
|
|
109
|
+
*/
|
|
110
|
+
isContinuationPage?: boolean;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export function buildTableRenderPlan(
|
|
@@ -112,7 +119,20 @@ export function buildTableRenderPlan(
|
|
|
112
119
|
|
|
113
120
|
const bandClasses = buildBandClasses(block.rows, resolved);
|
|
114
121
|
const verticalMerges = collectVerticalMerges(block.rows);
|
|
115
|
-
|
|
122
|
+
// R3: header rows are repeated on every continuation page. When the
|
|
123
|
+
// render-plan builder is invoked for a page with `isContinuationPage`,
|
|
124
|
+
// every row with `isHeader === true` gets an entry in repeatedHeaderRows.
|
|
125
|
+
// First-page plans (or tables that fit on a single page) return an empty
|
|
126
|
+
// array. `virtualFragmentId` is deterministic so consumers can key off it.
|
|
127
|
+
const repeatedHeaderRows: RepeatedHeaderRowRef[] = input.isContinuationPage
|
|
128
|
+
? block.rows
|
|
129
|
+
.map((row, rowIndex) => ({ row, rowIndex }))
|
|
130
|
+
.filter(({ row }) => row.isHeader === true)
|
|
131
|
+
.map(({ rowIndex }) => ({
|
|
132
|
+
sourceRowIndex: rowIndex,
|
|
133
|
+
virtualFragmentId: `fragment-${blockId}-vheader-p${pageIndex}-r${rowIndex}`,
|
|
134
|
+
}))
|
|
135
|
+
: [];
|
|
116
136
|
const columnResizeHandles = buildColumnResizeHandles(columnsTwips, tableHeightTwips);
|
|
117
137
|
|
|
118
138
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CanonicalRunFormatting,
|
|
2
3
|
NumberingCatalog,
|
|
3
4
|
NumberingLevelDefinition,
|
|
4
5
|
ParagraphNode,
|
|
@@ -23,6 +24,7 @@ export interface NumberingPrefixResult {
|
|
|
23
24
|
paragraphStyleId?: string;
|
|
24
25
|
isLegalNumbering?: boolean;
|
|
25
26
|
geometry: ResolvedNumberingGeometry;
|
|
27
|
+
markerRunProperties?: CanonicalRunFormatting;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface NumberingPrefixResolver {
|
|
@@ -90,6 +92,9 @@ export function createNumberingPrefixResolver(
|
|
|
90
92
|
? { paragraphStyleId: resolved.effectiveLevel.paragraphStyleId }
|
|
91
93
|
: {}),
|
|
92
94
|
...(resolved.effectiveLevel.isLegalNumbering ? { isLegalNumbering: true } : {}),
|
|
95
|
+
...(resolved.geometry.markerRunProperties
|
|
96
|
+
? { markerRunProperties: resolved.geometry.markerRunProperties }
|
|
97
|
+
: {}),
|
|
93
98
|
geometry: resolved.geometry,
|
|
94
99
|
};
|
|
95
100
|
}
|