@beyondwork/docx-react-component 1.0.71 → 1.0.72
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 +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +243 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/io/export/serialize-headers-footers.ts +1 -0
- package/src/io/export/serialize-main-document.ts +13 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +11 -0
- package/src/io/ooxml/parse-main-document.ts +21 -5
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +257 -1
- package/src/runtime/layout/paginated-layout-engine.ts +134 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +49 -3
- package/src/runtime/scopes/semantic-scope-types.ts +8 -0
- package/src/runtime/surface-projection.ts +22 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -4
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +37 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +5 -2
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +73 -8
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +1 -0
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* queryScopes (live) / getMarkup (live) / getGuard (live) /
|
|
5
5
|
* createScope (live-with-adapter) / attachMetadata (live-with-adapter) /
|
|
6
6
|
* getVisibilityPolicy · getVisibilityPolicies · setVisibilityPolicy ·
|
|
7
|
-
* clearVisibilityPolicy (live — W10 state-classes X1)
|
|
7
|
+
* clearVisibilityPolicy (live — W10 state-classes X1) /
|
|
8
|
+
* scopeTags (live — coord-10 L11-4 tag-catalog read).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
@@ -17,6 +18,13 @@ import type {
|
|
|
17
18
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
18
19
|
import { createScopeFromBlockId } from "../../../runtime/workflow/scope-writer.ts";
|
|
19
20
|
import { attachScopeMetadata } from "../../../runtime/workflow/metadata-writer.ts";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_REGISTRY_ENTRIES,
|
|
23
|
+
DEFAULT_UNKNOWN_BEHAVIOR,
|
|
24
|
+
createScopeTagRegistry,
|
|
25
|
+
type ScopeTagBehavior,
|
|
26
|
+
type ScopeTagRegistry,
|
|
27
|
+
} from "../../../runtime/workflow/scope-tag-registry.ts";
|
|
20
28
|
|
|
21
29
|
export const queryScopesMetadata: ApiV3FnMetadata = {
|
|
22
30
|
name: "runtime.workflow.queryScopes",
|
|
@@ -79,6 +87,25 @@ export interface CreateScopeInput {
|
|
|
79
87
|
* leak in). Agents pick per scope family per coord-09 §1.14.
|
|
80
88
|
*/
|
|
81
89
|
readonly assoc?: { readonly start: -1 | 1; readonly end: -1 | 1 };
|
|
90
|
+
/**
|
|
91
|
+
* Coord-08 §9 / coord-09 §1.13 (A3) — caller-steerable identity
|
|
92
|
+
* strategy for the enumerated scope's `ScopeHandle.stableRef`.
|
|
93
|
+
* Passthrough to L06 scope-writer + L08 compiler. Honored for
|
|
94
|
+
* `"scope-id"` and `"semantic-path"` today; `"bookmark"` and
|
|
95
|
+
* `"runtime-handle"` fall back to the compiler default (bookmark
|
|
96
|
+
* lookup not wired in phase 1). See
|
|
97
|
+
* `src/runtime/scopes/enumerate-scopes.ts::stableRefHintForScopeId`.
|
|
98
|
+
*
|
|
99
|
+
* Ownership: L08 owns the selection policy; L06 owns the primitive
|
|
100
|
+
* (`scope-writer.ts::CreateScopeFromBlockIdInput.stableRefHint`);
|
|
101
|
+
* L07 is passthrough (this field); L02 owns the `ScopeStableRef`
|
|
102
|
+
* value shape.
|
|
103
|
+
*/
|
|
104
|
+
readonly stableRefHint?:
|
|
105
|
+
| "scope-id"
|
|
106
|
+
| "bookmark"
|
|
107
|
+
| "semantic-path"
|
|
108
|
+
| "runtime-handle";
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
export interface CreateScopeResult {
|
|
@@ -273,6 +300,68 @@ export const subscribeMarkupModePolicyMetadata: ApiV3FnMetadata = {
|
|
|
273
300
|
rwdReference: "§Runtime API § runtime.workflow.subscribeMarkupModePolicy",
|
|
274
301
|
};
|
|
275
302
|
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Coord-10 L11-4 — scope-tag catalog read, graduating the pragmatic
|
|
305
|
+
// `createScopeTagRegistry` re-export through `src/api/public-types.ts`
|
|
306
|
+
// (refactor/11 §4.17 commit `7a2d2fc0`, 2026-04-24) to a proper v3 family
|
|
307
|
+
// entry. Three methods: factory + catalog enumeration + per-tag peek. All
|
|
308
|
+
// A-canonical — the catalog is code-derived but describes canonical-shape
|
|
309
|
+
// invariants: every tag it catalogs is encoded into customXml (via scope-
|
|
310
|
+
// marker / bookmark / comment structures) and broadcast via crdt along
|
|
311
|
+
// with scope-marker commits.
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export const createScopeTagRegistryMetadata: ApiV3FnMetadata = {
|
|
315
|
+
name: "runtime.workflow.createScopeTagRegistry",
|
|
316
|
+
status: "live",
|
|
317
|
+
sourceLayer: "workflow-review",
|
|
318
|
+
liveEvidence: {
|
|
319
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
320
|
+
commit: "coord-10-l11-4",
|
|
321
|
+
},
|
|
322
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
323
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
324
|
+
stateClass: "A-canonical",
|
|
325
|
+
persistsTo: "customXml",
|
|
326
|
+
broadcastsVia: "crdt",
|
|
327
|
+
rwdReference:
|
|
328
|
+
"§Runtime API § runtime.workflow.createScopeTagRegistry. Mints a fresh default-seeded ScopeTagRegistry. Hosts register custom annotation families on the returned registry via `.register(...)`. Retires the pragmatic `createScopeTagRegistry` re-export through public-types.ts (refactor/11 §4.17 2026-04-24) — the v3 family is the long-term home per coord-10 L11-4.",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export const listScopeTagsMetadata: ApiV3FnMetadata = {
|
|
332
|
+
name: "runtime.workflow.listScopeTags",
|
|
333
|
+
status: "live",
|
|
334
|
+
sourceLayer: "workflow-review",
|
|
335
|
+
liveEvidence: {
|
|
336
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
337
|
+
commit: "coord-10-l11-4",
|
|
338
|
+
},
|
|
339
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
340
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
341
|
+
stateClass: "A-canonical",
|
|
342
|
+
persistsTo: "customXml",
|
|
343
|
+
broadcastsVia: "crdt",
|
|
344
|
+
rwdReference:
|
|
345
|
+
"§Runtime API § runtime.workflow.listScopeTags. Enumerates the shipped default catalog as `[tagType, behavior]` pairs without constructing a registry. Does not reflect host-registered custom tags — callers that want those iterate `createScopeTagRegistry().list()` on their own instance.",
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const getScopeTagMetadata: ApiV3FnMetadata = {
|
|
349
|
+
name: "runtime.workflow.getScopeTag",
|
|
350
|
+
status: "live",
|
|
351
|
+
sourceLayer: "workflow-review",
|
|
352
|
+
liveEvidence: {
|
|
353
|
+
runnerTest: "test/api/v3/runtime/workflow-scope-tags.test.ts",
|
|
354
|
+
commit: "coord-10-l11-4",
|
|
355
|
+
},
|
|
356
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
357
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "scope-tag-catalog" },
|
|
358
|
+
stateClass: "A-canonical",
|
|
359
|
+
persistsTo: "customXml",
|
|
360
|
+
broadcastsVia: "crdt",
|
|
361
|
+
rwdReference:
|
|
362
|
+
"§Runtime API § runtime.workflow.getScopeTag. Peeks a default tag's behavior by name. Returns DEFAULT_UNKNOWN_BEHAVIOR for unknown tag types (bail-on-cross) — matches `createScopeTagRegistry().get(unknownTagType)`.",
|
|
363
|
+
};
|
|
364
|
+
|
|
276
365
|
export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
277
366
|
return {
|
|
278
367
|
queryScopes(filter?: unknown) {
|
|
@@ -301,6 +390,9 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
301
390
|
mode: input.mode,
|
|
302
391
|
label: input.label,
|
|
303
392
|
...(input.assoc ? { assoc: input.assoc } : {}),
|
|
393
|
+
...(input.stableRefHint
|
|
394
|
+
? { stableRefHint: input.stableRefHint }
|
|
395
|
+
: {}),
|
|
304
396
|
});
|
|
305
397
|
emitUxResponse(runtime, {
|
|
306
398
|
apiFn: createScopeMetadata.name,
|
|
@@ -430,5 +522,26 @@ export function createWorkflowFamily(runtime: RuntimeApiHandle) {
|
|
|
430
522
|
}
|
|
431
523
|
return { status: "scope-not-found" };
|
|
432
524
|
},
|
|
525
|
+
|
|
526
|
+
createScopeTagRegistry(): ScopeTagRegistry {
|
|
527
|
+
// @endStateApi — live. Coord-10 L11-4. Mints a fresh default-
|
|
528
|
+
// seeded registry. Hosts register custom annotation families on
|
|
529
|
+
// the returned registry via `.register(...)`.
|
|
530
|
+
return createScopeTagRegistry();
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
listScopeTags(): readonly (readonly [string, ScopeTagBehavior])[] {
|
|
534
|
+
// @endStateApi — live. Coord-10 L11-4. Enumerates the shipped
|
|
535
|
+
// default catalog as `[tagType, behavior]` pairs. Does not
|
|
536
|
+
// reflect host-registered customs on any specific registry.
|
|
537
|
+
return Object.entries(DEFAULT_REGISTRY_ENTRIES);
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
getScopeTag(tagType: string): ScopeTagBehavior {
|
|
541
|
+
// @endStateApi — live. Coord-10 L11-4. Peeks a default tag's
|
|
542
|
+
// behavior by name. Returns `DEFAULT_UNKNOWN_BEHAVIOR` (bail-on-
|
|
543
|
+
// cross) for unknown tag types.
|
|
544
|
+
return DEFAULT_REGISTRY_ENTRIES[tagType] ?? DEFAULT_UNKNOWN_BEHAVIOR;
|
|
545
|
+
},
|
|
433
546
|
};
|
|
434
547
|
}
|
package/src/api/v3/ui/_types.ts
CHANGED
|
@@ -233,6 +233,26 @@ export type ScrollTarget =
|
|
|
233
233
|
| { kind: "revision"; value: string; behavior?: ScrollTargetBehavior }
|
|
234
234
|
| { kind: "page"; value: number; behavior?: ScrollTargetBehavior };
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Result of `ui.viewport.scrollToPage(n)` — the settled state after
|
|
238
|
+
* the scroll dispatch lands.
|
|
239
|
+
*
|
|
240
|
+
* - `actualPage` is the 1-based page number that was actually
|
|
241
|
+
* targeted. Differs from the requested `pageNumber` when the caller
|
|
242
|
+
* asked for a page beyond the document's resolved bounds —
|
|
243
|
+
* implementation clamps and reports the clamped value here.
|
|
244
|
+
* - `scrollY` is the document-relative Y offset (in CSS px) of the
|
|
245
|
+
* target page's top edge, sourced from
|
|
246
|
+
* `handle.geometry.getPage(index).frame.topPx`. Useful for consumers
|
|
247
|
+
* that want to verify their own scroll container landed at the
|
|
248
|
+
* expected coordinate (visual-fidelity harness: deterministic
|
|
249
|
+
* per-page capture).
|
|
250
|
+
*/
|
|
251
|
+
export interface ScrollToPageResult {
|
|
252
|
+
readonly actualPage: number;
|
|
253
|
+
readonly scrollY: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
236
256
|
export interface SelectionRangeInput {
|
|
237
257
|
readonly anchor: number;
|
|
238
258
|
readonly head: number;
|
|
@@ -369,6 +389,21 @@ export interface ApiV3UiViewport {
|
|
|
369
389
|
get(): ViewportState;
|
|
370
390
|
subscribe(listener: UiListener<ViewportState>): UiUnsubscribe;
|
|
371
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Scroll the mounted surface to a specific 1-based page number.
|
|
394
|
+
* Resolves via `handle.geometry.getPage`; dispatches through the
|
|
395
|
+
* bound controller's `dispatchScroll` hook. Returns the settled
|
|
396
|
+
* `{ actualPage, scrollY }` or `null` when geometry cannot resolve
|
|
397
|
+
* any page / no controller bound / no dispatchScroll hook.
|
|
398
|
+
* Clamps pageNumber to the document's valid range; `actualPage`
|
|
399
|
+
* reflects the clamp. coord-10 §γ first-class replacement for the
|
|
400
|
+
* visual-fidelity harness's DOM-scrape fallback.
|
|
401
|
+
*/
|
|
402
|
+
scrollToPage(
|
|
403
|
+
pageNumber: number,
|
|
404
|
+
opts?: { behavior?: ScrollTargetBehavior },
|
|
405
|
+
): Promise<ScrollToPageResult | null>;
|
|
406
|
+
|
|
372
407
|
/**
|
|
373
408
|
* X5 · Composed effective markup mode. Merges L06's class-A
|
|
374
409
|
* `WorkflowMarkupModePolicy` (via `handle.getMarkupModePolicy()`)
|
package/src/api/v3/ui/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
UiListener,
|
|
28
28
|
UiUnsubscribe,
|
|
29
29
|
WorkflowMarkupMode,
|
|
30
|
+
ScrollTargetBehavior,
|
|
31
|
+
ScrollToPageResult,
|
|
30
32
|
} from "./_types.ts";
|
|
31
33
|
import type { UiApiContext } from "./_context.ts";
|
|
32
34
|
import { readComposedViewport } from "./_context.ts";
|
|
@@ -84,6 +86,35 @@ export const subscribeMetadata: ApiV3FnMetadata = {
|
|
|
84
86
|
rwdReference: "§UI API § ui.viewport.subscribe. Adapter delegates to UiController.subscribeViewport; throws when the active binding has no hook. Subscribe call emits one `ux.response.ui.viewport.subscribe` acknowledgement; per-tick ViewportState deliveries flow through the listener (rAF-coalesced, U7).",
|
|
85
87
|
};
|
|
86
88
|
|
|
89
|
+
// ----- scrollToPage (coord-10 §γ — visual-fidelity / Go-to-page UX) -----
|
|
90
|
+
|
|
91
|
+
export const scrollToPageMetadata: ApiV3FnMetadata = {
|
|
92
|
+
name: "ui.viewport.scrollToPage",
|
|
93
|
+
status: "live-with-adapter",
|
|
94
|
+
sourceLayer: "presentation",
|
|
95
|
+
liveEvidence: {
|
|
96
|
+
runnerTest: "test/api/v3/ui/scroll-to-page.test.ts",
|
|
97
|
+
commit: "refactor-10-slice-scroll-to-page",
|
|
98
|
+
},
|
|
99
|
+
uxIntent: {
|
|
100
|
+
uiVisible: true,
|
|
101
|
+
expectsUxResponse: "surface-refresh",
|
|
102
|
+
expectedDelta: "mounted surface scrolls to the target page's top edge",
|
|
103
|
+
},
|
|
104
|
+
agentMetadata: {
|
|
105
|
+
readOrMutate: "mutate",
|
|
106
|
+
boundedScope: "session",
|
|
107
|
+
auditCategory: "ui-viewport-scroll",
|
|
108
|
+
},
|
|
109
|
+
// Scroll position is a class-C local view state (per-session /
|
|
110
|
+
// per-mounted-instance); changing it doesn't mutate canonical document
|
|
111
|
+
// state and is not broadcast across collab peers.
|
|
112
|
+
stateClass: "C-local",
|
|
113
|
+
persistsTo: "none",
|
|
114
|
+
rwdReference:
|
|
115
|
+
"§UI API § ui.viewport.scrollToPage. Resolves pageNumber → scrollY via handle.geometry.getPage(pageIndex); dispatches through controller.dispatchScroll({ kind:'page', value, behavior }); returns the settled {actualPage, scrollY}. 1-based page numbers; clamps to [1, pageCount]. First-class API for visual-fidelity harness + 'Go to page N' UX — replaces DOM-scrape fallback (coord-10 §γ). When L07 ships runtime.viewport.getPageAnchor / getPageGeometry (coord-07 §2.9), this wrapper may be simplified to delegate to those primitives; the public shape stays stable.",
|
|
116
|
+
};
|
|
117
|
+
|
|
87
118
|
// ----- X5 markup-mode metadata (state-classes cross-cutting Slice X5) -----
|
|
88
119
|
|
|
89
120
|
/**
|
|
@@ -249,6 +280,87 @@ export function createViewportFamily(ctx: UiApiContext) {
|
|
|
249
280
|
return unsubscribe;
|
|
250
281
|
},
|
|
251
282
|
|
|
283
|
+
// ----- scrollToPage (coord-10 §γ) -----
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Scroll the mounted surface to a specific 1-based page number.
|
|
287
|
+
*
|
|
288
|
+
* Resolution: `handle.geometry.getPage(pageNumber - 1)` supplies the
|
|
289
|
+
* page's `frame.topPx` (deterministic, renderer-frame coordinates).
|
|
290
|
+
* Dispatch: `controller.dispatchScroll({ kind: "page", value, behavior })`
|
|
291
|
+
* — same seam as `ui.surface.scrollTo`, so any consumer-specific
|
|
292
|
+
* scroll-container routing lives on the shell side.
|
|
293
|
+
*
|
|
294
|
+
* Clamping: `pageNumber < 1` resolves to page 1. A request beyond
|
|
295
|
+
* the document's page count returns the last valid page's scrollY;
|
|
296
|
+
* `actualPage` reflects the clamp so callers can detect it.
|
|
297
|
+
*
|
|
298
|
+
* Returns `null` when (a) no controller is bound, (b) the controller
|
|
299
|
+
* has no `dispatchScroll` hook, or (c) geometry cannot resolve any
|
|
300
|
+
* page (pre-paint / empty doc). Callers that need explicit failure
|
|
301
|
+
* handling check `result !== null` before trusting the scroll.
|
|
302
|
+
*/
|
|
303
|
+
async scrollToPage(
|
|
304
|
+
pageNumber: number,
|
|
305
|
+
opts?: { behavior?: ScrollTargetBehavior },
|
|
306
|
+
): Promise<ScrollToPageResult | null> {
|
|
307
|
+
const controller = ctx.binding?.controller;
|
|
308
|
+
if (!controller) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"ui.viewport.scrollToPage: no controller bound — call ui.session.bind(controller) first",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (!controller.dispatchScroll) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`ui.viewport.scrollToPage: controller of kind "${controller.kind}" did not provide a dispatchScroll hook`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Clamp to [1, pageCount]. The geometry facet's `getPage(index)`
|
|
320
|
+
// takes a 0-based index; we accept 1-based input and convert.
|
|
321
|
+
// Walk forward from the clamped target to find the first page the
|
|
322
|
+
// facet actually resolves — this handles sparse / pre-paint
|
|
323
|
+
// states where higher indices may be null.
|
|
324
|
+
const getPage = ctx.handle.geometry?.getPage;
|
|
325
|
+
if (typeof getPage !== "function") return null;
|
|
326
|
+
|
|
327
|
+
const requestedClampedLow = Math.max(1, Math.floor(pageNumber));
|
|
328
|
+
// Try the requested page; if null, scan downward through lower
|
|
329
|
+
// indices to land on the largest resolvable page (the doc's last
|
|
330
|
+
// populated page). If nothing resolves, return null.
|
|
331
|
+
let resolved: { pageIndex: number; scrollY: number } | null = null;
|
|
332
|
+
for (let i = requestedClampedLow - 1; i >= 0; i--) {
|
|
333
|
+
const page = getPage.call(ctx.handle.geometry, i);
|
|
334
|
+
if (page) {
|
|
335
|
+
resolved = { pageIndex: i, scrollY: page.frame.topPx };
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!resolved) return null;
|
|
340
|
+
|
|
341
|
+
const actualPage = resolved.pageIndex + 1;
|
|
342
|
+
const behavior = opts?.behavior;
|
|
343
|
+
await controller.dispatchScroll({
|
|
344
|
+
kind: "page",
|
|
345
|
+
value: actualPage,
|
|
346
|
+
...(behavior ? { behavior } : {}),
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
emitUxResponse(ctx.handle, {
|
|
350
|
+
apiFn: scrollToPageMetadata.name,
|
|
351
|
+
intent: scrollToPageMetadata.uxIntent.expectedDelta ?? "",
|
|
352
|
+
mockOrLive: "live-with-adapter",
|
|
353
|
+
uiVisible: true,
|
|
354
|
+
expectedDelta: scrollToPageMetadata.uxIntent.expectedDelta,
|
|
355
|
+
actualDelta: {
|
|
356
|
+
kind: "surface-refresh",
|
|
357
|
+
payload: { page: actualPage, scrollY: resolved.scrollY },
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return { actualPage, scrollY: resolved.scrollY };
|
|
362
|
+
},
|
|
363
|
+
|
|
252
364
|
// ----- X5 markup-mode (state-classes cross-cutting Slice X5) -----
|
|
253
365
|
|
|
254
366
|
getEffectiveMarkupMode(): WorkflowMarkupMode {
|
|
@@ -509,6 +509,7 @@ function getInlineLength(node: InlineNode): number {
|
|
|
509
509
|
case "tab":
|
|
510
510
|
case "hard_break":
|
|
511
511
|
case "column_break":
|
|
512
|
+
case "page_break":
|
|
512
513
|
case "symbol":
|
|
513
514
|
case "image":
|
|
514
515
|
case "opaque_inline":
|
|
@@ -555,6 +556,7 @@ function getInlineDisplayText(node: InlineNode): string {
|
|
|
555
556
|
return "\t";
|
|
556
557
|
case "hard_break":
|
|
557
558
|
case "column_break":
|
|
559
|
+
case "page_break":
|
|
558
560
|
return "\n";
|
|
559
561
|
case "symbol":
|
|
560
562
|
return node.char;
|
|
@@ -329,6 +329,7 @@ export function getTableStructureContext(
|
|
|
329
329
|
columnCount,
|
|
330
330
|
selectedCellCount,
|
|
331
331
|
isSimpleTable: simpleTable,
|
|
332
|
+
alignment: target.alignment ?? null,
|
|
332
333
|
currentCell: {
|
|
333
334
|
rowIndex: effectiveSelection.anchorCell.rowIndex,
|
|
334
335
|
columnIndex: effectiveSelection.anchorCell.columnIndex,
|
|
@@ -284,6 +284,7 @@ function serializeInlineNode(node: InlineNode): string {
|
|
|
284
284
|
throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
|
|
285
285
|
case "image":
|
|
286
286
|
case "column_break":
|
|
287
|
+
case "page_break":
|
|
287
288
|
case "symbol":
|
|
288
289
|
default:
|
|
289
290
|
throw new Error(`Cannot safely serialize ${node.type} content in header/footer sub-parts.`);
|
|
@@ -581,6 +581,8 @@ function serializeTableInlineNode(
|
|
|
581
581
|
return "<w:r><w:tab/></w:r>";
|
|
582
582
|
case "column_break":
|
|
583
583
|
return "<w:r><w:br w:type=\"column\"/></w:r>";
|
|
584
|
+
case "page_break":
|
|
585
|
+
return "<w:r><w:br w:type=\"page\"/></w:r>";
|
|
584
586
|
case "hard_break":
|
|
585
587
|
return "<w:r><w:br/></w:r>";
|
|
586
588
|
case "symbol": {
|
|
@@ -1010,6 +1012,17 @@ function serializeInlineNode(
|
|
|
1010
1012
|
boundaries,
|
|
1011
1013
|
};
|
|
1012
1014
|
}
|
|
1015
|
+
case "page_break": {
|
|
1016
|
+
const xml = `<w:r><w:br w:type="page"/></w:r>`;
|
|
1017
|
+
const boundaries = new Map<number, number>();
|
|
1018
|
+
boundaries.set(cursor, xmlOffset);
|
|
1019
|
+
boundaries.set(cursor + 1, xmlOffset + xml.length);
|
|
1020
|
+
return {
|
|
1021
|
+
xml,
|
|
1022
|
+
cursor: cursor + 1,
|
|
1023
|
+
boundaries,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1013
1026
|
case "hard_break": {
|
|
1014
1027
|
const xml = serializeRun({ kind: "hard_break" });
|
|
1015
1028
|
const boundaries = new Map<number, number>();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
8
|
CanonicalParagraphFormatting,
|
|
9
|
+
FrameProperties,
|
|
9
10
|
ParagraphBorders,
|
|
10
11
|
ParagraphIndentation,
|
|
11
12
|
ParagraphShading,
|
|
@@ -92,6 +93,34 @@ function buildSpacingXml(s: ParagraphSpacing | undefined): string {
|
|
|
92
93
|
return attrs.length > 0 ? `<w:spacing ${attrs.join(" ")}/>` : "";
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function buildFrameXml(f: FrameProperties | undefined): string {
|
|
97
|
+
if (!f) return "";
|
|
98
|
+
// Prefer parsed rawXml when available — preserves extension attributes
|
|
99
|
+
// (`w14:*`, `w15:*`, `mc:Ignorable`) that the typed field set doesn't
|
|
100
|
+
// cover. When rawXml isn't present (parser couldn't capture the source
|
|
101
|
+
// string), emit from typed fields only; extension attrs are lost in
|
|
102
|
+
// that round-trip path, but every CCEP-class framed paragraph we've
|
|
103
|
+
// seen uses only modelled attributes.
|
|
104
|
+
if (f.rawXml) return f.rawXml;
|
|
105
|
+
const attrs: string[] = [];
|
|
106
|
+
if (f.widthTwips !== undefined) attrs.push(`w:w="${f.widthTwips}"`);
|
|
107
|
+
if (f.heightTwips !== undefined) attrs.push(`w:h="${f.heightTwips}"`);
|
|
108
|
+
if (f.hRule) attrs.push(`w:hRule="${escXml(f.hRule)}"`);
|
|
109
|
+
if (f.xTwips !== undefined) attrs.push(`w:x="${f.xTwips}"`);
|
|
110
|
+
if (f.yTwips !== undefined) attrs.push(`w:y="${f.yTwips}"`);
|
|
111
|
+
if (f.xAlign) attrs.push(`w:xAlign="${escXml(f.xAlign)}"`);
|
|
112
|
+
if (f.yAlign) attrs.push(`w:yAlign="${escXml(f.yAlign)}"`);
|
|
113
|
+
if (f.hAnchor) attrs.push(`w:hAnchor="${escXml(f.hAnchor)}"`);
|
|
114
|
+
if (f.vAnchor) attrs.push(`w:vAnchor="${escXml(f.vAnchor)}"`);
|
|
115
|
+
if (f.wrap) attrs.push(`w:wrap="${escXml(f.wrap)}"`);
|
|
116
|
+
if (f.hSpaceTwips !== undefined) attrs.push(`w:hSpace="${f.hSpaceTwips}"`);
|
|
117
|
+
if (f.vSpaceTwips !== undefined) attrs.push(`w:vSpace="${f.vSpaceTwips}"`);
|
|
118
|
+
if (f.dropCap) attrs.push(`w:dropCap="${escXml(f.dropCap)}"`);
|
|
119
|
+
if (f.lines !== undefined) attrs.push(`w:lines="${f.lines}"`);
|
|
120
|
+
if (f.anchorLock !== undefined) attrs.push(`w:anchorLock="${f.anchorLock ? "1" : "0"}"`);
|
|
121
|
+
return attrs.length > 0 ? `<w:framePr ${attrs.join(" ")}/>` : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
function buildIndentXml(i: ParagraphIndentation | undefined): string {
|
|
96
125
|
if (!i) return "";
|
|
97
126
|
const attrs: string[] = [];
|
|
@@ -114,6 +143,11 @@ export function buildParagraphPropertiesXml(
|
|
|
114
143
|
parts.push(toggleEl("keepLines", pPr.keepLines));
|
|
115
144
|
parts.push(toggleEl("pageBreakBefore", pPr.pageBreakBefore));
|
|
116
145
|
|
|
146
|
+
// 2. framePr (ECMA-376 §17.3.1 canonical order slot, between pageBreakBefore
|
|
147
|
+
// and pBdr). Emit before pBdr so the OpenXML SDK validator accepts a framed
|
|
148
|
+
// paragraph that also carries borders (coord-04 §1.18.d).
|
|
149
|
+
parts.push(buildFrameXml(pPr.frameProperties));
|
|
150
|
+
|
|
117
151
|
// 4. pBdr
|
|
118
152
|
parts.push(buildBordersXml(pPr.borders));
|
|
119
153
|
|
|
@@ -481,6 +481,17 @@ function normalizeInlineChildren(
|
|
|
481
481
|
normalized.push({ type: "column_break" });
|
|
482
482
|
state.cursor += 1;
|
|
483
483
|
break;
|
|
484
|
+
case "page_break":
|
|
485
|
+
// coord-04 §1.18.5 follow-up: the fde93da3 cross-layer page_break
|
|
486
|
+
// ship added parse + surface-projection + pagination but missed
|
|
487
|
+
// this normalize-text switch. Without this case, every
|
|
488
|
+
// `<w:br w:type="page"/>` run parsed by L01 falls through and gets
|
|
489
|
+
// silently dropped during canonical assembly — so L04's
|
|
490
|
+
// `hasPageBreak` never fires on real documents. Mirrors the
|
|
491
|
+
// `column_break` branch.
|
|
492
|
+
normalized.push({ type: "page_break" });
|
|
493
|
+
state.cursor += 1;
|
|
494
|
+
break;
|
|
484
495
|
case "chart_preview":
|
|
485
496
|
registerComplexPreviewMedia(state, node);
|
|
486
497
|
normalized.push({
|
|
@@ -271,6 +271,7 @@ export type ParsedInlineNode =
|
|
|
271
271
|
| ParsedTextNode
|
|
272
272
|
| ParsedBreakNode
|
|
273
273
|
| ParsedColumnBreakNode
|
|
274
|
+
| ParsedPageBreakNode
|
|
274
275
|
| ParsedTabNode
|
|
275
276
|
| ParsedSymbolNode
|
|
276
277
|
| ParsedImageNode
|
|
@@ -306,6 +307,10 @@ export interface ParsedColumnBreakNode {
|
|
|
306
307
|
type: "column_break";
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
export interface ParsedPageBreakNode {
|
|
311
|
+
type: "page_break";
|
|
312
|
+
}
|
|
313
|
+
|
|
309
314
|
export interface ParsedTabNode {
|
|
310
315
|
type: "tab";
|
|
311
316
|
}
|
|
@@ -350,7 +355,7 @@ export interface ParsedImageNode {
|
|
|
350
355
|
export interface ParsedHyperlinkNode {
|
|
351
356
|
type: "hyperlink";
|
|
352
357
|
href: string;
|
|
353
|
-
children: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedTabNode | ParsedSymbolNode>;
|
|
358
|
+
children: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedPageBreakNode | ParsedTabNode | ParsedSymbolNode>;
|
|
354
359
|
rawXml: string;
|
|
355
360
|
}
|
|
356
361
|
|
|
@@ -606,7 +611,7 @@ interface XmlTextNode {
|
|
|
606
611
|
type XmlNode = XmlElementNode | XmlTextNode;
|
|
607
612
|
|
|
608
613
|
interface RunParseResult {
|
|
609
|
-
nodes: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedTabNode | ParsedSymbolNode>;
|
|
614
|
+
nodes: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedPageBreakNode | ParsedTabNode | ParsedSymbolNode>;
|
|
610
615
|
supported: boolean;
|
|
611
616
|
}
|
|
612
617
|
|
|
@@ -2390,7 +2395,9 @@ function parseRun(
|
|
|
2390
2395
|
break;
|
|
2391
2396
|
}
|
|
2392
2397
|
case "br":
|
|
2393
|
-
if (
|
|
2398
|
+
if (isPageBreak(child)) {
|
|
2399
|
+
result.push({ type: "page_break" });
|
|
2400
|
+
} else if (isColumnBreak(child)) {
|
|
2394
2401
|
result.push({ type: "column_break" });
|
|
2395
2402
|
} else if (isSimpleLineBreak(child)) {
|
|
2396
2403
|
result.push({ type: "hard_break" });
|
|
@@ -2714,7 +2721,7 @@ function parseHyperlink(
|
|
|
2714
2721
|
};
|
|
2715
2722
|
}
|
|
2716
2723
|
|
|
2717
|
-
const children: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedTabNode | ParsedSymbolNode> = [];
|
|
2724
|
+
const children: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedPageBreakNode | ParsedTabNode | ParsedSymbolNode> = [];
|
|
2718
2725
|
|
|
2719
2726
|
for (const child of node.children) {
|
|
2720
2727
|
if (child.type !== "element") {
|
|
@@ -2764,7 +2771,7 @@ function parseRunContentOnly(
|
|
|
2764
2771
|
}
|
|
2765
2772
|
|
|
2766
2773
|
const marks = marksResult.marks;
|
|
2767
|
-
const nodes: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedTabNode | ParsedSymbolNode> = [];
|
|
2774
|
+
const nodes: Array<ParsedTextNode | ParsedBreakNode | ParsedColumnBreakNode | ParsedPageBreakNode | ParsedTabNode | ParsedSymbolNode> = [];
|
|
2768
2775
|
|
|
2769
2776
|
for (const child of node.children) {
|
|
2770
2777
|
if (child.type !== "element") {
|
|
@@ -2812,6 +2819,10 @@ function parseRunContentOnly(
|
|
|
2812
2819
|
break;
|
|
2813
2820
|
}
|
|
2814
2821
|
case "br":
|
|
2822
|
+
if (isPageBreak(child)) {
|
|
2823
|
+
nodes.push({ type: "page_break" });
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2815
2826
|
if (isColumnBreak(child)) {
|
|
2816
2827
|
nodes.push({ type: "column_break" });
|
|
2817
2828
|
break;
|
|
@@ -3149,6 +3160,11 @@ function isColumnBreak(node: XmlElementNode): boolean {
|
|
|
3149
3160
|
return value === "column";
|
|
3150
3161
|
}
|
|
3151
3162
|
|
|
3163
|
+
function isPageBreak(node: XmlElementNode): boolean {
|
|
3164
|
+
const value = (node.attributes["w:type"] ?? node.attributes.type ?? "").toLowerCase();
|
|
3165
|
+
return value === "page";
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3152
3168
|
function findChildElement(node: XmlElementNode, childLocalName: string): XmlElementNode {
|
|
3153
3169
|
const child = node.children.find(
|
|
3154
3170
|
(entry): entry is XmlElementNode =>
|