@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `PAGE` and `NUMPAGES` field values per page.
|
|
3
|
+
*
|
|
4
|
+
* These two field families (now in `SupportedFieldFamily`) compute differently
|
|
5
|
+
* from `REF` / `PAGEREF` because the resolved value depends on the *owning
|
|
6
|
+
* page* of each header/footer instance, not on a single document-level
|
|
7
|
+
* computation. A header part is shared across many pages; the same PAGE
|
|
8
|
+
* field must render "1" on page 1, "2" on page 2, and so on.
|
|
9
|
+
*
|
|
10
|
+
* The resolver is a pure function over the page graph — no DOM, no side
|
|
11
|
+
* effects. Consumers (per-page header/footer bands) call
|
|
12
|
+
* `resolvePageFieldDisplayText` at render time to substitute the cached
|
|
13
|
+
* preserve-only text that currently sits inside the paragraph inline atom.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RuntimePageGraph, RuntimePageNode } from "./page-graph.ts";
|
|
17
|
+
import type { SupportedFieldFamily } from "../../model/canonical-document.ts";
|
|
18
|
+
|
|
19
|
+
export interface PageFieldContext {
|
|
20
|
+
/** The page this header/footer copy renders on. */
|
|
21
|
+
page: RuntimePageNode;
|
|
22
|
+
/** The graph the page belongs to (for NUMPAGES resolution). */
|
|
23
|
+
graph: RuntimePageGraph;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the display text for a PAGE / NUMPAGES field on a specific page.
|
|
28
|
+
*
|
|
29
|
+
* Returns the original cached `displayText` for unsupported families so the
|
|
30
|
+
* caller can use this as a single resolution helper.
|
|
31
|
+
*/
|
|
32
|
+
export function resolvePageFieldDisplayText(
|
|
33
|
+
family: SupportedFieldFamily | string,
|
|
34
|
+
cachedDisplayText: string,
|
|
35
|
+
context: PageFieldContext,
|
|
36
|
+
): string {
|
|
37
|
+
switch (family) {
|
|
38
|
+
case "PAGE":
|
|
39
|
+
return String(context.page.stories.displayPageNumber);
|
|
40
|
+
case "NUMPAGES":
|
|
41
|
+
// Blank fillers (evenPage/oddPage section breaks) don't count toward
|
|
42
|
+
// NUMPAGES — the graph already excludes them from contentPageCount.
|
|
43
|
+
return String(context.graph.contentPageCount);
|
|
44
|
+
default:
|
|
45
|
+
return cachedDisplayText;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a lookup keyed by pageId → (fieldFamily → resolvedText). Useful when
|
|
51
|
+
* a renderer wants to bulk-resolve every page's fields before emitting DOM.
|
|
52
|
+
* Absent keys mean no override; the cached field text should be used.
|
|
53
|
+
*/
|
|
54
|
+
export function buildPageFieldResolutionTable(
|
|
55
|
+
graph: RuntimePageGraph,
|
|
56
|
+
families: ReadonlySet<string> = new Set(["PAGE", "NUMPAGES"]),
|
|
57
|
+
): Map<string, Map<string, string>> {
|
|
58
|
+
const result = new Map<string, Map<string, string>>();
|
|
59
|
+
for (const page of graph.pages) {
|
|
60
|
+
const perPage = new Map<string, string>();
|
|
61
|
+
for (const family of families) {
|
|
62
|
+
perPage.set(
|
|
63
|
+
family,
|
|
64
|
+
resolvePageFieldDisplayText(family, "", { page, graph }),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
result.set(page.pageId, perPage);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten header/footer sub-part blocks to short text previews with PAGE /
|
|
3
|
+
* NUMPAGES fields resolved for the owning page. Used by the in-flow page
|
|
4
|
+
* chrome widgets so the bands show live content ("Page 3 of 5") rather
|
|
5
|
+
* than a static "Header" / "Footer" placeholder.
|
|
6
|
+
*
|
|
7
|
+
* This is a **preview**, not a full renderer — we flatten to plain text
|
|
8
|
+
* and cap the length. A per-page band is ~32 px tall; anything more
|
|
9
|
+
* elaborate belongs to the full editing path, which is entered by double-
|
|
10
|
+
* clicking the band.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
BlockNode,
|
|
15
|
+
HeaderDocument,
|
|
16
|
+
FooterDocument,
|
|
17
|
+
InlineNode,
|
|
18
|
+
} from "../../model/canonical-document.ts";
|
|
19
|
+
import type {
|
|
20
|
+
RuntimePageGraph,
|
|
21
|
+
RuntimePageNode,
|
|
22
|
+
} from "./page-graph.ts";
|
|
23
|
+
import { resolvePageFieldDisplayText } from "./resolve-page-fields.ts";
|
|
24
|
+
|
|
25
|
+
const MAX_PREVIEW_CHARS = 140;
|
|
26
|
+
|
|
27
|
+
export interface PagePreviewMaps {
|
|
28
|
+
/** pageId → short text preview of the resolved header story (if any). */
|
|
29
|
+
headerPreviewByPageId: Map<string, string>;
|
|
30
|
+
/** pageId → short text preview of the resolved footer story (if any). */
|
|
31
|
+
footerPreviewByPageId: Map<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildPagePreviewMaps(
|
|
35
|
+
graph: RuntimePageGraph,
|
|
36
|
+
subParts: {
|
|
37
|
+
headers?: ReadonlyArray<HeaderDocument>;
|
|
38
|
+
footers?: ReadonlyArray<FooterDocument>;
|
|
39
|
+
},
|
|
40
|
+
): PagePreviewMaps {
|
|
41
|
+
const headerPreviewByPageId = new Map<string, string>();
|
|
42
|
+
const footerPreviewByPageId = new Map<string, string>();
|
|
43
|
+
|
|
44
|
+
for (const page of graph.pages) {
|
|
45
|
+
if (page.isBlankFiller) continue;
|
|
46
|
+
const headerStory = page.stories.header;
|
|
47
|
+
if (headerStory && headerStory.kind === "header") {
|
|
48
|
+
const source = findSubPart(subParts.headers, headerStory.relationshipId);
|
|
49
|
+
if (source) {
|
|
50
|
+
headerPreviewByPageId.set(
|
|
51
|
+
page.pageId,
|
|
52
|
+
flattenBlocksToPreview(source.blocks, page, graph),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const footerStory = page.stories.footer;
|
|
57
|
+
if (footerStory && footerStory.kind === "footer") {
|
|
58
|
+
const source = findSubPart(subParts.footers, footerStory.relationshipId);
|
|
59
|
+
if (source) {
|
|
60
|
+
footerPreviewByPageId.set(
|
|
61
|
+
page.pageId,
|
|
62
|
+
flattenBlocksToPreview(source.blocks, page, graph),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { headerPreviewByPageId, footerPreviewByPageId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findSubPart<T extends HeaderDocument | FooterDocument>(
|
|
72
|
+
parts: ReadonlyArray<T> | undefined,
|
|
73
|
+
relationshipId: string,
|
|
74
|
+
): T | undefined {
|
|
75
|
+
if (!parts) return undefined;
|
|
76
|
+
return parts.find((p) => p.relationshipId === relationshipId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function flattenBlocksToPreview(
|
|
80
|
+
blocks: ReadonlyArray<BlockNode>,
|
|
81
|
+
page: RuntimePageNode,
|
|
82
|
+
graph: RuntimePageGraph,
|
|
83
|
+
): string {
|
|
84
|
+
const parts: string[] = [];
|
|
85
|
+
for (const block of blocks) {
|
|
86
|
+
collectBlockText(block, page, graph, parts);
|
|
87
|
+
if (joinPreview(parts).length >= MAX_PREVIEW_CHARS) break;
|
|
88
|
+
}
|
|
89
|
+
return truncate(joinPreview(parts));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectBlockText(
|
|
93
|
+
block: BlockNode,
|
|
94
|
+
page: RuntimePageNode,
|
|
95
|
+
graph: RuntimePageGraph,
|
|
96
|
+
out: string[],
|
|
97
|
+
): void {
|
|
98
|
+
switch (block.type) {
|
|
99
|
+
case "paragraph":
|
|
100
|
+
for (const child of block.children) {
|
|
101
|
+
collectInlineText(child, page, graph, out);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case "table":
|
|
105
|
+
for (const row of block.rows) {
|
|
106
|
+
for (const cell of row.cells) {
|
|
107
|
+
for (const childBlock of cell.children) {
|
|
108
|
+
collectBlockText(childBlock, page, graph, out);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "sdt":
|
|
114
|
+
for (const child of block.children) {
|
|
115
|
+
collectBlockText(child, page, graph, out);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
// opaque_block / section_break / custom_xml / alt_chunk — no preview text.
|
|
119
|
+
default:
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectInlineText(
|
|
125
|
+
inline: InlineNode,
|
|
126
|
+
page: RuntimePageNode,
|
|
127
|
+
graph: RuntimePageGraph,
|
|
128
|
+
out: string[],
|
|
129
|
+
): void {
|
|
130
|
+
switch (inline.type) {
|
|
131
|
+
case "text":
|
|
132
|
+
out.push(inline.text);
|
|
133
|
+
break;
|
|
134
|
+
case "tab":
|
|
135
|
+
out.push(" ");
|
|
136
|
+
break;
|
|
137
|
+
case "hard_break":
|
|
138
|
+
out.push(" ");
|
|
139
|
+
break;
|
|
140
|
+
case "field": {
|
|
141
|
+
const family = inline.fieldFamily ?? classifyFieldInstructionLocal(inline.instruction);
|
|
142
|
+
const cached = flattenInline(inline.children);
|
|
143
|
+
if (family === "PAGE" || family === "NUMPAGES") {
|
|
144
|
+
out.push(
|
|
145
|
+
resolvePageFieldDisplayText(family, cached, { page, graph }),
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
out.push(cached);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "hyperlink":
|
|
153
|
+
for (const child of inline.children) {
|
|
154
|
+
collectInlineText(child, page, graph, out);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
// bookmark_start/end, opaque_inline, note_ref etc — skip.
|
|
158
|
+
default:
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function flattenInline(inlines: ReadonlyArray<InlineNode> | undefined): string {
|
|
164
|
+
if (!inlines) return "";
|
|
165
|
+
let buf = "";
|
|
166
|
+
for (const inline of inlines) {
|
|
167
|
+
if (inline.type === "text") buf += inline.text;
|
|
168
|
+
else if (inline.type === "hard_break" || inline.type === "tab") buf += " ";
|
|
169
|
+
}
|
|
170
|
+
return buf;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function classifyFieldInstructionLocal(instr: string): string {
|
|
174
|
+
const match = /^\s*(\w+)/.exec(instr);
|
|
175
|
+
return match ? match[1]!.toUpperCase() : "UNKNOWN";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function joinPreview(parts: readonly string[]): string {
|
|
179
|
+
return parts.join("").replace(/\s+/g, " ").trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function truncate(s: string): string {
|
|
183
|
+
if (s.length <= MAX_PREVIEW_CHARS) return s;
|
|
184
|
+
return s.slice(0, MAX_PREVIEW_CHARS - 1).trimEnd() + "…";
|
|
185
|
+
}
|
|
@@ -151,11 +151,11 @@ export function resolveBlockFormatting(
|
|
|
151
151
|
fontSizeHalfPoints: fontInfo.fontSizeHalfPoints,
|
|
152
152
|
averageCharWidthTwips: fontInfo.avgCharWidth,
|
|
153
153
|
tabStops: resolveTabStops(block),
|
|
154
|
-
keepNext: Boolean(block.keepNext),
|
|
155
|
-
keepLines: Boolean(block.keepLines),
|
|
156
|
-
pageBreakBefore: Boolean(block.pageBreakBefore),
|
|
157
|
-
widowControl: block.widowControl !== false, // default true in Word
|
|
158
|
-
contextualSpacing: Boolean(block.contextualSpacing),
|
|
154
|
+
keepNext: Boolean(block.keepNext ?? block.resolvedParagraphFormatting?.keepNext),
|
|
155
|
+
keepLines: Boolean(block.keepLines ?? block.resolvedParagraphFormatting?.keepLines),
|
|
156
|
+
pageBreakBefore: Boolean(block.pageBreakBefore ?? block.resolvedParagraphFormatting?.pageBreakBefore),
|
|
157
|
+
widowControl: (block.widowControl ?? block.resolvedParagraphFormatting?.widowControl) !== false, // default true in Word
|
|
158
|
+
contextualSpacing: Boolean(block.contextualSpacing ?? block.resolvedParagraphFormatting?.contextualSpacing),
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -166,9 +166,10 @@ export function resolveBlockFormatting(
|
|
|
166
166
|
function resolveSpacing(
|
|
167
167
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
168
168
|
): ParagraphSpacing {
|
|
169
|
-
//
|
|
169
|
+
// Priority: numbering geometry > direct paragraph > style cascade
|
|
170
170
|
const numbering = block.resolvedNumbering?.geometry.spacing;
|
|
171
171
|
const direct = block.spacing;
|
|
172
|
+
const cascade = block.resolvedParagraphFormatting?.spacing;
|
|
172
173
|
|
|
173
174
|
const normalizeLineRule = (
|
|
174
175
|
rule: string | undefined,
|
|
@@ -177,21 +178,15 @@ function resolveSpacing(
|
|
|
177
178
|
return undefined;
|
|
178
179
|
};
|
|
179
180
|
|
|
180
|
-
if (numbering) {
|
|
181
|
+
if (numbering || direct || cascade) {
|
|
181
182
|
return {
|
|
182
|
-
before: numbering
|
|
183
|
-
after: numbering
|
|
184
|
-
line: numbering
|
|
185
|
-
lineRule:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (direct) {
|
|
190
|
-
return {
|
|
191
|
-
before: direct.before,
|
|
192
|
-
after: direct.after,
|
|
193
|
-
line: direct.line,
|
|
194
|
-
lineRule: normalizeLineRule(direct.lineRule),
|
|
183
|
+
before: numbering?.before ?? direct?.before ?? cascade?.before,
|
|
184
|
+
after: numbering?.after ?? direct?.after ?? cascade?.after,
|
|
185
|
+
line: numbering?.line ?? direct?.line ?? cascade?.line,
|
|
186
|
+
lineRule:
|
|
187
|
+
normalizeLineRule(numbering?.lineRule) ??
|
|
188
|
+
normalizeLineRule(direct?.lineRule) ??
|
|
189
|
+
normalizeLineRule(cascade?.lineRule),
|
|
195
190
|
};
|
|
196
191
|
}
|
|
197
192
|
|
|
@@ -228,16 +223,25 @@ function resolveIndentation(
|
|
|
228
223
|
};
|
|
229
224
|
}
|
|
230
225
|
|
|
231
|
-
const
|
|
232
|
-
|
|
226
|
+
const direct = block.indentation;
|
|
227
|
+
const cascade = block.resolvedParagraphFormatting?.indentation;
|
|
228
|
+
|
|
229
|
+
if (!direct && !cascade) {
|
|
233
230
|
return { left: 0, right: 0, firstLine: 0, hanging: 0 };
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
return {
|
|
237
|
-
left:
|
|
238
|
-
right:
|
|
239
|
-
firstLine:
|
|
240
|
-
hanging:
|
|
234
|
+
left: direct?.left ?? cascade?.left ?? 0,
|
|
235
|
+
right: direct?.right ?? cascade?.right ?? 0,
|
|
236
|
+
firstLine: direct?.firstLine ?? cascade?.firstLine ?? 0,
|
|
237
|
+
hanging:
|
|
238
|
+
direct?.hanging ??
|
|
239
|
+
cascade?.hanging ??
|
|
240
|
+
(typeof direct?.firstLine === "number" && direct.firstLine < 0
|
|
241
|
+
? Math.abs(direct.firstLine)
|
|
242
|
+
: typeof cascade?.firstLine === "number" && cascade.firstLine < 0
|
|
243
|
+
? Math.abs(cascade.firstLine)
|
|
244
|
+
: 0),
|
|
241
245
|
};
|
|
242
246
|
}
|
|
243
247
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table render plan (P3e).
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/reference/docx/runtime-rendering-and-chrome-phase.md` §1, the
|
|
5
|
+
* render kernel consumes a `TableRenderPlan` per table per page so the
|
|
6
|
+
* chrome can:
|
|
7
|
+
* - render band-aware cell styling without pre-flattening into inline
|
|
8
|
+
* CSS strings,
|
|
9
|
+
* - position column-resize grips at logical column edges without
|
|
10
|
+
* re-measuring the DOM,
|
|
11
|
+
* - show repeated header rows on continuation pages (when row-level
|
|
12
|
+
* pagination lands; today the field is empty),
|
|
13
|
+
* - walk vertical-merge chains for merged-cell hit-testing.
|
|
14
|
+
*
|
|
15
|
+
* The plan is a pure function of the table block + resolved style
|
|
16
|
+
* resolution + gridColumns + the effective page column width. It does
|
|
17
|
+
* not read DOM or mutate state.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
SurfaceBlockSnapshot,
|
|
22
|
+
SurfaceTableCellSnapshot,
|
|
23
|
+
SurfaceTableRowSnapshot,
|
|
24
|
+
} from "../../api/public-types";
|
|
25
|
+
import type { TableStyleConditionalRegion } from "../../model/canonical-document.ts";
|
|
26
|
+
import type { ResolvedTableStyleResolution } from "../table-style-resolver.ts";
|
|
27
|
+
|
|
28
|
+
// ─── Public shapes ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export type TableBandRegion = TableStyleConditionalRegion;
|
|
31
|
+
|
|
32
|
+
export interface TableCellBandAssignment {
|
|
33
|
+
rowIndex: number;
|
|
34
|
+
columnIndex: number;
|
|
35
|
+
regions: readonly TableBandRegion[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TableRowBandAssignment {
|
|
39
|
+
rowIndex: number;
|
|
40
|
+
regions: readonly TableBandRegion[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableBandClasses {
|
|
44
|
+
/** Active regions per row (firstRow, lastRow, band1Horz, band2Horz). */
|
|
45
|
+
rows: readonly TableRowBandAssignment[];
|
|
46
|
+
/** Active regions per cell (row regions ∪ column regions). */
|
|
47
|
+
cells: readonly TableCellBandAssignment[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VerticalMergeRef {
|
|
51
|
+
/** Logical column the chain lives in. */
|
|
52
|
+
columnIndex: number;
|
|
53
|
+
/** Row where the chain starts (verticalMerge: "restart"). */
|
|
54
|
+
startRowIndex: number;
|
|
55
|
+
/** Inclusive row where the chain ends. */
|
|
56
|
+
endRowIndex: number;
|
|
57
|
+
/** Horizontal span of the merged cell. */
|
|
58
|
+
columnSpan: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RepeatedHeaderRowRef {
|
|
62
|
+
/** Row index in the source table whose header is repeated. */
|
|
63
|
+
sourceRowIndex: number;
|
|
64
|
+
/** Virtual fragment id the render kernel uses to address this repeat. */
|
|
65
|
+
virtualFragmentId: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ColumnResizeHandle {
|
|
69
|
+
/** Logical column index the handle sits at (0-based, right edge of column i). */
|
|
70
|
+
columnIndex: number;
|
|
71
|
+
/** X-origin of the handle in twips, measured from the table's left edge. */
|
|
72
|
+
originTwips: number;
|
|
73
|
+
/** Handle height in twips (table visual height). */
|
|
74
|
+
heightTwips: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TableRenderPlan {
|
|
78
|
+
blockId: string;
|
|
79
|
+
pageIndex: number;
|
|
80
|
+
/** Logical column widths in twips (may be scaled from canonical). */
|
|
81
|
+
columnsTwips: readonly number[];
|
|
82
|
+
/** Band-class assignments derived from resolved table style. */
|
|
83
|
+
bandClasses: TableBandClasses;
|
|
84
|
+
/** Vertical-merge chains in the visible table range. */
|
|
85
|
+
verticalMerges: readonly VerticalMergeRef[];
|
|
86
|
+
/** Header rows repeated on this page (empty until row-level split lands). */
|
|
87
|
+
repeatedHeaderRows: readonly RepeatedHeaderRowRef[];
|
|
88
|
+
/** Column-resize handle origins for chrome grip placement. */
|
|
89
|
+
columnResizeHandles: readonly ColumnResizeHandle[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Builder ────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export interface BuildTableRenderPlanInput {
|
|
95
|
+
blockId: string;
|
|
96
|
+
pageIndex: number;
|
|
97
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>;
|
|
98
|
+
resolved: ResolvedTableStyleResolution;
|
|
99
|
+
/** Total height of the table in twips (for grip geometry). */
|
|
100
|
+
tableHeightTwips: number;
|
|
101
|
+
/** If `columnsTwips` was scaled to the canvas width, pass the scale
|
|
102
|
+
* factor here; otherwise leave undefined. */
|
|
103
|
+
columnsTwipsScale?: number;
|
|
104
|
+
/**
|
|
105
|
+
* R3: true when this plan is for a continuation page of a multi-page
|
|
106
|
+
* table (i.e. a page other than the one the table first appears on).
|
|
107
|
+
* When true, header rows from the source table are emitted in
|
|
108
|
+
* `repeatedHeaderRows` so the consumer can prepend them on render.
|
|
109
|
+
*/
|
|
110
|
+
isContinuationPage?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildTableRenderPlan(
|
|
114
|
+
input: BuildTableRenderPlanInput,
|
|
115
|
+
): TableRenderPlan {
|
|
116
|
+
const { blockId, pageIndex, block, resolved, tableHeightTwips } = input;
|
|
117
|
+
const scale = input.columnsTwipsScale ?? 1;
|
|
118
|
+
const columnsTwips = block.gridColumns.map((w) => Math.round(w * scale));
|
|
119
|
+
|
|
120
|
+
const bandClasses = buildBandClasses(block.rows, resolved);
|
|
121
|
+
const verticalMerges = collectVerticalMerges(block.rows);
|
|
122
|
+
// R3: header rows are repeated on every continuation page. When the
|
|
123
|
+
// render-plan builder is invoked for a page with `isContinuationPage`,
|
|
124
|
+
// every row with `isHeader === true` gets an entry in repeatedHeaderRows.
|
|
125
|
+
// First-page plans (or tables that fit on a single page) return an empty
|
|
126
|
+
// array. `virtualFragmentId` is deterministic so consumers can key off it.
|
|
127
|
+
const repeatedHeaderRows: RepeatedHeaderRowRef[] = input.isContinuationPage
|
|
128
|
+
? block.rows
|
|
129
|
+
.map((row, rowIndex) => ({ row, rowIndex }))
|
|
130
|
+
.filter(({ row }) => row.isHeader === true)
|
|
131
|
+
.map(({ rowIndex }) => ({
|
|
132
|
+
sourceRowIndex: rowIndex,
|
|
133
|
+
virtualFragmentId: `fragment-${blockId}-vheader-p${pageIndex}-r${rowIndex}`,
|
|
134
|
+
}))
|
|
135
|
+
: [];
|
|
136
|
+
const columnResizeHandles = buildColumnResizeHandles(columnsTwips, tableHeightTwips);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
blockId,
|
|
140
|
+
pageIndex,
|
|
141
|
+
columnsTwips,
|
|
142
|
+
bandClasses,
|
|
143
|
+
verticalMerges,
|
|
144
|
+
repeatedHeaderRows,
|
|
145
|
+
columnResizeHandles,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Internals ───────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function buildBandClasses(
|
|
152
|
+
rows: readonly SurfaceTableRowSnapshot[],
|
|
153
|
+
resolved: ResolvedTableStyleResolution,
|
|
154
|
+
): TableBandClasses {
|
|
155
|
+
const rowAssignments: TableRowBandAssignment[] = [];
|
|
156
|
+
const cellAssignments: TableCellBandAssignment[] = [];
|
|
157
|
+
|
|
158
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
159
|
+
const resolvedRow = resolved.rows[rowIndex];
|
|
160
|
+
const rowRegions = resolvedRow?.style.activeConditionalRegions ?? [];
|
|
161
|
+
rowAssignments.push({ rowIndex, regions: rowRegions });
|
|
162
|
+
|
|
163
|
+
const resolvedCells = resolvedRow?.cells ?? [];
|
|
164
|
+
const sourceRow = rows[rowIndex]!;
|
|
165
|
+
let columnCursor = sourceRow.gridBefore ?? 0;
|
|
166
|
+
for (let cellIndex = 0; cellIndex < sourceRow.cells.length; cellIndex += 1) {
|
|
167
|
+
const cell = sourceRow.cells[cellIndex]!;
|
|
168
|
+
const columnSpan = Math.max(1, cell.colspan ?? 1);
|
|
169
|
+
const resolvedCell = resolvedCells[cellIndex];
|
|
170
|
+
const regions = resolvedCell?.activeConditionalRegions ?? [];
|
|
171
|
+
cellAssignments.push({
|
|
172
|
+
rowIndex,
|
|
173
|
+
columnIndex: columnCursor,
|
|
174
|
+
regions,
|
|
175
|
+
});
|
|
176
|
+
columnCursor += columnSpan;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { rows: rowAssignments, cells: cellAssignments };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectVerticalMerges(
|
|
184
|
+
rows: readonly SurfaceTableRowSnapshot[],
|
|
185
|
+
): VerticalMergeRef[] {
|
|
186
|
+
const merges: VerticalMergeRef[] = [];
|
|
187
|
+
// Track active chains by column: maps startColumn → { startRow, span }.
|
|
188
|
+
const active = new Map<number, { startRow: number; span: number }>();
|
|
189
|
+
|
|
190
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
|
|
191
|
+
const row = rows[rowIndex]!;
|
|
192
|
+
let columnCursor = row.gridBefore ?? 0;
|
|
193
|
+
const seenThisRow = new Set<number>();
|
|
194
|
+
|
|
195
|
+
for (const cell of row.cells) {
|
|
196
|
+
const span = Math.max(1, cell.colspan ?? 1);
|
|
197
|
+
if (cell.verticalMerge === "restart") {
|
|
198
|
+
active.set(columnCursor, { startRow: rowIndex, span });
|
|
199
|
+
seenThisRow.add(columnCursor);
|
|
200
|
+
} else if (cell.verticalMerge === "continue") {
|
|
201
|
+
seenThisRow.add(columnCursor);
|
|
202
|
+
// chain stays open; ended rowIndex extends via the flush below
|
|
203
|
+
}
|
|
204
|
+
columnCursor += span;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Flush chains that did NOT appear as a continue in this row.
|
|
208
|
+
for (const [startColumn, chain] of [...active.entries()]) {
|
|
209
|
+
if (!seenThisRow.has(startColumn)) {
|
|
210
|
+
merges.push({
|
|
211
|
+
columnIndex: startColumn,
|
|
212
|
+
startRowIndex: chain.startRow,
|
|
213
|
+
endRowIndex: rowIndex - 1,
|
|
214
|
+
columnSpan: chain.span,
|
|
215
|
+
});
|
|
216
|
+
active.delete(startColumn);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Flush any still-open chains at end of table.
|
|
222
|
+
for (const [startColumn, chain] of active.entries()) {
|
|
223
|
+
merges.push({
|
|
224
|
+
columnIndex: startColumn,
|
|
225
|
+
startRowIndex: chain.startRow,
|
|
226
|
+
endRowIndex: rows.length - 1,
|
|
227
|
+
columnSpan: chain.span,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return merges;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildColumnResizeHandles(
|
|
235
|
+
columnsTwips: readonly number[],
|
|
236
|
+
heightTwips: number,
|
|
237
|
+
): ColumnResizeHandle[] {
|
|
238
|
+
const handles: ColumnResizeHandle[] = [];
|
|
239
|
+
let cursor = 0;
|
|
240
|
+
for (let i = 0; i < columnsTwips.length - 1; i += 1) {
|
|
241
|
+
cursor += columnsTwips[i] ?? 0;
|
|
242
|
+
handles.push({
|
|
243
|
+
columnIndex: i,
|
|
244
|
+
originTwips: cursor,
|
|
245
|
+
heightTwips,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return handles;
|
|
249
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CanonicalRunFormatting,
|
|
2
3
|
NumberingCatalog,
|
|
3
4
|
NumberingLevelDefinition,
|
|
4
5
|
ParagraphNode,
|
|
@@ -23,6 +24,7 @@ export interface NumberingPrefixResult {
|
|
|
23
24
|
paragraphStyleId?: string;
|
|
24
25
|
isLegalNumbering?: boolean;
|
|
25
26
|
geometry: ResolvedNumberingGeometry;
|
|
27
|
+
markerRunProperties?: CanonicalRunFormatting;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export interface NumberingPrefixResolver {
|
|
@@ -90,6 +92,9 @@ export function createNumberingPrefixResolver(
|
|
|
90
92
|
? { paragraphStyleId: resolved.effectiveLevel.paragraphStyleId }
|
|
91
93
|
: {}),
|
|
92
94
|
...(resolved.effectiveLevel.isLegalNumbering ? { isLegalNumbering: true } : {}),
|
|
95
|
+
...(resolved.geometry.markerRunProperties
|
|
96
|
+
? { markerRunProperties: resolved.geometry.markerRunProperties }
|
|
97
|
+
: {}),
|
|
93
98
|
geometry: resolved.geometry,
|
|
94
99
|
};
|
|
95
100
|
}
|