@beyondwork/docx-react-component 1.0.60 → 1.0.61
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/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-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
|
@@ -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) {
|
|
@@ -197,6 +197,7 @@ export function buildPageStackWithSplits(
|
|
|
197
197
|
mainSurface: EditorSurfaceSnapshot,
|
|
198
198
|
measurementProvider?: LayoutMeasurementProvider,
|
|
199
199
|
): PageStackResultWithSplits {
|
|
200
|
+
const defaultTabInterval = document.subParts?.settings?.defaultTabStop ?? 720;
|
|
200
201
|
const pages: DocumentPageSnapshot[] = [];
|
|
201
202
|
const splitsByBlock = new Map<string, ParagraphLineSlice[]>();
|
|
202
203
|
// P8.1b — aggregate note allocations and fragments across all sections,
|
|
@@ -270,6 +271,7 @@ export function buildPageStackWithSplits(
|
|
|
270
271
|
document.subParts?.footnoteCollection,
|
|
271
272
|
measurementProvider,
|
|
272
273
|
cache,
|
|
274
|
+
defaultTabInterval,
|
|
273
275
|
);
|
|
274
276
|
const paginated = paginatedResult.pages;
|
|
275
277
|
|
|
@@ -795,6 +797,7 @@ function measureBlockHeight(
|
|
|
795
797
|
columnWidth: number,
|
|
796
798
|
measurementProvider?: LayoutMeasurementProvider,
|
|
797
799
|
cache?: MeasurementCache,
|
|
800
|
+
defaultTabInterval = 720,
|
|
798
801
|
): number {
|
|
799
802
|
if (!block) return 0;
|
|
800
803
|
|
|
@@ -804,7 +807,7 @@ function measureBlockHeight(
|
|
|
804
807
|
const compute = (): number => {
|
|
805
808
|
switch (block.kind) {
|
|
806
809
|
case "paragraph": {
|
|
807
|
-
const formatting = resolveBlockFormatting(block);
|
|
810
|
+
const formatting = resolveBlockFormatting(block, defaultTabInterval);
|
|
808
811
|
if (formatting) {
|
|
809
812
|
// Provider path: sum per-line heights so canvas-backed measurements
|
|
810
813
|
// that emit variable line heights (mixed inline font sizes, etc.)
|
|
@@ -837,14 +840,26 @@ function measureBlockHeight(
|
|
|
837
840
|
return estimateBlockHeight(block, columnWidth);
|
|
838
841
|
}
|
|
839
842
|
case "table":
|
|
840
|
-
return measureTableHeight(
|
|
843
|
+
return measureTableHeight(
|
|
844
|
+
block,
|
|
845
|
+
columnWidth,
|
|
846
|
+
measurementProvider,
|
|
847
|
+
cache,
|
|
848
|
+
defaultTabInterval,
|
|
849
|
+
);
|
|
841
850
|
case "sdt_block":
|
|
842
851
|
return Math.max(
|
|
843
852
|
MIN_BLOCK_HEIGHT_TWIPS,
|
|
844
853
|
block.children.reduce(
|
|
845
854
|
(total, child) =>
|
|
846
855
|
total +
|
|
847
|
-
measureBlockHeight(
|
|
856
|
+
measureBlockHeight(
|
|
857
|
+
child,
|
|
858
|
+
columnWidth,
|
|
859
|
+
measurementProvider,
|
|
860
|
+
cache,
|
|
861
|
+
defaultTabInterval,
|
|
862
|
+
),
|
|
848
863
|
0,
|
|
849
864
|
),
|
|
850
865
|
);
|
|
@@ -875,6 +890,7 @@ function measureTableHeight(
|
|
|
875
890
|
columnWidth: number,
|
|
876
891
|
measurementProvider?: LayoutMeasurementProvider,
|
|
877
892
|
cache?: MeasurementCache,
|
|
893
|
+
defaultTabInterval = 720,
|
|
878
894
|
): number {
|
|
879
895
|
const TABLE_ROW_PADDING_TWIPS = 120;
|
|
880
896
|
let totalHeight = 0;
|
|
@@ -920,6 +936,7 @@ function measureTableHeight(
|
|
|
920
936
|
cellWidth,
|
|
921
937
|
measurementProvider,
|
|
922
938
|
cache,
|
|
939
|
+
defaultTabInterval,
|
|
923
940
|
);
|
|
924
941
|
}
|
|
925
942
|
contentHeight = Math.max(contentHeight, cellContentHeight + TABLE_ROW_PADDING_TWIPS);
|
|
@@ -1091,8 +1108,7 @@ function resolveTabAdvance(
|
|
|
1091
1108
|
avgCharWidth: number,
|
|
1092
1109
|
columnWidth: number,
|
|
1093
1110
|
): number {
|
|
1094
|
-
|
|
1095
|
-
const defaultTabInterval = 720;
|
|
1111
|
+
const defaultTabInterval = formatting.defaultTabInterval;
|
|
1096
1112
|
const currentPosition = currentChars * avgCharWidth + formatting.indentLeft;
|
|
1097
1113
|
|
|
1098
1114
|
if (formatting.tabStops.length === 0) {
|
|
@@ -1156,6 +1172,7 @@ function paginateSectionBlocks(
|
|
|
1156
1172
|
footnotes: FootnoteCollection | undefined,
|
|
1157
1173
|
measurementProvider?: LayoutMeasurementProvider,
|
|
1158
1174
|
cache?: MeasurementCache,
|
|
1175
|
+
defaultTabInterval = 720,
|
|
1159
1176
|
): Omit<DocumentPageSnapshot, "pageIndex">[] {
|
|
1160
1177
|
return paginateSectionBlocksWithSplits(
|
|
1161
1178
|
section,
|
|
@@ -1164,6 +1181,7 @@ function paginateSectionBlocks(
|
|
|
1164
1181
|
footnotes,
|
|
1165
1182
|
measurementProvider,
|
|
1166
1183
|
cache,
|
|
1184
|
+
defaultTabInterval,
|
|
1167
1185
|
).pages;
|
|
1168
1186
|
}
|
|
1169
1187
|
|
|
@@ -1174,6 +1192,7 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1174
1192
|
footnotes: FootnoteCollection | undefined,
|
|
1175
1193
|
measurementProvider?: LayoutMeasurementProvider,
|
|
1176
1194
|
cache?: MeasurementCache,
|
|
1195
|
+
defaultTabInterval = 720,
|
|
1177
1196
|
): SectionPaginationResult {
|
|
1178
1197
|
if (blocks.length === 0) {
|
|
1179
1198
|
return {
|
|
@@ -1329,18 +1348,32 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1329
1348
|
const columnWidth =
|
|
1330
1349
|
columnMetrics[Math.min(columnIndex, columnMetrics.length - 1)]?.width ??
|
|
1331
1350
|
getUsableColumnWidth(layout);
|
|
1332
|
-
const baseHeight = measureBlockHeight(
|
|
1351
|
+
const baseHeight = measureBlockHeight(
|
|
1352
|
+
block,
|
|
1353
|
+
columnWidth,
|
|
1354
|
+
measurementProvider,
|
|
1355
|
+
cache,
|
|
1356
|
+
defaultTabInterval,
|
|
1357
|
+
);
|
|
1333
1358
|
|
|
1334
1359
|
// keepNext: this paragraph must stay with the next one on the same page
|
|
1335
1360
|
const keepWithNextHeight =
|
|
1336
1361
|
block.kind === "paragraph" && block.keepNext
|
|
1337
1362
|
? baseHeight +
|
|
1338
|
-
measureBlockHeight(
|
|
1363
|
+
measureBlockHeight(
|
|
1364
|
+
blocks[index + 1],
|
|
1365
|
+
columnWidth,
|
|
1366
|
+
measurementProvider,
|
|
1367
|
+
cache,
|
|
1368
|
+
defaultTabInterval,
|
|
1369
|
+
)
|
|
1339
1370
|
: baseHeight;
|
|
1340
1371
|
|
|
1341
1372
|
// keepLines: the entire paragraph must fit on one page.
|
|
1342
1373
|
// If it doesn't fit and there's already content on this page, break before it.
|
|
1343
|
-
const formatting = block.kind === "paragraph"
|
|
1374
|
+
const formatting = block.kind === "paragraph"
|
|
1375
|
+
? resolveBlockFormatting(block, defaultTabInterval)
|
|
1376
|
+
: null;
|
|
1344
1377
|
const keepLinesActive = formatting?.keepLines ?? false;
|
|
1345
1378
|
|
|
1346
1379
|
const noteHeight = estimateFootnoteReservation(block, footnotes, columnWidth, reservedNotes);
|
|
@@ -122,13 +122,14 @@ export function buildResolvedFormattingState(
|
|
|
122
122
|
const usedFamilies = new Set<string>();
|
|
123
123
|
|
|
124
124
|
const theme = document.subParts?.resolvedTheme;
|
|
125
|
-
|
|
125
|
+
const tabs = buildTabSettings(document.subParts?.settings);
|
|
126
|
+
collectFormatting(mainSurface.blocks, paragraphs, runs, usedFamilies, theme, tabs.defaultTabInterval);
|
|
126
127
|
|
|
127
128
|
return {
|
|
128
129
|
paragraphs,
|
|
129
130
|
runs,
|
|
130
131
|
fonts: buildFontCatalog(usedFamilies, document.subParts),
|
|
131
|
-
tabs
|
|
132
|
+
tabs,
|
|
132
133
|
numbering: buildNumberingGeometry(document.subParts),
|
|
133
134
|
settings: buildDocumentSettings(document.subParts?.settings),
|
|
134
135
|
revision: revisionCounter,
|
|
@@ -141,11 +142,12 @@ function collectFormatting(
|
|
|
141
142
|
runs: Map<RunId, ResolvedRunFormatting>,
|
|
142
143
|
usedFamilies: Set<string>,
|
|
143
144
|
theme: ResolvedTheme | undefined,
|
|
145
|
+
defaultTabInterval: number,
|
|
144
146
|
): void {
|
|
145
147
|
for (const block of blocks) {
|
|
146
148
|
switch (block.kind) {
|
|
147
149
|
case "paragraph": {
|
|
148
|
-
const formatting = resolveBlockFormatting(block);
|
|
150
|
+
const formatting = resolveBlockFormatting(block, defaultTabInterval);
|
|
149
151
|
if (formatting) {
|
|
150
152
|
paragraphs.set(block.blockId, formatting);
|
|
151
153
|
}
|
|
@@ -163,12 +165,12 @@ function collectFormatting(
|
|
|
163
165
|
case "table":
|
|
164
166
|
for (const row of block.rows) {
|
|
165
167
|
for (const cell of row.cells) {
|
|
166
|
-
collectFormatting(cell.content, paragraphs, runs, usedFamilies, theme);
|
|
168
|
+
collectFormatting(cell.content, paragraphs, runs, usedFamilies, theme, defaultTabInterval);
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
break;
|
|
170
172
|
case "sdt_block":
|
|
171
|
-
collectFormatting(block.children, paragraphs, runs, usedFamilies, theme);
|
|
173
|
+
collectFormatting(block.children, paragraphs, runs, usedFamilies, theme, defaultTabInterval);
|
|
172
174
|
break;
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -244,11 +246,11 @@ function buildFontCatalog(
|
|
|
244
246
|
};
|
|
245
247
|
}
|
|
246
248
|
|
|
247
|
-
function buildTabSettings(
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
function buildTabSettings(
|
|
250
|
+
settings: DocumentSettings | undefined,
|
|
251
|
+
): ResolvedTabSettings {
|
|
250
252
|
return {
|
|
251
|
-
defaultTabInterval: 720,
|
|
253
|
+
defaultTabInterval: settings?.defaultTabStop ?? 720,
|
|
252
254
|
};
|
|
253
255
|
}
|
|
254
256
|
|
|
@@ -100,6 +100,8 @@ export interface ResolvedParagraphFormatting {
|
|
|
100
100
|
averageCharWidthTwips: number;
|
|
101
101
|
/** Effective tab stops sorted by position. */
|
|
102
102
|
tabStops: LayoutTabStop[];
|
|
103
|
+
/** Document default tab interval in twips. */
|
|
104
|
+
defaultTabInterval: number;
|
|
103
105
|
/** Keep with next paragraph. */
|
|
104
106
|
keepNext: boolean;
|
|
105
107
|
/** Keep all lines of this paragraph on the same page. */
|
|
@@ -140,6 +142,7 @@ export interface ResolvedTableRowFormatting {
|
|
|
140
142
|
|
|
141
143
|
export function resolveBlockFormatting(
|
|
142
144
|
block: SurfaceBlockSnapshot,
|
|
145
|
+
defaultTabInterval = 720,
|
|
143
146
|
): ResolvedParagraphFormatting | null {
|
|
144
147
|
if (block.kind !== "paragraph") {
|
|
145
148
|
return null;
|
|
@@ -163,6 +166,7 @@ export function resolveBlockFormatting(
|
|
|
163
166
|
fontSizeHalfPoints: fontInfo.fontSizeHalfPoints,
|
|
164
167
|
averageCharWidthTwips: fontInfo.avgCharWidth,
|
|
165
168
|
tabStops: resolveTabStops(block),
|
|
169
|
+
defaultTabInterval,
|
|
166
170
|
keepNext: Boolean(block.keepNext ?? block.resolvedParagraphFormatting?.keepNext),
|
|
167
171
|
keepLines: Boolean(block.keepLines ?? block.resolvedParagraphFormatting?.keepLines),
|
|
168
172
|
pageBreakBefore: Boolean(block.pageBreakBefore ?? block.resolvedParagraphFormatting?.pageBreakBefore),
|
|
@@ -144,8 +144,10 @@ function advanceSequence(
|
|
|
144
144
|
currentLevel: number,
|
|
145
145
|
levelDefinitions: Map<number, NumberingLevelDefinition>,
|
|
146
146
|
): void {
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
for (let level = currentLevel + 1; level < state.counters.length; level += 1) {
|
|
148
|
+
if (shouldResetDeeperLevel(level, currentLevel, levelDefinitions)) {
|
|
149
|
+
state.counters[level] = undefined;
|
|
150
|
+
}
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
// Initialize any skipped parent levels so legal outline %1.%2.%3. patterns
|
|
@@ -163,6 +165,28 @@ function advanceSequence(
|
|
|
163
165
|
state.lastLevel = currentLevel;
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
function shouldResetDeeperLevel(
|
|
169
|
+
level: number,
|
|
170
|
+
triggeringLevel: number,
|
|
171
|
+
levelDefinitions: Map<number, NumberingLevelDefinition>,
|
|
172
|
+
): boolean {
|
|
173
|
+
const restartAfterLevel = levelDefinitions.get(level)?.restartAfterLevel;
|
|
174
|
+
if (restartAfterLevel === 0) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const defaultRestartLevel = level - 1;
|
|
179
|
+
const restartIndex = restartAfterLevel !== undefined
|
|
180
|
+
? restartAfterLevel - 1
|
|
181
|
+
: defaultRestartLevel;
|
|
182
|
+
|
|
183
|
+
if (restartIndex >= level) {
|
|
184
|
+
return triggeringLevel <= defaultRestartLevel;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return triggeringLevel <= restartIndex;
|
|
188
|
+
}
|
|
189
|
+
|
|
166
190
|
function getLevelStartAt(
|
|
167
191
|
level: number,
|
|
168
192
|
levelDefinitions: Map<number, NumberingLevelDefinition>,
|
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
BorderSpec,
|
|
33
33
|
TableBorders,
|
|
34
34
|
TableCellBorders,
|
|
35
|
+
TableCellMargins,
|
|
35
36
|
TableNode,
|
|
36
37
|
TextMark,
|
|
37
38
|
DrawingFrameNode,
|
|
@@ -491,6 +492,10 @@ function createTableBlock(
|
|
|
491
492
|
.map((region) => `band-${region}`)
|
|
492
493
|
.join(" ")
|
|
493
494
|
: null;
|
|
495
|
+
const cellPadding = resolveEffectiveCellMargins(
|
|
496
|
+
cell.margins,
|
|
497
|
+
resolvedTable.table?.cellMargins,
|
|
498
|
+
);
|
|
494
499
|
cells.push({
|
|
495
500
|
gridSpan: cell.gridSpan ?? 1,
|
|
496
501
|
verticalMerge: cell.verticalMerge ?? null,
|
|
@@ -502,6 +507,10 @@ function createTableBlock(
|
|
|
502
507
|
...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
|
|
503
508
|
...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
|
|
504
509
|
...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
|
|
510
|
+
...(cellPadding.top !== undefined ? { paddingTop: cellPadding.top } : {}),
|
|
511
|
+
...(cellPadding.right !== undefined ? { paddingRight: cellPadding.right } : {}),
|
|
512
|
+
...(cellPadding.bottom !== undefined ? { paddingBottom: cellPadding.bottom } : {}),
|
|
513
|
+
...(cellPadding.left !== undefined ? { paddingLeft: cellPadding.left } : {}),
|
|
505
514
|
// R3.a Phase 2: per-cell text-flow direction copied from canonical
|
|
506
515
|
// TableCellNode. Node-view renders `tbRl` / `btLr` as CSS writing-mode.
|
|
507
516
|
...(cell.textDirection ? { textDirection: cell.textDirection } : {}),
|
|
@@ -626,6 +635,18 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
|
|
|
626
635
|
return rowSpans;
|
|
627
636
|
}
|
|
628
637
|
|
|
638
|
+
function resolveEffectiveCellMargins(
|
|
639
|
+
direct: TableCellMargins | undefined,
|
|
640
|
+
tableDefault: TableCellMargins | undefined,
|
|
641
|
+
): TableCellMargins {
|
|
642
|
+
return {
|
|
643
|
+
...(direct?.top !== undefined ? { top: direct.top } : tableDefault?.top !== undefined ? { top: tableDefault.top } : {}),
|
|
644
|
+
...(direct?.right !== undefined ? { right: direct.right } : tableDefault?.right !== undefined ? { right: tableDefault.right } : {}),
|
|
645
|
+
...(direct?.bottom !== undefined ? { bottom: direct.bottom } : tableDefault?.bottom !== undefined ? { bottom: tableDefault.bottom } : {}),
|
|
646
|
+
...(direct?.left !== undefined ? { left: direct.left } : tableDefault?.left !== undefined ? { left: tableDefault.left } : {}),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
629
650
|
/**
|
|
630
651
|
* SOW gap G3 — resolve the final paintable fill for a cell `w:shd`. Precedence
|
|
631
652
|
* matches Word: a concrete `w:fill` (non-"auto") wins; otherwise the theme
|
|
@@ -660,6 +681,42 @@ function resolveCellShadingFill(
|
|
|
660
681
|
return resolved.startsWith("#") ? resolved.slice(1) : resolved;
|
|
661
682
|
}
|
|
662
683
|
|
|
684
|
+
function resolveSurfaceParagraphShading(
|
|
685
|
+
shading: ParagraphNode["shading"],
|
|
686
|
+
themeResolver: ThemeColorResolver | undefined,
|
|
687
|
+
): NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["shading"]> {
|
|
688
|
+
const resolvedFill = resolveCellShadingFill(shading, themeResolver);
|
|
689
|
+
return {
|
|
690
|
+
...(shading?.val ? { val: shading.val } : {}),
|
|
691
|
+
...(shading?.color ? { color: shading.color } : {}),
|
|
692
|
+
...(resolvedFill !== undefined
|
|
693
|
+
? { fill: resolvedFill }
|
|
694
|
+
: shading?.fill !== undefined
|
|
695
|
+
? { fill: shading.fill }
|
|
696
|
+
: {}),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function resolveSurfaceParagraphFormatting(
|
|
701
|
+
formatting: CanonicalParagraphFormatting,
|
|
702
|
+
themeResolver: ThemeColorResolver | undefined,
|
|
703
|
+
): CanonicalParagraphFormatting {
|
|
704
|
+
if (!formatting.shading) {
|
|
705
|
+
return formatting;
|
|
706
|
+
}
|
|
707
|
+
const resolvedFill = resolveCellShadingFill(formatting.shading, themeResolver);
|
|
708
|
+
if (resolvedFill === undefined && formatting.shading.fill === undefined) {
|
|
709
|
+
return formatting;
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
...formatting,
|
|
713
|
+
shading: {
|
|
714
|
+
...formatting.shading,
|
|
715
|
+
...(resolvedFill !== undefined ? { fill: resolvedFill } : {}),
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
663
720
|
/**
|
|
664
721
|
* SOW gap G1 — derive relative column widths (percent 0–100, sum 100)
|
|
665
722
|
* from the canonical `gridColumns` (twips). Only populated when the table
|
|
@@ -842,6 +899,9 @@ function createParagraphBlock(
|
|
|
842
899
|
nextCursor: number;
|
|
843
900
|
lockedFragmentIds: string[];
|
|
844
901
|
} {
|
|
902
|
+
const themeResolver = document.subParts?.canonicalTheme
|
|
903
|
+
? new ThemeColorResolver(document.subParts.canonicalTheme)
|
|
904
|
+
: undefined;
|
|
845
905
|
// L7 Phase 2.9 — viewport bail. When the paragraph is outside the
|
|
846
906
|
// viewport, the returned block is discarded (the outer caller in
|
|
847
907
|
// `createEditorSurfaceSnapshot` replaces it with a placeholder-culled
|
|
@@ -871,6 +931,9 @@ function createParagraphBlock(
|
|
|
871
931
|
{ styleId: paragraph.styleId, direct: directParagraphFormatting },
|
|
872
932
|
stylesCatalog,
|
|
873
933
|
);
|
|
934
|
+
const surfaceResolvedParagraphFormatting = resolvedParagraphFormatting
|
|
935
|
+
? resolveSurfaceParagraphFormatting(resolvedParagraphFormatting, themeResolver)
|
|
936
|
+
: undefined;
|
|
874
937
|
|
|
875
938
|
// Task 11: compute cascaded marker run formatting (expensive).
|
|
876
939
|
const markerRunProperties =
|
|
@@ -905,8 +968,8 @@ function createParagraphBlock(
|
|
|
905
968
|
},
|
|
906
969
|
}
|
|
907
970
|
: {}),
|
|
908
|
-
...(
|
|
909
|
-
? { resolvedParagraphFormatting }
|
|
971
|
+
...(surfaceResolvedParagraphFormatting && Object.keys(surfaceResolvedParagraphFormatting).length > 0
|
|
972
|
+
? { resolvedParagraphFormatting: surfaceResolvedParagraphFormatting }
|
|
910
973
|
: {}),
|
|
911
974
|
...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
|
|
912
975
|
...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
|
|
@@ -915,28 +978,26 @@ function createParagraphBlock(
|
|
|
915
978
|
: {}),
|
|
916
979
|
...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
|
|
917
980
|
...(paragraph.borders ? { borders: paragraph.borders } : {}),
|
|
918
|
-
...(paragraph.shading
|
|
981
|
+
...(paragraph.shading
|
|
982
|
+
? { shading: resolveSurfaceParagraphShading(paragraph.shading, themeResolver) }
|
|
983
|
+
: {}),
|
|
919
984
|
...(paragraph.tabStops && paragraph.tabStops.length > 0
|
|
920
985
|
? { tabStops: paragraph.tabStops.map((tabStop) => toSurfaceTabStop(tabStop)) }
|
|
921
986
|
: {}),
|
|
922
|
-
...(paragraph.keepNext ? { keepNext:
|
|
923
|
-
...(paragraph.keepLines ? { keepLines:
|
|
924
|
-
...(paragraph.pageBreakBefore ? { pageBreakBefore:
|
|
987
|
+
...(paragraph.keepNext !== undefined ? { keepNext: paragraph.keepNext } : {}),
|
|
988
|
+
...(paragraph.keepLines !== undefined ? { keepLines: paragraph.keepLines } : {}),
|
|
989
|
+
...(paragraph.pageBreakBefore !== undefined ? { pageBreakBefore: paragraph.pageBreakBefore } : {}),
|
|
925
990
|
...(paragraph.widowControl !== undefined ? { widowControl: paragraph.widowControl } : {}),
|
|
926
991
|
...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
|
|
927
|
-
...(paragraph.bidi ? { bidi:
|
|
928
|
-
...(paragraph.suppressLineNumbers
|
|
992
|
+
...(paragraph.bidi !== undefined ? { bidi: paragraph.bidi } : {}),
|
|
993
|
+
...(paragraph.suppressLineNumbers !== undefined
|
|
994
|
+
? { suppressLineNumbers: paragraph.suppressLineNumbers }
|
|
995
|
+
: {}),
|
|
929
996
|
segments: [],
|
|
930
997
|
};
|
|
931
998
|
const lockedFragmentIds: string[] = [];
|
|
932
999
|
let cursor = start;
|
|
933
1000
|
const children = Array.isArray(paragraph.children) ? paragraph.children : [];
|
|
934
|
-
// Build once per paragraph block — ThemeColorResolver is a thin wrapper
|
|
935
|
-
// around CanonicalTheme that applies clrMap remapping. Constructed here so
|
|
936
|
-
// it is not recreated on every text segment (inner hot loop).
|
|
937
|
-
const themeResolver = document.subParts?.canonicalTheme
|
|
938
|
-
? new ThemeColorResolver(document.subParts.canonicalTheme)
|
|
939
|
-
: undefined;
|
|
940
1001
|
|
|
941
1002
|
for (const child of children) {
|
|
942
1003
|
const result = appendInlineSegments(
|
|
@@ -42,6 +42,10 @@ type TableCellAttrs = {
|
|
|
42
42
|
borderRight?: string | null;
|
|
43
43
|
borderBottom?: string | null;
|
|
44
44
|
borderLeft?: string | null;
|
|
45
|
+
paddingTop?: number | null;
|
|
46
|
+
paddingRight?: number | null;
|
|
47
|
+
paddingBottom?: number | null;
|
|
48
|
+
paddingLeft?: number | null;
|
|
45
49
|
/** R3.a Phase 2 — per-cell text-flow direction. */
|
|
46
50
|
textDirection?: "lrTb" | "tbRl" | "btLr" | null;
|
|
47
51
|
bandClasses?: string | null;
|
|
@@ -83,6 +87,12 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
83
87
|
const backgroundColor =
|
|
84
88
|
dom.getAttribute("data-cell-background") ?? dom.style.backgroundColor ?? null;
|
|
85
89
|
const verticalAlign = dom.getAttribute("data-vertical-align") as "top" | "center" | "bottom" | null;
|
|
90
|
+
const readPadding = (name: string): number | null => {
|
|
91
|
+
const raw = dom.getAttribute(name);
|
|
92
|
+
if (!raw) return null;
|
|
93
|
+
const parsed = Number.parseInt(raw, 10);
|
|
94
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
95
|
+
};
|
|
86
96
|
|
|
87
97
|
return {
|
|
88
98
|
colspan,
|
|
@@ -99,6 +109,10 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
|
|
|
99
109
|
borderRight: dom.getAttribute("data-border-right"),
|
|
100
110
|
borderBottom: dom.getAttribute("data-border-bottom"),
|
|
101
111
|
borderLeft: dom.getAttribute("data-border-left"),
|
|
112
|
+
paddingTop: readPadding("data-cell-padding-top"),
|
|
113
|
+
paddingRight: readPadding("data-cell-padding-right"),
|
|
114
|
+
paddingBottom: readPadding("data-cell-padding-bottom"),
|
|
115
|
+
paddingLeft: readPadding("data-cell-padding-left"),
|
|
102
116
|
bandClasses: dom.getAttribute("data-band-classes"),
|
|
103
117
|
};
|
|
104
118
|
}
|
|
@@ -133,6 +147,10 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
|
|
|
133
147
|
if (nodeAttrs.borderRight) attrs["data-border-right"] = nodeAttrs.borderRight;
|
|
134
148
|
if (nodeAttrs.borderBottom) attrs["data-border-bottom"] = nodeAttrs.borderBottom;
|
|
135
149
|
if (nodeAttrs.borderLeft) attrs["data-border-left"] = nodeAttrs.borderLeft;
|
|
150
|
+
if (typeof nodeAttrs.paddingTop === "number") attrs["data-cell-padding-top"] = String(nodeAttrs.paddingTop);
|
|
151
|
+
if (typeof nodeAttrs.paddingRight === "number") attrs["data-cell-padding-right"] = String(nodeAttrs.paddingRight);
|
|
152
|
+
if (typeof nodeAttrs.paddingBottom === "number") attrs["data-cell-padding-bottom"] = String(nodeAttrs.paddingBottom);
|
|
153
|
+
if (typeof nodeAttrs.paddingLeft === "number") attrs["data-cell-padding-left"] = String(nodeAttrs.paddingLeft);
|
|
136
154
|
if (nodeAttrs.bandClasses) {
|
|
137
155
|
attrs["data-band-classes"] = nodeAttrs.bandClasses;
|
|
138
156
|
// Concatenate band classes onto the base `class` so Tailwind's @apply resolves at parse time.
|
|
@@ -151,6 +169,10 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
|
|
|
151
169
|
if (bBottom) styles.push(`border-bottom: ${bBottom}`);
|
|
152
170
|
const bLeft = safeCssBorder(nodeAttrs.borderLeft);
|
|
153
171
|
if (bLeft) styles.push(`border-left: ${bLeft}`);
|
|
172
|
+
if (typeof nodeAttrs.paddingTop === "number") styles.push(`padding-top: ${nodeAttrs.paddingTop / 20}pt`);
|
|
173
|
+
if (typeof nodeAttrs.paddingRight === "number") styles.push(`padding-right: ${nodeAttrs.paddingRight / 20}pt`);
|
|
174
|
+
if (typeof nodeAttrs.paddingBottom === "number") styles.push(`padding-bottom: ${nodeAttrs.paddingBottom / 20}pt`);
|
|
175
|
+
if (typeof nodeAttrs.paddingLeft === "number") styles.push(`padding-left: ${nodeAttrs.paddingLeft / 20}pt`);
|
|
154
176
|
if (styles.length > 0) attrs.style = styles.join("; ");
|
|
155
177
|
|
|
156
178
|
return attrs;
|
|
@@ -190,6 +212,10 @@ const tableCellSpecAttrs = {
|
|
|
190
212
|
borderRight: { default: null },
|
|
191
213
|
borderBottom: { default: null },
|
|
192
214
|
borderLeft: { default: null },
|
|
215
|
+
paddingTop: { default: null },
|
|
216
|
+
paddingRight: { default: null },
|
|
217
|
+
paddingBottom: { default: null },
|
|
218
|
+
paddingLeft: { default: null },
|
|
193
219
|
/**
|
|
194
220
|
* R3.a Phase 2 — per-cell text-flow direction copied from
|
|
195
221
|
* `TableCellNode.textDirection` ("lrTb" | "tbRl" | "btLr"). Node-view maps
|