@beyondwork/docx-react-component 1.0.86 → 1.0.88
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 +580 -40
- 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 +8 -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/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- 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/preserve-position.ts +31 -6
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +92 -50
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +32 -3
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -0
|
@@ -312,6 +312,7 @@ export function parseBookmarkEnd(
|
|
|
312
312
|
|
|
313
313
|
import type {
|
|
314
314
|
CanonicalDocument,
|
|
315
|
+
BlockNode,
|
|
315
316
|
DocumentNode,
|
|
316
317
|
FieldFamily,
|
|
317
318
|
FieldNode,
|
|
@@ -322,13 +323,16 @@ import type {
|
|
|
322
323
|
ParagraphNode,
|
|
323
324
|
SubPartsCatalog,
|
|
324
325
|
SupportedFieldFamily,
|
|
326
|
+
TocCachedEntry,
|
|
325
327
|
TocEntry,
|
|
328
|
+
TocInstructionModel,
|
|
329
|
+
TocRegion,
|
|
326
330
|
TocStructure,
|
|
327
331
|
} from "../../model/canonical-document.ts";
|
|
328
332
|
import { parseFieldSwitches } from "./parse-field-switches.ts";
|
|
329
333
|
|
|
330
334
|
const FIELD_FAMILY_PATTERN =
|
|
331
|
-
/^\s*(REF|PAGEREF|NOTEREF|TOC|PAGE|NUMPAGES|DATE|TIME|AUTHOR|FILENAME|MERGEFIELD|IF|SEQ|INDEX|TC|STYLEREF|SECTIONPAGES)\b/i;
|
|
335
|
+
/^\s*(REF|PAGEREF|NOTEREF|TOC|PAGE|NUMPAGES|DATE|TIME|AUTHOR|FILENAME|MERGEFIELD|IF|SEQ|INDEX|TC|FORMTEXT|FORMCHECKBOX|FORMDROPDOWN|STYLEREF|SECTIONPAGES)\b/i;
|
|
332
336
|
|
|
333
337
|
const SUPPORTED_FAMILIES = new Set<string>([
|
|
334
338
|
"REF",
|
|
@@ -415,9 +419,9 @@ export function buildFieldRegistry(
|
|
|
415
419
|
const root = document.content;
|
|
416
420
|
const supported: FieldRegistryEntry[] = [];
|
|
417
421
|
const preserveOnly: FieldRegistryEntry[] = [];
|
|
422
|
+
const tocFieldEntries: FieldRegistryEntry[] = [];
|
|
418
423
|
let fieldIndex = 0;
|
|
419
424
|
let paragraphIndex = -1;
|
|
420
|
-
let tocInstruction: string | undefined;
|
|
421
425
|
|
|
422
426
|
walkFieldDocument(root, (node, pIdx) => {
|
|
423
427
|
paragraphIndex = pIdx;
|
|
@@ -439,8 +443,8 @@ export function buildFieldRegistry(
|
|
|
439
443
|
};
|
|
440
444
|
if (classification.supported) {
|
|
441
445
|
supported.push(entry);
|
|
442
|
-
if (classification.family === "TOC"
|
|
443
|
-
|
|
446
|
+
if (classification.family === "TOC") {
|
|
447
|
+
tocFieldEntries.push(entry);
|
|
444
448
|
}
|
|
445
449
|
} else {
|
|
446
450
|
preserveOnly.push(entry);
|
|
@@ -477,13 +481,23 @@ export function buildFieldRegistry(
|
|
|
477
481
|
});
|
|
478
482
|
}
|
|
479
483
|
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
484
|
+
const tocRegions = buildTocRegions(document, tocFieldEntries);
|
|
485
|
+
const firstToc = tocRegions[0];
|
|
486
|
+
const tocStructure = firstToc
|
|
487
|
+
? {
|
|
488
|
+
instruction: firstToc.instruction.raw,
|
|
489
|
+
levelRange: firstToc.instruction.outlineRange,
|
|
490
|
+
entries: firstToc.generatedEntries,
|
|
491
|
+
status: firstToc.status,
|
|
492
|
+
}
|
|
493
|
+
: tocFieldEntries[0]
|
|
494
|
+
? buildTocStructure(document, tocFieldEntries[0].instruction)
|
|
495
|
+
: undefined;
|
|
483
496
|
|
|
484
497
|
return {
|
|
485
498
|
supported,
|
|
486
499
|
preserveOnly,
|
|
500
|
+
...(tocRegions.length > 0 ? { tocRegions } : {}),
|
|
487
501
|
...(tocStructure ? { tocStructure } : {}),
|
|
488
502
|
};
|
|
489
503
|
}
|
|
@@ -494,11 +508,120 @@ export function buildFieldRegistry(
|
|
|
494
508
|
* Defaults to 1-9 if no \\o switch is present.
|
|
495
509
|
*/
|
|
496
510
|
export function parseTocLevelRange(instruction: string): { from: number; to: number } {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
511
|
+
return parseTocInstruction(instruction).outlineRange;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function parseTocInstruction(instruction: string): TocInstructionModel {
|
|
515
|
+
const model: TocInstructionModel = {
|
|
516
|
+
raw: instruction,
|
|
517
|
+
outlineRange: { from: 1, to: 9 },
|
|
518
|
+
};
|
|
519
|
+
const unsupportedSwitches: string[] = [];
|
|
520
|
+
|
|
521
|
+
for (const sw of readFieldSwitches(instruction)) {
|
|
522
|
+
const code = sw.code.toLowerCase();
|
|
523
|
+
switch (code) {
|
|
524
|
+
case "o": {
|
|
525
|
+
const range = parseLevelRangeValue(sw.value);
|
|
526
|
+
if (range) {
|
|
527
|
+
model.outlineRange = range;
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "h":
|
|
532
|
+
model.hyperlink = true;
|
|
533
|
+
break;
|
|
534
|
+
case "z":
|
|
535
|
+
model.hidePageNumbersInWeb = true;
|
|
536
|
+
break;
|
|
537
|
+
case "u":
|
|
538
|
+
model.useOutlineLevels = true;
|
|
539
|
+
break;
|
|
540
|
+
case "t": {
|
|
541
|
+
const styleMap = parseTocStyleMap(sw.value);
|
|
542
|
+
if (styleMap && styleMap.length > 0) {
|
|
543
|
+
model.styleMap = styleMap;
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "b":
|
|
548
|
+
if (sw.value) model.bookmarkName = sw.value;
|
|
549
|
+
break;
|
|
550
|
+
case "c":
|
|
551
|
+
if (sw.value) model.sequenceIdentifier = sw.value;
|
|
552
|
+
break;
|
|
553
|
+
case "f":
|
|
554
|
+
if (sw.value) model.tcIdentifier = sw.value;
|
|
555
|
+
break;
|
|
556
|
+
case "p":
|
|
557
|
+
if (sw.value) model.entryPageSeparator = sw.value;
|
|
558
|
+
break;
|
|
559
|
+
case "d":
|
|
560
|
+
if (sw.value) model.sequenceSeparator = sw.value;
|
|
561
|
+
break;
|
|
562
|
+
case "n": {
|
|
563
|
+
const range = parseLevelRangeValue(sw.value);
|
|
564
|
+
model.omitPageNumbers = range ?? true;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
unsupportedSwitches.push(sw.code);
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (unsupportedSwitches.length > 0) {
|
|
574
|
+
model.unsupportedSwitches = unsupportedSwitches;
|
|
575
|
+
}
|
|
576
|
+
return model;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function readFieldSwitches(instruction: string): Array<{ code: string; value?: string }> {
|
|
580
|
+
const switches: Array<{ code: string; value?: string }> = [];
|
|
581
|
+
const re = /\\([A-Za-z])(?:\s+(?:"([^"]*)"|([^\\\s]+)))?/gu;
|
|
582
|
+
let match: RegExpExecArray | null;
|
|
583
|
+
while ((match = re.exec(instruction)) !== null) {
|
|
584
|
+
const code = match[1] ?? "";
|
|
585
|
+
const rawValue = match[2] !== undefined ? match[2] : match[3]?.trim();
|
|
586
|
+
switches.push({
|
|
587
|
+
code,
|
|
588
|
+
...(rawValue !== undefined ? { value: rawValue } : {}),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return switches;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function parseLevelRangeValue(value: string | undefined): { from: number; to: number } | undefined {
|
|
595
|
+
if (!value) return undefined;
|
|
596
|
+
const match = /^\s*(\d+)\s*-\s*(\d+)\s*$/u.exec(value);
|
|
597
|
+
if (!match) return undefined;
|
|
598
|
+
const from = Number.parseInt(match[1]!, 10);
|
|
599
|
+
const to = Number.parseInt(match[2]!, 10);
|
|
600
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) return undefined;
|
|
601
|
+
return {
|
|
602
|
+
from: Math.max(1, Math.min(9, from)),
|
|
603
|
+
to: Math.max(1, Math.min(9, to)),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function parseTocStyleMap(
|
|
608
|
+
value: string | undefined,
|
|
609
|
+
): TocInstructionModel["styleMap"] {
|
|
610
|
+
if (!value) return undefined;
|
|
611
|
+
const tokens = value
|
|
612
|
+
.split(",")
|
|
613
|
+
.map((token) => token.trim())
|
|
614
|
+
.filter((token) => token.length > 0);
|
|
615
|
+
const entries: NonNullable<TocInstructionModel["styleMap"]> = [];
|
|
616
|
+
for (let index = 0; index + 1 < tokens.length; index += 2) {
|
|
617
|
+
const styleName = tokens[index]!;
|
|
618
|
+
const level = Number.parseInt(tokens[index + 1]!, 10);
|
|
619
|
+
if (!Number.isInteger(level) || level < 1 || level > 9) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
entries.push({ styleName, level });
|
|
500
623
|
}
|
|
501
|
-
return
|
|
624
|
+
return entries.length > 0 ? entries : undefined;
|
|
502
625
|
}
|
|
503
626
|
|
|
504
627
|
/**
|
|
@@ -562,6 +685,281 @@ export function buildTocStructure(
|
|
|
562
685
|
};
|
|
563
686
|
}
|
|
564
687
|
|
|
688
|
+
function buildTocRegions(
|
|
689
|
+
document: Pick<CanonicalDocument, "content" | "styles">,
|
|
690
|
+
tocFields: readonly FieldRegistryEntry[],
|
|
691
|
+
): TocRegion[] {
|
|
692
|
+
if (tocFields.length === 0) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const paragraphIndexByNode = new WeakMap<ParagraphNode, number>();
|
|
697
|
+
walkFieldDocument(document.content, (node, pIdx) => {
|
|
698
|
+
if (node.type === "paragraph") {
|
|
699
|
+
paragraphIndexByNode.set(node as ParagraphNode, pIdx);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const tocFieldByParagraph = new Map<number, FieldRegistryEntry>();
|
|
704
|
+
for (const entry of tocFields) {
|
|
705
|
+
if (!tocFieldByParagraph.has(entry.paragraphIndex)) {
|
|
706
|
+
tocFieldByParagraph.set(entry.paragraphIndex, entry);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const regions: TocRegion[] = [];
|
|
711
|
+
collectTocRegionsFromBlocks(
|
|
712
|
+
document,
|
|
713
|
+
document.content.children,
|
|
714
|
+
paragraphIndexByNode,
|
|
715
|
+
tocFieldByParagraph,
|
|
716
|
+
regions,
|
|
717
|
+
"body",
|
|
718
|
+
"body",
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const seenFieldIndexes = new Set(regions.map((region) => region.sourceFieldIndex));
|
|
722
|
+
for (const entry of tocFields) {
|
|
723
|
+
if (seenFieldIndexes.has(entry.fieldIndex)) continue;
|
|
724
|
+
const generated = buildTocStructure(document, entry.instruction).entries;
|
|
725
|
+
regions.push({
|
|
726
|
+
tocId: `toc-${regions.length + 1}`,
|
|
727
|
+
sourceFieldIndex: entry.fieldIndex,
|
|
728
|
+
instruction: parseTocInstruction(entry.instruction),
|
|
729
|
+
resultRange: {
|
|
730
|
+
fromParagraphIndex: entry.paragraphIndex,
|
|
731
|
+
toParagraphIndex: entry.paragraphIndex,
|
|
732
|
+
},
|
|
733
|
+
parentKind: "body",
|
|
734
|
+
sourcePath: `field:${entry.fieldIndex}`,
|
|
735
|
+
cachedEntries: [],
|
|
736
|
+
generatedEntries: generated,
|
|
737
|
+
status: "stale",
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return regions;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function collectTocRegionsFromBlocks(
|
|
745
|
+
document: Pick<CanonicalDocument, "content" | "styles">,
|
|
746
|
+
blocks: readonly BlockNode[],
|
|
747
|
+
paragraphIndexByNode: WeakMap<ParagraphNode, number>,
|
|
748
|
+
tocFieldByParagraph: ReadonlyMap<number, FieldRegistryEntry>,
|
|
749
|
+
regions: TocRegion[],
|
|
750
|
+
parentKind: TocRegion["parentKind"],
|
|
751
|
+
sourcePath: string,
|
|
752
|
+
): void {
|
|
753
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
754
|
+
const block = blocks[index];
|
|
755
|
+
|
|
756
|
+
if (block.type === "paragraph") {
|
|
757
|
+
const paragraphIndex = paragraphIndexByNode.get(block);
|
|
758
|
+
const tocField = paragraphIndex === undefined
|
|
759
|
+
? undefined
|
|
760
|
+
: tocFieldByParagraph.get(paragraphIndex);
|
|
761
|
+
if (tocField && startsTocParagraphRegion(blocks, index)) {
|
|
762
|
+
const endIndex = findTocParagraphRegionEnd(blocks, index);
|
|
763
|
+
const paragraphs = blocks
|
|
764
|
+
.slice(index, endIndex + 1)
|
|
765
|
+
.filter((candidate): candidate is ParagraphNode => candidate.type === "paragraph");
|
|
766
|
+
const cachedEntries = paragraphs
|
|
767
|
+
.map((paragraph) => extractCachedTocEntry(paragraph, paragraphIndexByNode))
|
|
768
|
+
.filter((entry): entry is TocCachedEntry => Boolean(entry));
|
|
769
|
+
const generatedEntries = buildTocStructure(document, tocField.instruction).entries;
|
|
770
|
+
const firstParagraph = paragraphs[0];
|
|
771
|
+
const lastParagraph = paragraphs[paragraphs.length - 1];
|
|
772
|
+
const fromParagraphIndex = firstParagraph
|
|
773
|
+
? paragraphIndexByNode.get(firstParagraph) ?? tocField.paragraphIndex
|
|
774
|
+
: tocField.paragraphIndex;
|
|
775
|
+
const toParagraphIndex = lastParagraph
|
|
776
|
+
? paragraphIndexByNode.get(lastParagraph) ?? fromParagraphIndex
|
|
777
|
+
: fromParagraphIndex;
|
|
778
|
+
regions.push({
|
|
779
|
+
tocId: `toc-${regions.length + 1}`,
|
|
780
|
+
sourceFieldIndex: tocField.fieldIndex,
|
|
781
|
+
instruction: parseTocInstruction(tocField.instruction),
|
|
782
|
+
resultRange: { fromParagraphIndex, toParagraphIndex },
|
|
783
|
+
parentKind,
|
|
784
|
+
sourcePath: `${sourcePath}/${index}`,
|
|
785
|
+
cachedEntries,
|
|
786
|
+
generatedEntries,
|
|
787
|
+
status: tocEntriesMatch(cachedEntries, generatedEntries) ? "current" : "stale",
|
|
788
|
+
});
|
|
789
|
+
index = endIndex;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (block.type === "sdt") {
|
|
795
|
+
collectTocRegionsFromBlocks(
|
|
796
|
+
document,
|
|
797
|
+
block.children,
|
|
798
|
+
paragraphIndexByNode,
|
|
799
|
+
tocFieldByParagraph,
|
|
800
|
+
regions,
|
|
801
|
+
"sdt",
|
|
802
|
+
`${sourcePath}/sdt[${index}]`,
|
|
803
|
+
);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (block.type === "custom_xml") {
|
|
808
|
+
collectTocRegionsFromBlocks(
|
|
809
|
+
document,
|
|
810
|
+
block.children,
|
|
811
|
+
paragraphIndexByNode,
|
|
812
|
+
tocFieldByParagraph,
|
|
813
|
+
regions,
|
|
814
|
+
"custom_xml",
|
|
815
|
+
`${sourcePath}/customXml[${index}]`,
|
|
816
|
+
);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (block.type === "table") {
|
|
821
|
+
block.rows.forEach((row, rowIndex) => {
|
|
822
|
+
row.cells.forEach((cell, cellIndex) => {
|
|
823
|
+
collectTocRegionsFromBlocks(
|
|
824
|
+
document,
|
|
825
|
+
cell.children,
|
|
826
|
+
paragraphIndexByNode,
|
|
827
|
+
tocFieldByParagraph,
|
|
828
|
+
regions,
|
|
829
|
+
"table_cell",
|
|
830
|
+
`${sourcePath}/table[${index}]/row[${rowIndex}]/cell[${cellIndex}]`,
|
|
831
|
+
);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function startsTocParagraphRegion(blocks: readonly BlockNode[], index: number): boolean {
|
|
839
|
+
const current = blocks[index];
|
|
840
|
+
if (current?.type === "paragraph" && isTocParagraphStyle(current.styleId)) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
const next = blocks[index + 1];
|
|
844
|
+
return next?.type === "paragraph" && isTocParagraphStyle(next.styleId);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function findTocParagraphRegionEnd(blocks: readonly BlockNode[], startIndex: number): number {
|
|
848
|
+
let endIndex = startIndex;
|
|
849
|
+
while (endIndex + 1 < blocks.length) {
|
|
850
|
+
const next = blocks[endIndex + 1];
|
|
851
|
+
if (next?.type !== "paragraph" || !isTocParagraphStyle(next.styleId)) {
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
endIndex += 1;
|
|
855
|
+
}
|
|
856
|
+
return endIndex;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function isTocParagraphStyle(styleId: string | undefined): boolean {
|
|
860
|
+
return /^TOC\d+$/u.test(styleId ?? "");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function tocLevelFromStyle(styleId: string | undefined): number {
|
|
864
|
+
const match = /^TOC(\d+)$/u.exec(styleId ?? "");
|
|
865
|
+
if (!match) return 1;
|
|
866
|
+
return Math.max(1, Math.min(9, Number.parseInt(match[1]!, 10)));
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function extractCachedTocEntry(
|
|
870
|
+
paragraph: ParagraphNode,
|
|
871
|
+
paragraphIndexByNode: WeakMap<ParagraphNode, number>,
|
|
872
|
+
): TocCachedEntry | undefined {
|
|
873
|
+
if (!isTocParagraphStyle(paragraph.styleId)) {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
const parts = flattenTocResultParts(paragraph.children);
|
|
877
|
+
const displayText = parts.map((part) => part.kind === "tab" ? "\t" : part.text).join("");
|
|
878
|
+
const trimmedDisplay = displayText.trim();
|
|
879
|
+
if (trimmedDisplay.length === 0) {
|
|
880
|
+
return undefined;
|
|
881
|
+
}
|
|
882
|
+
const lastTabIndex = parts.map((part) => part.kind).lastIndexOf("tab");
|
|
883
|
+
const titleParts = lastTabIndex >= 0 ? parts.slice(0, lastTabIndex) : parts;
|
|
884
|
+
const pageParts = lastTabIndex >= 0 ? parts.slice(lastTabIndex + 1) : [];
|
|
885
|
+
const text = titleParts
|
|
886
|
+
.filter((part): part is { kind: "text"; text: string; bookmarkName?: string } => part.kind === "text")
|
|
887
|
+
.map((part) => part.text)
|
|
888
|
+
.join("")
|
|
889
|
+
.trim();
|
|
890
|
+
const pageText = pageParts
|
|
891
|
+
.filter((part): part is { kind: "text"; text: string; bookmarkName?: string } => part.kind === "text")
|
|
892
|
+
.map((part) => part.text)
|
|
893
|
+
.join("")
|
|
894
|
+
.trim();
|
|
895
|
+
const bookmarkName = parts.find(
|
|
896
|
+
(part): part is { kind: "text"; text: string; bookmarkName: string } =>
|
|
897
|
+
part.kind === "text" && Boolean(part.bookmarkName),
|
|
898
|
+
)?.bookmarkName;
|
|
899
|
+
return {
|
|
900
|
+
text: text || trimmedDisplay,
|
|
901
|
+
level: tocLevelFromStyle(paragraph.styleId),
|
|
902
|
+
paragraphIndex: paragraphIndexByNode.get(paragraph) ?? -1,
|
|
903
|
+
...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
|
|
904
|
+
...(bookmarkName ? { bookmarkName } : {}),
|
|
905
|
+
...(pageText ? { pageText } : {}),
|
|
906
|
+
displayText,
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function flattenTocResultParts(
|
|
911
|
+
children: readonly InlineNode[],
|
|
912
|
+
): Array<{ kind: "text"; text: string; bookmarkName?: string } | { kind: "tab" }> {
|
|
913
|
+
const parts: Array<{ kind: "text"; text: string; bookmarkName?: string } | { kind: "tab" }> = [];
|
|
914
|
+
for (const child of children) {
|
|
915
|
+
switch (child.type) {
|
|
916
|
+
case "text":
|
|
917
|
+
parts.push({ kind: "text", text: child.text });
|
|
918
|
+
break;
|
|
919
|
+
case "tab":
|
|
920
|
+
parts.push({ kind: "tab" });
|
|
921
|
+
break;
|
|
922
|
+
case "hard_break":
|
|
923
|
+
parts.push({ kind: "text", text: "\n" });
|
|
924
|
+
break;
|
|
925
|
+
case "hyperlink": {
|
|
926
|
+
const bookmarkName = child.href?.startsWith("#") ? child.href.slice(1) : undefined;
|
|
927
|
+
const nested = flattenTocResultParts(child.children);
|
|
928
|
+
for (const part of nested) {
|
|
929
|
+
parts.push(part.kind === "text" && bookmarkName ? { ...part, bookmarkName } : part);
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case "field":
|
|
934
|
+
if (child.fieldFamily !== "TOC") {
|
|
935
|
+
parts.push(...flattenTocResultParts(child.children));
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
default:
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return parts;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function tocEntriesMatch(
|
|
946
|
+
cachedEntries: readonly TocCachedEntry[],
|
|
947
|
+
generatedEntries: readonly TocEntry[],
|
|
948
|
+
): boolean {
|
|
949
|
+
if (cachedEntries.length !== generatedEntries.length) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
return cachedEntries.every((entry, index) => {
|
|
953
|
+
const generated = generatedEntries[index];
|
|
954
|
+
if (!generated) return false;
|
|
955
|
+
return entry.level === generated.level && normalizeTocText(entry.text) === normalizeTocText(generated.text);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function normalizeTocText(text: string): string {
|
|
960
|
+
return text.replace(/\s+/gu, " ").trim();
|
|
961
|
+
}
|
|
962
|
+
|
|
565
963
|
/**
|
|
566
964
|
* Deterministic refresh helper for REF fields.
|
|
567
965
|
* Given a bookmark name map and the document content, resolves the display
|
|
@@ -627,6 +1025,7 @@ export function refreshFieldRegistry(
|
|
|
627
1025
|
return {
|
|
628
1026
|
supported: refreshed,
|
|
629
1027
|
preserveOnly: registry.preserveOnly,
|
|
1028
|
+
...(registry.tocRegions ? { tocRegions: registry.tocRegions } : {}),
|
|
630
1029
|
...(registry.tocStructure ? { tocStructure: registry.tocStructure } : {}),
|
|
631
1030
|
};
|
|
632
1031
|
}
|
|
@@ -838,4 +1237,3 @@ function findFirstChildEl(node: XmlElementNode, childLocalName: string): XmlElem
|
|
|
838
1237
|
(c): c is XmlElementNode => c.type === "element" && localName(c.name) === childLocalName,
|
|
839
1238
|
);
|
|
840
1239
|
}
|
|
841
|
-
|