@beyondwork/docx-react-component 1.0.60 → 1.0.62
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 +33 -44
- package/src/api/public-types.ts +41 -0
- package/src/io/docx-session.ts +167 -8
- package/src/io/export/serialize-footnotes.ts +36 -5
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +25 -18
- package/src/io/export/serialize-paragraph-formatting.ts +6 -0
- package/src/io/export/serialize-settings.ts +130 -3
- package/src/io/normalize/normalize-text.ts +8 -4
- package/src/io/ooxml/classify-embedding.ts +193 -0
- package/src/io/ooxml/parse-footnotes.ts +11 -0
- package/src/io/ooxml/parse-headers-footers.ts +117 -42
- package/src/io/ooxml/parse-main-document.ts +20 -8
- package/src/io/ooxml/parse-object.ts +23 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
- package/src/io/ooxml/parse-settings.ts +91 -1
- package/src/model/canonical-document.ts +36 -2
- package/src/runtime/document-runtime.ts +424 -0
- package/src/runtime/footnote-resolver.ts +32 -8
- package/src/runtime/layout/layout-engine-version.ts +7 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
- package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
- package/src/runtime/layout/paginated-layout-engine.ts +41 -8
- package/src/runtime/layout/resolved-formatting-document.ts +11 -9
- package/src/runtime/layout/resolved-formatting-state.ts +4 -0
- package/src/runtime/numbering-prefix.ts +26 -2
- package/src/runtime/surface-projection.ts +75 -14
- package/src/runtime/table-schema.ts +26 -0
- package/src/ui/WordReviewEditor.tsx +25 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
AddCommentResult,
|
|
24
24
|
AddScopeParams,
|
|
25
25
|
AddScopeResult,
|
|
26
|
+
ClearHighlightOptions,
|
|
26
27
|
CommentSidebarSnapshot,
|
|
27
28
|
CommentSidebarThreadSnapshot,
|
|
28
29
|
CompatibilityReport,
|
|
@@ -159,6 +160,7 @@ import {
|
|
|
159
160
|
insertScopeMarkers,
|
|
160
161
|
removeScopeMarkers,
|
|
161
162
|
} from "../core/commands/add-scope.ts";
|
|
163
|
+
import { applyFormattingOperationToDocument } from "../core/commands/formatting-commands.ts";
|
|
162
164
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
163
165
|
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
164
166
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
@@ -481,6 +483,22 @@ export interface DocumentRuntime {
|
|
|
481
483
|
rejectChange(changeId: string): void;
|
|
482
484
|
acceptAllChanges(): void;
|
|
483
485
|
rejectAllChanges(): void;
|
|
486
|
+
/**
|
|
487
|
+
* Clears the highlight (background color) on a range in the currently
|
|
488
|
+
* active story. The caller's selection is NOT moved.
|
|
489
|
+
*
|
|
490
|
+
* When `options.range` is omitted, the current document selection is used.
|
|
491
|
+
*
|
|
492
|
+
* Tracked-changes / suggesting mode is honored: in suggesting mode the
|
|
493
|
+
* clear is recorded as an `rPrChange` property-change suggestion when the
|
|
494
|
+
* resolved range is a single bounded text segment, and is reported via
|
|
495
|
+
* `command_blocked` when the range spans multiple segments.
|
|
496
|
+
*
|
|
497
|
+
* When `options.expandToFullHighlight` is `true`, the resolved range is
|
|
498
|
+
* grown outward before clearing to cover the entire contiguous highlighted
|
|
499
|
+
* span it touches. Expansion stops at paragraph and table-cell boundaries.
|
|
500
|
+
*/
|
|
501
|
+
clearHighlight(options?: ClearHighlightOptions): void;
|
|
484
502
|
openStory(target: EditorStoryTarget): boolean;
|
|
485
503
|
closeStory(): void;
|
|
486
504
|
getActiveStory(): EditorStoryTarget;
|
|
@@ -3705,6 +3723,143 @@ export function createDocumentRuntime(
|
|
|
3705
3723
|
origin: createOrigin("api", clock()),
|
|
3706
3724
|
});
|
|
3707
3725
|
},
|
|
3726
|
+
clearHighlight(options) {
|
|
3727
|
+
const resolvedRange = resolveClearHighlightRange(
|
|
3728
|
+
options?.range,
|
|
3729
|
+
state.selection,
|
|
3730
|
+
);
|
|
3731
|
+
if (!resolvedRange) {
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (viewState.documentMode === "viewing") {
|
|
3735
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3736
|
+
code: "document_viewing_mode",
|
|
3737
|
+
message: "Cannot clear highlight in viewing mode.",
|
|
3738
|
+
}]);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
const surfaceBlocks = getActiveStorySurfaceBlocks();
|
|
3743
|
+
if (!surfaceBlocks) {
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
const inputFrom = Math.min(resolvedRange.from, resolvedRange.to);
|
|
3748
|
+
const inputTo = Math.max(resolvedRange.from, resolvedRange.to);
|
|
3749
|
+
let targetFrom = inputFrom;
|
|
3750
|
+
let targetTo = inputTo;
|
|
3751
|
+
if (options?.expandToFullHighlight === true) {
|
|
3752
|
+
const expanded = expandRangeToHighlightExtent(
|
|
3753
|
+
surfaceBlocks,
|
|
3754
|
+
inputFrom,
|
|
3755
|
+
inputTo,
|
|
3756
|
+
);
|
|
3757
|
+
targetFrom = expanded.from;
|
|
3758
|
+
targetTo = expanded.to;
|
|
3759
|
+
}
|
|
3760
|
+
if (targetFrom === targetTo) {
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
const activeStoryDocument =
|
|
3765
|
+
activeStory.kind === "main"
|
|
3766
|
+
? state.document
|
|
3767
|
+
: {
|
|
3768
|
+
...state.document,
|
|
3769
|
+
content: {
|
|
3770
|
+
type: "doc" as const,
|
|
3771
|
+
children: [...getStoryBlocks(state.document, activeStory)],
|
|
3772
|
+
},
|
|
3773
|
+
};
|
|
3774
|
+
const syntheticSnapshot: RuntimeRenderSnapshot = {
|
|
3775
|
+
...cachedRenderSnapshot,
|
|
3776
|
+
...(activeStory.kind === "main" ? {} : { activeStory: MAIN_STORY_TARGET }),
|
|
3777
|
+
selection: {
|
|
3778
|
+
anchor: targetFrom,
|
|
3779
|
+
head: targetTo,
|
|
3780
|
+
isCollapsed: false,
|
|
3781
|
+
activeRange: {
|
|
3782
|
+
kind: "range",
|
|
3783
|
+
from: targetFrom,
|
|
3784
|
+
to: targetTo,
|
|
3785
|
+
assoc: { start: -1, end: 1 },
|
|
3786
|
+
},
|
|
3787
|
+
},
|
|
3788
|
+
};
|
|
3789
|
+
|
|
3790
|
+
const suggesting =
|
|
3791
|
+
getEffectiveDocumentMode(state.selection) === "suggesting";
|
|
3792
|
+
if (suggesting) {
|
|
3793
|
+
if (activeStory.kind !== "main") {
|
|
3794
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3795
|
+
code: "suggesting_unsupported",
|
|
3796
|
+
message: `"clearHighlight" is not supported in suggesting mode for this story.`,
|
|
3797
|
+
}]);
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const segment = findSingleSelectedTextSegment(syntheticSnapshot);
|
|
3801
|
+
if (!segment) {
|
|
3802
|
+
this.emitBlockedCommand("clearHighlight", [{
|
|
3803
|
+
code: "suggesting_unsupported",
|
|
3804
|
+
message: `"clearHighlight" requires one bounded text segment in suggesting mode.`,
|
|
3805
|
+
}]);
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
const beforeXml = buildRunPropertyBeforeXml(segment);
|
|
3809
|
+
const mutation = applyFormattingOperationToDocument(
|
|
3810
|
+
activeStoryDocument,
|
|
3811
|
+
syntheticSnapshot,
|
|
3812
|
+
{ type: "set-highlight-color", color: null },
|
|
3813
|
+
);
|
|
3814
|
+
if (!mutation.changed) {
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
const timestamp = clock();
|
|
3818
|
+
const nextDocument = appendPropertyChangeSuggestion(
|
|
3819
|
+
mutation.document,
|
|
3820
|
+
{ from: segment.from, to: segment.to },
|
|
3821
|
+
{
|
|
3822
|
+
originalRevisionType: "rPrChange",
|
|
3823
|
+
xmlTag: "rPrChange",
|
|
3824
|
+
beforeXml,
|
|
3825
|
+
semanticKind: "formatting-change",
|
|
3826
|
+
storyTarget: activeStory,
|
|
3827
|
+
authorId: defaultAuthorId ?? undefined,
|
|
3828
|
+
},
|
|
3829
|
+
timestamp,
|
|
3830
|
+
);
|
|
3831
|
+
this.dispatch({
|
|
3832
|
+
type: "document.replace",
|
|
3833
|
+
document: nextDocument,
|
|
3834
|
+
mapping: createEmptyMapping(),
|
|
3835
|
+
origin: createOrigin("api", timestamp),
|
|
3836
|
+
});
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
const result = applyFormattingOperationToDocument(
|
|
3841
|
+
activeStoryDocument,
|
|
3842
|
+
syntheticSnapshot,
|
|
3843
|
+
{ type: "set-highlight-color", color: null },
|
|
3844
|
+
);
|
|
3845
|
+
if (!result.changed) {
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const nextDocument =
|
|
3849
|
+
activeStory.kind === "main"
|
|
3850
|
+
? result.document
|
|
3851
|
+
: replaceStoryBlocks(
|
|
3852
|
+
state.document,
|
|
3853
|
+
activeStory,
|
|
3854
|
+
result.document.content.children,
|
|
3855
|
+
);
|
|
3856
|
+
this.dispatch({
|
|
3857
|
+
type: "document.replace",
|
|
3858
|
+
document: nextDocument,
|
|
3859
|
+
mapping: createEmptyMapping(),
|
|
3860
|
+
origin: createOrigin("api", clock()),
|
|
3861
|
+
});
|
|
3862
|
+
},
|
|
3708
3863
|
openStory(target) {
|
|
3709
3864
|
const normalizedTarget =
|
|
3710
3865
|
target.kind === "header" || target.kind === "footer"
|
|
@@ -3815,6 +3970,7 @@ export function createDocumentRuntime(
|
|
|
3815
3970
|
collection,
|
|
3816
3971
|
collectSectionPropertiesInOrder(state.document),
|
|
3817
3972
|
state.document,
|
|
3973
|
+
state.document.subParts?.settings,
|
|
3818
3974
|
);
|
|
3819
3975
|
},
|
|
3820
3976
|
layout: layoutFacet,
|
|
@@ -7468,3 +7624,271 @@ async function upgradeMeasurementProvider(
|
|
|
7468
7624
|
// fall through — the empirical backend remains in place
|
|
7469
7625
|
}
|
|
7470
7626
|
}
|
|
7627
|
+
|
|
7628
|
+
function rangesOverlap(
|
|
7629
|
+
leftFrom: number,
|
|
7630
|
+
leftTo: number,
|
|
7631
|
+
rightFrom: number,
|
|
7632
|
+
rightTo: number,
|
|
7633
|
+
): boolean {
|
|
7634
|
+
return leftFrom < rightTo && rightFrom < leftTo;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
function resolveClearHighlightRange(
|
|
7638
|
+
inputRange: EditorAnchorProjection | undefined,
|
|
7639
|
+
selection: import("../core/state/editor-state.ts").SelectionSnapshot,
|
|
7640
|
+
): { from: number; to: number } | null {
|
|
7641
|
+
if (inputRange !== undefined) {
|
|
7642
|
+
if (inputRange.kind !== "range") return null;
|
|
7643
|
+
return { from: inputRange.from, to: inputRange.to };
|
|
7644
|
+
}
|
|
7645
|
+
const active = selection.activeRange;
|
|
7646
|
+
if (active.kind !== "range") return null;
|
|
7647
|
+
return { from: active.range.from, to: active.range.to };
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
function expandRangeToHighlightExtent(
|
|
7651
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7652
|
+
inputFrom: number,
|
|
7653
|
+
inputTo: number,
|
|
7654
|
+
): { from: number; to: number } {
|
|
7655
|
+
let from = inputFrom;
|
|
7656
|
+
let to = inputTo;
|
|
7657
|
+
forEachParagraphBlock(blocks, (paragraph) => {
|
|
7658
|
+
if (!rangesOverlap(from, to, paragraph.from, paragraph.to)) {
|
|
7659
|
+
return;
|
|
7660
|
+
}
|
|
7661
|
+
const textSegments = paragraph.segments.filter(
|
|
7662
|
+
(segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
|
|
7663
|
+
segment.kind === "text",
|
|
7664
|
+
);
|
|
7665
|
+
const expanded = expandRangeWithinParagraph(textSegments, inputFrom, inputTo);
|
|
7666
|
+
if (expanded.from < from) from = expanded.from;
|
|
7667
|
+
if (expanded.to > to) to = expanded.to;
|
|
7668
|
+
});
|
|
7669
|
+
return { from, to };
|
|
7670
|
+
}
|
|
7671
|
+
|
|
7672
|
+
function expandRangeWithinParagraph(
|
|
7673
|
+
segments: Array<Extract<SurfaceInlineSegment, { kind: "text" }>>,
|
|
7674
|
+
inputFrom: number,
|
|
7675
|
+
inputTo: number,
|
|
7676
|
+
): { from: number; to: number } {
|
|
7677
|
+
let from = inputFrom;
|
|
7678
|
+
let to = inputTo;
|
|
7679
|
+
let touchedLeftIndex = -1;
|
|
7680
|
+
let touchedRightIndex = -1;
|
|
7681
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
7682
|
+
const segment = segments[i]!;
|
|
7683
|
+
if (!isHighlightedSegment(segment)) continue;
|
|
7684
|
+
if (rangesOverlap(inputFrom, inputTo, segment.from, segment.to)) {
|
|
7685
|
+
if (touchedLeftIndex === -1) touchedLeftIndex = i;
|
|
7686
|
+
touchedRightIndex = i;
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
if (touchedLeftIndex === -1) {
|
|
7690
|
+
return { from, to };
|
|
7691
|
+
}
|
|
7692
|
+
for (let i = touchedLeftIndex; i >= 0; i -= 1) {
|
|
7693
|
+
const segment = segments[i]!;
|
|
7694
|
+
if (!isHighlightedSegment(segment)) break;
|
|
7695
|
+
if (segment.to < from) break;
|
|
7696
|
+
if (segment.from < from) from = segment.from;
|
|
7697
|
+
}
|
|
7698
|
+
for (let i = touchedRightIndex; i < segments.length; i += 1) {
|
|
7699
|
+
const segment = segments[i]!;
|
|
7700
|
+
if (!isHighlightedSegment(segment)) break;
|
|
7701
|
+
if (segment.from > to) break;
|
|
7702
|
+
if (segment.to > to) to = segment.to;
|
|
7703
|
+
}
|
|
7704
|
+
return { from, to };
|
|
7705
|
+
}
|
|
7706
|
+
|
|
7707
|
+
function isHighlightedSegment(
|
|
7708
|
+
segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
|
|
7709
|
+
): boolean {
|
|
7710
|
+
const bg = segment.markAttrs?.backgroundColor;
|
|
7711
|
+
return typeof bg === "string" && bg.length > 0;
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
function forEachParagraphBlock(
|
|
7715
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7716
|
+
visit: (paragraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>) => void,
|
|
7717
|
+
): void {
|
|
7718
|
+
for (const block of blocks) {
|
|
7719
|
+
if (block.kind === "paragraph") {
|
|
7720
|
+
visit(block);
|
|
7721
|
+
continue;
|
|
7722
|
+
}
|
|
7723
|
+
if (block.kind === "table") {
|
|
7724
|
+
for (const row of block.rows) {
|
|
7725
|
+
for (const cell of row.cells) {
|
|
7726
|
+
forEachParagraphBlock(cell.content, visit);
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
continue;
|
|
7730
|
+
}
|
|
7731
|
+
if (block.kind === "sdt_block") {
|
|
7732
|
+
forEachParagraphBlock(block.children, visit);
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7737
|
+
function findSingleSelectedTextSegment(
|
|
7738
|
+
snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
|
|
7739
|
+
): Extract<SurfaceInlineSegment, { kind: "text" }> | null {
|
|
7740
|
+
if (
|
|
7741
|
+
!snapshot.surface ||
|
|
7742
|
+
snapshot.selection.activeRange.kind !== "range" ||
|
|
7743
|
+
snapshot.selection.isCollapsed
|
|
7744
|
+
) {
|
|
7745
|
+
return null;
|
|
7746
|
+
}
|
|
7747
|
+
const selectionFrom = Math.min(snapshot.selection.anchor, snapshot.selection.head);
|
|
7748
|
+
const selectionTo = Math.max(snapshot.selection.anchor, snapshot.selection.head);
|
|
7749
|
+
const segments = collectSelectedTextSegments(
|
|
7750
|
+
snapshot.surface.blocks,
|
|
7751
|
+
selectionFrom,
|
|
7752
|
+
selectionTo,
|
|
7753
|
+
);
|
|
7754
|
+
if (segments.length !== 1) {
|
|
7755
|
+
return null;
|
|
7756
|
+
}
|
|
7757
|
+
const [segment] = segments;
|
|
7758
|
+
if (!segment || segment.from !== selectionFrom || segment.to !== selectionTo) {
|
|
7759
|
+
return null;
|
|
7760
|
+
}
|
|
7761
|
+
return segment;
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
function collectSelectedTextSegments(
|
|
7765
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
7766
|
+
selectionFrom: number,
|
|
7767
|
+
selectionTo: number,
|
|
7768
|
+
output: Array<Extract<SurfaceInlineSegment, { kind: "text" }>> = [],
|
|
7769
|
+
): Array<Extract<SurfaceInlineSegment, { kind: "text" }>> {
|
|
7770
|
+
for (const block of blocks) {
|
|
7771
|
+
if (block.kind === "paragraph") {
|
|
7772
|
+
for (const segment of block.segments) {
|
|
7773
|
+
if (
|
|
7774
|
+
segment.kind === "text" &&
|
|
7775
|
+
rangesOverlap(selectionFrom, selectionTo, segment.from, segment.to)
|
|
7776
|
+
) {
|
|
7777
|
+
output.push(segment);
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
continue;
|
|
7781
|
+
}
|
|
7782
|
+
if (block.kind === "table") {
|
|
7783
|
+
for (const row of block.rows) {
|
|
7784
|
+
for (const cell of row.cells) {
|
|
7785
|
+
collectSelectedTextSegments(cell.content, selectionFrom, selectionTo, output);
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
continue;
|
|
7789
|
+
}
|
|
7790
|
+
if (block.kind === "sdt_block") {
|
|
7791
|
+
collectSelectedTextSegments(block.children, selectionFrom, selectionTo, output);
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
return output;
|
|
7795
|
+
}
|
|
7796
|
+
|
|
7797
|
+
function buildRunPropertyBeforeXml(
|
|
7798
|
+
segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
|
|
7799
|
+
): string {
|
|
7800
|
+
const parts: string[] = [];
|
|
7801
|
+
const marks = new Set(segment.marks ?? []);
|
|
7802
|
+
if (marks.has("bold")) parts.push("<w:b/>");
|
|
7803
|
+
if (marks.has("italic")) parts.push("<w:i/>");
|
|
7804
|
+
if (marks.has("underline")) parts.push("<w:u w:val=\"single\"/>");
|
|
7805
|
+
if (marks.has("strikethrough")) parts.push("<w:strike/>");
|
|
7806
|
+
if (marks.has("superscript")) parts.push("<w:vertAlign w:val=\"superscript\"/>");
|
|
7807
|
+
if (marks.has("subscript")) parts.push("<w:vertAlign w:val=\"subscript\"/>");
|
|
7808
|
+
if (segment.markAttrs?.fontFamily) {
|
|
7809
|
+
parts.push(
|
|
7810
|
+
`<w:rFonts w:ascii="${escapeAttributeXml(segment.markAttrs.fontFamily)}" w:hAnsi="${escapeAttributeXml(segment.markAttrs.fontFamily)}"/>`,
|
|
7811
|
+
);
|
|
7812
|
+
}
|
|
7813
|
+
if (segment.markAttrs?.fontSize !== undefined) {
|
|
7814
|
+
parts.push(`<w:sz w:val="${segment.markAttrs.fontSize}"/>`);
|
|
7815
|
+
}
|
|
7816
|
+
if (segment.markAttrs?.textColor) {
|
|
7817
|
+
parts.push(`<w:color w:val="${escapeAttributeXml(segment.markAttrs.textColor)}"/>`);
|
|
7818
|
+
}
|
|
7819
|
+
if (segment.markAttrs?.backgroundColor) {
|
|
7820
|
+
parts.push(
|
|
7821
|
+
`<w:shd w:val="clear" w:color="auto" w:fill="${escapeAttributeXml(segment.markAttrs.backgroundColor)}"/>`,
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7824
|
+
return `<w:rPr>${parts.join("")}</w:rPr>`;
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
function escapeAttributeXml(value: string): string {
|
|
7828
|
+
return value
|
|
7829
|
+
.replace(/&/g, "&")
|
|
7830
|
+
.replace(/</g, "<")
|
|
7831
|
+
.replace(/>/g, ">")
|
|
7832
|
+
.replace(/"/g, """);
|
|
7833
|
+
}
|
|
7834
|
+
|
|
7835
|
+
function appendPropertyChangeSuggestion(
|
|
7836
|
+
document: CanonicalDocumentEnvelope,
|
|
7837
|
+
anchor: { from: number; to: number },
|
|
7838
|
+
input: {
|
|
7839
|
+
originalRevisionType: "rPrChange" | "pPrChange";
|
|
7840
|
+
xmlTag: "rPrChange" | "pPrChange";
|
|
7841
|
+
beforeXml: string;
|
|
7842
|
+
semanticKind: "formatting-change" | "paragraph-property-change";
|
|
7843
|
+
storyTarget: EditorStoryTarget;
|
|
7844
|
+
authorId?: string;
|
|
7845
|
+
},
|
|
7846
|
+
timestamp: string,
|
|
7847
|
+
): CanonicalDocumentEnvelope {
|
|
7848
|
+
const existing = document.review.revisions;
|
|
7849
|
+
const changeId = createRuntimeSuggestionChangeId(existing, timestamp);
|
|
7850
|
+
const resolvedAuthorId = input.authorId ?? "unknown";
|
|
7851
|
+
return {
|
|
7852
|
+
...document,
|
|
7853
|
+
review: {
|
|
7854
|
+
...document.review,
|
|
7855
|
+
revisions: {
|
|
7856
|
+
...existing,
|
|
7857
|
+
[changeId]: {
|
|
7858
|
+
changeId,
|
|
7859
|
+
kind: "property-change",
|
|
7860
|
+
anchor: createRangeAnchor(anchor.from, anchor.to, { start: 1, end: -1 }),
|
|
7861
|
+
authorId: resolvedAuthorId,
|
|
7862
|
+
createdAt: timestamp,
|
|
7863
|
+
warningIds: [],
|
|
7864
|
+
metadata: {
|
|
7865
|
+
source: "runtime",
|
|
7866
|
+
storyTarget: input.storyTarget,
|
|
7867
|
+
suggestionId: changeId,
|
|
7868
|
+
semanticKind: input.semanticKind,
|
|
7869
|
+
originalRevisionType: input.originalRevisionType,
|
|
7870
|
+
propertyChangeData: {
|
|
7871
|
+
xmlTag: input.xmlTag,
|
|
7872
|
+
beforeXml: input.beforeXml,
|
|
7873
|
+
},
|
|
7874
|
+
},
|
|
7875
|
+
status: "open",
|
|
7876
|
+
},
|
|
7877
|
+
},
|
|
7878
|
+
},
|
|
7879
|
+
};
|
|
7880
|
+
}
|
|
7881
|
+
|
|
7882
|
+
function createRuntimeSuggestionChangeId(
|
|
7883
|
+
existing: CanonicalDocumentEnvelope["review"]["revisions"],
|
|
7884
|
+
timestamp: string,
|
|
7885
|
+
): string {
|
|
7886
|
+
const base = `change-${timestamp.replace(/[^0-9]/gu, "")}`;
|
|
7887
|
+
let counter = Object.keys(existing).length + 1;
|
|
7888
|
+
let candidate = `${base}-p${counter}`;
|
|
7889
|
+
while (existing[candidate]) {
|
|
7890
|
+
counter += 1;
|
|
7891
|
+
candidate = `${base}-p${counter}`;
|
|
7892
|
+
}
|
|
7893
|
+
return candidate;
|
|
7894
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BlockNode,
|
|
3
3
|
CanonicalDocument,
|
|
4
|
+
DocumentSettings,
|
|
4
5
|
EndnoteProperties,
|
|
5
6
|
FootnoteCollection,
|
|
6
7
|
FootnoteProperties,
|
|
@@ -15,14 +16,13 @@ export interface FootnoteResolver {
|
|
|
15
16
|
getEndnoteCount(): number;
|
|
16
17
|
/**
|
|
17
18
|
* Resolve the effective `<w:footnotePr>` for a given section. Returns the
|
|
18
|
-
* section's typed properties when present,
|
|
19
|
-
*
|
|
20
|
-
* defaults from `settings.xml` are not yet wired in — that's a later slice.
|
|
19
|
+
* section's typed properties when present, otherwise the settings-level
|
|
20
|
+
* default from `settings.xml` when available.
|
|
21
21
|
*/
|
|
22
22
|
getFootnoteProperties(sectionIndex?: number): FootnoteProperties | undefined;
|
|
23
23
|
/**
|
|
24
24
|
* Resolve the effective `<w:endnotePr>` for a given section. Same semantics
|
|
25
|
-
* as `getFootnoteProperties` but reads `endnotePr
|
|
25
|
+
* as `getFootnoteProperties` but reads `endnotePr`.
|
|
26
26
|
*/
|
|
27
27
|
getEndnoteProperties(sectionIndex?: number): EndnoteProperties | undefined;
|
|
28
28
|
/**
|
|
@@ -70,6 +70,7 @@ export function createFootnoteResolver(
|
|
|
70
70
|
collection: FootnoteCollection,
|
|
71
71
|
sections?: readonly SectionProperties[],
|
|
72
72
|
document?: CanonicalDocument,
|
|
73
|
+
settings?: DocumentSettings,
|
|
73
74
|
): FootnoteResolver {
|
|
74
75
|
return {
|
|
75
76
|
getContinuationSeparatorContent(kind) {
|
|
@@ -87,12 +88,20 @@ export function createFootnoteResolver(
|
|
|
87
88
|
return Object.keys(collection.endnotes).length;
|
|
88
89
|
},
|
|
89
90
|
getFootnoteProperties(sectionIndex) {
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
return resolveFootnoteLikeProperties(
|
|
92
|
+
sectionIndex,
|
|
93
|
+
sections,
|
|
94
|
+
settings?.footnotePr,
|
|
95
|
+
"footnotePr",
|
|
96
|
+
);
|
|
92
97
|
},
|
|
93
98
|
getEndnoteProperties(sectionIndex) {
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
return resolveFootnoteLikeProperties(
|
|
100
|
+
sectionIndex,
|
|
101
|
+
sections,
|
|
102
|
+
settings?.endnotePr,
|
|
103
|
+
"endnotePr",
|
|
104
|
+
);
|
|
96
105
|
},
|
|
97
106
|
getEndnotesForSection(sectionIndex) {
|
|
98
107
|
if (!sections || !document) return EMPTY_READONLY_STRING_ARRAY;
|
|
@@ -107,6 +116,21 @@ export function createFootnoteResolver(
|
|
|
107
116
|
|
|
108
117
|
const EMPTY_READONLY_STRING_ARRAY: readonly string[] = Object.freeze([]);
|
|
109
118
|
|
|
119
|
+
function resolveFootnoteLikeProperties<T extends FootnoteProperties | EndnoteProperties>(
|
|
120
|
+
sectionIndex: number | undefined,
|
|
121
|
+
sections: readonly SectionProperties[] | undefined,
|
|
122
|
+
defaultProps: T | undefined,
|
|
123
|
+
key: "footnotePr" | "endnotePr",
|
|
124
|
+
): T | undefined {
|
|
125
|
+
if (sectionIndex !== undefined && sections) {
|
|
126
|
+
const sectionProps = sections[sectionIndex]?.[key];
|
|
127
|
+
if (sectionProps) {
|
|
128
|
+
return sectionProps as T;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return defaultProps;
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
/**
|
|
111
135
|
* Walk the document's top-level block children and collect endnote IDs
|
|
112
136
|
* referenced from the block range belonging to `targetSectionIndex`.
|
|
@@ -215,8 +215,14 @@
|
|
|
215
215
|
* CO3.8 `isStaleParaInd` heuristic with a broader `isDegenerateParaInd`
|
|
216
216
|
* guard (hanging===left → use level geometry) that fixes the APS
|
|
217
217
|
* Supply paragraph pattern. Cache envelopes from v22 invalidate.
|
|
218
|
+
* 24 — Merge from `main` after the non-SDT closeout. The layout measurement
|
|
219
|
+
* and resolved-formatting stack changed in `measurement-backend-canvas`,
|
|
220
|
+
* `measurement-backend-empirical`, `paginated-layout-engine`,
|
|
221
|
+
* `resolved-formatting-document`, and `resolved-formatting-state`.
|
|
222
|
+
* Persisted prerender/layout caches must invalidate so geometry derived
|
|
223
|
+
* under v23 is never reused against the merged layout pipeline.
|
|
218
224
|
*/
|
|
219
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
225
|
+
export const LAYOUT_ENGINE_VERSION = 24 as const;
|
|
220
226
|
|
|
221
227
|
/**
|
|
222
228
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -188,7 +188,7 @@ export function createCanvasBackend(
|
|
|
188
188
|
case "tab": {
|
|
189
189
|
// Advance to the next tab stop (in twips).
|
|
190
190
|
const avgCharWidth = formatting.averageCharWidthTwips;
|
|
191
|
-
const defaultTabInterval =
|
|
191
|
+
const defaultTabInterval = formatting.defaultTabInterval;
|
|
192
192
|
const position = currentLineWidth + formatting.indentLeft;
|
|
193
193
|
let nextTab = Math.ceil((position + 1) / defaultTabInterval) * defaultTabInterval;
|
|
194
194
|
for (const tab of formatting.tabStops) {
|
|
@@ -187,7 +187,7 @@ function resolveTabAdvance(
|
|
|
187
187
|
currentChars: number,
|
|
188
188
|
): number {
|
|
189
189
|
const avgCharWidth = formatting.averageCharWidthTwips;
|
|
190
|
-
const defaultTabInterval =
|
|
190
|
+
const defaultTabInterval = formatting.defaultTabInterval;
|
|
191
191
|
const currentPosition = currentChars * avgCharWidth + formatting.indentLeft;
|
|
192
192
|
|
|
193
193
|
if (formatting.tabStops.length === 0) {
|