@beyondwork/docx-react-component 1.0.36 → 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/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +402 -1
- package/src/core/commands/index.ts +18 -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/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- 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/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -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 +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- 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 +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
- 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 +505 -144
- 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-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decoration resolver — single pass that turns canonical decoration sources
|
|
3
|
+
* (workflow scopes, comments, revisions, search, locked zones) into the
|
|
4
|
+
* five lanes of `DecorationIndex` that chrome consumes.
|
|
5
|
+
*
|
|
6
|
+
* Per runtime-rendering-and-chrome-phase.md §1, the kernel's MVP leaves
|
|
7
|
+
* `DecorationIndex` empty; chrome surfaces that need per-decoration anchor
|
|
8
|
+
* rects re-project from runtime offsets themselves. The resolver fixes
|
|
9
|
+
* that by walking each source once, resolving its body rect through the
|
|
10
|
+
* kernel's anchor index, and emitting a `RenderBlockDecoration` entry with
|
|
11
|
+
* the stable rect. Chrome reads the resolved lanes directly and never
|
|
12
|
+
* touches the DOM or re-runs anchor math.
|
|
13
|
+
*
|
|
14
|
+
* The resolver is pure: same inputs produce the same output. The kernel
|
|
15
|
+
* invokes it once per frame build, cached against the frame's revision.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
CommentDecorationModel,
|
|
20
|
+
CommentDecorationThread,
|
|
21
|
+
} from "../../ui/headless/comment-decoration-model.ts";
|
|
22
|
+
import type {
|
|
23
|
+
RevisionDecorationModel,
|
|
24
|
+
RevisionDecorationEntry,
|
|
25
|
+
} from "../../ui/headless/revision-decoration-model.ts";
|
|
26
|
+
import type { ScopeRailSegment } from "../workflow-rail-segments.ts";
|
|
27
|
+
import type {
|
|
28
|
+
DecorationIndex,
|
|
29
|
+
RenderAnchorIndex,
|
|
30
|
+
RenderBlockDecoration,
|
|
31
|
+
RenderFrameRect,
|
|
32
|
+
} from "./render-frame-types.ts";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Input shapes
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export interface SearchMatchRange {
|
|
39
|
+
/** Stable identifier the search plugin emits for each match. */
|
|
40
|
+
matchId: string;
|
|
41
|
+
/** Inclusive runtime offset. */
|
|
42
|
+
from: number;
|
|
43
|
+
/** Exclusive runtime offset. */
|
|
44
|
+
to: number;
|
|
45
|
+
/** True when this match is the currently-focused "active" result. */
|
|
46
|
+
isActive?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LockedRangeInput {
|
|
50
|
+
/** Stable id of the lock (fragmentId for locked zones, scopeId for preserve-only scopes). */
|
|
51
|
+
lockId: string;
|
|
52
|
+
/** Inclusive runtime offset. */
|
|
53
|
+
from: number;
|
|
54
|
+
/** Exclusive runtime offset. */
|
|
55
|
+
to: number;
|
|
56
|
+
/** Optional human label. */
|
|
57
|
+
label?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResolveDecorationIndexInput {
|
|
61
|
+
anchorIndex: RenderAnchorIndex;
|
|
62
|
+
/**
|
|
63
|
+
* Scope rail segments covering the active story. The resolver re-uses
|
|
64
|
+
* their anchor rects when present (`bodyTintRect`) and falls back to
|
|
65
|
+
* `anchorIndex.bySelection(from, to)` otherwise.
|
|
66
|
+
*/
|
|
67
|
+
workflowSegments?: readonly ScopeRailSegment[];
|
|
68
|
+
comments?: CommentDecorationModel | null | undefined;
|
|
69
|
+
revisions?: RevisionDecorationModel | null | undefined;
|
|
70
|
+
searchMatches?: readonly SearchMatchRange[];
|
|
71
|
+
lockedRanges?: readonly LockedRangeInput[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Entry point
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export function resolveDecorationIndex(
|
|
79
|
+
input: ResolveDecorationIndexInput,
|
|
80
|
+
): DecorationIndex {
|
|
81
|
+
const workflow: RenderBlockDecoration[] = [];
|
|
82
|
+
const comments: RenderBlockDecoration[] = [];
|
|
83
|
+
const revisions: RenderBlockDecoration[] = [];
|
|
84
|
+
const search: RenderBlockDecoration[] = [];
|
|
85
|
+
const locked: RenderBlockDecoration[] = [];
|
|
86
|
+
|
|
87
|
+
if (input.workflowSegments) {
|
|
88
|
+
for (const segment of input.workflowSegments) {
|
|
89
|
+
const frame =
|
|
90
|
+
segment.bodyTintRect ??
|
|
91
|
+
input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset);
|
|
92
|
+
if (!frame) continue;
|
|
93
|
+
workflow.push({
|
|
94
|
+
kind: "workflow",
|
|
95
|
+
refId: segment.scopeId,
|
|
96
|
+
frame,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (input.comments) {
|
|
102
|
+
for (const thread of input.comments.threads) {
|
|
103
|
+
const frame = resolveRange(
|
|
104
|
+
input.anchorIndex,
|
|
105
|
+
thread.from,
|
|
106
|
+
thread.to,
|
|
107
|
+
);
|
|
108
|
+
if (!frame) continue;
|
|
109
|
+
comments.push({
|
|
110
|
+
kind: "comment",
|
|
111
|
+
refId: thread.commentId,
|
|
112
|
+
frame,
|
|
113
|
+
});
|
|
114
|
+
// Track active thread separately so chrome can prioritize rendering.
|
|
115
|
+
// (We don't have a per-decoration "isActive" field today; chrome
|
|
116
|
+
// reads `CommentDecorationModel.activeCommentId` alongside the
|
|
117
|
+
// decoration lane and picks out the matching refId.)
|
|
118
|
+
void asCommentThread(thread);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (input.revisions) {
|
|
123
|
+
for (const revision of input.revisions.revisions) {
|
|
124
|
+
const frame = resolveRange(
|
|
125
|
+
input.anchorIndex,
|
|
126
|
+
revision.from,
|
|
127
|
+
revision.to,
|
|
128
|
+
);
|
|
129
|
+
if (!frame) continue;
|
|
130
|
+
revisions.push({
|
|
131
|
+
kind: "revision",
|
|
132
|
+
refId: revision.revisionId,
|
|
133
|
+
frame,
|
|
134
|
+
});
|
|
135
|
+
void asRevisionEntry(revision);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (input.searchMatches) {
|
|
140
|
+
for (const match of input.searchMatches) {
|
|
141
|
+
const frame = resolveRange(input.anchorIndex, match.from, match.to);
|
|
142
|
+
if (!frame) continue;
|
|
143
|
+
search.push({
|
|
144
|
+
kind: "search",
|
|
145
|
+
refId: match.matchId,
|
|
146
|
+
frame,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (input.lockedRanges) {
|
|
152
|
+
for (const lock of input.lockedRanges) {
|
|
153
|
+
const frame = resolveRange(input.anchorIndex, lock.from, lock.to);
|
|
154
|
+
if (!frame) continue;
|
|
155
|
+
locked.push({
|
|
156
|
+
kind: "locked",
|
|
157
|
+
refId: lock.lockId,
|
|
158
|
+
frame,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { workflow, comments, revisions, search, locked };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Internals
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
function resolveRange(
|
|
171
|
+
anchorIndex: RenderAnchorIndex,
|
|
172
|
+
from: number,
|
|
173
|
+
to: number,
|
|
174
|
+
): RenderFrameRect | null {
|
|
175
|
+
if (from >= to) {
|
|
176
|
+
return anchorIndex.byRuntimeOffset(from);
|
|
177
|
+
}
|
|
178
|
+
return anchorIndex.bySelection(from, to);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Type preservation helpers so the input shapes stay referenced and
|
|
182
|
+
// `tsgo --noEmit` catches shape drift if the upstream models change.
|
|
183
|
+
function asCommentThread(thread: CommentDecorationThread): CommentDecorationThread {
|
|
184
|
+
return thread;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function asRevisionEntry(entry: RevisionDecorationEntry): RevisionDecorationEntry {
|
|
188
|
+
return entry;
|
|
189
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-owned render kernel — internal API surface.
|
|
3
|
+
*
|
|
4
|
+
* The kernel consumes the layout facet and emits `RenderFrame` objects
|
|
5
|
+
* that every chrome surface reads. Nothing in this module is part of
|
|
6
|
+
* the package's public API; consumers go through `ref.layout.getRenderFrame`
|
|
7
|
+
* (facet) rather than wiring the kernel directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createRenderKernel,
|
|
12
|
+
sumDeltasBefore,
|
|
13
|
+
type RenderKernel,
|
|
14
|
+
type CreateRenderKernelInput,
|
|
15
|
+
type DecorationSources,
|
|
16
|
+
type PendingOpDelta,
|
|
17
|
+
} from "./render-kernel.ts";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
createPendingOpDeltaReader,
|
|
21
|
+
type PendingOpDeltaReader,
|
|
22
|
+
type PendingOpDeltaSnapshot,
|
|
23
|
+
type CreatePendingOpDeltaReaderInput,
|
|
24
|
+
} from "./pending-op-delta-reader.ts";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
resolveDecorationIndex,
|
|
28
|
+
type ResolveDecorationIndexInput,
|
|
29
|
+
type SearchMatchRange,
|
|
30
|
+
type LockedRangeInput,
|
|
31
|
+
} from "./decoration-resolver.ts";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
DEFAULT_PX_PER_TWIP,
|
|
35
|
+
EMPTY_DECORATION_INDEX,
|
|
36
|
+
defaultChromeReservations,
|
|
37
|
+
resolveDefaultZoom,
|
|
38
|
+
type DecorationIndex,
|
|
39
|
+
type PageChromeReservations,
|
|
40
|
+
type RenderAnchorIndex,
|
|
41
|
+
type RenderAnchorQuery,
|
|
42
|
+
type RenderBlock,
|
|
43
|
+
type RenderBlockDecoration,
|
|
44
|
+
type RenderFrame,
|
|
45
|
+
type RenderFrameQueryOptions,
|
|
46
|
+
type RenderFrameRect,
|
|
47
|
+
type RenderHitResult,
|
|
48
|
+
type RenderKernelEvent,
|
|
49
|
+
type RenderPoint,
|
|
50
|
+
type RenderLine,
|
|
51
|
+
type RenderLineAnchor,
|
|
52
|
+
type RenderPage,
|
|
53
|
+
type RenderPageRegions,
|
|
54
|
+
type RenderStoryRegion,
|
|
55
|
+
type RenderZoom,
|
|
56
|
+
type DefaultZoomInput,
|
|
57
|
+
} from "./render-frame-types.ts";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending-op delta reader — thin accessor over `createRenderKernel`'s
|
|
3
|
+
* `getPendingOpDeltas` seam so chrome surfaces can reason about the
|
|
4
|
+
* predicted-dispatch window without reaching into the kernel's internals.
|
|
5
|
+
*
|
|
6
|
+
* Chrome must NOT mutate anchor rects during the predicted-dispatch window —
|
|
7
|
+
* per `bounded-local-first-interaction-architecture.md`, the rects settle
|
|
8
|
+
* when `TextCommandAck` reconciles. Chrome surfaces that want to *mask*
|
|
9
|
+
* during that window (disable buttons, show a spinner) read a flag from
|
|
10
|
+
* this module instead of polling the kernel directly.
|
|
11
|
+
*
|
|
12
|
+
* The reader is decoupled from the kernel so the predicted lane can be
|
|
13
|
+
* swapped or gated without touching chrome code.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PendingOpDelta } from "./render-kernel.ts";
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Reader contract
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Snapshot of the predicted-dispatch state at the moment it was read.
|
|
24
|
+
*
|
|
25
|
+
* `deltas` is the same shape the kernel's anchor index receives; chrome
|
|
26
|
+
* uses the summary fields below instead of walking the array in hot paths.
|
|
27
|
+
*/
|
|
28
|
+
export interface PendingOpDeltaSnapshot {
|
|
29
|
+
/** Raw delta array, identical to what the kernel received. */
|
|
30
|
+
deltas: readonly PendingOpDelta[];
|
|
31
|
+
/** True when any pending op is outstanding. */
|
|
32
|
+
hasPending: boolean;
|
|
33
|
+
/** Count of pending ops (for perf probe sampling). */
|
|
34
|
+
pendingCount: number;
|
|
35
|
+
/** Net character delta summed across all pending ops. */
|
|
36
|
+
netCharacterDelta: number;
|
|
37
|
+
/** Lowest runtime offset touched by any pending op, or null when empty. */
|
|
38
|
+
earliestTouchedOffset: number | null;
|
|
39
|
+
/** Highest runtime offset touched by any pending op, or null when empty. */
|
|
40
|
+
latestTouchedOffset: number | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PendingOpDeltaReader {
|
|
44
|
+
/** Read the current snapshot. Safe to call every render. */
|
|
45
|
+
read(): PendingOpDeltaSnapshot;
|
|
46
|
+
/**
|
|
47
|
+
* Predicate: is a given runtime offset inside the range touched by the
|
|
48
|
+
* pending window? Chrome uses this to decide whether a selection anchor
|
|
49
|
+
* is stable yet.
|
|
50
|
+
*/
|
|
51
|
+
isOffsetPending(offset: number): boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CreatePendingOpDeltaReaderInput {
|
|
55
|
+
/**
|
|
56
|
+
* Reads the current pending-op deltas — typically
|
|
57
|
+
* `() => localEditSessionState.getPendingDeltas()` or the kernel's own
|
|
58
|
+
* `getPendingOpDeltas` accessor. When absent, the reader returns an
|
|
59
|
+
* empty snapshot.
|
|
60
|
+
*/
|
|
61
|
+
getDeltas?: () => readonly PendingOpDelta[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Factory
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const EMPTY_SNAPSHOT: PendingOpDeltaSnapshot = Object.freeze({
|
|
69
|
+
deltas: [],
|
|
70
|
+
hasPending: false,
|
|
71
|
+
pendingCount: 0,
|
|
72
|
+
netCharacterDelta: 0,
|
|
73
|
+
earliestTouchedOffset: null,
|
|
74
|
+
latestTouchedOffset: null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export function createPendingOpDeltaReader(
|
|
78
|
+
input: CreatePendingOpDeltaReaderInput = {},
|
|
79
|
+
): PendingOpDeltaReader {
|
|
80
|
+
const getDeltas = input.getDeltas;
|
|
81
|
+
|
|
82
|
+
function read(): PendingOpDeltaSnapshot {
|
|
83
|
+
if (!getDeltas) {
|
|
84
|
+
return EMPTY_SNAPSHOT;
|
|
85
|
+
}
|
|
86
|
+
const deltas = getDeltas();
|
|
87
|
+
if (deltas.length === 0) {
|
|
88
|
+
return EMPTY_SNAPSHOT;
|
|
89
|
+
}
|
|
90
|
+
return summarize(deltas);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
read,
|
|
95
|
+
isOffsetPending(offset) {
|
|
96
|
+
const snapshot = read();
|
|
97
|
+
if (!snapshot.hasPending) return false;
|
|
98
|
+
const earliest = snapshot.earliestTouchedOffset;
|
|
99
|
+
const latest = snapshot.latestTouchedOffset;
|
|
100
|
+
if (earliest === null || latest === null) return false;
|
|
101
|
+
return offset >= earliest && offset <= latest;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Internals
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function summarize(
|
|
111
|
+
deltas: readonly PendingOpDelta[],
|
|
112
|
+
): PendingOpDeltaSnapshot {
|
|
113
|
+
let net = 0;
|
|
114
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
115
|
+
let latest = Number.NEGATIVE_INFINITY;
|
|
116
|
+
for (const delta of deltas) {
|
|
117
|
+
net += delta.delta;
|
|
118
|
+
if (delta.fromRuntime < earliest) earliest = delta.fromRuntime;
|
|
119
|
+
if (delta.fromRuntime > latest) latest = delta.fromRuntime;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
deltas,
|
|
123
|
+
hasPending: true,
|
|
124
|
+
pendingCount: deltas.length,
|
|
125
|
+
netCharacterDelta: net,
|
|
126
|
+
earliestTouchedOffset: Number.isFinite(earliest) ? earliest : null,
|
|
127
|
+
latestTouchedOffset: Number.isFinite(latest) ? latest : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape definitions for the runtime-owned render kernel (Phase R1).
|
|
3
|
+
*
|
|
4
|
+
* Per runtime-rendering-and-chrome-phase.md §1 the render kernel projects the
|
|
5
|
+
* layout engine's page graph plus the decoration sources into a stable
|
|
6
|
+
* `RenderFrame` that every chrome surface reads. Today the kernel emits a
|
|
7
|
+
* minimum-viable frame so consumers can start wiring against the shape;
|
|
8
|
+
* richer decoration resolution, wrap rects, and table render plans land as
|
|
9
|
+
* the chrome phase continues.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
EditorStoryTarget,
|
|
14
|
+
PageLayoutSnapshot,
|
|
15
|
+
} from "../../api/public-types";
|
|
16
|
+
import type {
|
|
17
|
+
PublicBlockFragment,
|
|
18
|
+
PublicLineBox,
|
|
19
|
+
PublicPageNode,
|
|
20
|
+
PublicPageRegion,
|
|
21
|
+
PublicMeasurementFidelity,
|
|
22
|
+
} from "../layout/public-facet.ts";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Core frame shape
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
export interface RenderFrame {
|
|
29
|
+
/** Revision stamp the kernel used to build this frame. */
|
|
30
|
+
revision: number;
|
|
31
|
+
/** Measurement fidelity the frame was computed against. */
|
|
32
|
+
measurementFidelity: PublicMeasurementFidelity;
|
|
33
|
+
/** Story the mounted surface renders. */
|
|
34
|
+
activeStory: EditorStoryTarget;
|
|
35
|
+
/** Zoom the frame was composed for. */
|
|
36
|
+
zoom: RenderZoom;
|
|
37
|
+
/** Page frames in document order. */
|
|
38
|
+
pages: RenderPage[];
|
|
39
|
+
/** Decoration index (per-frame, keyed by canonical runtime ranges). */
|
|
40
|
+
decorationIndex: DecorationIndex;
|
|
41
|
+
/** Anchor index for chrome placement. */
|
|
42
|
+
anchorIndex: RenderAnchorIndex;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RenderPage {
|
|
46
|
+
page: PublicPageNode;
|
|
47
|
+
/** Top-left in frame-local pixel coordinates (post-zoom). */
|
|
48
|
+
frame: RenderFrameRect;
|
|
49
|
+
regions: RenderPageRegions;
|
|
50
|
+
chromeReservations: PageChromeReservations;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RenderPageRegions {
|
|
54
|
+
body: RenderStoryRegion;
|
|
55
|
+
header?: RenderStoryRegion;
|
|
56
|
+
footer?: RenderStoryRegion;
|
|
57
|
+
columns?: readonly RenderStoryRegion[];
|
|
58
|
+
footnoteArea?: RenderStoryRegion;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RenderStoryRegion {
|
|
62
|
+
storyTarget: EditorStoryTarget;
|
|
63
|
+
region: PublicPageRegion;
|
|
64
|
+
frame: RenderFrameRect;
|
|
65
|
+
blocks: RenderBlock[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface RenderBlock {
|
|
69
|
+
fragment: PublicBlockFragment;
|
|
70
|
+
frame: RenderFrameRect;
|
|
71
|
+
kind: "paragraph" | "table" | "opaque" | "image-float" | "synthetic";
|
|
72
|
+
lines: RenderLine[];
|
|
73
|
+
blockDecorations: RenderBlockDecoration[];
|
|
74
|
+
/**
|
|
75
|
+
* Table geometry + band classes, attached only when `kind === "table"`.
|
|
76
|
+
* Populated by the layout facet's `getTableRenderPlan(blockId)` during
|
|
77
|
+
* frame construction so chrome can read columns, merges, and resize-
|
|
78
|
+
* handle origins without walking the canonical model.
|
|
79
|
+
*/
|
|
80
|
+
tablePlan?: import("../layout/table-render-plan.ts").TableRenderPlan | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RenderLine {
|
|
84
|
+
line: PublicLineBox;
|
|
85
|
+
frame: RenderFrameRect;
|
|
86
|
+
/**
|
|
87
|
+
* Anchor metadata for chrome hit-testing and balloon placement. Today
|
|
88
|
+
* every line carries a single anchor covering its fragment; richer per-
|
|
89
|
+
* run anchors land with the kernel's full line-box projection.
|
|
90
|
+
*/
|
|
91
|
+
anchors: RenderLineAnchor[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RenderLineAnchor {
|
|
95
|
+
/** Runtime offset this anchor represents. */
|
|
96
|
+
runtimeOffset: number;
|
|
97
|
+
/** Screen-relative rect the anchor occupies. */
|
|
98
|
+
frame: RenderFrameRect;
|
|
99
|
+
/** Optional fragment or block id hint. */
|
|
100
|
+
fragmentId?: string;
|
|
101
|
+
blockId?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface RenderBlockDecoration {
|
|
105
|
+
kind: "workflow" | "comment" | "revision" | "search" | "locked";
|
|
106
|
+
/** Decoration identifier (scope id, comment id, revision id, etc.). */
|
|
107
|
+
refId: string;
|
|
108
|
+
frame: RenderFrameRect;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Geometry
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* All rects are in frame-local pixels at the kernel's chosen zoom. The
|
|
117
|
+
* kernel applies `RenderZoom.pxPerTwip` to the page graph's twip values
|
|
118
|
+
* before returning a frame.
|
|
119
|
+
*/
|
|
120
|
+
export interface RenderFrameRect {
|
|
121
|
+
leftPx: number;
|
|
122
|
+
topPx: number;
|
|
123
|
+
widthPx: number;
|
|
124
|
+
heightPx: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RenderZoom {
|
|
128
|
+
/** Pixels per twip (e.g. 96 dpi at 100% = 0.0667). */
|
|
129
|
+
pxPerTwip: number;
|
|
130
|
+
/** Viewport width in px. */
|
|
131
|
+
viewportWidthPx: number;
|
|
132
|
+
/** Fit mode the zoom was resolved from. */
|
|
133
|
+
fitMode: "fixed" | "page-width" | "one-page";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PageChromeReservations {
|
|
137
|
+
/** Twips reserved to the left of the page frame for the workflow rail. */
|
|
138
|
+
railLaneTwips: number;
|
|
139
|
+
/** Twips reserved to the right for comment balloons. */
|
|
140
|
+
balloonLaneTwips: number;
|
|
141
|
+
/** Twips reserved at the bottom of the body for footnotes. */
|
|
142
|
+
footnoteAreaTwips: number;
|
|
143
|
+
/** Cached post-zoom page frame in pixels. */
|
|
144
|
+
pageFrameWidthPx: number;
|
|
145
|
+
pageFrameHeightPx: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Decoration and anchor indexes
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
export interface DecorationIndex {
|
|
153
|
+
workflow: readonly RenderBlockDecoration[];
|
|
154
|
+
comments: readonly RenderBlockDecoration[];
|
|
155
|
+
revisions: readonly RenderBlockDecoration[];
|
|
156
|
+
search: readonly RenderBlockDecoration[];
|
|
157
|
+
locked: readonly RenderBlockDecoration[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface RenderAnchorIndex {
|
|
161
|
+
byRuntimeOffset(
|
|
162
|
+
offset: number,
|
|
163
|
+
story?: EditorStoryTarget,
|
|
164
|
+
): RenderFrameRect | null;
|
|
165
|
+
byBlockId(blockId: string): RenderFrameRect | null;
|
|
166
|
+
byFragmentId(fragmentId: string): RenderFrameRect | null;
|
|
167
|
+
byPageIndex(pageIndex: number): RenderFrameRect | null;
|
|
168
|
+
/**
|
|
169
|
+
* Union rect covering a selection range. Resolves one rect per runtime
|
|
170
|
+
* offset and unions them, so chrome surfaces anchoring on selections
|
|
171
|
+
* (selection toolbar, suggestion card, comment thread tool) get a single
|
|
172
|
+
* tight rect without repeating the union math per consumer. Returns
|
|
173
|
+
* `null` when no offset in the range resolves to an anchor.
|
|
174
|
+
*
|
|
175
|
+
* Inclusive `fromOffset`, exclusive `toOffset` — matches the selection
|
|
176
|
+
* model used by `SelectionSnapshot`. When `fromOffset === toOffset`,
|
|
177
|
+
* falls back to `byRuntimeOffset(fromOffset)` so collapsed carets
|
|
178
|
+
* resolve to their line-box rect.
|
|
179
|
+
*/
|
|
180
|
+
bySelection(
|
|
181
|
+
fromOffset: number,
|
|
182
|
+
toOffset: number,
|
|
183
|
+
story?: EditorStoryTarget,
|
|
184
|
+
): RenderFrameRect | null;
|
|
185
|
+
/**
|
|
186
|
+
* Locate a table cell's rect given its owning table block id and logical
|
|
187
|
+
* (rowIndex, columnIndex). Reads from the attached `TableRenderPlan` on
|
|
188
|
+
* the render block so chrome tools (border picker, cell context bar)
|
|
189
|
+
* position without re-measuring the DOM. Returns `null` when the block
|
|
190
|
+
* is not a table, the plan is absent, or the indices fall outside the
|
|
191
|
+
* cell matrix.
|
|
192
|
+
*/
|
|
193
|
+
byTableCell(
|
|
194
|
+
tableBlockId: string,
|
|
195
|
+
rowIndex: number,
|
|
196
|
+
columnIndex: number,
|
|
197
|
+
): RenderFrameRect | null;
|
|
198
|
+
/**
|
|
199
|
+
* Locate the vertical grip at the right edge of `columnIndex` on a
|
|
200
|
+
* table. Returns a zero-width rect whose `leftPx` is the grip origin
|
|
201
|
+
* and whose `heightPx` spans the table's visible height on the page.
|
|
202
|
+
*/
|
|
203
|
+
byTableColumnEdge(
|
|
204
|
+
tableBlockId: string,
|
|
205
|
+
columnIndex: number,
|
|
206
|
+
): RenderFrameRect | null;
|
|
207
|
+
/**
|
|
208
|
+
* Locate the horizontal grip at the bottom edge of `rowIndex`. Returns
|
|
209
|
+
* a zero-height rect whose `topPx` is the grip origin and whose
|
|
210
|
+
* `widthPx` spans the table's visible width.
|
|
211
|
+
*/
|
|
212
|
+
byTableRowEdge(
|
|
213
|
+
tableBlockId: string,
|
|
214
|
+
rowIndex: number,
|
|
215
|
+
): RenderFrameRect | null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Queries + events
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
export interface RenderFrameQueryOptions {
|
|
223
|
+
story?: EditorStoryTarget;
|
|
224
|
+
pageRange?: { fromPageIndex: number; toPageIndex: number };
|
|
225
|
+
includeDecorations?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface RenderAnchorQuery {
|
|
229
|
+
kind:
|
|
230
|
+
| "runtime-offset"
|
|
231
|
+
| "block-id"
|
|
232
|
+
| "fragment-id"
|
|
233
|
+
| "scope-id"
|
|
234
|
+
| "comment-id"
|
|
235
|
+
| "revision-id"
|
|
236
|
+
| "page-index";
|
|
237
|
+
value: string | number;
|
|
238
|
+
story?: EditorStoryTarget;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface RenderHitResult {
|
|
242
|
+
pageIndex: number;
|
|
243
|
+
regionKind: PublicPageRegion["kind"];
|
|
244
|
+
blockId: string;
|
|
245
|
+
fragmentId: string;
|
|
246
|
+
lineIndex: number;
|
|
247
|
+
runtimeOffset: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Point in the shell's overlay coordinate space (not the viewport, not
|
|
252
|
+
* the document CSS). Chrome surfaces compute this from a pointer event
|
|
253
|
+
* via `chrome-overlay-projector.ts` (same projector that maps rects out
|
|
254
|
+
* of the overlay) and pass it into `facet.hitTest`.
|
|
255
|
+
*/
|
|
256
|
+
export interface RenderPoint {
|
|
257
|
+
xPx: number;
|
|
258
|
+
yPx: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export type RenderKernelEvent =
|
|
262
|
+
| { kind: "frame_built"; revision: number; reason: "full" | "incremental" }
|
|
263
|
+
| {
|
|
264
|
+
kind: "frame_diff";
|
|
265
|
+
revision: number;
|
|
266
|
+
pageRange: { fromPageIndex: number; toPageIndex: number };
|
|
267
|
+
}
|
|
268
|
+
| { kind: "decoration_resolved"; revision: number };
|
|
269
|
+
|
|
270
|
+
export interface DefaultZoomInput {
|
|
271
|
+
pxPerTwip?: number;
|
|
272
|
+
viewportWidthPx?: number;
|
|
273
|
+
fitMode?: RenderZoom["fitMode"];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Conservative default zoom: 96 dpi at 100% = 1 inch per 96 pixels, and
|
|
278
|
+
* 1 inch = 1440 twips, so 1 twip = 96/1440 ≈ 0.0667 px. Consumers pass a
|
|
279
|
+
* real zoom when they have a viewport; this keeps the kernel testable
|
|
280
|
+
* without a DOM.
|
|
281
|
+
*/
|
|
282
|
+
export const DEFAULT_PX_PER_TWIP = 96 / 1440;
|
|
283
|
+
|
|
284
|
+
export function resolveDefaultZoom(input: DefaultZoomInput = {}): RenderZoom {
|
|
285
|
+
return {
|
|
286
|
+
pxPerTwip: input.pxPerTwip ?? DEFAULT_PX_PER_TWIP,
|
|
287
|
+
viewportWidthPx: input.viewportWidthPx ?? 900,
|
|
288
|
+
fitMode: input.fitMode ?? "fixed",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const EMPTY_DECORATION_INDEX: DecorationIndex = Object.freeze({
|
|
293
|
+
workflow: Object.freeze([]) as readonly RenderBlockDecoration[],
|
|
294
|
+
comments: Object.freeze([]) as readonly RenderBlockDecoration[],
|
|
295
|
+
revisions: Object.freeze([]) as readonly RenderBlockDecoration[],
|
|
296
|
+
search: Object.freeze([]) as readonly RenderBlockDecoration[],
|
|
297
|
+
locked: Object.freeze([]) as readonly RenderBlockDecoration[],
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Default chrome reservations. Ship Letter-compatible defaults so the
|
|
302
|
+
* kernel can be instantiated without a host injecting chrome sizes; chrome
|
|
303
|
+
* consumers override via `RenderPage.chromeReservations` once they know
|
|
304
|
+
* their own layout.
|
|
305
|
+
*/
|
|
306
|
+
export function defaultChromeReservations(
|
|
307
|
+
layout: PageLayoutSnapshot,
|
|
308
|
+
zoom: RenderZoom,
|
|
309
|
+
): PageChromeReservations {
|
|
310
|
+
return {
|
|
311
|
+
railLaneTwips: 360,
|
|
312
|
+
balloonLaneTwips: 2160,
|
|
313
|
+
footnoteAreaTwips: 0,
|
|
314
|
+
pageFrameWidthPx: layout.pageWidth * zoom.pxPerTwip,
|
|
315
|
+
pageFrameHeightPx: layout.pageHeight * zoom.pxPerTwip,
|
|
316
|
+
};
|
|
317
|
+
}
|