@beyondwork/docx-react-component 1.0.57 → 1.0.59
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 +1 -1
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +1149 -8
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +120 -39
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +165 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +544 -35
- package/src/runtime/document-search.ts +176 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +293 -18
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DocumentNavigationSnapshot,
|
|
3
|
+
EditorAnchorProjection,
|
|
3
4
|
EditorStoryTarget,
|
|
4
5
|
SearchOptions,
|
|
5
6
|
SearchResultSnapshot,
|
|
6
7
|
SelectionSnapshot,
|
|
8
|
+
SurfaceBlockSnapshot,
|
|
9
|
+
SurfaceInlineSegment,
|
|
10
|
+
TextStyleFilter,
|
|
7
11
|
} from "../api/public-types";
|
|
12
|
+
import { EditorApiError } from "../api/public-types.ts";
|
|
8
13
|
import {
|
|
9
14
|
MAIN_STORY_TARGET,
|
|
10
15
|
storyTargetsEqual,
|
|
@@ -23,6 +28,7 @@ import {
|
|
|
23
28
|
resolveSectionForStoryTarget,
|
|
24
29
|
} from "./document-layout.ts";
|
|
25
30
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
31
|
+
import { resolveScope } from "./scope-resolver.ts";
|
|
26
32
|
|
|
27
33
|
export function searchDocument(
|
|
28
34
|
document: CanonicalDocumentEnvelope,
|
|
@@ -143,3 +149,173 @@ function getActiveSearchResultIndex(
|
|
|
143
149
|
|
|
144
150
|
return activeIndex >= 0 ? activeIndex : 0;
|
|
145
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Phase C §C3 — find all text matches in the document, respecting the new
|
|
155
|
+
* `regex`, `inScope`, and `inStory` options. Throws `EditorApiError` with
|
|
156
|
+
* `code: "search_invalid_regex"` if `options.regex === true` and `query`
|
|
157
|
+
* is not a valid JavaScript regular expression pattern.
|
|
158
|
+
*
|
|
159
|
+
* Returns an array of `EditorAnchorProjection` values (range anchors) that
|
|
160
|
+
* compose directly with `setSelection`, `addScope`, `getLocationForAnchor`.
|
|
161
|
+
*/
|
|
162
|
+
export function findTextMatches(
|
|
163
|
+
document: CanonicalDocumentEnvelope,
|
|
164
|
+
selection: SelectionSnapshot,
|
|
165
|
+
query: string,
|
|
166
|
+
options: SearchOptions = {},
|
|
167
|
+
): EditorAnchorProjection[] {
|
|
168
|
+
const normalizedQuery = query.trim();
|
|
169
|
+
if (!normalizedQuery) return [];
|
|
170
|
+
|
|
171
|
+
if (options.regex) {
|
|
172
|
+
try {
|
|
173
|
+
const flags = options.matchCase ? "ug" : "uig";
|
|
174
|
+
new RegExp(normalizedQuery, flags);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new EditorApiError({
|
|
177
|
+
code: "search_invalid_regex",
|
|
178
|
+
message: `Invalid regex pattern: ${normalizedQuery}`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const storyTarget: EditorStoryTarget = options.inStory ?? MAIN_STORY_TARGET;
|
|
184
|
+
const surface = createEditorSurfaceSnapshot(
|
|
185
|
+
document,
|
|
186
|
+
createSelectionSnapshot(selection.anchor, selection.head),
|
|
187
|
+
storyTarget,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
let results = searchSurfaceBlocks(surface.blocks, normalizedQuery, options);
|
|
191
|
+
|
|
192
|
+
if (options.inScope) {
|
|
193
|
+
const scopeAnchor = resolveScope(document, options.inScope);
|
|
194
|
+
if (!scopeAnchor || scopeAnchor.kind !== "range") {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const { from: scopeFrom, to: scopeTo } = scopeAnchor;
|
|
198
|
+
results = results.filter(
|
|
199
|
+
(r) => r.from >= scopeFrom && r.to <= scopeTo,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return results.map((r) => ({
|
|
204
|
+
kind: "range" as const,
|
|
205
|
+
from: r.from,
|
|
206
|
+
to: r.to,
|
|
207
|
+
assoc: { start: -1 as const, end: 1 as const },
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Phase C §C4 — `findTextMatches` + post-match style filtering.
|
|
213
|
+
* See `WordReviewEditorRef.findTextWithStyle` for the full contract.
|
|
214
|
+
*/
|
|
215
|
+
export function findTextWithStyleMatches(
|
|
216
|
+
document: CanonicalDocumentEnvelope,
|
|
217
|
+
selection: SelectionSnapshot,
|
|
218
|
+
query: string,
|
|
219
|
+
filter: TextStyleFilter,
|
|
220
|
+
options: SearchOptions = {},
|
|
221
|
+
): EditorAnchorProjection[] {
|
|
222
|
+
const anchors = findTextMatches(document, selection, query, options);
|
|
223
|
+
if (
|
|
224
|
+
filter.inHeading === undefined &&
|
|
225
|
+
!filter.hasFormatting &&
|
|
226
|
+
!filter.anyFormatting
|
|
227
|
+
) {
|
|
228
|
+
return anchors;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const storyTarget: EditorStoryTarget = options.inStory ?? MAIN_STORY_TARGET;
|
|
232
|
+
const surface = createEditorSurfaceSnapshot(
|
|
233
|
+
document,
|
|
234
|
+
createSelectionSnapshot(selection.anchor, selection.head),
|
|
235
|
+
storyTarget,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return anchors.filter((anchor) => {
|
|
239
|
+
if (anchor.kind !== "range") return true;
|
|
240
|
+
const { from: matchFrom, to: matchTo } = anchor;
|
|
241
|
+
return matchPassesStyleFilter(surface.blocks, matchFrom, matchTo, filter);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function findParagraphContaining(
|
|
246
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
247
|
+
pos: number,
|
|
248
|
+
): Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null {
|
|
249
|
+
for (const block of blocks) {
|
|
250
|
+
if (block.kind === "paragraph" && block.from <= pos && pos < block.to) {
|
|
251
|
+
return block;
|
|
252
|
+
}
|
|
253
|
+
if (block.kind === "table") {
|
|
254
|
+
for (const row of block.rows) {
|
|
255
|
+
for (const cell of row.cells) {
|
|
256
|
+
const found = findParagraphContaining(cell.content, pos);
|
|
257
|
+
if (found) return found;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (block.kind === "sdt_block") {
|
|
262
|
+
const found = findParagraphContaining(block.children, pos);
|
|
263
|
+
if (found) return found;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function matchPassesStyleFilter(
|
|
270
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
271
|
+
matchFrom: number,
|
|
272
|
+
matchTo: number,
|
|
273
|
+
filter: TextStyleFilter,
|
|
274
|
+
): boolean {
|
|
275
|
+
// Find the paragraph block containing the match start (recursively — handles table cells)
|
|
276
|
+
const block = findParagraphContaining(blocks, matchFrom);
|
|
277
|
+
if (!block) return false;
|
|
278
|
+
|
|
279
|
+
if (filter.inHeading) {
|
|
280
|
+
const isHeading =
|
|
281
|
+
(block.outlineLevel !== undefined && block.outlineLevel !== null) ||
|
|
282
|
+
/^heading\d/iu.test(block.styleId ?? "");
|
|
283
|
+
if (!isHeading) return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!filter.hasFormatting && !filter.anyFormatting) return true;
|
|
287
|
+
|
|
288
|
+
// Collect text segments overlapping the match range
|
|
289
|
+
const textSegments = block.segments.filter(
|
|
290
|
+
(seg): seg is SurfaceInlineSegment & { kind: "text" } =>
|
|
291
|
+
seg.kind === "text" && seg.from < matchTo && seg.to > matchFrom,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (textSegments.length === 0) return false;
|
|
295
|
+
|
|
296
|
+
if (filter.hasFormatting) {
|
|
297
|
+
const required = filter.hasFormatting;
|
|
298
|
+
for (const seg of textSegments) {
|
|
299
|
+
const marks = seg.marks ?? [];
|
|
300
|
+
if (required.bold && !marks.includes("bold")) return false;
|
|
301
|
+
if (required.italic && !marks.includes("italic")) return false;
|
|
302
|
+
if (required.underline && !marks.includes("underline")) return false;
|
|
303
|
+
if (required.strikethrough && !marks.includes("strikethrough")) return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (filter.anyFormatting) {
|
|
308
|
+
const any = filter.anyFormatting;
|
|
309
|
+
const keys = (["bold", "italic", "underline", "strikethrough"] as const).filter(
|
|
310
|
+
(k) => any[k],
|
|
311
|
+
);
|
|
312
|
+
if (keys.length > 0) {
|
|
313
|
+
for (const seg of textSegments) {
|
|
314
|
+
const marks = seg.marks ?? [];
|
|
315
|
+
if (!keys.some((k) => marks.includes(k))) return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
@@ -30,7 +30,11 @@ import type {
|
|
|
30
30
|
SelectionSnapshot,
|
|
31
31
|
} from "../../core/state/editor-state.ts";
|
|
32
32
|
import type { TextTransactionResult } from "../../core/state/text-transaction.ts";
|
|
33
|
-
import {
|
|
33
|
+
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
34
|
+
import {
|
|
35
|
+
createSurfaceNodeSelectionProbe,
|
|
36
|
+
validateSelectionAgainstDocument,
|
|
37
|
+
} from "../selection/post-edit-validator.ts";
|
|
34
38
|
|
|
35
39
|
export type EditResult = TextTransactionResult;
|
|
36
40
|
|
|
@@ -97,7 +101,19 @@ export interface EditLayer {
|
|
|
97
101
|
*/
|
|
98
102
|
function validateResult(result: TextTransactionResult): TextTransactionResult {
|
|
99
103
|
const maxOffset = result.storyText.length;
|
|
100
|
-
const
|
|
104
|
+
const options = result.selection.activeRange.kind === "node"
|
|
105
|
+
? {
|
|
106
|
+
isValidNodeTarget: createSurfaceNodeSelectionProbe(
|
|
107
|
+
createEditorSurfaceSnapshot(result.document, result.selection),
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
: undefined;
|
|
111
|
+
const validated = validateSelectionAgainstDocument(
|
|
112
|
+
result.document,
|
|
113
|
+
result.selection,
|
|
114
|
+
maxOffset,
|
|
115
|
+
options,
|
|
116
|
+
);
|
|
101
117
|
if (validated === result.selection) {
|
|
102
118
|
return result;
|
|
103
119
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
BlockNode,
|
|
3
|
+
CanonicalDocument,
|
|
2
4
|
EndnoteProperties,
|
|
3
5
|
FootnoteCollection,
|
|
4
6
|
FootnoteProperties,
|
|
7
|
+
InlineNode,
|
|
5
8
|
SectionProperties,
|
|
6
9
|
} from "../model/canonical-document.ts";
|
|
7
10
|
|
|
@@ -22,11 +25,51 @@ export interface FootnoteResolver {
|
|
|
22
25
|
* as `getFootnoteProperties` but reads `endnotePr` from the section list.
|
|
23
26
|
*/
|
|
24
27
|
getEndnoteProperties(sectionIndex?: number): EndnoteProperties | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Lane 3b Phase 8 P8.2 — return the endnote IDs that should render at
|
|
30
|
+
* the end of the given section (i.e. the section's `endnotePr.pos` is
|
|
31
|
+
* `"sectEnd"`, per ECMA-376 §17.11.4). Order matches document
|
|
32
|
+
* traversal of the endnote reference markers inside the section's
|
|
33
|
+
* block range.
|
|
34
|
+
*
|
|
35
|
+
* The walk recurses into tables (row → cell → cell-blocks), SDT, and
|
|
36
|
+
* custom_xml containers, and inside paragraphs it recurses into
|
|
37
|
+
* hyperlinks. `FootnoteRefNode`s with `noteKind: "footnote"` are
|
|
38
|
+
* ignored (only `"endnote"` is collected). Opaque inline/block
|
|
39
|
+
* preservation is not parsed, so endnote references hidden inside
|
|
40
|
+
* `OpaqueInlineNode.rawXml` will be missed — a known preservation
|
|
41
|
+
* limitation.
|
|
42
|
+
*
|
|
43
|
+
* Returns an empty array when any of:
|
|
44
|
+
* - the section's `endnotePr.pos` is `"docEnd"` or unset (default
|
|
45
|
+
* per ECMA-376 §17.11.4);
|
|
46
|
+
* - `sectionIndex` is out of range (negative or ≥ `sections.length`);
|
|
47
|
+
* - the section contains no endnote references;
|
|
48
|
+
* - the resolver was constructed without a `document` argument
|
|
49
|
+
* (the block walk requires the canonical document — pass it via
|
|
50
|
+
* `createFootnoteResolver(collection, sections, document)`).
|
|
51
|
+
*
|
|
52
|
+
* Complexity: O(N) where N is the number of top-level blocks + total
|
|
53
|
+
* inline children. No memoization — callers that invoke this
|
|
54
|
+
* per-section on large documents should cache results against the
|
|
55
|
+
* document revision themselves. Today's sole consumer is runtime
|
|
56
|
+
* bucketing data only; no render-kernel integration yet.
|
|
57
|
+
*
|
|
58
|
+
* Dedup contract with `facet.getDocumentEndnoteBlocks()`: the
|
|
59
|
+
* facet-level document-end pool still enumerates ALL endnotes
|
|
60
|
+
* regardless of section `pos` as of this slice. Consumers that
|
|
61
|
+
* render both per-section and document-end buckets MUST subtract
|
|
62
|
+
* the union of sectEnd results from the document-end pool to avoid
|
|
63
|
+
* double-rendering. A future slice will move the dedup into the
|
|
64
|
+
* facet itself.
|
|
65
|
+
*/
|
|
66
|
+
getEndnotesForSection(sectionIndex: number): readonly string[];
|
|
25
67
|
}
|
|
26
68
|
|
|
27
69
|
export function createFootnoteResolver(
|
|
28
70
|
collection: FootnoteCollection,
|
|
29
71
|
sections?: readonly SectionProperties[],
|
|
72
|
+
document?: CanonicalDocument,
|
|
30
73
|
): FootnoteResolver {
|
|
31
74
|
return {
|
|
32
75
|
getContinuationSeparatorContent(kind) {
|
|
@@ -51,5 +94,92 @@ export function createFootnoteResolver(
|
|
|
51
94
|
if (sectionIndex === undefined || !sections) return undefined;
|
|
52
95
|
return sections[sectionIndex]?.endnotePr;
|
|
53
96
|
},
|
|
97
|
+
getEndnotesForSection(sectionIndex) {
|
|
98
|
+
if (!sections || !document) return EMPTY_READONLY_STRING_ARRAY;
|
|
99
|
+
const section = sections[sectionIndex];
|
|
100
|
+
if (!section || section.endnotePr?.pos !== "sectEnd") {
|
|
101
|
+
return EMPTY_READONLY_STRING_ARRAY;
|
|
102
|
+
}
|
|
103
|
+
return collectEndnoteIdsInSection(document, sectionIndex);
|
|
104
|
+
},
|
|
54
105
|
};
|
|
55
106
|
}
|
|
107
|
+
|
|
108
|
+
const EMPTY_READONLY_STRING_ARRAY: readonly string[] = Object.freeze([]);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Walk the document's top-level block children and collect endnote IDs
|
|
112
|
+
* referenced from the block range belonging to `targetSectionIndex`.
|
|
113
|
+
*
|
|
114
|
+
* Sections are delimited by `SectionBreakNode` blocks: the first section
|
|
115
|
+
* owns blocks from the document start up to (and including) the first
|
|
116
|
+
* section_break, the next section owns blocks up to the next break, and
|
|
117
|
+
* so on. The final section (index = number of section_breaks) owns any
|
|
118
|
+
* remaining blocks and inherits from `subParts.finalSectionProperties`.
|
|
119
|
+
*
|
|
120
|
+
* We don't recurse into tables / SDT / custom_xml here — OOXML
|
|
121
|
+
* endnoteReference markers are inline runs and may appear nested inside
|
|
122
|
+
* table cells. We DO walk those.
|
|
123
|
+
*/
|
|
124
|
+
function collectEndnoteIdsInSection(
|
|
125
|
+
document: CanonicalDocument,
|
|
126
|
+
targetSectionIndex: number,
|
|
127
|
+
): readonly string[] {
|
|
128
|
+
const ids: string[] = [];
|
|
129
|
+
let currentSection = 0;
|
|
130
|
+
for (const block of document.content.children) {
|
|
131
|
+
if (currentSection === targetSectionIndex) {
|
|
132
|
+
collectEndnoteIdsFromBlock(block, ids);
|
|
133
|
+
}
|
|
134
|
+
if (block.type === "section_break") {
|
|
135
|
+
currentSection += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return ids;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function collectEndnoteIdsFromBlock(block: BlockNode, out: string[]): void {
|
|
142
|
+
if (block.type === "paragraph") {
|
|
143
|
+
for (const child of block.children) {
|
|
144
|
+
collectEndnoteIdsFromInline(child, out);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (block.type === "table") {
|
|
149
|
+
for (const row of block.rows) {
|
|
150
|
+
for (const cell of row.cells) {
|
|
151
|
+
for (const cellBlock of cell.children) {
|
|
152
|
+
collectEndnoteIdsFromBlock(cellBlock, out);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
159
|
+
for (const child of block.children) {
|
|
160
|
+
collectEndnoteIdsFromBlock(child, out);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Other block kinds (section_break, alt_chunk, opaque_block) don't
|
|
165
|
+
// carry endnote references inline.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectEndnoteIdsFromInline(node: InlineNode, out: string[]): void {
|
|
169
|
+
if ((node as { type: string }).type === "footnote_ref") {
|
|
170
|
+
const ref = node as { type: "footnote_ref"; noteId: string; noteKind: "footnote" | "endnote" };
|
|
171
|
+
if (ref.noteKind === "endnote") {
|
|
172
|
+
out.push(ref.noteId);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Hyperlinks carry inline children that may contain endnote refs.
|
|
177
|
+
if ((node as { type: string }).type === "hyperlink") {
|
|
178
|
+
const hyperlink = node as { children?: readonly InlineNode[] };
|
|
179
|
+
if (hyperlink.children) {
|
|
180
|
+
for (const child of hyperlink.children) {
|
|
181
|
+
collectEndnoteIdsFromInline(child, out);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -447,22 +447,49 @@ export function createLayoutEngine(
|
|
|
447
447
|
if (freshSnapshots.length === 0 && firstDirty > 0) {
|
|
448
448
|
return null;
|
|
449
449
|
}
|
|
450
|
-
|
|
450
|
+
|
|
451
|
+
// Phase A — end-offset convergence: the first fresh page whose endOffset
|
|
452
|
+
// equals the prior graph page at the same global index marks the point
|
|
453
|
+
// where re-pagination would produce identical page boundaries from here on
|
|
454
|
+
// (same startOffset → same blocks → same measurements → same breaks).
|
|
455
|
+
// Prior RuntimePageNodes beyond this point are reused by reference; we
|
|
456
|
+
// skip calling buildPageGraph for them. sectionIndex is checked alongside
|
|
457
|
+
// endOffset to guard against continuous-section-merge edge cases.
|
|
458
|
+
let convergenceIndex = freshSnapshots.length; // default: no convergence found
|
|
459
|
+
for (let i = 0; i < freshSnapshots.length; i++) {
|
|
460
|
+
const priorPage = priorGraph.pages[firstDirty + i];
|
|
461
|
+
if (
|
|
462
|
+
priorPage !== undefined &&
|
|
463
|
+
freshSnapshots[i]!.endOffset === priorPage.endOffset &&
|
|
464
|
+
freshSnapshots[i]!.sectionIndex === priorPage.sectionIndex
|
|
465
|
+
) {
|
|
466
|
+
convergenceIndex = i + 1; // include this page; skip everything after
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const freshSnapshotsToRebuild = freshSnapshots.slice(0, convergenceIndex);
|
|
471
|
+
const convergedTailStart =
|
|
472
|
+
convergenceIndex < freshSnapshots.length
|
|
473
|
+
? firstDirty + convergenceIndex
|
|
474
|
+
: undefined;
|
|
475
|
+
|
|
476
|
+
const freshStories = resolvePageStories(freshSnapshotsToRebuild);
|
|
451
477
|
// Project fragments for the fresh tail pages, threading paragraph
|
|
452
478
|
// line-range splits produced by intra-paragraph pagination.
|
|
453
479
|
const freshBodyFragmentsByPageIndex = projectSurfaceBlocksToPageFragments(
|
|
454
480
|
mainSurface,
|
|
455
|
-
|
|
481
|
+
freshSnapshotsToRebuild,
|
|
456
482
|
freshResult.splits,
|
|
457
483
|
);
|
|
458
484
|
// P8.1b — merge per-note fragments into the fresh fragments map.
|
|
459
485
|
const freshFragmentsByPageIndex = new Map(freshBodyFragmentsByPageIndex);
|
|
460
486
|
for (const [pageIndex, noteFragments] of (freshResult.noteFragmentsByPageIndex ?? new Map())) {
|
|
487
|
+
if (pageIndex >= firstDirty + convergenceIndex) continue; // beyond convergence point
|
|
461
488
|
const existing = freshFragmentsByPageIndex.get(pageIndex) ?? [];
|
|
462
489
|
freshFragmentsByPageIndex.set(pageIndex, [...existing, ...noteFragments]);
|
|
463
490
|
}
|
|
464
491
|
const freshGraph = buildPageGraph({
|
|
465
|
-
pages:
|
|
492
|
+
pages: freshSnapshotsToRebuild,
|
|
466
493
|
sections,
|
|
467
494
|
stories: freshStories,
|
|
468
495
|
fragmentsByPageIndex: freshFragmentsByPageIndex,
|
|
@@ -470,7 +497,7 @@ export function createLayoutEngine(
|
|
|
470
497
|
});
|
|
471
498
|
const freshNodes = freshGraph.pages;
|
|
472
499
|
|
|
473
|
-
const splicedGraph = spliceGraph(priorGraph, freshNodes, firstDirty);
|
|
500
|
+
const splicedGraph = spliceGraph(priorGraph, freshNodes, firstDirty, convergedTailStart);
|
|
474
501
|
|
|
475
502
|
// Field dirtiness diff and resolved-formatting update run against the
|
|
476
503
|
// full spliced graph so NUMPAGES/PAGE tracking remains accurate.
|
|
@@ -179,8 +179,44 @@
|
|
|
179
179
|
* leak stale field-family projections. No pixel-geometry change;
|
|
180
180
|
* cache envelopes from v18 invalidate because the invalidation
|
|
181
181
|
* classifier's contract corrected.
|
|
182
|
+
* 20 — Phase E.1 gutter parity: `page-graph.ts::buildRegions` now subtracts
|
|
183
|
+
* `layout.gutter` from body width, matching the pre-existing
|
|
184
|
+
* subtraction in `src/runtime/page-layout-estimation.ts:153`. Prior
|
|
185
|
+
* to v20 body regions, multi-column gaps, and header/footer widths
|
|
186
|
+
* on documents with a non-zero `w:gutter` (mirrored/bound layouts)
|
|
187
|
+
* overran the intended body by `gutter` twips. Zero effect on docs
|
|
188
|
+
* without `w:gutter`; cache envelopes from v19 invalidate because
|
|
189
|
+
* body-width geometry changed for gutter-bearing documents.
|
|
190
|
+
* 21 — Phase A paginator resume: `incrementalRelayout` detects end-offset
|
|
191
|
+
* convergence after `buildPageStackFromWithSplits` returns and stops
|
|
192
|
+
* rebuilding `RuntimePageNode`s once `freshSnapshots[i].endOffset ===
|
|
193
|
+
* priorGraph.pages[firstDirty + i].endOffset`. Pages beyond the
|
|
194
|
+
* convergence point are taken from `priorGraph` by reference via the
|
|
195
|
+
* new `convergedTailStart` parameter on `spliceGraph`. Eliminates
|
|
196
|
+
* O(N) `buildPageGraph` work for the stable document tail on bounded
|
|
197
|
+
* invalidations. Cache envelopes from v20 invalidate because the
|
|
198
|
+
* spliceGraph contract (and pageId stability semantics) changed.
|
|
199
|
+
* 22 — N1 (L8 Phase D): `TwPageStackOverlayLayer` moved from
|
|
200
|
+
* `TwChromeOverlay` (z-30 context) to a direct child of
|
|
201
|
+
* `wre-page-surface` at z-0, before the z-10 PM wrapper. Per-page
|
|
202
|
+
* card `backgroundColor` changed from transparent to
|
|
203
|
+
* `var(--color-page-bg)` so each page gets its own opaque paper
|
|
204
|
+
* background behind PM text. `pageShellMetrics.pageFrameStyle`
|
|
205
|
+
* (bg/border/shadow) no longer spread on `[data-paper-frame]` —
|
|
206
|
+
* paper chrome now lives on overlay cards. DOM structure of the
|
|
207
|
+
* page-break widget unchanged; overlay placement and stacking
|
|
208
|
+
* context changed. Cache envelopes from v21 invalidate because
|
|
209
|
+
* the overlay placement contract changed.
|
|
210
|
+
* 23 — CO3 T1/T4: numbering marker lane wiring. `pm-schema.ts` and
|
|
211
|
+
* `tw-page-block-view.helpers.ts` now emit `margin-left: -(width/20)pt`
|
|
212
|
+
* on the marker span (layout-computed gutter from `markerLane.start`)
|
|
213
|
+
* and use `text-align` instead of `justify-content` for decimal
|
|
214
|
+
* right-alignment. `resolved-numbering-geometry.ts` replaces the
|
|
215
|
+
* CO3.8 `isStaleParaInd` heuristic with a broader `isDegenerateParaInd`
|
|
216
|
+
* guard (hanging===left → use level geometry) that fixes the APS
|
|
217
|
+
* Supply paragraph pattern. Cache envelopes from v22 invalidate.
|
|
182
218
|
*/
|
|
183
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
219
|
+
export const LAYOUT_ENGINE_VERSION = 23 as const;
|
|
184
220
|
|
|
185
221
|
/**
|
|
186
222
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -356,8 +356,13 @@ function buildRegions(
|
|
|
356
356
|
stories: ResolvedPageStories,
|
|
357
357
|
noteAllocations: readonly RuntimeNoteAllocation[] = [],
|
|
358
358
|
): RuntimePageRegions {
|
|
359
|
+
// Gutter (w:gutter / pgMar @gutter) reduces usable body width on the bound
|
|
360
|
+
// edge. Parity fix with `src/runtime/page-layout-estimation.ts:153`, which
|
|
361
|
+
// already subtracts `layout.gutter`; prior to this edit the graph path
|
|
362
|
+
// ignored it and consumers computed regions + columns against a body that
|
|
363
|
+
// was `gutter` twips too wide. Defaults to 0 when no gutter is declared.
|
|
359
364
|
const bodyWidth =
|
|
360
|
-
layout.pageWidth - layout.marginLeft - layout.marginRight;
|
|
365
|
+
layout.pageWidth - layout.marginLeft - layout.marginRight - layout.gutter;
|
|
361
366
|
let bodyHeight =
|
|
362
367
|
layout.pageHeight - layout.marginTop - layout.marginBottom;
|
|
363
368
|
|
|
@@ -530,6 +535,7 @@ export function spliceGraph(
|
|
|
530
535
|
prior: RuntimePageGraph,
|
|
531
536
|
freshPages: readonly RuntimePageNode[],
|
|
532
537
|
firstDirtyIndex: number,
|
|
538
|
+
convergedTailStart?: number,
|
|
533
539
|
): RuntimePageGraph {
|
|
534
540
|
graphRevision += 1;
|
|
535
541
|
const clampedFirst = Math.max(0, Math.min(firstDirtyIndex, prior.pages.length));
|
|
@@ -557,10 +563,17 @@ export function spliceGraph(
|
|
|
557
563
|
}
|
|
558
564
|
const stableTailPrefix = priorTail.slice(0, reusedCount);
|
|
559
565
|
const divergentTail = freshPages.slice(reusedCount);
|
|
566
|
+
// Phase A — convergedTailStart: pages in the prior graph from this index
|
|
567
|
+
// onward are known-identical to what fresh re-pagination would produce
|
|
568
|
+
// (detected via end-offset convergence in incrementalRelayout). Take them
|
|
569
|
+
// by reference without rebuilding RuntimePageNodes.
|
|
570
|
+
const convergedTail =
|
|
571
|
+
convergedTailStart !== undefined ? prior.pages.slice(convergedTailStart) : [];
|
|
560
572
|
const nextPages: RuntimePageNode[] = [
|
|
561
573
|
...preserved,
|
|
562
574
|
...stableTailPrefix,
|
|
563
575
|
...divergentTail,
|
|
576
|
+
...convergedTail,
|
|
564
577
|
];
|
|
565
578
|
|
|
566
579
|
const survivingPageIds = new Set(nextPages.map((page) => page.pageId));
|
|
@@ -110,6 +110,17 @@ export interface ResolvedParagraphFormatting {
|
|
|
110
110
|
widowControl: boolean;
|
|
111
111
|
/** Contextual spacing — suppress before/after when adjacent styles match. */
|
|
112
112
|
contextualSpacing: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Numbering marker lane geometry in twips. Present when the block has a
|
|
115
|
+
* resolved numbering instance with a non-zero hanging indent. Derived from
|
|
116
|
+
* `resolvedNumbering.geometry.markerLane`; consumers emit
|
|
117
|
+
* `margin-left: -(widthTwips/20)pt` on the marker span.
|
|
118
|
+
*/
|
|
119
|
+
numberingMarkerBox?: {
|
|
120
|
+
startTwips: number;
|
|
121
|
+
widthTwips: number;
|
|
122
|
+
textStartTwips: number;
|
|
123
|
+
};
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
export interface ResolvedTableRowFormatting {
|
|
@@ -138,6 +149,7 @@ export function resolveBlockFormatting(
|
|
|
138
149
|
const indent = resolveIndentation(block);
|
|
139
150
|
const fontInfo = resolveDominantFont(block);
|
|
140
151
|
const lineHeight = resolveLineHeight(spacing, fontInfo.fontSizeHalfPoints);
|
|
152
|
+
const markerLane = block.resolvedNumbering?.geometry?.markerLane;
|
|
141
153
|
|
|
142
154
|
return {
|
|
143
155
|
spacingBefore: spacing.before ?? 0,
|
|
@@ -156,6 +168,15 @@ export function resolveBlockFormatting(
|
|
|
156
168
|
pageBreakBefore: Boolean(block.pageBreakBefore ?? block.resolvedParagraphFormatting?.pageBreakBefore),
|
|
157
169
|
widowControl: (block.widowControl ?? block.resolvedParagraphFormatting?.widowControl) !== false, // default true in Word
|
|
158
170
|
contextualSpacing: Boolean(block.contextualSpacing ?? block.resolvedParagraphFormatting?.contextualSpacing),
|
|
171
|
+
...(markerLane && markerLane.width > 0
|
|
172
|
+
? {
|
|
173
|
+
numberingMarkerBox: {
|
|
174
|
+
startTwips: markerLane.start,
|
|
175
|
+
widthTwips: markerLane.width,
|
|
176
|
+
textStartTwips: markerLane.textStart,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
: {}),
|
|
159
180
|
};
|
|
160
181
|
}
|
|
161
182
|
|
|
@@ -25,6 +25,8 @@ export interface NumberingPrefixResult {
|
|
|
25
25
|
isLegalNumbering?: boolean;
|
|
26
26
|
geometry: ResolvedNumberingGeometry;
|
|
27
27
|
markerRunProperties?: CanonicalRunFormatting;
|
|
28
|
+
/** Resolved media-catalog ID for picture-bullet images. Set when the numbering level has `picBulletId` and the catalog resolves it to a `NumPicBullet` entry with a `mediaId`. */
|
|
29
|
+
picBulletMediaId?: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface NumberingPrefixResolver {
|
|
@@ -82,6 +84,12 @@ export function createNumberingPrefixResolver(
|
|
|
82
84
|
}
|
|
83
85
|
const visibleText = resolved.effectiveLevel.format === "none" ? null : text;
|
|
84
86
|
|
|
87
|
+
const picBulletId = resolved.effectiveLevel.picBulletId;
|
|
88
|
+
const picBulletMediaId =
|
|
89
|
+
picBulletId != null
|
|
90
|
+
? catalog.numPicBullets?.[picBulletId]?.mediaId
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
85
93
|
return {
|
|
86
94
|
text: visibleText,
|
|
87
95
|
level: resolved.effectiveLevel.level,
|
|
@@ -96,6 +104,7 @@ export function createNumberingPrefixResolver(
|
|
|
96
104
|
? { markerRunProperties: resolved.geometry.markerRunProperties }
|
|
97
105
|
: {}),
|
|
98
106
|
geometry: resolved.geometry,
|
|
107
|
+
...(picBulletMediaId != null ? { picBulletMediaId } : {}),
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
|
|
@@ -139,6 +148,14 @@ function advanceSequence(
|
|
|
139
148
|
state.counters.length = currentLevel + 1;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
// Initialize any skipped parent levels so legal outline %1.%2.%3. patterns
|
|
152
|
+
// don't produce empty segments on a level jump.
|
|
153
|
+
for (let i = 0; i < currentLevel; i++) {
|
|
154
|
+
if (state.counters[i] === undefined) {
|
|
155
|
+
state.counters[i] = getLevelStartAt(i, levelDefinitions);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
const startAt = getLevelStartAt(currentLevel, levelDefinitions);
|
|
143
160
|
const currentValue = state.counters[currentLevel];
|
|
144
161
|
state.counters[currentLevel] =
|