@beyondwork/docx-react-component 1.0.85 → 1.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/public-types.ts +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +112 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +75 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -2
|
@@ -291,6 +291,7 @@ import type {
|
|
|
291
291
|
ParagraphNode,
|
|
292
292
|
SectionProperties,
|
|
293
293
|
SubPartsCatalog,
|
|
294
|
+
TocRegion,
|
|
294
295
|
} from "../model/canonical-document.ts";
|
|
295
296
|
import {
|
|
296
297
|
isSupportedFieldFamily,
|
|
@@ -3565,6 +3566,7 @@ export function createDocumentRuntime(
|
|
|
3565
3566
|
emitError(runtimeError);
|
|
3566
3567
|
return {
|
|
3567
3568
|
kind: "rejected",
|
|
3569
|
+
refreshClass: "blocked",
|
|
3568
3570
|
opId: (command.origin as { opId?: string } | undefined)?.opId,
|
|
3569
3571
|
newRevisionToken: "",
|
|
3570
3572
|
blockedReasons: [
|
|
@@ -5319,6 +5321,7 @@ export function createDocumentRuntime(
|
|
|
5319
5321
|
});
|
|
5320
5322
|
return completeDispatch({
|
|
5321
5323
|
kind: "rejected",
|
|
5324
|
+
refreshClass: "blocked",
|
|
5322
5325
|
opId,
|
|
5323
5326
|
newRevisionToken: "",
|
|
5324
5327
|
blockedReasons: [{ code: "suggesting_unsupported", message }],
|
|
@@ -5335,6 +5338,7 @@ export function createDocumentRuntime(
|
|
|
5335
5338
|
});
|
|
5336
5339
|
return completeDispatch({
|
|
5337
5340
|
kind: "rejected",
|
|
5341
|
+
refreshClass: "blocked",
|
|
5338
5342
|
opId,
|
|
5339
5343
|
newRevisionToken: "",
|
|
5340
5344
|
blockedReasons: blockedReasons.map((r) => ({ code: r.code, message: r.message })),
|
|
@@ -5408,6 +5412,7 @@ export function createDocumentRuntime(
|
|
|
5408
5412
|
});
|
|
5409
5413
|
return completeDispatch({
|
|
5410
5414
|
kind: "equivalent",
|
|
5415
|
+
refreshClass: "local-text-equivalent",
|
|
5411
5416
|
opId,
|
|
5412
5417
|
newRevisionToken: state.revisionToken,
|
|
5413
5418
|
});
|
|
@@ -5475,6 +5480,7 @@ export function createDocumentRuntime(
|
|
|
5475
5480
|
if (meta.invalidatesStructures) {
|
|
5476
5481
|
return {
|
|
5477
5482
|
kind: "structural-divergence",
|
|
5483
|
+
refreshClass: "full-projection",
|
|
5478
5484
|
opId,
|
|
5479
5485
|
newRevisionToken,
|
|
5480
5486
|
scopeTagTouches: touches,
|
|
@@ -5499,6 +5505,7 @@ export function createDocumentRuntime(
|
|
|
5499
5505
|
const adjustedRange = computeAdjustedRange(priorState, transaction);
|
|
5500
5506
|
return {
|
|
5501
5507
|
kind: "adjusted",
|
|
5508
|
+
refreshClass: "surface-only",
|
|
5502
5509
|
opId,
|
|
5503
5510
|
newRevisionToken,
|
|
5504
5511
|
adjustedRange,
|
|
@@ -5508,6 +5515,7 @@ export function createDocumentRuntime(
|
|
|
5508
5515
|
|
|
5509
5516
|
return {
|
|
5510
5517
|
kind: "equivalent",
|
|
5518
|
+
refreshClass: "local-text-equivalent",
|
|
5511
5519
|
opId,
|
|
5512
5520
|
newRevisionToken,
|
|
5513
5521
|
scopeTagTouches: touches,
|
|
@@ -5685,6 +5693,10 @@ export function createDocumentRuntime(
|
|
|
5685
5693
|
emit({
|
|
5686
5694
|
type: "toc_auto_refreshed",
|
|
5687
5695
|
documentId: state.documentId,
|
|
5696
|
+
...(refreshed.result.tocId ? { tocId: refreshed.result.tocId } : {}),
|
|
5697
|
+
...(refreshed.document.fieldRegistry?.tocRegions
|
|
5698
|
+
? { regionCount: refreshed.document.fieldRegistry.tocRegions.length }
|
|
5699
|
+
: {}),
|
|
5688
5700
|
entryCount: refreshed.result.entryCount,
|
|
5689
5701
|
trigger: flushedTrigger,
|
|
5690
5702
|
});
|
|
@@ -7161,6 +7173,29 @@ function refreshDocumentTableOfContents(
|
|
|
7161
7173
|
changed: boolean;
|
|
7162
7174
|
protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
|
|
7163
7175
|
} {
|
|
7176
|
+
const selectedRegion = selectTocRegion(document.fieldRegistry?.tocRegions, options?.tocId);
|
|
7177
|
+
const refreshMode = options?.mode ?? "regenerate";
|
|
7178
|
+
if (refreshMode === "preserveCached") {
|
|
7179
|
+
const cachedEntries = selectedRegion?.cachedEntries ?? [];
|
|
7180
|
+
return {
|
|
7181
|
+
document,
|
|
7182
|
+
result: {
|
|
7183
|
+
...(selectedRegion ? { tocId: selectedRegion.tocId } : {}),
|
|
7184
|
+
mode: "preserveCached",
|
|
7185
|
+
status: selectedRegion?.status ?? (cachedEntries.length > 0 ? "stale" : "missing"),
|
|
7186
|
+
entryCount: cachedEntries.length,
|
|
7187
|
+
entries: cachedEntries.map((entry) => ({
|
|
7188
|
+
level: entry.level,
|
|
7189
|
+
text: entry.text,
|
|
7190
|
+
pageIndex: tocPageTextToPageIndex(entry.pageText),
|
|
7191
|
+
...(entry.pageText ? { pageText: entry.pageText } : {}),
|
|
7192
|
+
source: "cached",
|
|
7193
|
+
})),
|
|
7194
|
+
},
|
|
7195
|
+
changed: false,
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7164
7199
|
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
7165
7200
|
// Build a single O(N) map from paragraph offset → bookmark name so the
|
|
7166
7201
|
// per-heading lookup below is O(1) instead of O(N) per heading.
|
|
@@ -7189,16 +7224,16 @@ function refreshDocumentTableOfContents(
|
|
|
7189
7224
|
}
|
|
7190
7225
|
}
|
|
7191
7226
|
let changed = false;
|
|
7192
|
-
let resultEntries:
|
|
7227
|
+
let resultEntries: RuntimeTocEntry[] = [];
|
|
7193
7228
|
let changedFrom: number | undefined;
|
|
7194
7229
|
let changedTo: number | undefined;
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
return field;
|
|
7198
|
-
}
|
|
7230
|
+
|
|
7231
|
+
const buildEntriesForField = (field: FieldNode): RuntimeTocEntry[] => {
|
|
7199
7232
|
const levelRange = options?.maxLevel
|
|
7200
7233
|
? { from: 1, to: options.maxLevel }
|
|
7201
|
-
:
|
|
7234
|
+
: selectedRegion && normalizeTocInstruction(selectedRegion.instruction.raw) === normalizeTocInstruction(field.instruction)
|
|
7235
|
+
? selectedRegion.instruction.outlineRange
|
|
7236
|
+
: parseTocLevelRange(field.instruction);
|
|
7202
7237
|
const entries = navigation.headings
|
|
7203
7238
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
7204
7239
|
.map((heading) => {
|
|
@@ -7213,6 +7248,30 @@ function refreshDocumentTableOfContents(
|
|
|
7213
7248
|
if (resultEntries.length === 0) {
|
|
7214
7249
|
resultEntries = entries;
|
|
7215
7250
|
}
|
|
7251
|
+
return entries;
|
|
7252
|
+
};
|
|
7253
|
+
|
|
7254
|
+
const tocRangeRefresh = refreshTocBlockRanges(
|
|
7255
|
+
document.content.children,
|
|
7256
|
+
buildEntriesForField,
|
|
7257
|
+
resolveDisplayPageNumber,
|
|
7258
|
+
selectedRegion,
|
|
7259
|
+
);
|
|
7260
|
+
if (tocRangeRefresh.changed) {
|
|
7261
|
+
changed = true;
|
|
7262
|
+
}
|
|
7263
|
+
|
|
7264
|
+
const nextChildren = refreshBlocksWithCursor(tocRangeRefresh.blocks, (field, range) => {
|
|
7265
|
+
if (field.fieldFamily !== "TOC") {
|
|
7266
|
+
return field;
|
|
7267
|
+
}
|
|
7268
|
+
if (selectedRegion && normalizeTocInstruction(field.instruction) !== normalizeTocInstruction(selectedRegion.instruction.raw)) {
|
|
7269
|
+
return field;
|
|
7270
|
+
}
|
|
7271
|
+
if (field.refreshStatus === "current" && field.children.length === 0) {
|
|
7272
|
+
return field;
|
|
7273
|
+
}
|
|
7274
|
+
const entries = buildEntriesForField(field);
|
|
7216
7275
|
const nextField: FieldNode = {
|
|
7217
7276
|
...field,
|
|
7218
7277
|
children: buildTocInlineNodes(entries, resolveDisplayPageNumber),
|
|
@@ -7228,7 +7287,7 @@ function refreshDocumentTableOfContents(
|
|
|
7228
7287
|
if (!changed) {
|
|
7229
7288
|
return {
|
|
7230
7289
|
document,
|
|
7231
|
-
result:
|
|
7290
|
+
result: buildTocRefreshResult(selectedRegion, "regenerate", resultEntries, "current"),
|
|
7232
7291
|
changed: false,
|
|
7233
7292
|
};
|
|
7234
7293
|
}
|
|
@@ -7245,13 +7304,24 @@ function refreshDocumentTableOfContents(
|
|
|
7245
7304
|
styles: nextDocument.styles,
|
|
7246
7305
|
subParts: nextDocument.subParts,
|
|
7247
7306
|
});
|
|
7248
|
-
|
|
7307
|
+
const nextTocRegions = nextRegistry.tocRegions?.map((region) => {
|
|
7308
|
+
if (!selectedRegion || normalizeTocInstruction(region.instruction.raw) === normalizeTocInstruction(selectedRegion.instruction.raw)) {
|
|
7309
|
+
return { ...region, status: "current" as const };
|
|
7310
|
+
}
|
|
7311
|
+
return region;
|
|
7312
|
+
});
|
|
7313
|
+
nextDocument.fieldRegistry = nextRegistry.tocStructure || nextTocRegions
|
|
7249
7314
|
? {
|
|
7250
7315
|
...nextRegistry,
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7316
|
+
...(nextTocRegions ? { tocRegions: nextTocRegions } : {}),
|
|
7317
|
+
...(nextRegistry.tocStructure
|
|
7318
|
+
? {
|
|
7319
|
+
tocStructure: {
|
|
7320
|
+
...nextRegistry.tocStructure,
|
|
7321
|
+
status: "current",
|
|
7322
|
+
},
|
|
7323
|
+
}
|
|
7324
|
+
: {}),
|
|
7255
7325
|
}
|
|
7256
7326
|
: nextRegistry;
|
|
7257
7327
|
let protectionSelection:
|
|
@@ -7263,12 +7333,267 @@ function refreshDocumentTableOfContents(
|
|
|
7263
7333
|
|
|
7264
7334
|
return {
|
|
7265
7335
|
document: nextDocument,
|
|
7266
|
-
result:
|
|
7336
|
+
result: buildTocRefreshResult(selectedRegion, "regenerate", resultEntries, "current"),
|
|
7267
7337
|
changed: true,
|
|
7268
7338
|
...(protectionSelection ? { protectionSelection } : {}),
|
|
7269
7339
|
};
|
|
7270
7340
|
}
|
|
7271
7341
|
|
|
7342
|
+
type RuntimeTocEntry = {
|
|
7343
|
+
level: number;
|
|
7344
|
+
text: string;
|
|
7345
|
+
pageIndex: number;
|
|
7346
|
+
bookmarkName?: string;
|
|
7347
|
+
};
|
|
7348
|
+
|
|
7349
|
+
function selectTocRegion(
|
|
7350
|
+
regions: readonly TocRegion[] | undefined,
|
|
7351
|
+
tocId: string | undefined,
|
|
7352
|
+
): TocRegion | undefined {
|
|
7353
|
+
if (!regions || regions.length === 0) {
|
|
7354
|
+
return undefined;
|
|
7355
|
+
}
|
|
7356
|
+
if (!tocId) {
|
|
7357
|
+
return regions[0];
|
|
7358
|
+
}
|
|
7359
|
+
return regions.find((region) => region.tocId === tocId);
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
function buildTocRefreshResult(
|
|
7363
|
+
region: TocRegion | undefined,
|
|
7364
|
+
mode: "preserveCached" | "regenerate",
|
|
7365
|
+
entries: readonly RuntimeTocEntry[],
|
|
7366
|
+
status: "current" | "stale" | "missing",
|
|
7367
|
+
): TocRefreshResult {
|
|
7368
|
+
return {
|
|
7369
|
+
...(region ? { tocId: region.tocId } : {}),
|
|
7370
|
+
mode,
|
|
7371
|
+
status,
|
|
7372
|
+
entryCount: entries.length,
|
|
7373
|
+
entries: entries.map((entry) => ({
|
|
7374
|
+
level: entry.level,
|
|
7375
|
+
text: entry.text,
|
|
7376
|
+
pageIndex: entry.pageIndex,
|
|
7377
|
+
source: "generated",
|
|
7378
|
+
})),
|
|
7379
|
+
};
|
|
7380
|
+
}
|
|
7381
|
+
|
|
7382
|
+
function tocPageTextToPageIndex(pageText: string | undefined): number {
|
|
7383
|
+
const value = Number.parseInt(pageText ?? "", 10);
|
|
7384
|
+
return Number.isFinite(value) && value > 0 ? value - 1 : 0;
|
|
7385
|
+
}
|
|
7386
|
+
|
|
7387
|
+
function normalizeTocInstruction(instruction: string): string {
|
|
7388
|
+
return instruction.replace(/\s+/gu, " ").trim();
|
|
7389
|
+
}
|
|
7390
|
+
|
|
7391
|
+
function refreshTocBlockRanges(
|
|
7392
|
+
blocks: readonly BlockNode[],
|
|
7393
|
+
buildEntriesForField: (field: FieldNode) => RuntimeTocEntry[],
|
|
7394
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7395
|
+
selectedRegion?: TocRegion,
|
|
7396
|
+
): { blocks: BlockNode[]; changed: boolean } {
|
|
7397
|
+
const nextBlocks: BlockNode[] = [];
|
|
7398
|
+
let changed = false;
|
|
7399
|
+
|
|
7400
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
7401
|
+
const block = blocks[index];
|
|
7402
|
+
|
|
7403
|
+
if (block.type === "paragraph") {
|
|
7404
|
+
const tocField = findTocFieldNode(block);
|
|
7405
|
+
if (
|
|
7406
|
+
tocField &&
|
|
7407
|
+
(!selectedRegion || normalizeTocInstruction(tocField.instruction) === normalizeTocInstruction(selectedRegion.instruction.raw)) &&
|
|
7408
|
+
startsTocParagraphRegion(blocks, index)
|
|
7409
|
+
) {
|
|
7410
|
+
const endIndex = findTocParagraphRegionEnd(blocks, index);
|
|
7411
|
+
const region = blocks.slice(index, endIndex + 1).filter(
|
|
7412
|
+
(candidate): candidate is ParagraphNode => candidate.type === "paragraph",
|
|
7413
|
+
);
|
|
7414
|
+
const replacement = buildTocRegionParagraphs(
|
|
7415
|
+
region,
|
|
7416
|
+
tocField,
|
|
7417
|
+
buildEntriesForField(tocField),
|
|
7418
|
+
resolveDisplayPageNumber,
|
|
7419
|
+
);
|
|
7420
|
+
if (tocRegionSignature(region) !== tocRegionSignature(replacement)) {
|
|
7421
|
+
changed = true;
|
|
7422
|
+
}
|
|
7423
|
+
nextBlocks.push(...replacement);
|
|
7424
|
+
index = endIndex;
|
|
7425
|
+
continue;
|
|
7426
|
+
}
|
|
7427
|
+
nextBlocks.push(block);
|
|
7428
|
+
continue;
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
7432
|
+
const refreshed = refreshTocBlockRanges(
|
|
7433
|
+
block.children,
|
|
7434
|
+
buildEntriesForField,
|
|
7435
|
+
resolveDisplayPageNumber,
|
|
7436
|
+
selectedRegion,
|
|
7437
|
+
);
|
|
7438
|
+
if (refreshed.changed) {
|
|
7439
|
+
changed = true;
|
|
7440
|
+
nextBlocks.push({ ...block, children: refreshed.blocks });
|
|
7441
|
+
} else {
|
|
7442
|
+
nextBlocks.push(block);
|
|
7443
|
+
}
|
|
7444
|
+
continue;
|
|
7445
|
+
}
|
|
7446
|
+
|
|
7447
|
+
if (block.type === "table") {
|
|
7448
|
+
let tableChanged = false;
|
|
7449
|
+
const rows = block.rows.map((row) => {
|
|
7450
|
+
let rowChanged = false;
|
|
7451
|
+
const cells = row.cells.map((cell) => {
|
|
7452
|
+
const refreshed = refreshTocBlockRanges(
|
|
7453
|
+
cell.children,
|
|
7454
|
+
buildEntriesForField,
|
|
7455
|
+
resolveDisplayPageNumber,
|
|
7456
|
+
selectedRegion,
|
|
7457
|
+
);
|
|
7458
|
+
if (!refreshed.changed) {
|
|
7459
|
+
return cell;
|
|
7460
|
+
}
|
|
7461
|
+
tableChanged = true;
|
|
7462
|
+
rowChanged = true;
|
|
7463
|
+
return { ...cell, children: refreshed.blocks };
|
|
7464
|
+
});
|
|
7465
|
+
return rowChanged ? { ...row, cells } : row;
|
|
7466
|
+
});
|
|
7467
|
+
nextBlocks.push(tableChanged ? { ...block, rows } : block);
|
|
7468
|
+
changed = changed || tableChanged;
|
|
7469
|
+
continue;
|
|
7470
|
+
}
|
|
7471
|
+
|
|
7472
|
+
nextBlocks.push(block);
|
|
7473
|
+
}
|
|
7474
|
+
|
|
7475
|
+
return { blocks: nextBlocks, changed };
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
function startsTocParagraphRegion(blocks: readonly BlockNode[], index: number): boolean {
|
|
7479
|
+
const current = blocks[index];
|
|
7480
|
+
if (current?.type === "paragraph" && isTocParagraphStyle(current.styleId)) {
|
|
7481
|
+
return true;
|
|
7482
|
+
}
|
|
7483
|
+
const next = blocks[index + 1];
|
|
7484
|
+
return next?.type === "paragraph" && isTocParagraphStyle(next.styleId);
|
|
7485
|
+
}
|
|
7486
|
+
|
|
7487
|
+
function findTocParagraphRegionEnd(blocks: readonly BlockNode[], startIndex: number): number {
|
|
7488
|
+
let endIndex = startIndex;
|
|
7489
|
+
while (endIndex + 1 < blocks.length) {
|
|
7490
|
+
const next = blocks[endIndex + 1];
|
|
7491
|
+
if (next?.type !== "paragraph" || !isTocParagraphStyle(next.styleId)) {
|
|
7492
|
+
break;
|
|
7493
|
+
}
|
|
7494
|
+
endIndex += 1;
|
|
7495
|
+
}
|
|
7496
|
+
return endIndex;
|
|
7497
|
+
}
|
|
7498
|
+
|
|
7499
|
+
function isTocParagraphStyle(styleId: string | undefined): boolean {
|
|
7500
|
+
return /^TOC\d+$/u.test(styleId ?? "");
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
function findTocFieldNode(paragraph: ParagraphNode): FieldNode | undefined {
|
|
7504
|
+
return paragraph.children.find(
|
|
7505
|
+
(child): child is FieldNode => child.type === "field" && child.fieldFamily === "TOC",
|
|
7506
|
+
);
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7509
|
+
function buildTocRegionParagraphs(
|
|
7510
|
+
existingRegion: readonly ParagraphNode[],
|
|
7511
|
+
tocField: FieldNode,
|
|
7512
|
+
entries: readonly RuntimeTocEntry[],
|
|
7513
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7514
|
+
): ParagraphNode[] {
|
|
7515
|
+
const firstTemplate = existingRegion[0];
|
|
7516
|
+
if (!firstTemplate) {
|
|
7517
|
+
return [];
|
|
7518
|
+
}
|
|
7519
|
+
const templateByStyle = new Map<string, ParagraphNode>();
|
|
7520
|
+
for (const paragraph of existingRegion) {
|
|
7521
|
+
if (paragraph.styleId && !templateByStyle.has(paragraph.styleId)) {
|
|
7522
|
+
templateByStyle.set(paragraph.styleId, paragraph);
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
const refreshedField: FieldNode = {
|
|
7526
|
+
...tocField,
|
|
7527
|
+
children: [],
|
|
7528
|
+
refreshStatus: "current",
|
|
7529
|
+
};
|
|
7530
|
+
|
|
7531
|
+
if (entries.length === 0) {
|
|
7532
|
+
return [{ ...firstTemplate, children: [refreshedField] }];
|
|
7533
|
+
}
|
|
7534
|
+
|
|
7535
|
+
return entries.map((entry, index) => {
|
|
7536
|
+
const styleId = `TOC${Math.max(1, Math.min(9, entry.level))}`;
|
|
7537
|
+
const template = templateByStyle.get(styleId) ?? firstTemplate;
|
|
7538
|
+
const children = buildTocEntryInlineNodes(entry, resolveDisplayPageNumber);
|
|
7539
|
+
return {
|
|
7540
|
+
...template,
|
|
7541
|
+
styleId,
|
|
7542
|
+
children: index === 0 ? [refreshedField, ...children] : children,
|
|
7543
|
+
};
|
|
7544
|
+
});
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
function buildTocEntryInlineNodes(
|
|
7548
|
+
entry: RuntimeTocEntry,
|
|
7549
|
+
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
7550
|
+
): InlineNode[] {
|
|
7551
|
+
const children: InlineNode[] = [];
|
|
7552
|
+
if (entry.bookmarkName) {
|
|
7553
|
+
children.push({
|
|
7554
|
+
type: "hyperlink",
|
|
7555
|
+
href: `#${entry.bookmarkName}`,
|
|
7556
|
+
children: [{ type: "text", text: entry.text }],
|
|
7557
|
+
});
|
|
7558
|
+
} else {
|
|
7559
|
+
children.push({ type: "text", text: entry.text });
|
|
7560
|
+
}
|
|
7561
|
+
children.push({ type: "tab" });
|
|
7562
|
+
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
7563
|
+
children.push({
|
|
7564
|
+
type: "text",
|
|
7565
|
+
text: String(displayed ?? entry.pageIndex + 1),
|
|
7566
|
+
});
|
|
7567
|
+
return children;
|
|
7568
|
+
}
|
|
7569
|
+
|
|
7570
|
+
function tocRegionSignature(paragraphs: readonly ParagraphNode[]): string {
|
|
7571
|
+
return paragraphs
|
|
7572
|
+
.map((paragraph) => `${paragraph.styleId ?? ""}\u0001${tocInlineSignature(paragraph.children)}`)
|
|
7573
|
+
.join("\u0002");
|
|
7574
|
+
}
|
|
7575
|
+
|
|
7576
|
+
function tocInlineSignature(children: readonly InlineNode[]): string {
|
|
7577
|
+
return children
|
|
7578
|
+
.map((child) => {
|
|
7579
|
+
switch (child.type) {
|
|
7580
|
+
case "text":
|
|
7581
|
+
return `t:${child.text}`;
|
|
7582
|
+
case "tab":
|
|
7583
|
+
return "tab";
|
|
7584
|
+
case "hard_break":
|
|
7585
|
+
return "br";
|
|
7586
|
+
case "hyperlink":
|
|
7587
|
+
return `link:${child.href}:${tocInlineSignature(child.children)}`;
|
|
7588
|
+
case "field":
|
|
7589
|
+
return `field:${child.fieldFamily ?? ""}:${child.refreshStatus ?? ""}:${child.instruction}:${tocInlineSignature(child.children)}`;
|
|
7590
|
+
default:
|
|
7591
|
+
return child.type;
|
|
7592
|
+
}
|
|
7593
|
+
})
|
|
7594
|
+
.join("\u0001");
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7272
7597
|
function refreshBlocksWithCursor(
|
|
7273
7598
|
blocks: readonly BlockNode[],
|
|
7274
7599
|
visitField: (field: FieldNode, range: { from: number; to: number }) => FieldNode,
|
|
@@ -205,3 +205,52 @@ export function formatPageNumber(n: number, format: string | undefined): string
|
|
|
205
205
|
return String(n);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
+
|
|
209
|
+
export interface ChapterPageNumberFormatOptions {
|
|
210
|
+
format?: string;
|
|
211
|
+
chapterNumber?: string;
|
|
212
|
+
chapterSeparator?: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Format a PAGE/PAGEREF value using section page-numbering settings.
|
|
217
|
+
*
|
|
218
|
+
* `chapStyle` names the heading level/style whose numbering contributes the
|
|
219
|
+
* chapter marker; pagination resolves that marker per page and stores it as
|
|
220
|
+
* `chapterNumber`. When no marker is available, the display degrades to the
|
|
221
|
+
* normal page number instead of inventing a chapter prefix.
|
|
222
|
+
*/
|
|
223
|
+
export function formatPageNumberWithChapter(
|
|
224
|
+
n: number,
|
|
225
|
+
options: ChapterPageNumberFormatOptions | undefined,
|
|
226
|
+
): string {
|
|
227
|
+
const pageNumber = formatPageNumber(n, options?.format);
|
|
228
|
+
const chapterNumber = normalizeChapterNumber(options?.chapterNumber);
|
|
229
|
+
if (!chapterNumber) {
|
|
230
|
+
return pageNumber;
|
|
231
|
+
}
|
|
232
|
+
return `${chapterNumber}${chapterSeparatorToText(options?.chapterSeparator)}${pageNumber}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeChapterNumber(value: string | undefined): string | undefined {
|
|
236
|
+
const trimmed = value?.trim();
|
|
237
|
+
return trimmed && trimmed.length > 0 ? trimmed : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function chapterSeparatorToText(value: string | undefined): string {
|
|
241
|
+
switch (value) {
|
|
242
|
+
case "period":
|
|
243
|
+
return ".";
|
|
244
|
+
case "colon":
|
|
245
|
+
return ":";
|
|
246
|
+
case "emDash":
|
|
247
|
+
return "—";
|
|
248
|
+
case "enDash":
|
|
249
|
+
return "–";
|
|
250
|
+
case "hyphen":
|
|
251
|
+
case undefined:
|
|
252
|
+
return "-";
|
|
253
|
+
default:
|
|
254
|
+
return "-";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
} from "../../../model/canonical-document.ts";
|
|
16
16
|
import { resolvePageFieldDisplayText } from "../../layout/resolve-page-fields.ts";
|
|
17
17
|
import type { RuntimePageGraph, RuntimePageNode } from "../../layout/page-graph.ts";
|
|
18
|
-
import { formatPageNumber } from "./page-number-format.ts";
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* Layer-03 alias for the layout module's `RuntimePageGraph`. Exists so
|
|
@@ -214,18 +213,27 @@ export function createFieldResolver(input: FieldResolverInput): FieldResolver {
|
|
|
214
213
|
// preserve the existing display text unchanged.
|
|
215
214
|
return undefined;
|
|
216
215
|
|
|
217
|
-
case "NOTEREF":
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
216
|
+
case "NOTEREF": {
|
|
217
|
+
if (!entry.fieldTarget) {
|
|
218
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
219
|
+
}
|
|
220
|
+
const bookmark = bookmarkMap.get(entry.fieldTarget);
|
|
221
|
+
if (!bookmark) {
|
|
222
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
223
|
+
}
|
|
224
|
+
const paragraph = findParagraphByIndex(contentRoot, bookmark.paragraphIndex);
|
|
225
|
+
if (!paragraph) {
|
|
226
|
+
return { displayText: "", refreshStatus: "unresolvable" };
|
|
227
|
+
}
|
|
228
|
+
const noteText = resolveNoteReferenceText(paragraph, bookmark.bookmarkId);
|
|
229
|
+
return noteText !== undefined
|
|
230
|
+
? {
|
|
231
|
+
displayText: noteText,
|
|
232
|
+
refreshStatus: "current",
|
|
233
|
+
...(entry.switches?.hyperlink ? { asHyperlink: true as const } : {}),
|
|
234
|
+
}
|
|
235
|
+
: { displayText: "", refreshStatus: "unresolvable" };
|
|
236
|
+
}
|
|
229
237
|
|
|
230
238
|
default:
|
|
231
239
|
return undefined;
|
|
@@ -284,33 +292,7 @@ function resolvePageGraphFieldText(
|
|
|
284
292
|
page: RuntimePageNode,
|
|
285
293
|
graph: RuntimePageGraph,
|
|
286
294
|
): string {
|
|
287
|
-
|
|
288
|
-
case "PAGE":
|
|
289
|
-
return formatPageNumber(
|
|
290
|
-
page.stories.displayPageNumber,
|
|
291
|
-
page.layout?.pageNumbering?.format,
|
|
292
|
-
);
|
|
293
|
-
case "NUMPAGES":
|
|
294
|
-
return String(graph.contentPageCount);
|
|
295
|
-
case "SECTIONPAGES":
|
|
296
|
-
return formatPageNumber(
|
|
297
|
-
countContentPagesInSection(graph, page.sectionIndex),
|
|
298
|
-
page.layout?.pageNumbering?.format,
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function countContentPagesInSection(
|
|
304
|
-
graph: RuntimePageGraph,
|
|
305
|
-
sectionIndex: number,
|
|
306
|
-
): number {
|
|
307
|
-
let count = 0;
|
|
308
|
-
for (const page of graph.pages) {
|
|
309
|
-
if (page.sectionIndex === sectionIndex && !page.isBlankFiller) {
|
|
310
|
-
count += 1;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
return count;
|
|
295
|
+
return resolvePageFieldDisplayText(family, "", { page, graph });
|
|
314
296
|
}
|
|
315
297
|
|
|
316
298
|
/**
|
|
@@ -411,6 +393,45 @@ function resolveRefText(
|
|
|
411
393
|
return result;
|
|
412
394
|
}
|
|
413
395
|
|
|
396
|
+
function findParagraphByIndex(
|
|
397
|
+
root: DocumentContainerNode,
|
|
398
|
+
paragraphIndex: number,
|
|
399
|
+
): ParagraphNode | undefined {
|
|
400
|
+
let index = -1;
|
|
401
|
+
let result: ParagraphNode | undefined;
|
|
402
|
+
walkParagraphs(root, (para) => {
|
|
403
|
+
if (result !== undefined) return;
|
|
404
|
+
index += 1;
|
|
405
|
+
if (index === paragraphIndex) {
|
|
406
|
+
result = para;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function resolveNoteReferenceText(
|
|
413
|
+
paragraph: ParagraphNode,
|
|
414
|
+
bookmarkId: string,
|
|
415
|
+
): string | undefined {
|
|
416
|
+
let inside = false;
|
|
417
|
+
for (const child of paragraph.children) {
|
|
418
|
+
if (child.type === "bookmark_start" && child.bookmarkId === bookmarkId) {
|
|
419
|
+
inside = true;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (child.type === "bookmark_end" && child.bookmarkId === bookmarkId) {
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
if (!inside) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (child.type === "footnote_ref") {
|
|
429
|
+
return child.noteId;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
414
435
|
/**
|
|
415
436
|
* Resolve a STYLEREF field by finding the first paragraph whose `styleId`
|
|
416
437
|
* matches `styleId` and returning its flattened text (trimmed).
|
|
@@ -56,7 +56,10 @@ import {
|
|
|
56
56
|
type RuntimePageGraph,
|
|
57
57
|
type RuntimePageNode,
|
|
58
58
|
} from "./page-graph.ts";
|
|
59
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
projectLineBoxesForPageFragments,
|
|
61
|
+
projectSurfaceBlocksToPageFragments,
|
|
62
|
+
} from "./project-block-fragments.ts";
|
|
60
63
|
import {
|
|
61
64
|
resolvePageStories,
|
|
62
65
|
resolveTotalPageCount,
|
|
@@ -466,6 +469,12 @@ export function createLayoutEngine(
|
|
|
466
469
|
pages,
|
|
467
470
|
pageStack.splits,
|
|
468
471
|
pageStack.columnByBlockIdByPageIndex,
|
|
472
|
+
pageStack.fragmentMeasurementsByPageIndex,
|
|
473
|
+
);
|
|
474
|
+
const lineBoxesByPageIndex = projectLineBoxesForPageFragments(
|
|
475
|
+
pages,
|
|
476
|
+
bodyFragmentsByPageIndex,
|
|
477
|
+
pageStack.fragmentMeasurementsByPageIndex,
|
|
469
478
|
);
|
|
470
479
|
// P8.1b — merge per-note fragments (regionKind: "footnote-area") into the
|
|
471
480
|
// main fragments map so buildPageGraph sees them alongside body fragments.
|
|
@@ -479,6 +488,7 @@ export function createLayoutEngine(
|
|
|
479
488
|
sections,
|
|
480
489
|
stories,
|
|
481
490
|
fragmentsByPageIndex,
|
|
491
|
+
lineBoxesByPageIndex,
|
|
482
492
|
noteAllocationsByPageIndex: pageStack.noteAllocationsByPageIndex,
|
|
483
493
|
});
|
|
484
494
|
|
|
@@ -638,6 +648,12 @@ export function createLayoutEngine(
|
|
|
638
648
|
freshSnapshotsToRebuild,
|
|
639
649
|
freshResult.splits,
|
|
640
650
|
freshResult.columnByBlockIdByPageIndex,
|
|
651
|
+
freshResult.fragmentMeasurementsByPageIndex,
|
|
652
|
+
);
|
|
653
|
+
const freshLineBoxesByPageIndex = projectLineBoxesForPageFragments(
|
|
654
|
+
freshSnapshotsToRebuild,
|
|
655
|
+
freshBodyFragmentsByPageIndex,
|
|
656
|
+
freshResult.fragmentMeasurementsByPageIndex,
|
|
641
657
|
);
|
|
642
658
|
// P8.1b — merge per-note fragments into the fresh fragments map.
|
|
643
659
|
const freshFragmentsByPageIndex = new Map(freshBodyFragmentsByPageIndex);
|
|
@@ -651,6 +667,7 @@ export function createLayoutEngine(
|
|
|
651
667
|
sections,
|
|
652
668
|
stories: freshStories,
|
|
653
669
|
fragmentsByPageIndex: freshFragmentsByPageIndex,
|
|
670
|
+
lineBoxesByPageIndex: freshLineBoxesByPageIndex,
|
|
654
671
|
noteAllocationsByPageIndex: freshResult.noteAllocationsByPageIndex,
|
|
655
672
|
});
|
|
656
673
|
const freshNodes = freshGraph.pages;
|