@beyondwork/docx-react-component 1.0.37 → 1.0.38
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 +1 -1
- package/src/api/public-types.ts +319 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
|
@@ -43,6 +43,39 @@ import type {
|
|
|
43
43
|
LayoutEngineQueryInput,
|
|
44
44
|
} from "./layout-engine-instance.ts";
|
|
45
45
|
import type { PageFragmentMapper } from "./page-fragment-mapper.ts";
|
|
46
|
+
import {
|
|
47
|
+
PAGE_FORMAT_CATALOG,
|
|
48
|
+
matchPageFormat,
|
|
49
|
+
type PageFormatDefinition,
|
|
50
|
+
type ActivePageFormat,
|
|
51
|
+
} from "./page-format-catalog.ts";
|
|
52
|
+
import {
|
|
53
|
+
MARGIN_PRESET_CATALOG,
|
|
54
|
+
matchMarginPreset,
|
|
55
|
+
type MarginPresetDefinition,
|
|
56
|
+
type ActiveMarginPreset,
|
|
57
|
+
} from "./margin-preset-catalog.ts";
|
|
58
|
+
import {
|
|
59
|
+
collectScopeRailSegments,
|
|
60
|
+
type ScopeRailSegment,
|
|
61
|
+
} from "../workflow-rail-segments.ts";
|
|
62
|
+
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
63
|
+
import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
|
|
64
|
+
import { createSelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
65
|
+
import { resolveTableStyleResolution } from "../table-style-resolver.ts";
|
|
66
|
+
import { buildTableRenderPlan } from "./table-render-plan.ts";
|
|
67
|
+
import type {
|
|
68
|
+
SurfaceBlockSnapshot,
|
|
69
|
+
} from "../../api/public-types";
|
|
70
|
+
|
|
71
|
+
export type {
|
|
72
|
+
PageFormatDefinition,
|
|
73
|
+
ActivePageFormat,
|
|
74
|
+
MarginPresetDefinition,
|
|
75
|
+
ActiveMarginPreset,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type { ScopeRailSegment };
|
|
46
79
|
|
|
47
80
|
// ---------------------------------------------------------------------------
|
|
48
81
|
// Public read model types (shape-stable, cloned at the facet boundary)
|
|
@@ -224,8 +257,35 @@ export type LayoutFacetEvent =
|
|
|
224
257
|
kind: "measurement_backend_ready";
|
|
225
258
|
fidelity: PublicMeasurementFidelity;
|
|
226
259
|
revision: number;
|
|
260
|
+
}
|
|
261
|
+
| {
|
|
262
|
+
kind: "incremental_relayout";
|
|
263
|
+
revision: number;
|
|
264
|
+
pageRange: { fromPageIndex: number; toPageIndex: number };
|
|
265
|
+
reason?: LayoutFacetInvalidationReason;
|
|
266
|
+
}
|
|
267
|
+
| {
|
|
268
|
+
kind: "render_frame_ready";
|
|
269
|
+
revision: number;
|
|
270
|
+
pageRange?: { fromPageIndex: number; toPageIndex: number };
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
kind: "zoom_changed";
|
|
274
|
+
revision: number;
|
|
275
|
+
zoom: RenderZoomSummary;
|
|
227
276
|
};
|
|
228
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Minimal zoom summary carried with `zoom_changed`. The render kernel
|
|
280
|
+
* (when shipped) provides richer zoom metadata; consumers today only need
|
|
281
|
+
* the resolved px-per-twip plus the fit mode to reposition chrome.
|
|
282
|
+
*/
|
|
283
|
+
export interface RenderZoomSummary {
|
|
284
|
+
pxPerTwip: number;
|
|
285
|
+
viewportWidthPx: number;
|
|
286
|
+
fitMode: "fixed" | "page-width" | "one-page";
|
|
287
|
+
}
|
|
288
|
+
|
|
229
289
|
export type LayoutFacetInvalidationReason =
|
|
230
290
|
| { kind: "content-edit"; from: number; to: number }
|
|
231
291
|
| { kind: "section-change"; sectionIndex: number }
|
|
@@ -267,10 +327,69 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
267
327
|
getDisplayPageNumber(pageIndex: number): number | null;
|
|
268
328
|
getLineBoxes(
|
|
269
329
|
pageIndex: number,
|
|
270
|
-
options?: { region?: "
|
|
330
|
+
options?: { region?: PublicPageRegion["kind"]; columnIndex?: number },
|
|
271
331
|
): PublicLineBox[];
|
|
332
|
+
/** Back-compat alias; prefer `getLineBoxes(pageIndex, { region })`. */
|
|
333
|
+
getLineBoxesForRegion(
|
|
334
|
+
pageIndex: number,
|
|
335
|
+
region: PublicPageRegion["kind"],
|
|
336
|
+
options?: { columnIndex?: number },
|
|
337
|
+
): readonly PublicLineBox[];
|
|
272
338
|
getFragmentsForPage(pageIndex: number): PublicBlockFragment[];
|
|
273
339
|
|
|
340
|
+
// Page-format catalog --------------------------------------------------
|
|
341
|
+
getPageFormatCatalog(): readonly PageFormatDefinition[];
|
|
342
|
+
getActivePageFormat(sectionIndex: number): ActivePageFormat | null;
|
|
343
|
+
getMarginPresetCatalog(): readonly MarginPresetDefinition[];
|
|
344
|
+
getActiveMarginPreset(sectionIndex: number): ActiveMarginPreset | null;
|
|
345
|
+
|
|
346
|
+
// Render-frame access (R1) --------------------------------------------
|
|
347
|
+
/**
|
|
348
|
+
* Return the active `RenderFrame` produced by the runtime-owned render
|
|
349
|
+
* kernel. Optional when the host runtime has not yet installed a kernel
|
|
350
|
+
* — returns `null` in that case so consumers can fall back to the page-
|
|
351
|
+
* graph reads above.
|
|
352
|
+
*/
|
|
353
|
+
getRenderFrame?(
|
|
354
|
+
options?: import("../render/index.ts").RenderFrameQueryOptions,
|
|
355
|
+
): import("../render/index.ts").RenderFrame | null;
|
|
356
|
+
|
|
357
|
+
/** Return the render-kernel zoom, if a kernel is installed. */
|
|
358
|
+
getRenderZoom?(): import("../render/index.ts").RenderZoom | null;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Hit-test a point in the mounted shell's coordinate space against the
|
|
362
|
+
* current render frame. Returns the deepest matching region with runtime
|
|
363
|
+
* offset, block id, fragment id, and line index so chrome surfaces can
|
|
364
|
+
* turn mouse coordinates into canonical positions without consulting the
|
|
365
|
+
* DOM. Returns `null` when no kernel is installed, or when the point
|
|
366
|
+
* falls outside every page.
|
|
367
|
+
*/
|
|
368
|
+
hitTest?(
|
|
369
|
+
pointInRoot: import("../render/index.ts").RenderPoint,
|
|
370
|
+
): import("../render/index.ts").RenderHitResult | null;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve anchor rects for a `RenderAnchorQuery`. Returns `[]` when the
|
|
374
|
+
* kernel is absent or the query does not match any anchor. Chrome
|
|
375
|
+
* surfaces that need a single rect can read `getAnchorRects(q)[0]`;
|
|
376
|
+
* selection-spanning surfaces read the full list and union as needed.
|
|
377
|
+
*/
|
|
378
|
+
getAnchorRects?(
|
|
379
|
+
query: import("../render/index.ts").RenderAnchorQuery,
|
|
380
|
+
): readonly import("../render/index.ts").RenderFrameRect[];
|
|
381
|
+
|
|
382
|
+
// Scope rail segments (R3a) -------------------------------------------
|
|
383
|
+
/**
|
|
384
|
+
* Return workflow rail segments active on a given page. Returns an empty
|
|
385
|
+
* list when the host runtime did not supply workflow data to the facet.
|
|
386
|
+
*/
|
|
387
|
+
getScopeRailSegments(
|
|
388
|
+
pageIndex: number,
|
|
389
|
+
): readonly import("../workflow-rail-segments.ts").ScopeRailSegment[];
|
|
390
|
+
/** Return every scope rail segment across the document. */
|
|
391
|
+
getAllScopeRailSegments(): readonly import("../workflow-rail-segments.ts").ScopeRailSegment[];
|
|
392
|
+
|
|
274
393
|
// Measurement exposure -------------------------------------------------
|
|
275
394
|
getResolvedFormatting(blockId: string): PublicResolvedParagraphFormatting | null;
|
|
276
395
|
getResolvedRunFormatting(runId: string): PublicResolvedRunFormatting | null;
|
|
@@ -278,6 +397,20 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
278
397
|
getMeasurementFidelity(): PublicMeasurementFidelity;
|
|
279
398
|
whenMeasurementReady(): Promise<void>;
|
|
280
399
|
|
|
400
|
+
// Table render plan (P3e consumed by the render kernel, P4) ------------
|
|
401
|
+
/**
|
|
402
|
+
* Build a `TableRenderPlan` for a table block on a given page. Returns
|
|
403
|
+
* `null` when the blockId does not resolve to a table in the current
|
|
404
|
+
* surface. The plan carries columnsTwips, bandClasses, verticalMerges,
|
|
405
|
+
* repeatedHeaderRows, and columnResizeHandles so chrome can render
|
|
406
|
+
* band-aware cell styling and place column-resize grips without
|
|
407
|
+
* walking canonical state.
|
|
408
|
+
*/
|
|
409
|
+
getTableRenderPlan(
|
|
410
|
+
blockId: string,
|
|
411
|
+
pageIndex: number,
|
|
412
|
+
): import("./table-render-plan.ts").TableRenderPlan | null;
|
|
413
|
+
|
|
281
414
|
// Fields ---------------------------------------------------------------
|
|
282
415
|
getDirtyFieldFamilies(): readonly string[];
|
|
283
416
|
getFieldDirtinessReport(): PublicFieldDirtinessReport;
|
|
@@ -293,6 +426,27 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
293
426
|
export interface CreateLayoutFacetInput {
|
|
294
427
|
engine: LayoutEngineInstance;
|
|
295
428
|
getQueryInput: () => LayoutEngineQueryInput;
|
|
429
|
+
/**
|
|
430
|
+
* Optional render-kernel accessor. When supplied, the facet exposes
|
|
431
|
+
* `getRenderFrame` + `getRenderZoom` that delegate to the kernel.
|
|
432
|
+
*/
|
|
433
|
+
renderKernel?: () =>
|
|
434
|
+
| import("../render/index.ts").RenderKernel
|
|
435
|
+
| null
|
|
436
|
+
| undefined;
|
|
437
|
+
/**
|
|
438
|
+
* Optional workflow-segments accessor. When supplied, the facet computes
|
|
439
|
+
* `getScopeRailSegments` / `getAllScopeRailSegments` by joining the host-
|
|
440
|
+
* supplied workflow state with the current page graph. When omitted,
|
|
441
|
+
* both methods return empty arrays.
|
|
442
|
+
*/
|
|
443
|
+
getWorkflowRailInput?: () =>
|
|
444
|
+
| Omit<
|
|
445
|
+
import("../workflow-rail-segments.ts").CollectScopeRailSegmentsInput,
|
|
446
|
+
"pageGraph"
|
|
447
|
+
>
|
|
448
|
+
| null
|
|
449
|
+
| undefined;
|
|
296
450
|
}
|
|
297
451
|
|
|
298
452
|
export function createLayoutFacet(
|
|
@@ -312,7 +466,18 @@ export function createLayoutFacet(
|
|
|
312
466
|
|
|
313
467
|
const listeners = new Set<(event: LayoutFacetEvent) => void>();
|
|
314
468
|
const unsubscribeEngine = engine.subscribe((event: LayoutEngineEvent) => {
|
|
315
|
-
|
|
469
|
+
// `incremental_relayout` needs the last page index so the facet can
|
|
470
|
+
// report a `pageRange: [fromPageIndex, toPageIndex]` without asking the
|
|
471
|
+
// engine for an extra read. Use a best-effort lookup; on zero-page
|
|
472
|
+
// graphs the range collapses to the same index.
|
|
473
|
+
let lastPageIndex = 0;
|
|
474
|
+
try {
|
|
475
|
+
const pageCount = engine.getPageGraph(getQueryInput()).pages.length;
|
|
476
|
+
lastPageIndex = Math.max(0, pageCount - 1);
|
|
477
|
+
} catch {
|
|
478
|
+
lastPageIndex = 0;
|
|
479
|
+
}
|
|
480
|
+
const facetEvent = toFacetEvent(event, lastPageIndex);
|
|
316
481
|
if (!facetEvent) return;
|
|
317
482
|
for (const listener of listeners) {
|
|
318
483
|
try {
|
|
@@ -356,15 +521,10 @@ export function createLayoutFacet(
|
|
|
356
521
|
.filter((node): node is PublicSectionNode => node !== null);
|
|
357
522
|
},
|
|
358
523
|
|
|
359
|
-
getPageForOffset(offset,
|
|
524
|
+
getPageForOffset(offset, story) {
|
|
360
525
|
const graph = currentGraph();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return toPublicPageNode(page, graph);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const last = graph.pages[graph.pages.length - 1];
|
|
367
|
-
return last ? toPublicPageNode(last, graph) : null;
|
|
526
|
+
const page = findPageForOffsetAndStory(graph, offset, story);
|
|
527
|
+
return page ? toPublicPageNode(page, graph) : null;
|
|
368
528
|
},
|
|
369
529
|
|
|
370
530
|
getPageSpanForSelection(selection) {
|
|
@@ -422,11 +582,106 @@ export function createLayoutFacet(
|
|
|
422
582
|
const node = graph.pages[pageIndex];
|
|
423
583
|
if (!node) return [];
|
|
424
584
|
const region = options?.region ?? "body";
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
585
|
+
return collectLineBoxesForRegion(
|
|
586
|
+
node,
|
|
587
|
+
region,
|
|
588
|
+
options?.columnIndex,
|
|
589
|
+
).map((box) => toPublicLineBox(box));
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
getLineBoxesForRegion(pageIndex, region, options) {
|
|
593
|
+
const graph = currentGraph();
|
|
594
|
+
const node = graph.pages[pageIndex];
|
|
595
|
+
if (!node) return [];
|
|
596
|
+
return collectLineBoxesForRegion(node, region, options?.columnIndex).map(
|
|
597
|
+
(box) => toPublicLineBox(box),
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
getPageFormatCatalog() {
|
|
602
|
+
return PAGE_FORMAT_CATALOG;
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
getActivePageFormat(sectionIndex) {
|
|
606
|
+
const graph = currentGraph();
|
|
607
|
+
const sectionPages = graph.pages.filter(
|
|
608
|
+
(p) => p.sectionIndex === sectionIndex,
|
|
609
|
+
);
|
|
610
|
+
const firstPage = sectionPages[0];
|
|
611
|
+
if (!firstPage) return null;
|
|
612
|
+
return matchPageFormat({
|
|
613
|
+
sectionIndex,
|
|
614
|
+
widthTwips: firstPage.layout.pageWidth,
|
|
615
|
+
heightTwips: firstPage.layout.pageHeight,
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
getMarginPresetCatalog() {
|
|
620
|
+
return MARGIN_PRESET_CATALOG;
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
getActiveMarginPreset(sectionIndex) {
|
|
624
|
+
const graph = currentGraph();
|
|
625
|
+
const sectionPages = graph.pages.filter(
|
|
626
|
+
(p) => p.sectionIndex === sectionIndex,
|
|
627
|
+
);
|
|
628
|
+
const firstPage = sectionPages[0];
|
|
629
|
+
if (!firstPage) return null;
|
|
630
|
+
const layout = firstPage.layout;
|
|
631
|
+
// `mirrorMargins` is a document-level setting
|
|
632
|
+
// (`w:mirrorMargins`) that the current canonical model does not
|
|
633
|
+
// surface onto the per-section layout snapshot; pass `false` until
|
|
634
|
+
// that plumbing exists so the Mirrored preset only matches when
|
|
635
|
+
// callers pass the preset-builder helper explicitly.
|
|
636
|
+
return matchMarginPreset({
|
|
637
|
+
sectionIndex,
|
|
638
|
+
topTwips: layout.marginTop,
|
|
639
|
+
bottomTwips: layout.marginBottom,
|
|
640
|
+
leftTwips: layout.marginLeft,
|
|
641
|
+
rightTwips: layout.marginRight,
|
|
642
|
+
gutterTwips: layout.gutter,
|
|
643
|
+
mirrored: false,
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
getRenderFrame(options) {
|
|
648
|
+
const kernel = input.renderKernel?.();
|
|
649
|
+
if (!kernel) return null;
|
|
650
|
+
return kernel.getRenderFrame(options);
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
getRenderZoom() {
|
|
654
|
+
const kernel = input.renderKernel?.();
|
|
655
|
+
if (!kernel) return null;
|
|
656
|
+
return kernel.getZoom();
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
hitTest(pointInRoot) {
|
|
660
|
+
const kernel = input.renderKernel?.();
|
|
661
|
+
if (!kernel) return null;
|
|
662
|
+
const frame = kernel.getRenderFrame();
|
|
663
|
+
return resolveHitTest(frame, pointInRoot);
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
getAnchorRects(query) {
|
|
667
|
+
const kernel = input.renderKernel?.();
|
|
668
|
+
if (!kernel) return [];
|
|
669
|
+
const frame = kernel.getRenderFrame();
|
|
670
|
+
return resolveAnchorRects(frame, query);
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
getScopeRailSegments(pageIndex) {
|
|
674
|
+
return collectScopeRailSegmentsForQuery(
|
|
675
|
+
input.getWorkflowRailInput?.(),
|
|
676
|
+
currentGraph(),
|
|
677
|
+
).filter((segment) => segment.pageIndex === pageIndex);
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
getAllScopeRailSegments() {
|
|
681
|
+
return collectScopeRailSegmentsForQuery(
|
|
682
|
+
input.getWorkflowRailInput?.(),
|
|
683
|
+
currentGraph(),
|
|
684
|
+
);
|
|
430
685
|
},
|
|
431
686
|
|
|
432
687
|
getFragmentsForPage(pageIndex) {
|
|
@@ -481,6 +736,33 @@ export function createLayoutFacet(
|
|
|
481
736
|
return engine.whenMeasurementReady();
|
|
482
737
|
},
|
|
483
738
|
|
|
739
|
+
getTableRenderPlan(blockId, pageIndex) {
|
|
740
|
+
const graph = currentGraph();
|
|
741
|
+
const fragment = graph.fragments.find((f) => f.blockId === blockId);
|
|
742
|
+
if (!fragment) return null;
|
|
743
|
+
const queryInput = getQueryInput();
|
|
744
|
+
const surface = lazyMainSurfaceForFacet(queryInput);
|
|
745
|
+
const tableBlock = findTableBlockByBlockId(surface.blocks, blockId);
|
|
746
|
+
if (!tableBlock) return null;
|
|
747
|
+
const resolved = resolveTableStyleResolutionForPlan(
|
|
748
|
+
tableBlock,
|
|
749
|
+
queryInput.document,
|
|
750
|
+
);
|
|
751
|
+
if (!resolved) return null;
|
|
752
|
+
// Sum all fragments for this block so the grip height spans every
|
|
753
|
+
// page the table occupies.
|
|
754
|
+
const tableHeightTwips = graph.fragments
|
|
755
|
+
.filter((f) => f.blockId === blockId)
|
|
756
|
+
.reduce((total, f) => total + f.heightTwips, 0);
|
|
757
|
+
return buildTableRenderPlan({
|
|
758
|
+
blockId,
|
|
759
|
+
pageIndex,
|
|
760
|
+
block: tableBlock,
|
|
761
|
+
resolved,
|
|
762
|
+
tableHeightTwips,
|
|
763
|
+
});
|
|
764
|
+
},
|
|
765
|
+
|
|
484
766
|
getDirtyFieldFamilies() {
|
|
485
767
|
return engine.getDirtyFieldFamilies();
|
|
486
768
|
},
|
|
@@ -671,7 +953,10 @@ function toPublicRunFormatting(
|
|
|
671
953
|
};
|
|
672
954
|
}
|
|
673
955
|
|
|
674
|
-
function toFacetEvent(
|
|
956
|
+
function toFacetEvent(
|
|
957
|
+
event: LayoutEngineEvent,
|
|
958
|
+
lastPageIndex: number,
|
|
959
|
+
): LayoutFacetEvent | null {
|
|
675
960
|
switch (event.kind) {
|
|
676
961
|
case "layout_recomputed":
|
|
677
962
|
return {
|
|
@@ -699,7 +984,415 @@ function toFacetEvent(event: LayoutEngineEvent): LayoutFacetEvent | null {
|
|
|
699
984
|
(event.fidelity as PublicMeasurementFidelity | undefined) ?? "empirical",
|
|
700
985
|
revision: event.revision,
|
|
701
986
|
};
|
|
987
|
+
case "incremental_relayout": {
|
|
988
|
+
const fromPageIndex =
|
|
989
|
+
event.firstDirtyPageIndex !== undefined
|
|
990
|
+
? Math.max(0, event.firstDirtyPageIndex)
|
|
991
|
+
: 0;
|
|
992
|
+
const toPageIndex = Math.max(fromPageIndex, lastPageIndex);
|
|
993
|
+
return {
|
|
994
|
+
kind: "incremental_relayout",
|
|
995
|
+
revision: event.revision,
|
|
996
|
+
pageRange: { fromPageIndex, toPageIndex },
|
|
997
|
+
...(event.reason ? { reason: event.reason } : {}),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
702
1000
|
default:
|
|
703
1001
|
return null;
|
|
704
1002
|
}
|
|
705
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Resolve the page a given offset + story should surface on.
|
|
1007
|
+
*
|
|
1008
|
+
* Contract (per runtime-rendering-and-chrome-phase.md §2.3):
|
|
1009
|
+
* - Main story: the first content page containing the offset (unchanged).
|
|
1010
|
+
* - Header/footer story: the first page in the relevant section that
|
|
1011
|
+
* renders a header/footer matching the target variant. When the
|
|
1012
|
+
* section-index hint is omitted, the first page rendering any
|
|
1013
|
+
* variant of this kind is returned.
|
|
1014
|
+
* - Footnote/endnote story: the first page that reserves a note
|
|
1015
|
+
* allocation for the given note (by note id); falls back to the
|
|
1016
|
+
* first content page so UI surfaces always have an anchor.
|
|
1017
|
+
*/
|
|
1018
|
+
function findPageForOffsetAndStory(
|
|
1019
|
+
graph: RuntimePageGraph,
|
|
1020
|
+
offset: number,
|
|
1021
|
+
story?: EditorStoryTarget,
|
|
1022
|
+
): RuntimePageNode | null {
|
|
1023
|
+
if (!story || story.kind === "main") {
|
|
1024
|
+
for (const page of graph.pages) {
|
|
1025
|
+
if (!page.isBlankFiller && offset < page.endOffset) {
|
|
1026
|
+
return page;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const last = graph.pages[graph.pages.length - 1];
|
|
1030
|
+
return last ?? null;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (story.kind === "header" || story.kind === "footer") {
|
|
1034
|
+
const targetKind = story.kind;
|
|
1035
|
+
const targetVariant = story.variant;
|
|
1036
|
+
const targetRelationshipId = story.relationshipId;
|
|
1037
|
+
const sectionIndex = story.sectionIndex;
|
|
1038
|
+
const candidates = graph.pages.filter((page) => {
|
|
1039
|
+
if (page.isBlankFiller) return false;
|
|
1040
|
+
if (sectionIndex !== undefined && page.sectionIndex !== sectionIndex) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
const side =
|
|
1044
|
+
targetKind === "header" ? page.stories.header : page.stories.footer;
|
|
1045
|
+
if (!side) return false;
|
|
1046
|
+
if (side.kind !== "header" && side.kind !== "footer") return false;
|
|
1047
|
+
if (targetVariant !== undefined && side.variant !== targetVariant) {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
if (
|
|
1051
|
+
targetRelationshipId !== undefined &&
|
|
1052
|
+
side.relationshipId !== targetRelationshipId
|
|
1053
|
+
) {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
return true;
|
|
1057
|
+
});
|
|
1058
|
+
if (candidates[0]) return candidates[0];
|
|
1059
|
+
// Fallback: any page in the given section (or the first page overall)
|
|
1060
|
+
if (sectionIndex !== undefined) {
|
|
1061
|
+
const fallback = graph.pages.find(
|
|
1062
|
+
(p) => p.sectionIndex === sectionIndex && !p.isBlankFiller,
|
|
1063
|
+
);
|
|
1064
|
+
if (fallback) return fallback;
|
|
1065
|
+
}
|
|
1066
|
+
const first = graph.pages.find((p) => !p.isBlankFiller);
|
|
1067
|
+
return first ?? graph.pages[0] ?? null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (story.kind === "footnote" || story.kind === "endnote") {
|
|
1071
|
+
const noteKind = story.kind;
|
|
1072
|
+
const noteId = story.noteId;
|
|
1073
|
+
if (noteId !== undefined) {
|
|
1074
|
+
const page = graph.pages.find((p) =>
|
|
1075
|
+
p.noteAllocations.some(
|
|
1076
|
+
(a) => a.noteKind === noteKind && a.noteId === noteId,
|
|
1077
|
+
),
|
|
1078
|
+
);
|
|
1079
|
+
if (page) return page;
|
|
1080
|
+
}
|
|
1081
|
+
for (const page of graph.pages) {
|
|
1082
|
+
if (!page.isBlankFiller && offset < page.endOffset) {
|
|
1083
|
+
return page;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const last = graph.pages[graph.pages.length - 1];
|
|
1087
|
+
return last ?? null;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Select line boxes that belong to a given region on a page.
|
|
1095
|
+
*
|
|
1096
|
+
* Today the engine populates body line boxes only; header/footer/column/
|
|
1097
|
+
* footnote-area regions produce empty arrays until the render kernel lands
|
|
1098
|
+
* (Phase R1). The region filter is now exact: `body` returns only body
|
|
1099
|
+
* line boxes, everything else returns empty. Consumers should prefer
|
|
1100
|
+
* `getLineBoxes(pageIndex, { region: "body" })` — the other kinds are
|
|
1101
|
+
* scaffolded so UI can start reading through the facet without special-
|
|
1102
|
+
* casing region availability.
|
|
1103
|
+
*/
|
|
1104
|
+
function collectLineBoxesForRegion(
|
|
1105
|
+
node: RuntimePageNode,
|
|
1106
|
+
region: PublicPageRegion["kind"],
|
|
1107
|
+
_columnIndex: number | undefined,
|
|
1108
|
+
): readonly RuntimeLineBoxAlias[] {
|
|
1109
|
+
void _columnIndex;
|
|
1110
|
+
if (region === "body") {
|
|
1111
|
+
return node.lineBoxes;
|
|
1112
|
+
}
|
|
1113
|
+
return EMPTY_LINE_BOXES;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Use a shared alias so the region helper doesn't import the runtime
|
|
1117
|
+
// `RuntimeLineBox` type redundantly (`lineBoxes` is already strongly typed on
|
|
1118
|
+
// `RuntimePageNode`).
|
|
1119
|
+
type RuntimeLineBoxAlias = RuntimePageNode["lineBoxes"][number];
|
|
1120
|
+
const EMPTY_LINE_BOXES: readonly RuntimeLineBoxAlias[] = Object.freeze([]);
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Join the host-supplied workflow rail input with the current page graph.
|
|
1124
|
+
* Returns an empty array when no input is available so callers can always
|
|
1125
|
+
* iterate without null-checking.
|
|
1126
|
+
*/
|
|
1127
|
+
function collectScopeRailSegmentsForQuery(
|
|
1128
|
+
input:
|
|
1129
|
+
| Omit<
|
|
1130
|
+
import("../workflow-rail-segments.ts").CollectScopeRailSegmentsInput,
|
|
1131
|
+
"pageGraph"
|
|
1132
|
+
>
|
|
1133
|
+
| null
|
|
1134
|
+
| undefined,
|
|
1135
|
+
graph: RuntimePageGraph,
|
|
1136
|
+
): ScopeRailSegment[] {
|
|
1137
|
+
if (!input) return [];
|
|
1138
|
+
return collectScopeRailSegments({ ...input, pageGraph: graph });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function resolveHitTest(
|
|
1142
|
+
frame: import("../render/index.ts").RenderFrame | null,
|
|
1143
|
+
point: import("../render/index.ts").RenderPoint,
|
|
1144
|
+
): import("../render/index.ts").RenderHitResult | null {
|
|
1145
|
+
if (!frame) return null;
|
|
1146
|
+
for (const page of frame.pages) {
|
|
1147
|
+
if (!containsPoint(page.frame, point)) continue;
|
|
1148
|
+
const regionHit = hitTestRegion(page, point, "body");
|
|
1149
|
+
if (regionHit) return regionHit;
|
|
1150
|
+
const header = hitTestRegion(page, point, "header");
|
|
1151
|
+
if (header) return header;
|
|
1152
|
+
const footer = hitTestRegion(page, point, "footer");
|
|
1153
|
+
if (footer) return footer;
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function hitTestRegion(
|
|
1159
|
+
page: import("../render/index.ts").RenderPage,
|
|
1160
|
+
point: import("../render/index.ts").RenderPoint,
|
|
1161
|
+
kind: "body" | "header" | "footer",
|
|
1162
|
+
): import("../render/index.ts").RenderHitResult | null {
|
|
1163
|
+
const region =
|
|
1164
|
+
kind === "body"
|
|
1165
|
+
? page.regions.body
|
|
1166
|
+
: kind === "header"
|
|
1167
|
+
? page.regions.header
|
|
1168
|
+
: page.regions.footer;
|
|
1169
|
+
if (!region) return null;
|
|
1170
|
+
if (!containsPoint(region.frame, point)) return null;
|
|
1171
|
+
for (const block of region.blocks) {
|
|
1172
|
+
if (!containsPoint(block.frame, point)) continue;
|
|
1173
|
+
let bestLineIndex = -1;
|
|
1174
|
+
let bestLineDistance = Number.POSITIVE_INFINITY;
|
|
1175
|
+
for (let i = 0; i < block.lines.length; i++) {
|
|
1176
|
+
const line = block.lines[i]!;
|
|
1177
|
+
const midY = line.frame.topPx + line.frame.heightPx / 2;
|
|
1178
|
+
const distance = Math.abs(midY - point.yPx);
|
|
1179
|
+
if (distance < bestLineDistance) {
|
|
1180
|
+
bestLineDistance = distance;
|
|
1181
|
+
bestLineIndex = i;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const lineIndex = bestLineIndex >= 0 ? bestLineIndex : 0;
|
|
1185
|
+
const line = block.lines[lineIndex];
|
|
1186
|
+
const runtimeOffset = line?.anchors[0]?.runtimeOffset ?? block.fragment.from;
|
|
1187
|
+
return {
|
|
1188
|
+
pageIndex: page.page.pageIndex,
|
|
1189
|
+
regionKind: region.region.kind,
|
|
1190
|
+
blockId: block.fragment.blockId,
|
|
1191
|
+
fragmentId: block.fragment.fragmentId,
|
|
1192
|
+
lineIndex,
|
|
1193
|
+
runtimeOffset,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
// Point fell inside the region but between blocks — snap to the nearest block.
|
|
1197
|
+
let nearestBlock: import("../render/index.ts").RenderBlock | null = null;
|
|
1198
|
+
let nearestDistance = Number.POSITIVE_INFINITY;
|
|
1199
|
+
for (const block of region.blocks) {
|
|
1200
|
+
const distance = Math.min(
|
|
1201
|
+
Math.abs(point.yPx - block.frame.topPx),
|
|
1202
|
+
Math.abs(point.yPx - (block.frame.topPx + block.frame.heightPx)),
|
|
1203
|
+
);
|
|
1204
|
+
if (distance < nearestDistance) {
|
|
1205
|
+
nearestBlock = block;
|
|
1206
|
+
nearestDistance = distance;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (!nearestBlock) return null;
|
|
1210
|
+
return {
|
|
1211
|
+
pageIndex: page.page.pageIndex,
|
|
1212
|
+
regionKind: region.region.kind,
|
|
1213
|
+
blockId: nearestBlock.fragment.blockId,
|
|
1214
|
+
fragmentId: nearestBlock.fragment.fragmentId,
|
|
1215
|
+
lineIndex: 0,
|
|
1216
|
+
runtimeOffset: nearestBlock.fragment.from,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function containsPoint(
|
|
1221
|
+
rect: import("../render/index.ts").RenderFrameRect,
|
|
1222
|
+
point: import("../render/index.ts").RenderPoint,
|
|
1223
|
+
): boolean {
|
|
1224
|
+
return (
|
|
1225
|
+
point.xPx >= rect.leftPx &&
|
|
1226
|
+
point.xPx <= rect.leftPx + rect.widthPx &&
|
|
1227
|
+
point.yPx >= rect.topPx &&
|
|
1228
|
+
point.yPx <= rect.topPx + rect.heightPx
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function resolveAnchorRects(
|
|
1233
|
+
frame: import("../render/index.ts").RenderFrame | null,
|
|
1234
|
+
query: import("../render/index.ts").RenderAnchorQuery,
|
|
1235
|
+
): readonly import("../render/index.ts").RenderFrameRect[] {
|
|
1236
|
+
if (!frame) return [];
|
|
1237
|
+
switch (query.kind) {
|
|
1238
|
+
case "runtime-offset": {
|
|
1239
|
+
const offset =
|
|
1240
|
+
typeof query.value === "number" ? query.value : Number(query.value);
|
|
1241
|
+
if (!Number.isFinite(offset)) return [];
|
|
1242
|
+
const rect = frame.anchorIndex.byRuntimeOffset(offset, query.story);
|
|
1243
|
+
return rect ? [rect] : [];
|
|
1244
|
+
}
|
|
1245
|
+
case "block-id": {
|
|
1246
|
+
const rect = frame.anchorIndex.byBlockId(String(query.value));
|
|
1247
|
+
return rect ? [rect] : [];
|
|
1248
|
+
}
|
|
1249
|
+
case "fragment-id": {
|
|
1250
|
+
const rect = frame.anchorIndex.byFragmentId(String(query.value));
|
|
1251
|
+
return rect ? [rect] : [];
|
|
1252
|
+
}
|
|
1253
|
+
case "page-index": {
|
|
1254
|
+
const pageIndex =
|
|
1255
|
+
typeof query.value === "number" ? query.value : Number(query.value);
|
|
1256
|
+
if (!Number.isFinite(pageIndex)) return [];
|
|
1257
|
+
const rect = frame.anchorIndex.byPageIndex(pageIndex);
|
|
1258
|
+
return rect ? [rect] : [];
|
|
1259
|
+
}
|
|
1260
|
+
case "scope-id": {
|
|
1261
|
+
const id = String(query.value);
|
|
1262
|
+
return frame.decorationIndex.workflow
|
|
1263
|
+
.filter((decoration) => decoration.refId === id)
|
|
1264
|
+
.map((decoration) => decoration.frame);
|
|
1265
|
+
}
|
|
1266
|
+
case "comment-id": {
|
|
1267
|
+
const id = String(query.value);
|
|
1268
|
+
return frame.decorationIndex.comments
|
|
1269
|
+
.filter((decoration) => decoration.refId === id)
|
|
1270
|
+
.map((decoration) => decoration.frame);
|
|
1271
|
+
}
|
|
1272
|
+
case "revision-id": {
|
|
1273
|
+
const id = String(query.value);
|
|
1274
|
+
return frame.decorationIndex.revisions
|
|
1275
|
+
.filter((decoration) => decoration.refId === id)
|
|
1276
|
+
.map((decoration) => decoration.frame);
|
|
1277
|
+
}
|
|
1278
|
+
default: {
|
|
1279
|
+
const exhaustive: never = query.kind;
|
|
1280
|
+
void exhaustive;
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// ---------------------------------------------------------------------------
|
|
1287
|
+
// Table render plan helpers (P4)
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Produce (or reuse) the main surface snapshot the facet needs for table
|
|
1292
|
+
* plan resolution. The engine already rebuilds a surface on every full
|
|
1293
|
+
* relayout, but that output is not cached externally — for a per-call
|
|
1294
|
+
* `getTableRenderPlan` we reconstruct the surface with a static zero
|
|
1295
|
+
* selection. This is a pure read and only runs on chrome requests, so
|
|
1296
|
+
* the cost is bounded.
|
|
1297
|
+
*/
|
|
1298
|
+
function lazyMainSurfaceForFacet(
|
|
1299
|
+
input: LayoutEngineQueryInput,
|
|
1300
|
+
): ReturnType<typeof createEditorSurfaceSnapshot> {
|
|
1301
|
+
return createEditorSurfaceSnapshot(
|
|
1302
|
+
input.document,
|
|
1303
|
+
createSelectionSnapshot(0, 0),
|
|
1304
|
+
MAIN_STORY_TARGET,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Walk a flat surface block list (with recursion into sdt/table nested
|
|
1310
|
+
* children) to find a table block by its projected blockId.
|
|
1311
|
+
*/
|
|
1312
|
+
function findTableBlockByBlockId(
|
|
1313
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
1314
|
+
blockId: string,
|
|
1315
|
+
): Extract<SurfaceBlockSnapshot, { kind: "table" }> | null {
|
|
1316
|
+
for (const block of blocks) {
|
|
1317
|
+
if (block.kind === "table") {
|
|
1318
|
+
if (block.blockId === blockId) return block;
|
|
1319
|
+
// Recurse into nested tables (cell contents may contain tables).
|
|
1320
|
+
for (const row of block.rows) {
|
|
1321
|
+
for (const cell of row.cells) {
|
|
1322
|
+
const nested = findTableBlockByBlockId(cell.content, blockId);
|
|
1323
|
+
if (nested) return nested;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} else if (block.kind === "sdt_block") {
|
|
1327
|
+
const nested = findTableBlockByBlockId(block.children, blockId);
|
|
1328
|
+
if (nested) return nested;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Resolve the table-style cascade (direct properties → style chain → band
|
|
1336
|
+
* conditional formatting) for a surface table block. Needs the canonical
|
|
1337
|
+
* document to look up the underlying `TableNode` by blockId so the full
|
|
1338
|
+
* resolver runs against canonical state, not the pre-projected surface.
|
|
1339
|
+
*/
|
|
1340
|
+
function resolveTableStyleResolutionForPlan(
|
|
1341
|
+
surfaceTable: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
1342
|
+
document: LayoutEngineQueryInput["document"],
|
|
1343
|
+
): ReturnType<typeof resolveTableStyleResolution> | null {
|
|
1344
|
+
const canonicalTable = findCanonicalTableByBlockId(
|
|
1345
|
+
document.content.children,
|
|
1346
|
+
surfaceTable.blockId,
|
|
1347
|
+
{ counter: { value: 0 } },
|
|
1348
|
+
);
|
|
1349
|
+
if (!canonicalTable) return null;
|
|
1350
|
+
return resolveTableStyleResolution(
|
|
1351
|
+
canonicalTable,
|
|
1352
|
+
document.styles.tables ?? {},
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Walk canonical content depth-first incrementing the table counter to
|
|
1358
|
+
* locate the `TableNode` whose projected blockId would be `table-${N}`.
|
|
1359
|
+
* Mirrors the counter discipline in `src/runtime/surface-projection.ts`.
|
|
1360
|
+
*/
|
|
1361
|
+
function findCanonicalTableByBlockId(
|
|
1362
|
+
children: import("../../model/canonical-document.ts").BlockNode[],
|
|
1363
|
+
targetBlockId: string,
|
|
1364
|
+
state: { counter: { value: number } },
|
|
1365
|
+
): import("../../model/canonical-document.ts").TableNode | null {
|
|
1366
|
+
for (const child of children) {
|
|
1367
|
+
if (child.type === "table") {
|
|
1368
|
+
const index = state.counter.value;
|
|
1369
|
+
state.counter.value += 1;
|
|
1370
|
+
if (`table-${index}` === targetBlockId) return child;
|
|
1371
|
+
for (const row of child.rows) {
|
|
1372
|
+
for (const cell of row.cells) {
|
|
1373
|
+
const nested = findCanonicalTableByBlockId(
|
|
1374
|
+
cell.children,
|
|
1375
|
+
targetBlockId,
|
|
1376
|
+
state,
|
|
1377
|
+
);
|
|
1378
|
+
if (nested) return nested;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
} else if (child.type === "sdt") {
|
|
1382
|
+
const nested = findCanonicalTableByBlockId(
|
|
1383
|
+
child.children,
|
|
1384
|
+
targetBlockId,
|
|
1385
|
+
state,
|
|
1386
|
+
);
|
|
1387
|
+
if (nested) return nested;
|
|
1388
|
+
} else if (child.type === "custom_xml") {
|
|
1389
|
+
const nested = findCanonicalTableByBlockId(
|
|
1390
|
+
child.children,
|
|
1391
|
+
targetBlockId,
|
|
1392
|
+
state,
|
|
1393
|
+
);
|
|
1394
|
+
if (nested) return nested;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|