@beyondwork/docx-react-component 1.0.71 → 1.0.72
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 +964 -75
- package/package.json +1 -1
- package/src/api/public-types.ts +243 -1
- package/src/api/v3/_create.ts +16 -1
- package/src/api/v3/_runtime-handle.ts +2 -0
- package/src/api/v3/ai/evaluate.ts +113 -0
- package/src/api/v3/ai/outline.ts +140 -0
- package/src/api/v3/ai/replacement.ts +8 -0
- package/src/api/v3/ai/review.ts +342 -0
- package/src/api/v3/ai/stats.ts +62 -0
- package/src/api/v3/runtime/viewport.ts +181 -0
- package/src/api/v3/runtime/workflow.ts +114 -1
- package/src/api/v3/ui/_types.ts +35 -0
- package/src/api/v3/ui/index.ts +1 -0
- package/src/api/v3/ui/viewport.ts +112 -0
- package/src/compare/diff-engine.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/table-structure-commands.ts +1 -0
- package/src/io/export/serialize-headers-footers.ts +1 -0
- package/src/io/export/serialize-main-document.ts +13 -0
- package/src/io/export/serialize-paragraph-formatting.ts +34 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/normalize/normalize-text.ts +11 -0
- package/src/io/ooxml/parse-main-document.ts +21 -5
- package/src/io/ooxml/parse-paragraph-formatting.ts +105 -0
- package/src/model/canonical-document.ts +401 -1
- package/src/runtime/formatting/formatting-context.ts +2 -1
- package/src/runtime/geometry/overlay-rects.ts +7 -10
- package/src/runtime/layout/layout-engine-version.ts +257 -1
- package/src/runtime/layout/paginated-layout-engine.ts +134 -8
- package/src/runtime/layout/resolved-formatting-state.ts +108 -13
- package/src/runtime/markdown-sanitizer.ts +21 -4
- package/src/runtime/render/render-kernel.ts +21 -1
- package/src/runtime/scopes/audit-bundle.ts +8 -0
- package/src/runtime/scopes/compiler-service.ts +1 -0
- package/src/runtime/scopes/enumerate-scopes.ts +61 -3
- package/src/runtime/scopes/replacement/apply.ts +49 -3
- package/src/runtime/scopes/semantic-scope-types.ts +8 -0
- package/src/runtime/surface-projection.ts +22 -0
- package/src/runtime/workflow/coordinator.ts +3 -0
- package/src/runtime/workflow/scope-writer.ts +34 -0
- package/src/session/export/embedded-reconstitute.ts +37 -3
- package/src/session/import/embedded-offload.ts +26 -1
- package/src/shell/media-previews.ts +8 -6
- package/src/ui/WordReviewEditor.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +11 -0
- package/src/ui/headless/selection-helpers.ts +2 -2
- package/src/ui/runtime-shortcut-dispatch.ts +4 -4
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +22 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +11 -11
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +5 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +18 -1
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +22 -6
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +18 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +98 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -4
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +8 -1
- package/src/ui-tailwind/editor-surface/search-plugin.ts +2 -4
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +37 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +29 -4
- package/src/ui-tailwind/index.ts +4 -2
- package/src/ui-tailwind/page-chrome-model.ts +5 -7
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +5 -2
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +10 -1
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +4 -1
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +73 -8
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +4 -4
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +1 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type {
|
|
10
10
|
CanonicalParagraphFormatting,
|
|
11
|
+
FrameProperties,
|
|
11
12
|
ParagraphBorders,
|
|
12
13
|
ParagraphIndentation,
|
|
13
14
|
ParagraphShading,
|
|
@@ -49,9 +50,37 @@ const PPR_MODELLED_CHILDREN: ReadonlySet<string> = new Set([
|
|
|
49
50
|
"suppressLineNumbers",
|
|
50
51
|
"suppressAutoHyphens",
|
|
51
52
|
"outlineLvl",
|
|
53
|
+
"framePr",
|
|
52
54
|
"rPr",
|
|
53
55
|
]);
|
|
54
56
|
|
|
57
|
+
const FRAME_H_RULE_VOCAB = new Set<NonNullable<FrameProperties["hRule"]>>(["auto", "atLeast", "exact"]);
|
|
58
|
+
const FRAME_X_ALIGN_VOCAB = new Set<NonNullable<FrameProperties["xAlign"]>>([
|
|
59
|
+
"left",
|
|
60
|
+
"center",
|
|
61
|
+
"right",
|
|
62
|
+
"inside",
|
|
63
|
+
"outside",
|
|
64
|
+
]);
|
|
65
|
+
const FRAME_Y_ALIGN_VOCAB = new Set<NonNullable<FrameProperties["yAlign"]>>([
|
|
66
|
+
"top",
|
|
67
|
+
"center",
|
|
68
|
+
"bottom",
|
|
69
|
+
"inside",
|
|
70
|
+
"outside",
|
|
71
|
+
"inline",
|
|
72
|
+
]);
|
|
73
|
+
const FRAME_ANCHOR_VOCAB = new Set<NonNullable<FrameProperties["hAnchor"]>>(["text", "margin", "page"]);
|
|
74
|
+
const FRAME_WRAP_VOCAB = new Set<NonNullable<FrameProperties["wrap"]>>([
|
|
75
|
+
"around",
|
|
76
|
+
"auto",
|
|
77
|
+
"none",
|
|
78
|
+
"notBeside",
|
|
79
|
+
"tight",
|
|
80
|
+
"through",
|
|
81
|
+
]);
|
|
82
|
+
const FRAME_DROP_CAP_VOCAB = new Set<NonNullable<FrameProperties["dropCap"]>>(["none", "drop", "margin"]);
|
|
83
|
+
|
|
55
84
|
const PPR_GRAB_BAG_DESCRIPTOR: PropertyGrabBagDescriptor = {
|
|
56
85
|
modelledChildNames: PPR_MODELLED_CHILDREN,
|
|
57
86
|
modelledChildAttributes: new Map(),
|
|
@@ -164,6 +193,76 @@ function readShading(node: XmlElementNode): ParagraphShading | undefined {
|
|
|
164
193
|
};
|
|
165
194
|
}
|
|
166
195
|
|
|
196
|
+
/**
|
|
197
|
+
* `<w:framePr>` — paragraph text-frame properties (ECMA-376 §17.3.1.11).
|
|
198
|
+
*
|
|
199
|
+
* All children of framePr are attributes on a single self-closing element;
|
|
200
|
+
* this reader maps each modelled attribute to its typed field and ignores
|
|
201
|
+
* extension attributes (`w14:*`, `w15:*`, `mc:Ignorable`, etc.). Extension-
|
|
202
|
+
* attribute round-trip via `FrameProperties.rawXml` is a TODO — the current
|
|
203
|
+
* signature doesn't receive the source XML string, so rawXml is left unset.
|
|
204
|
+
* The typed attributes cover the CCEP cases we've seen (2-column inset
|
|
205
|
+
* text frames, drop-caps); extension attrs are rare in that corpus.
|
|
206
|
+
*/
|
|
207
|
+
function readFrameProperties(node: XmlElementNode): FrameProperties | undefined {
|
|
208
|
+
const out: FrameProperties = {};
|
|
209
|
+
const width = readIntAttr(node, "w:w");
|
|
210
|
+
if (width !== undefined) out.widthTwips = width;
|
|
211
|
+
const height = readIntAttr(node, "w:h");
|
|
212
|
+
if (height !== undefined) out.heightTwips = height;
|
|
213
|
+
const hRuleRaw = (node.attributes["w:hRule"] ?? node.attributes.hRule)?.trim();
|
|
214
|
+
if (hRuleRaw) {
|
|
215
|
+
const lower = hRuleRaw.charAt(0).toLowerCase() + hRuleRaw.slice(1);
|
|
216
|
+
const candidate = lower === "atleast" ? "atLeast" : lower;
|
|
217
|
+
if (FRAME_H_RULE_VOCAB.has(candidate as NonNullable<FrameProperties["hRule"]>)) {
|
|
218
|
+
out.hRule = candidate as NonNullable<FrameProperties["hRule"]>;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const x = readIntAttr(node, "w:x");
|
|
222
|
+
if (x !== undefined) out.xTwips = x;
|
|
223
|
+
const y = readIntAttr(node, "w:y");
|
|
224
|
+
if (y !== undefined) out.yTwips = y;
|
|
225
|
+
const xAlign = (node.attributes["w:xAlign"] ?? node.attributes.xAlign)?.toLowerCase().trim();
|
|
226
|
+
if (xAlign && FRAME_X_ALIGN_VOCAB.has(xAlign as NonNullable<FrameProperties["xAlign"]>)) {
|
|
227
|
+
out.xAlign = xAlign as NonNullable<FrameProperties["xAlign"]>;
|
|
228
|
+
}
|
|
229
|
+
const yAlign = (node.attributes["w:yAlign"] ?? node.attributes.yAlign)?.toLowerCase().trim();
|
|
230
|
+
if (yAlign && FRAME_Y_ALIGN_VOCAB.has(yAlign as NonNullable<FrameProperties["yAlign"]>)) {
|
|
231
|
+
out.yAlign = yAlign as NonNullable<FrameProperties["yAlign"]>;
|
|
232
|
+
}
|
|
233
|
+
const hAnchor = (node.attributes["w:hAnchor"] ?? node.attributes.hAnchor)?.toLowerCase().trim();
|
|
234
|
+
if (hAnchor && FRAME_ANCHOR_VOCAB.has(hAnchor as NonNullable<FrameProperties["hAnchor"]>)) {
|
|
235
|
+
out.hAnchor = hAnchor as NonNullable<FrameProperties["hAnchor"]>;
|
|
236
|
+
}
|
|
237
|
+
const vAnchor = (node.attributes["w:vAnchor"] ?? node.attributes.vAnchor)?.toLowerCase().trim();
|
|
238
|
+
if (vAnchor && FRAME_ANCHOR_VOCAB.has(vAnchor as NonNullable<FrameProperties["vAnchor"]>)) {
|
|
239
|
+
out.vAnchor = vAnchor as NonNullable<FrameProperties["vAnchor"]>;
|
|
240
|
+
}
|
|
241
|
+
const wrapRaw = (node.attributes["w:wrap"] ?? node.attributes.wrap)?.trim();
|
|
242
|
+
if (wrapRaw) {
|
|
243
|
+
const wrapLower = wrapRaw.charAt(0).toLowerCase() + wrapRaw.slice(1);
|
|
244
|
+
const wrapCandidate = wrapLower === "notbeside" ? "notBeside" : wrapLower;
|
|
245
|
+
if (FRAME_WRAP_VOCAB.has(wrapCandidate as NonNullable<FrameProperties["wrap"]>)) {
|
|
246
|
+
out.wrap = wrapCandidate as NonNullable<FrameProperties["wrap"]>;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const hSpace = readIntAttr(node, "w:hSpace");
|
|
250
|
+
if (hSpace !== undefined) out.hSpaceTwips = hSpace;
|
|
251
|
+
const vSpace = readIntAttr(node, "w:vSpace");
|
|
252
|
+
if (vSpace !== undefined) out.vSpaceTwips = vSpace;
|
|
253
|
+
const dropCap = (node.attributes["w:dropCap"] ?? node.attributes.dropCap)?.toLowerCase().trim();
|
|
254
|
+
if (dropCap && FRAME_DROP_CAP_VOCAB.has(dropCap as NonNullable<FrameProperties["dropCap"]>)) {
|
|
255
|
+
out.dropCap = dropCap as NonNullable<FrameProperties["dropCap"]>;
|
|
256
|
+
}
|
|
257
|
+
const lines = readIntAttr(node, "w:lines");
|
|
258
|
+
if (lines !== undefined) out.lines = lines;
|
|
259
|
+
const anchorLockRaw = (node.attributes["w:anchorLock"] ?? node.attributes.anchorLock)?.toLowerCase().trim();
|
|
260
|
+
if (anchorLockRaw !== undefined && anchorLockRaw.length > 0) {
|
|
261
|
+
out.anchorLock = !(anchorLockRaw === "false" || anchorLockRaw === "0" || anchorLockRaw === "off");
|
|
262
|
+
}
|
|
263
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
167
266
|
export function readParagraphProperties(
|
|
168
267
|
node: XmlElementNode | undefined,
|
|
169
268
|
): CanonicalParagraphFormatting | undefined {
|
|
@@ -237,6 +336,12 @@ export function readParagraphProperties(
|
|
|
237
336
|
const outline = readIntVal(findChildOptional(node, "outlineLvl"));
|
|
238
337
|
if (outline !== undefined) out.outlineLevel = outline;
|
|
239
338
|
|
|
339
|
+
const framePrNode = findChildOptional(node, "framePr");
|
|
340
|
+
if (framePrNode) {
|
|
341
|
+
const frameProperties = readFrameProperties(framePrNode);
|
|
342
|
+
if (frameProperties) out.frameProperties = frameProperties;
|
|
343
|
+
}
|
|
344
|
+
|
|
240
345
|
const rPrNode = findChildOptional(node, "rPr");
|
|
241
346
|
const markRpr = readRunProperties(rPrNode);
|
|
242
347
|
if (markRpr) out.paragraphMarkRunProperties = markRpr;
|
|
@@ -697,6 +697,7 @@ export type DocumentNode =
|
|
|
697
697
|
| HardBreakNode
|
|
698
698
|
| TabNode
|
|
699
699
|
| ColumnBreakNode
|
|
700
|
+
| PageBreakNode
|
|
700
701
|
| SymbolNode
|
|
701
702
|
| HyperlinkNode
|
|
702
703
|
| ImageNode
|
|
@@ -874,6 +875,20 @@ export interface CanonicalParagraphFormatting {
|
|
|
874
875
|
suppressLineNumbers?: boolean;
|
|
875
876
|
suppressAutoHyphens?: boolean;
|
|
876
877
|
paragraphMarkRunProperties?: CanonicalRunFormatting;
|
|
878
|
+
/**
|
|
879
|
+
* `<w:framePr>` — paragraph text-frame properties (ECMA-376 §17.3.1.11).
|
|
880
|
+
* Absent on ordinary paragraphs. When present, the paragraph renders
|
|
881
|
+
* inside a positioned text frame rather than in-flow. Used in CCEP-style
|
|
882
|
+
* templates for side-by-side "instructional column + body column"
|
|
883
|
+
* paragraphs and for drop-caps.
|
|
884
|
+
*
|
|
885
|
+
* Added 2026-04-23 per `docs/KNOWN-ISSUES-VISUAL.md §2.3 "w:framePr
|
|
886
|
+
* 2-column inset text frames"`. Before this type existed, L01's parser
|
|
887
|
+
* fell through to `OpaqueBlockNode` whenever `<w:pPr>` contained
|
|
888
|
+
* `<w:framePr>`, which hid the paragraph from L04 pagination and L11
|
|
889
|
+
* render. Canonical representation unblocks downstream adoption.
|
|
890
|
+
*/
|
|
891
|
+
frameProperties?: FrameProperties;
|
|
877
892
|
/**
|
|
878
893
|
* Unmodelled direct children of `<w:pPr>` captured verbatim for round-trip.
|
|
879
894
|
* See `src/io/ooxml/property-grab-bag.ts` for the mechanism and Lane 3 O2
|
|
@@ -888,6 +903,80 @@ export interface CanonicalParagraphFormatting {
|
|
|
888
903
|
unknownPropertyChildren?: UnknownPropertyChild[];
|
|
889
904
|
}
|
|
890
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Paragraph text-frame properties from `<w:framePr>` (ECMA-376 §17.3.1.11).
|
|
908
|
+
* Pure data — describes a positioned text frame that contains the
|
|
909
|
+
* paragraph. When `frameProperties` is set on a paragraph's formatting,
|
|
910
|
+
* L04 pagination and L11 render treat the paragraph as an out-of-flow
|
|
911
|
+
* frame positioned according to the fields below, not as an in-flow
|
|
912
|
+
* block.
|
|
913
|
+
*
|
|
914
|
+
* All fields optional; absence carries its OOXML default.
|
|
915
|
+
*/
|
|
916
|
+
export interface FrameProperties {
|
|
917
|
+
/** `w:w` — frame width in twips. Absence = auto-width from content. */
|
|
918
|
+
widthTwips?: number;
|
|
919
|
+
/** `w:h` — frame height in twips. Interpretation depends on `hRule`. */
|
|
920
|
+
heightTwips?: number;
|
|
921
|
+
/**
|
|
922
|
+
* `w:hRule` — height rule.
|
|
923
|
+
* - `"auto"` (default): height follows content.
|
|
924
|
+
* - `"atLeast"`: content grows the frame; `heightTwips` is the floor.
|
|
925
|
+
* - `"exact"`: content is clipped to `heightTwips`.
|
|
926
|
+
*/
|
|
927
|
+
hRule?: "auto" | "atLeast" | "exact";
|
|
928
|
+
/** `w:x` — horizontal absolute position in twips from `hAnchor`. */
|
|
929
|
+
xTwips?: number;
|
|
930
|
+
/** `w:y` — vertical absolute position in twips from `vAnchor`. */
|
|
931
|
+
yTwips?: number;
|
|
932
|
+
/**
|
|
933
|
+
* `w:xAlign` — horizontal alignment keyword; overrides `xTwips` if
|
|
934
|
+
* both are set (Word behavior). `"inside"` / `"outside"` are
|
|
935
|
+
* mirror-aware for even/odd pages.
|
|
936
|
+
*/
|
|
937
|
+
xAlign?: "left" | "center" | "right" | "inside" | "outside";
|
|
938
|
+
/**
|
|
939
|
+
* `w:yAlign` — vertical alignment keyword; overrides `yTwips` if
|
|
940
|
+
* both are set. `"inline"` places the frame in the text flow.
|
|
941
|
+
*/
|
|
942
|
+
yAlign?: "top" | "center" | "bottom" | "inside" | "outside" | "inline";
|
|
943
|
+
/** `w:hAnchor` — what `x` / `xAlign` is measured from. */
|
|
944
|
+
hAnchor?: "text" | "margin" | "page";
|
|
945
|
+
/** `w:vAnchor` — what `y` / `yAlign` is measured from. */
|
|
946
|
+
vAnchor?: "text" | "margin" | "page";
|
|
947
|
+
/**
|
|
948
|
+
* `w:wrap` — how surrounding text flows around the frame.
|
|
949
|
+
* `"around"`: text wraps on both sides; `"notBeside"`: text stops
|
|
950
|
+
* above/below the frame; `"none"`: no text on the frame line;
|
|
951
|
+
* `"auto"` / `"tight"` / `"through"`: extended wrap modes.
|
|
952
|
+
*/
|
|
953
|
+
wrap?: "around" | "auto" | "none" | "notBeside" | "tight" | "through";
|
|
954
|
+
/** `w:hSpace` — horizontal clear-space around the frame, twips. */
|
|
955
|
+
hSpaceTwips?: number;
|
|
956
|
+
/** `w:vSpace` — vertical clear-space around the frame, twips. */
|
|
957
|
+
vSpaceTwips?: number;
|
|
958
|
+
/**
|
|
959
|
+
* `w:dropCap` — drop-cap semantics when the frame holds an initial
|
|
960
|
+
* letter. `"none"` (default): not a drop-cap frame.
|
|
961
|
+
* `"drop"`: character spans `lines` lines, text wraps around it.
|
|
962
|
+
* `"margin"`: character hangs in the margin.
|
|
963
|
+
*/
|
|
964
|
+
dropCap?: "none" | "drop" | "margin";
|
|
965
|
+
/** `w:lines` — number of lines the drop-cap spans. Only meaningful when `dropCap !== "none"`. */
|
|
966
|
+
lines?: number;
|
|
967
|
+
/** `w:anchorLock` — prevents the frame's anchor paragraph from moving between pages during reflow. */
|
|
968
|
+
anchorLock?: boolean;
|
|
969
|
+
/**
|
|
970
|
+
* Verbatim source XML of the `<w:framePr>` element. Populated by
|
|
971
|
+
* the parser for lossless round-trip of extension attributes
|
|
972
|
+
* (`w14:*`, `w15:*`, `mc:Ignorable`, etc.) that the fields above do
|
|
973
|
+
* not model. When re-serializing, the writer should prefer the
|
|
974
|
+
* modeled fields and merge in any extension attributes from
|
|
975
|
+
* `rawXml` that aren't covered by the modeled set.
|
|
976
|
+
*/
|
|
977
|
+
rawXml?: string;
|
|
978
|
+
}
|
|
979
|
+
|
|
891
980
|
/**
|
|
892
981
|
* A single unmodelled direct child of an OOXML property container (pPr,
|
|
893
982
|
* rPr, tcPr, trPr, tblPr, sectPr). Captured verbatim so the serializer
|
|
@@ -956,6 +1045,24 @@ export interface ParagraphNode {
|
|
|
956
1045
|
bidi?: boolean;
|
|
957
1046
|
suppressLineNumbers?: boolean;
|
|
958
1047
|
cnfStyle?: string;
|
|
1048
|
+
/**
|
|
1049
|
+
* `<w:framePr>` — inline paragraph text-frame properties set
|
|
1050
|
+
* directly on the paragraph (not through the style cascade).
|
|
1051
|
+
* Mirrors `CanonicalParagraphFormatting.frameProperties`; the two
|
|
1052
|
+
* slots coexist because Word writes framePr at both levels: a
|
|
1053
|
+
* style may declare the frame in `docDefaults` / named style
|
|
1054
|
+
* (cascade path), or an individual paragraph may override with
|
|
1055
|
+
* its own `<w:pPr><w:framePr>` (direct path). L03's cascade
|
|
1056
|
+
* resolver prefers the direct-paragraph value when both are set,
|
|
1057
|
+
* matching Word's cascade semantics.
|
|
1058
|
+
*
|
|
1059
|
+
* Added 2026-04-23 per `cross-layer-coord-02.md §11 P1` follow-up
|
|
1060
|
+
* to the earlier FrameProperties landing (`86961dcb`). Without
|
|
1061
|
+
* this slot, direct-paragraph `<w:framePr>` would still flatten
|
|
1062
|
+
* to `OpaqueBlockNode` because the parser had no canonical target
|
|
1063
|
+
* at the node level.
|
|
1064
|
+
*/
|
|
1065
|
+
frameProperties?: FrameProperties;
|
|
959
1066
|
/**
|
|
960
1067
|
* Preserved w14 extension identifiers for this paragraph.
|
|
961
1068
|
* Round-trip (§2 A.7) requires these to survive import → export so the
|
|
@@ -1514,6 +1621,7 @@ export type InlineNode =
|
|
|
1514
1621
|
| TextNode
|
|
1515
1622
|
| HardBreakNode
|
|
1516
1623
|
| ColumnBreakNode
|
|
1624
|
+
| PageBreakNode
|
|
1517
1625
|
| TabNode
|
|
1518
1626
|
| SymbolNode
|
|
1519
1627
|
| HyperlinkNode
|
|
@@ -1570,6 +1678,24 @@ export interface ColumnBreakNode {
|
|
|
1570
1678
|
type: "column_break";
|
|
1571
1679
|
}
|
|
1572
1680
|
|
|
1681
|
+
/**
|
|
1682
|
+
* Explicit page break — `<w:br w:type="page"/>` in OOXML (ECMA-376 §17.3.3.1).
|
|
1683
|
+
* Forces the next content to start on a new page. Emitted by L01's
|
|
1684
|
+
* `parse-main-document.ts` when `<w:br w:type="page"/>` is encountered
|
|
1685
|
+
* inside a paragraph run; consumed by L04's pagination engine which
|
|
1686
|
+
* pushes a page boundary after placing any block whose segments carry
|
|
1687
|
+
* a `page_break`. Added 2026-04-23 per `cross-layer-coord-04.md §1.18.5`
|
|
1688
|
+
* + `cross-layer-coord-03.md §11` to close the explicit-page-break
|
|
1689
|
+
* silently-dropped visual regression.
|
|
1690
|
+
*
|
|
1691
|
+
* Not to be confused with `SectionBreakNode` (`<w:sectPr><w:type
|
|
1692
|
+
* w:val="page"/>`) which is a section-level boundary and carries
|
|
1693
|
+
* section properties; `PageBreakNode` is inline inside a run.
|
|
1694
|
+
*/
|
|
1695
|
+
export interface PageBreakNode {
|
|
1696
|
+
type: "page_break";
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1573
1699
|
export interface TabNode {
|
|
1574
1700
|
type: "tab";
|
|
1575
1701
|
}
|
|
@@ -1584,7 +1710,7 @@ export interface SymbolNode {
|
|
|
1584
1710
|
export interface HyperlinkNode {
|
|
1585
1711
|
type: "hyperlink";
|
|
1586
1712
|
href: string;
|
|
1587
|
-
children: Array<TextNode | HardBreakNode | ColumnBreakNode | TabNode | SymbolNode>;
|
|
1713
|
+
children: Array<TextNode | HardBreakNode | ColumnBreakNode | PageBreakNode | TabNode | SymbolNode>;
|
|
1588
1714
|
}
|
|
1589
1715
|
|
|
1590
1716
|
export interface ImageNode {
|
|
@@ -2284,6 +2410,12 @@ export function validateCanonicalDocument(
|
|
|
2284
2410
|
if (record.subParts !== undefined) {
|
|
2285
2411
|
validateSubPartsCatalog(record.subParts, "$.subParts", issues);
|
|
2286
2412
|
}
|
|
2413
|
+
if (record.fieldRegistry !== undefined) {
|
|
2414
|
+
validateFieldRegistry(record.fieldRegistry, "$.fieldRegistry", issues);
|
|
2415
|
+
}
|
|
2416
|
+
if (record.fontTable !== undefined) {
|
|
2417
|
+
validateCanonicalFontTable(record.fontTable, "$.fontTable", issues);
|
|
2418
|
+
}
|
|
2287
2419
|
validateDocumentReferences(record, issues);
|
|
2288
2420
|
|
|
2289
2421
|
return issues;
|
|
@@ -2646,6 +2778,7 @@ function validateDocumentNode(
|
|
|
2646
2778
|
return;
|
|
2647
2779
|
case "hard_break":
|
|
2648
2780
|
case "column_break":
|
|
2781
|
+
case "page_break":
|
|
2649
2782
|
case "tab":
|
|
2650
2783
|
return;
|
|
2651
2784
|
case "symbol":
|
|
@@ -3794,6 +3927,273 @@ function validateExactObjectKeys(
|
|
|
3794
3927
|
}
|
|
3795
3928
|
}
|
|
3796
3929
|
|
|
3930
|
+
const SUPPORTED_FIELD_FAMILIES: ReadonlySet<SupportedFieldFamily> = new Set([
|
|
3931
|
+
"REF",
|
|
3932
|
+
"PAGEREF",
|
|
3933
|
+
"NOTEREF",
|
|
3934
|
+
"TOC",
|
|
3935
|
+
"PAGE",
|
|
3936
|
+
"NUMPAGES",
|
|
3937
|
+
"STYLEREF",
|
|
3938
|
+
"SECTIONPAGES",
|
|
3939
|
+
]);
|
|
3940
|
+
|
|
3941
|
+
const PRESERVE_ONLY_FIELD_FAMILIES: ReadonlySet<PreserveOnlyFieldFamily> = new Set([
|
|
3942
|
+
"DATE",
|
|
3943
|
+
"TIME",
|
|
3944
|
+
"AUTHOR",
|
|
3945
|
+
"FILENAME",
|
|
3946
|
+
"MERGEFIELD",
|
|
3947
|
+
"IF",
|
|
3948
|
+
"SEQ",
|
|
3949
|
+
"INDEX",
|
|
3950
|
+
"TC",
|
|
3951
|
+
"FORMULA",
|
|
3952
|
+
"UNKNOWN",
|
|
3953
|
+
]);
|
|
3954
|
+
|
|
3955
|
+
const FIELD_REFRESH_STATUSES: ReadonlySet<FieldRefreshStatus> = new Set([
|
|
3956
|
+
"current",
|
|
3957
|
+
"stale",
|
|
3958
|
+
"unresolvable",
|
|
3959
|
+
"preserve-only",
|
|
3960
|
+
]);
|
|
3961
|
+
|
|
3962
|
+
const FONT_FAMILY_VALUES: ReadonlySet<string> = new Set([
|
|
3963
|
+
"roman",
|
|
3964
|
+
"swiss",
|
|
3965
|
+
"modern",
|
|
3966
|
+
"script",
|
|
3967
|
+
"decorative",
|
|
3968
|
+
]);
|
|
3969
|
+
|
|
3970
|
+
const FONT_PITCH_VALUES: ReadonlySet<string> = new Set([
|
|
3971
|
+
"fixed",
|
|
3972
|
+
"variable",
|
|
3973
|
+
"default",
|
|
3974
|
+
]);
|
|
3975
|
+
|
|
3976
|
+
function validateFieldRegistry(
|
|
3977
|
+
value: unknown,
|
|
3978
|
+
path: string,
|
|
3979
|
+
issues: ModelValidationIssue[],
|
|
3980
|
+
): void {
|
|
3981
|
+
const record = asPlainObject(value, path, issues);
|
|
3982
|
+
if (!record) {
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
if (!Array.isArray(record.supported)) {
|
|
3987
|
+
issues.push({ path: `${path}.supported`, message: "supported must be an array." });
|
|
3988
|
+
} else {
|
|
3989
|
+
record.supported.forEach((entry, index) => {
|
|
3990
|
+
validateFieldRegistryEntry(entry, `${path}.supported[${index}]`, issues);
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
if (!Array.isArray(record.preserveOnly)) {
|
|
3995
|
+
issues.push({ path: `${path}.preserveOnly`, message: "preserveOnly must be an array." });
|
|
3996
|
+
} else {
|
|
3997
|
+
record.preserveOnly.forEach((entry, index) => {
|
|
3998
|
+
validateFieldRegistryEntry(entry, `${path}.preserveOnly[${index}]`, issues);
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
if (record.tocStructure !== undefined) {
|
|
4003
|
+
validateTocStructure(record.tocStructure, `${path}.tocStructure`, issues);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
function validateFieldRegistryEntry(
|
|
4008
|
+
value: unknown,
|
|
4009
|
+
path: string,
|
|
4010
|
+
issues: ModelValidationIssue[],
|
|
4011
|
+
): void {
|
|
4012
|
+
const record = asPlainObject(value, path, issues);
|
|
4013
|
+
if (!record) {
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
if (!Number.isInteger(record.fieldIndex) || (record.fieldIndex as number) < 0) {
|
|
4018
|
+
issues.push({
|
|
4019
|
+
path: `${path}.fieldIndex`,
|
|
4020
|
+
message: "fieldIndex must be a non-negative integer.",
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
if (
|
|
4025
|
+
typeof record.fieldFamily !== "string" ||
|
|
4026
|
+
!(
|
|
4027
|
+
SUPPORTED_FIELD_FAMILIES.has(record.fieldFamily as SupportedFieldFamily) ||
|
|
4028
|
+
PRESERVE_ONLY_FIELD_FAMILIES.has(record.fieldFamily as PreserveOnlyFieldFamily)
|
|
4029
|
+
)
|
|
4030
|
+
) {
|
|
4031
|
+
issues.push({
|
|
4032
|
+
path: `${path}.fieldFamily`,
|
|
4033
|
+
message: "fieldFamily must be a SupportedFieldFamily or PreserveOnlyFieldFamily.",
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
if (typeof record.supported !== "boolean") {
|
|
4038
|
+
issues.push({ path: `${path}.supported`, message: "supported must be a boolean." });
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
expectString(record.instruction, `${path}.instruction`, issues);
|
|
4042
|
+
|
|
4043
|
+
if (record.fieldTarget !== undefined) {
|
|
4044
|
+
expectString(record.fieldTarget, `${path}.fieldTarget`, issues);
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
if (typeof record.displayText !== "string") {
|
|
4048
|
+
issues.push({ path: `${path}.displayText`, message: "displayText must be a string." });
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
if (!Number.isInteger(record.paragraphIndex) || (record.paragraphIndex as number) < 0) {
|
|
4052
|
+
issues.push({
|
|
4053
|
+
path: `${path}.paragraphIndex`,
|
|
4054
|
+
message: "paragraphIndex must be a non-negative integer.",
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
if (
|
|
4059
|
+
typeof record.refreshStatus !== "string" ||
|
|
4060
|
+
!FIELD_REFRESH_STATUSES.has(record.refreshStatus as FieldRefreshStatus)
|
|
4061
|
+
) {
|
|
4062
|
+
issues.push({
|
|
4063
|
+
path: `${path}.refreshStatus`,
|
|
4064
|
+
message:
|
|
4065
|
+
"refreshStatus must be one of: current, stale, unresolvable, preserve-only.",
|
|
4066
|
+
});
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
function validateTocStructure(
|
|
4071
|
+
value: unknown,
|
|
4072
|
+
path: string,
|
|
4073
|
+
issues: ModelValidationIssue[],
|
|
4074
|
+
): void {
|
|
4075
|
+
const record = asPlainObject(value, path, issues);
|
|
4076
|
+
if (!record) {
|
|
4077
|
+
return;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
expectString(record.instruction, `${path}.instruction`, issues);
|
|
4081
|
+
|
|
4082
|
+
const levelRange = asPlainObject(record.levelRange, `${path}.levelRange`, issues);
|
|
4083
|
+
if (levelRange) {
|
|
4084
|
+
if (!Number.isInteger(levelRange.from) || (levelRange.from as number) < 1) {
|
|
4085
|
+
issues.push({
|
|
4086
|
+
path: `${path}.levelRange.from`,
|
|
4087
|
+
message: "levelRange.from must be an integer ≥ 1.",
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
if (!Number.isInteger(levelRange.to) || (levelRange.to as number) < 1) {
|
|
4091
|
+
issues.push({
|
|
4092
|
+
path: `${path}.levelRange.to`,
|
|
4093
|
+
message: "levelRange.to must be an integer ≥ 1.",
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
if (!Array.isArray(record.entries)) {
|
|
4099
|
+
issues.push({ path: `${path}.entries`, message: "entries must be an array." });
|
|
4100
|
+
} else {
|
|
4101
|
+
record.entries.forEach((entry, index) => {
|
|
4102
|
+
const entryRecord = asPlainObject(entry, `${path}.entries[${index}]`, issues);
|
|
4103
|
+
if (!entryRecord) return;
|
|
4104
|
+
if (typeof entryRecord.text !== "string") {
|
|
4105
|
+
issues.push({
|
|
4106
|
+
path: `${path}.entries[${index}].text`,
|
|
4107
|
+
message: "text must be a string.",
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
if (
|
|
4111
|
+
!Number.isInteger(entryRecord.level) ||
|
|
4112
|
+
(entryRecord.level as number) < 1 ||
|
|
4113
|
+
(entryRecord.level as number) > 9
|
|
4114
|
+
) {
|
|
4115
|
+
issues.push({
|
|
4116
|
+
path: `${path}.entries[${index}].level`,
|
|
4117
|
+
message: "level must be an integer in [1..9].",
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
if (!Number.isInteger(entryRecord.paragraphIndex)) {
|
|
4121
|
+
issues.push({
|
|
4122
|
+
path: `${path}.entries[${index}].paragraphIndex`,
|
|
4123
|
+
message: "paragraphIndex must be an integer.",
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
if (entryRecord.styleId !== undefined) {
|
|
4127
|
+
expectString(entryRecord.styleId, `${path}.entries[${index}].styleId`, issues);
|
|
4128
|
+
}
|
|
4129
|
+
if (entryRecord.bookmarkName !== undefined) {
|
|
4130
|
+
expectString(
|
|
4131
|
+
entryRecord.bookmarkName,
|
|
4132
|
+
`${path}.entries[${index}].bookmarkName`,
|
|
4133
|
+
issues,
|
|
4134
|
+
);
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
if (record.status !== "current" && record.status !== "stale") {
|
|
4140
|
+
issues.push({
|
|
4141
|
+
path: `${path}.status`,
|
|
4142
|
+
message: "status must be 'current' or 'stale'.",
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
function validateCanonicalFontTable(
|
|
4148
|
+
value: unknown,
|
|
4149
|
+
path: string,
|
|
4150
|
+
issues: ModelValidationIssue[],
|
|
4151
|
+
): void {
|
|
4152
|
+
const record = asPlainObject(value, path, issues);
|
|
4153
|
+
if (!record) {
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
const fonts = asPlainObject(record.fonts, `${path}.fonts`, issues);
|
|
4158
|
+
if (!fonts) {
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
for (const [fontKey, fontEntry] of Object.entries(fonts)) {
|
|
4163
|
+
const fontPath = `${path}.fonts[${JSON.stringify(fontKey)}]`;
|
|
4164
|
+
const entry = asPlainObject(fontEntry, fontPath, issues);
|
|
4165
|
+
if (!entry) continue;
|
|
4166
|
+
|
|
4167
|
+
expectString(entry.name, `${fontPath}.name`, issues);
|
|
4168
|
+
|
|
4169
|
+
if (entry.family !== undefined) {
|
|
4170
|
+
if (typeof entry.family !== "string" || !FONT_FAMILY_VALUES.has(entry.family)) {
|
|
4171
|
+
issues.push({
|
|
4172
|
+
path: `${fontPath}.family`,
|
|
4173
|
+
message: "family must be one of: roman, swiss, modern, script, decorative.",
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
if (entry.pitch !== undefined) {
|
|
4179
|
+
if (typeof entry.pitch !== "string" || !FONT_PITCH_VALUES.has(entry.pitch)) {
|
|
4180
|
+
issues.push({
|
|
4181
|
+
path: `${fontPath}.pitch`,
|
|
4182
|
+
message: "pitch must be one of: fixed, variable, default.",
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
if (entry.charset !== undefined && !Number.isInteger(entry.charset)) {
|
|
4188
|
+
issues.push({ path: `${fontPath}.charset`, message: "charset must be an integer." });
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
if (entry.altName !== undefined) {
|
|
4192
|
+
expectString(entry.altName, `${fontPath}.altName`, issues);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
|
|
3797
4197
|
function validateSubPartsCatalog(
|
|
3798
4198
|
value: unknown,
|
|
3799
4199
|
path: string,
|
|
@@ -11,16 +11,13 @@
|
|
|
11
11
|
* overlay-layer.tsx`) re-exports this via a direct import so its public
|
|
12
12
|
* API surface is unchanged.
|
|
13
13
|
*
|
|
14
|
-
* **Coordinate-space
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the two gap constants before any production consumer wires the warm
|
|
22
|
-
* path. Until then the overlay component keeps its DOM-measurement path
|
|
23
|
-
* as the default and treats the geometry path as experimental.
|
|
14
|
+
* **Coordinate-space reconciliation — RESOLVED 2026-04-23
|
|
15
|
+
* (`LAYOUT_ENGINE_VERSION 51 → 52`, refactor/10 Slice L11-3).** Kernel
|
|
16
|
+
* `PAGE_GAP_PX` was bumped from 16 to 48 to match the DOM page-break
|
|
17
|
+
* widget's `interGapPx`. `page.frame.topPx` is now usable as an overlay
|
|
18
|
+
* rect for every page, not just page 0. The overlay component flipped
|
|
19
|
+
* to geometry-as-warm-path; the DOM-measurement branch remains as the
|
|
20
|
+
* cold-open fallback only.
|
|
24
21
|
*/
|
|
25
22
|
|
|
26
23
|
import type { GeometryFacet } from "./geometry-facet.ts";
|