@beyondwork/docx-react-component 1.0.85 → 1.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/public-types.ts +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +112 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +75 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.87",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -921,12 +921,22 @@ export interface UpdateFieldsResult {
|
|
|
921
921
|
|
|
922
922
|
/** Options for runtime-backed TOC refresh. */
|
|
923
923
|
export interface TocRefreshOptions {
|
|
924
|
+
/** Specific TOC region to update. Defaults to the first/all supported regions depending on mode. */
|
|
925
|
+
tocId?: string;
|
|
926
|
+
/** Preserve imported cached rows for visual parity, or regenerate from headings. Defaults to regenerate. */
|
|
927
|
+
mode?: "preserveCached" | "regenerate";
|
|
924
928
|
/** Maximum heading outline level to include (1–9, default 3). */
|
|
925
929
|
maxLevel?: number;
|
|
926
930
|
}
|
|
927
931
|
|
|
928
932
|
/** Result of a TOC refresh operation. */
|
|
929
933
|
export interface TocRefreshResult {
|
|
934
|
+
/** TOC region affected when the document has a first-class TOC region. */
|
|
935
|
+
tocId?: string;
|
|
936
|
+
/** Update mode used by the runtime. */
|
|
937
|
+
mode?: "preserveCached" | "regenerate";
|
|
938
|
+
/** Region freshness after the operation. */
|
|
939
|
+
status?: "current" | "stale" | "missing";
|
|
930
940
|
/** Number of TOC entries generated from heading structure. */
|
|
931
941
|
entryCount: number;
|
|
932
942
|
/** Heading entries that populated the TOC. */
|
|
@@ -934,6 +944,8 @@ export interface TocRefreshResult {
|
|
|
934
944
|
level: number;
|
|
935
945
|
text: string;
|
|
936
946
|
pageIndex: number;
|
|
947
|
+
pageText?: string;
|
|
948
|
+
source?: "cached" | "generated";
|
|
937
949
|
}>;
|
|
938
950
|
}
|
|
939
951
|
|
|
@@ -986,15 +998,30 @@ export interface TocEntrySnapshot {
|
|
|
986
998
|
level: number;
|
|
987
999
|
text: string;
|
|
988
1000
|
pageIndex?: number;
|
|
1001
|
+
pageText?: string;
|
|
1002
|
+
source?: "cached" | "generated";
|
|
989
1003
|
anchor?: EditorAnchorProjection;
|
|
990
1004
|
bookmarkName?: string;
|
|
991
1005
|
headingId?: string;
|
|
992
1006
|
}
|
|
993
1007
|
|
|
1008
|
+
export interface TocRegionSnapshot {
|
|
1009
|
+
tocId: string;
|
|
1010
|
+
status: "current" | "stale";
|
|
1011
|
+
sourceFieldIndex: number;
|
|
1012
|
+
instruction: string;
|
|
1013
|
+
cachedEntryCount: number;
|
|
1014
|
+
generatedEntryCount: number;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
994
1017
|
export interface TocSnapshot {
|
|
995
1018
|
status: "current" | "stale" | "missing";
|
|
1019
|
+
tocId?: string;
|
|
996
1020
|
sourceFieldIndex?: number;
|
|
997
1021
|
instruction?: string;
|
|
1022
|
+
source?: "cached" | "generated";
|
|
1023
|
+
regionCount?: number;
|
|
1024
|
+
regions?: TocRegionSnapshot[];
|
|
998
1025
|
entries: TocEntrySnapshot[];
|
|
999
1026
|
}
|
|
1000
1027
|
|
|
@@ -2580,6 +2607,13 @@ export interface AddScopeResult {
|
|
|
2580
2607
|
export interface ExportDocxOptions {
|
|
2581
2608
|
fileName?: string;
|
|
2582
2609
|
reason?: string;
|
|
2610
|
+
/**
|
|
2611
|
+
* Controls the browser download fallback used by the mounted
|
|
2612
|
+
* `WordReviewEditor` ref when no host/datastore `saveExport` adapter is
|
|
2613
|
+
* present. Defaults to `true`. Set to `false` to receive exported bytes
|
|
2614
|
+
* without triggering an anchor download.
|
|
2615
|
+
*/
|
|
2616
|
+
download?: boolean;
|
|
2583
2617
|
/**
|
|
2584
2618
|
* @experimental Lane 7c Slice 7c.4 (v2.0.0) — opt-in for Strict-flavor
|
|
2585
2619
|
* OOXML export (ECMA-376 / ISO 29500-1 Strict, namespaces under
|
|
@@ -3595,6 +3629,8 @@ export type WordReviewEditorEvent =
|
|
|
3595
3629
|
| {
|
|
3596
3630
|
type: "toc_auto_refreshed";
|
|
3597
3631
|
documentId: string;
|
|
3632
|
+
tocId?: string;
|
|
3633
|
+
regionCount?: number;
|
|
3598
3634
|
entryCount: number;
|
|
3599
3635
|
trigger: TocRefreshTrigger;
|
|
3600
3636
|
}
|
|
@@ -5802,6 +5838,13 @@ export type TextCommandAckKind =
|
|
|
5802
5838
|
| "rejected"
|
|
5803
5839
|
| "structural-divergence";
|
|
5804
5840
|
|
|
5841
|
+
export type TextCommandRefreshClass =
|
|
5842
|
+
| "selection-only"
|
|
5843
|
+
| "local-text-equivalent"
|
|
5844
|
+
| "surface-only"
|
|
5845
|
+
| "full-projection"
|
|
5846
|
+
| "blocked";
|
|
5847
|
+
|
|
5805
5848
|
export interface ScopeTagTouch {
|
|
5806
5849
|
/** Tag family: "comment" | "revision" | "field" | "bookmark" | "sdt" | "opaque" | custom string. */
|
|
5807
5850
|
tagType: string;
|
|
@@ -5827,6 +5870,12 @@ export interface ScopeTagTouch {
|
|
|
5827
5870
|
*/
|
|
5828
5871
|
export interface TextCommandAck {
|
|
5829
5872
|
kind: TextCommandAckKind;
|
|
5873
|
+
/**
|
|
5874
|
+
* Narrow post-ack refresh tier for editors that separate the typing hot
|
|
5875
|
+
* path from broader projection work. Older runtimes may omit this; callers
|
|
5876
|
+
* should derive a conservative tier from `kind` when absent.
|
|
5877
|
+
*/
|
|
5878
|
+
refreshClass?: TextCommandRefreshClass;
|
|
5830
5879
|
/** Opaque id echoed back from the predicted op. Undefined for canonical callers. */
|
|
5831
5880
|
opId?: string;
|
|
5832
5881
|
/** Revision token of the document AFTER commit. Empty string when `kind === "rejected"`. */
|
|
@@ -29,10 +29,7 @@ import type {
|
|
|
29
29
|
WordReviewEditorChromePreset,
|
|
30
30
|
WordReviewEditorChromeVisibility,
|
|
31
31
|
} from "../../public-types";
|
|
32
|
-
import {
|
|
33
|
-
resolveChromePresetOptions,
|
|
34
|
-
resolveChromeVisibilityForPreset,
|
|
35
|
-
} from "./chrome-preset-model";
|
|
32
|
+
import { resolveChromeVisibilityForPreset } from "./chrome-preset-model";
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
35
|
* Pure breakpoint resolver — inlined at layer 10 so chrome composition does
|
|
@@ -285,11 +282,6 @@ export function resolveChromeComposition(
|
|
|
285
282
|
input: ChromeCompositionInput,
|
|
286
283
|
): ChromeComposition {
|
|
287
284
|
const mode = deriveMode(input);
|
|
288
|
-
const options = resolveChromePresetOptions(
|
|
289
|
-
input.chromePreset,
|
|
290
|
-
input.chromeOptions,
|
|
291
|
-
input.role,
|
|
292
|
-
);
|
|
293
285
|
const visibility = resolveChromeVisibilityForPreset({
|
|
294
286
|
chromePreset: input.chromePreset,
|
|
295
287
|
chromeOptions: input.chromeOptions,
|
|
@@ -300,8 +292,7 @@ export function resolveChromeComposition(
|
|
|
300
292
|
const density: ChromeDensity = input.density ?? "standard";
|
|
301
293
|
const pinnedRailTabs: ReadonlySet<EditorRailTab> =
|
|
302
294
|
input.pinnedRailTabs ?? new Set<EditorRailTab>();
|
|
303
|
-
const railOpen =
|
|
304
|
-
input.railOpen ?? (options.showReviewRail && visibility.reviewRail);
|
|
295
|
+
const railOpen = input.railOpen ?? false;
|
|
305
296
|
const visibleTabs = resolveVisibleRailTabs(
|
|
306
297
|
railOpen,
|
|
307
298
|
diagnosticsSignal,
|
package/src/api/v3/ui/chrome.ts
CHANGED
|
@@ -277,10 +277,9 @@ export function createChromeFamily(ctx: UiApiContext) {
|
|
|
277
277
|
// isolation pattern as U9 overlay-visibility).
|
|
278
278
|
//
|
|
279
279
|
// `activeRailTabSet` distinguishes "user explicitly set activeRailTab"
|
|
280
|
-
// (including to `null
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// previously relied on the composer's mode-appropriate default.
|
|
280
|
+
// (including to `null`) from "user hasn't expressed an opinion".
|
|
281
|
+
// Without this flag, getComposition would force `null` for every caller
|
|
282
|
+
// that relies on the composer's closed-by-default rail posture.
|
|
284
283
|
let activeRailTab: EditorRailTab | null = null;
|
|
285
284
|
let activeRailTabSet = false;
|
|
286
285
|
const pinnedRailTabs = new Set<EditorRailTab>();
|
|
@@ -354,10 +353,9 @@ export function createChromeFamily(ctx: UiApiContext) {
|
|
|
354
353
|
// caller can override `activeRailTab` / `pinnedRailTabs` via input;
|
|
355
354
|
// when omitted AND the UI API's internal state has been set,
|
|
356
355
|
// internal state fills in. When the caller omits AND internal state
|
|
357
|
-
// is untouched (the initial-mount path), the composer
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
// never interacted with rail-state methods.
|
|
356
|
+
// is untouched (the initial-mount path), the composer keeps the rail
|
|
357
|
+
// closed. Mounted workspaces pass `railOpen` from their local toggle
|
|
358
|
+
// state when the user opens the rail.
|
|
361
359
|
const merged: ChromeCompositionInput = {
|
|
362
360
|
...input,
|
|
363
361
|
...(input.activeRailTab !== undefined
|
package/src/index.ts
CHANGED
|
@@ -203,6 +203,7 @@ export type {
|
|
|
203
203
|
DocumentOutlineHeadingSnapshot,
|
|
204
204
|
DocumentOutlineSnapshot,
|
|
205
205
|
TocEntrySnapshot,
|
|
206
|
+
TocRegionSnapshot,
|
|
206
207
|
TocSnapshot,
|
|
207
208
|
DocumentSectionSnapshot,
|
|
208
209
|
SnapshotRefreshInvalidateTarget,
|
|
@@ -255,6 +256,10 @@ export type {
|
|
|
255
256
|
WorkflowBlockedCommandReason,
|
|
256
257
|
WorkflowScopeSnapshot,
|
|
257
258
|
InteractionGuardSnapshot,
|
|
259
|
+
TextCommandAckKind,
|
|
260
|
+
TextCommandRefreshClass,
|
|
261
|
+
TextCommandAck,
|
|
262
|
+
ScopeTagTouch,
|
|
258
263
|
WorkflowMarkupKind,
|
|
259
264
|
WorkflowMarkupBase,
|
|
260
265
|
WorkflowHighlightMarkup,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AltChunkNode,
|
|
3
|
+
BlockNode,
|
|
3
4
|
BorderSpec,
|
|
4
5
|
CustomXmlNode,
|
|
5
6
|
DocumentRootNode,
|
|
7
|
+
FieldNode,
|
|
6
8
|
FootnoteProperties,
|
|
7
9
|
InlineNode,
|
|
8
10
|
LegacyFormFieldNode,
|
|
@@ -97,6 +99,8 @@ interface SerializationState {
|
|
|
97
99
|
usedBookmarkIds: Set<string>;
|
|
98
100
|
scopeBookmarkIds: Map<string, string>;
|
|
99
101
|
nextScopeBookmarkId: number;
|
|
102
|
+
/** Number of paragraph-range TOC fields reconstructed during serialization. */
|
|
103
|
+
tocRegionExportCount: number;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
interface InlineSerializationResult {
|
|
@@ -216,6 +220,7 @@ export function serializeMainDocument(
|
|
|
216
220
|
usedBookmarkIds: collectNumericBookmarkIds(content),
|
|
217
221
|
scopeBookmarkIds: new Map<string, string>(),
|
|
218
222
|
nextScopeBookmarkId: 100000,
|
|
223
|
+
tocRegionExportCount: 0,
|
|
219
224
|
};
|
|
220
225
|
const suffix = `</w:body>\n</w:document>`;
|
|
221
226
|
const bodyPieces: string[] = [];
|
|
@@ -228,8 +233,43 @@ export function serializeMainDocument(
|
|
|
228
233
|
let paragraphIndex = -1;
|
|
229
234
|
let previousWasParagraph = false;
|
|
230
235
|
|
|
231
|
-
for (
|
|
236
|
+
for (let blockIndex = 0; blockIndex < content.children.length; blockIndex += 1) {
|
|
237
|
+
const block = content.children[blockIndex]!;
|
|
232
238
|
if (block.type === "paragraph") {
|
|
239
|
+
const tocField = findSerializableTocField(block);
|
|
240
|
+
if (tocField && startsSerializableTocRegion(content.children, blockIndex)) {
|
|
241
|
+
const startIndex = blockIndex;
|
|
242
|
+
const endIndex = findSerializableTocRegionEnd(content.children, startIndex);
|
|
243
|
+
state.tocRegionExportCount += 1;
|
|
244
|
+
for (let regionIndex = startIndex; regionIndex <= endIndex; regionIndex += 1) {
|
|
245
|
+
const paragraph = content.children[regionIndex];
|
|
246
|
+
if (paragraph?.type !== "paragraph") continue;
|
|
247
|
+
if (previousWasParagraph) {
|
|
248
|
+
cursor += 1;
|
|
249
|
+
}
|
|
250
|
+
paragraphIndex += 1;
|
|
251
|
+
const serializedParagraph = serializeParagraphWithTocBoundary(
|
|
252
|
+
paragraph,
|
|
253
|
+
state,
|
|
254
|
+
cursor,
|
|
255
|
+
paragraphIndex,
|
|
256
|
+
tocField,
|
|
257
|
+
regionIndex === startIndex,
|
|
258
|
+
regionIndex === endIndex,
|
|
259
|
+
);
|
|
260
|
+
const bodyRelativeOffset = bodyLength;
|
|
261
|
+
bodyPieces.push(serializedParagraph.xml);
|
|
262
|
+
bodyLength += serializedParagraph.xml.length;
|
|
263
|
+
paragraphBoundaries.push(
|
|
264
|
+
offsetParagraphBoundary(serializedParagraph.boundary, bodyRelativeOffset),
|
|
265
|
+
);
|
|
266
|
+
cursor = serializedParagraph.nextCursor;
|
|
267
|
+
previousWasParagraph = true;
|
|
268
|
+
}
|
|
269
|
+
blockIndex = endIndex;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
233
273
|
if (previousWasParagraph) {
|
|
234
274
|
cursor += 1;
|
|
235
275
|
}
|
|
@@ -365,6 +405,7 @@ export function serializeMainDocument(
|
|
|
365
405
|
blockKindCounts,
|
|
366
406
|
documentXmlBytes: documentXml.length,
|
|
367
407
|
relationshipCount: state.relationships.length,
|
|
408
|
+
tocRegionExportCount: state.tocRegionExportCount,
|
|
368
409
|
ms: serializePerformanceNow() - started,
|
|
369
410
|
},
|
|
370
411
|
});
|
|
@@ -412,12 +453,94 @@ function serializeTableCellNode(
|
|
|
412
453
|
state: SerializationState,
|
|
413
454
|
): string {
|
|
414
455
|
const propertiesXml = buildCellPropertiesXml(cell);
|
|
415
|
-
const blocksXml = cell.children
|
|
416
|
-
.map((child) => serializeBlockNode(child, state))
|
|
417
|
-
.join("");
|
|
456
|
+
const blocksXml = serializeBlockSequence(cell.children, state);
|
|
418
457
|
return `<w:tc>${propertiesXml}${blocksXml || "<w:p/>"}</w:tc>`;
|
|
419
458
|
}
|
|
420
459
|
|
|
460
|
+
function serializeBlockSequence(
|
|
461
|
+
blocks: readonly BlockNode[],
|
|
462
|
+
state: SerializationState,
|
|
463
|
+
): string {
|
|
464
|
+
const pieces: string[] = [];
|
|
465
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
466
|
+
const block = blocks[index];
|
|
467
|
+
if (!block) continue;
|
|
468
|
+
if (block?.type === "paragraph") {
|
|
469
|
+
const tocField = findSerializableTocField(block);
|
|
470
|
+
if (tocField && startsSerializableTocRegion(blocks, index)) {
|
|
471
|
+
const endIndex = findSerializableTocRegionEnd(blocks, index);
|
|
472
|
+
state.tocRegionExportCount += 1;
|
|
473
|
+
for (let regionIndex = index; regionIndex <= endIndex; regionIndex += 1) {
|
|
474
|
+
const paragraph = blocks[regionIndex];
|
|
475
|
+
if (paragraph?.type !== "paragraph") continue;
|
|
476
|
+
pieces.push(
|
|
477
|
+
serializeTableCellParagraphWithTocBoundary(
|
|
478
|
+
paragraph,
|
|
479
|
+
state,
|
|
480
|
+
tocField,
|
|
481
|
+
regionIndex === index,
|
|
482
|
+
regionIndex === endIndex,
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
index = endIndex;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
pieces.push(serializeBlockNode(block, state));
|
|
491
|
+
}
|
|
492
|
+
return pieces.join("");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function findSerializableTocField(paragraph: ParagraphNode): FieldNode | undefined {
|
|
496
|
+
return paragraph.children.find(
|
|
497
|
+
(child): child is FieldNode => child.type === "field" && child.fieldFamily === "TOC",
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function startsSerializableTocRegion(blocks: readonly BlockNode[], index: number): boolean {
|
|
502
|
+
const current = blocks[index];
|
|
503
|
+
if (current?.type === "paragraph" && isSerializableTocParagraphStyle(current.styleId)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
const next = blocks[index + 1];
|
|
507
|
+
return next?.type === "paragraph" && isSerializableTocParagraphStyle(next.styleId);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function findSerializableTocRegionEnd(blocks: readonly BlockNode[], startIndex: number): number {
|
|
511
|
+
let endIndex = startIndex;
|
|
512
|
+
while (endIndex + 1 < blocks.length) {
|
|
513
|
+
const next = blocks[endIndex + 1];
|
|
514
|
+
if (next?.type !== "paragraph" || !isSerializableTocParagraphStyle(next.styleId)) {
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
endIndex += 1;
|
|
518
|
+
}
|
|
519
|
+
return endIndex;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isSerializableTocParagraphStyle(styleId: string | undefined): boolean {
|
|
523
|
+
return /^TOC\d+$/u.test(styleId ?? "");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function stripSerializableTocField(children: readonly InlineNode[]): InlineNode[] {
|
|
527
|
+
return children.filter(
|
|
528
|
+
(child) => !(child.type === "field" && child.fieldFamily === "TOC"),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function serializeTocFieldBeginXml(field: FieldNode): string {
|
|
533
|
+
return (
|
|
534
|
+
`<w:r><w:fldChar w:fldCharType="begin"/></w:r>` +
|
|
535
|
+
`<w:r><w:instrText xml:space="preserve"> ${escapeXml(field.instruction)} </w:instrText></w:r>` +
|
|
536
|
+
`<w:r><w:fldChar w:fldCharType="separate"/></w:r>`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function serializeTocFieldEndXml(): string {
|
|
541
|
+
return `<w:r><w:fldChar w:fldCharType="end"/></w:r>`;
|
|
542
|
+
}
|
|
543
|
+
|
|
421
544
|
function serializeBlockNode(
|
|
422
545
|
block: DocumentRootNode["children"][number],
|
|
423
546
|
state: SerializationState,
|
|
@@ -463,7 +586,7 @@ function serializeSdtNode(
|
|
|
463
586
|
state: SerializationState,
|
|
464
587
|
): string {
|
|
465
588
|
const propertiesXml = block.properties.propertiesXml ?? buildSdtPropertiesXml(block);
|
|
466
|
-
const childrenXml = block.children
|
|
589
|
+
const childrenXml = serializeBlockSequence(block.children, state) || "<w:p/>";
|
|
467
590
|
return `<w:sdt>${propertiesXml}<w:sdtContent>${childrenXml}</w:sdtContent></w:sdt>`;
|
|
468
591
|
}
|
|
469
592
|
|
|
@@ -482,7 +605,7 @@ function serializeCustomXmlNode(
|
|
|
482
605
|
attrs.push(`w:element="${escapeXmlAttribute(block.element)}"`);
|
|
483
606
|
}
|
|
484
607
|
const attrXml = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
485
|
-
const childrenXml = block.children
|
|
608
|
+
const childrenXml = serializeBlockSequence(block.children, state);
|
|
486
609
|
return `<w:customXml${attrXml}>${childrenXml || "<w:p/>"}</w:customXml>`;
|
|
487
610
|
}
|
|
488
611
|
|
|
@@ -559,6 +682,27 @@ function serializeTableCellParagraph(
|
|
|
559
682
|
return xml;
|
|
560
683
|
}
|
|
561
684
|
|
|
685
|
+
function serializeTableCellParagraphWithTocBoundary(
|
|
686
|
+
paragraph: ParagraphNode,
|
|
687
|
+
state: SerializationState,
|
|
688
|
+
tocField: FieldNode,
|
|
689
|
+
isFirst: boolean,
|
|
690
|
+
isLast: boolean,
|
|
691
|
+
): string {
|
|
692
|
+
let xml = buildParagraphOpenTag(paragraph, state);
|
|
693
|
+
const paragraphPropertiesXml = buildParagraphPropertiesXml(paragraph);
|
|
694
|
+
if (paragraphPropertiesXml.length > 0) {
|
|
695
|
+
xml += paragraphPropertiesXml;
|
|
696
|
+
}
|
|
697
|
+
const children = stripSerializableTocField(paragraph.children);
|
|
698
|
+
const childrenXml = children.map((child) => serializeTableInlineNode(child, state)).join("");
|
|
699
|
+
xml += isFirst ? serializeTocFieldBeginXml(tocField) : "";
|
|
700
|
+
xml += childrenXml || "<w:r><w:t></w:t></w:r>";
|
|
701
|
+
xml += isLast ? serializeTocFieldEndXml() : "";
|
|
702
|
+
xml += "</w:p>";
|
|
703
|
+
return xml;
|
|
704
|
+
}
|
|
705
|
+
|
|
562
706
|
/**
|
|
563
707
|
* A.7: build the paragraph opening tag. When the canonical paragraph
|
|
564
708
|
* carries a preserved `wordExtensionIds`, re-emit `w14:paraId` /
|
|
@@ -991,6 +1135,71 @@ function serializeParagraph(
|
|
|
991
1135
|
};
|
|
992
1136
|
}
|
|
993
1137
|
|
|
1138
|
+
function serializeParagraphWithTocBoundary(
|
|
1139
|
+
paragraph: ParagraphNode,
|
|
1140
|
+
state: SerializationState,
|
|
1141
|
+
cursor: number,
|
|
1142
|
+
paragraphIndex: number,
|
|
1143
|
+
tocField: FieldNode,
|
|
1144
|
+
isFirst: boolean,
|
|
1145
|
+
isLast: boolean,
|
|
1146
|
+
): ParagraphSerializationResult {
|
|
1147
|
+
let xml = buildParagraphOpenTag(paragraph, state);
|
|
1148
|
+
const boundaries = new Map<number, number>();
|
|
1149
|
+
const paragraphStart = 0;
|
|
1150
|
+
const paragraphStartTagEnd = xml.length;
|
|
1151
|
+
boundaries.set(cursor, paragraphStartTagEnd);
|
|
1152
|
+
|
|
1153
|
+
let paragraphPropertiesStart: number | undefined;
|
|
1154
|
+
let paragraphPropertiesEnd: number | undefined;
|
|
1155
|
+
|
|
1156
|
+
const paragraphPropertiesXml = buildParagraphPropertiesXml(paragraph);
|
|
1157
|
+
if (paragraphPropertiesXml.length > 0) {
|
|
1158
|
+
paragraphPropertiesStart = xml.length;
|
|
1159
|
+
xml += paragraphPropertiesXml;
|
|
1160
|
+
paragraphPropertiesEnd = xml.length;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (isFirst) {
|
|
1164
|
+
xml += serializeTocFieldBeginXml(tocField);
|
|
1165
|
+
}
|
|
1166
|
+
const strippedChildren = stripSerializableTocField(paragraph.children);
|
|
1167
|
+
const children = serializeParagraphChildren(strippedChildren, state, cursor, xml.length);
|
|
1168
|
+
xml += children.xml;
|
|
1169
|
+
const contentEmpty = children.xml.length === 0;
|
|
1170
|
+
if (contentEmpty) {
|
|
1171
|
+
xml += "<w:r><w:t></w:t></w:r>";
|
|
1172
|
+
}
|
|
1173
|
+
if (isLast) {
|
|
1174
|
+
xml += serializeTocFieldEndXml();
|
|
1175
|
+
}
|
|
1176
|
+
const paragraphEndTagStart = xml.length;
|
|
1177
|
+
xml += "</w:p>";
|
|
1178
|
+
|
|
1179
|
+
if (!children.boundaries.has(children.cursor)) {
|
|
1180
|
+
children.boundaries.set(children.cursor, paragraphEndTagStart);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
xml,
|
|
1185
|
+
nextCursor: children.cursor,
|
|
1186
|
+
boundary: {
|
|
1187
|
+
paragraphIndex,
|
|
1188
|
+
start: cursor,
|
|
1189
|
+
end: children.cursor,
|
|
1190
|
+
boundaries: children.boundaries,
|
|
1191
|
+
paragraphStart,
|
|
1192
|
+
paragraphStartTagEnd,
|
|
1193
|
+
paragraphEndTagStart,
|
|
1194
|
+
paragraphEnd: xml.length,
|
|
1195
|
+
...(paragraphPropertiesStart !== undefined
|
|
1196
|
+
? { paragraphPropertiesStart }
|
|
1197
|
+
: {}),
|
|
1198
|
+
...(paragraphPropertiesEnd !== undefined ? { paragraphPropertiesEnd } : {}),
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
994
1203
|
function serializeParagraphChildren(
|
|
995
1204
|
children: InlineNode[],
|
|
996
1205
|
state: SerializationState,
|
|
@@ -6,6 +6,7 @@ import type { DrawingFrameNode, AnchorGeometry } from "../../model/canonical-doc
|
|
|
6
6
|
import { parseAnchorGeometry } from "./parse-anchor.ts";
|
|
7
7
|
import { parsePicture } from "./parse-picture.ts";
|
|
8
8
|
import { parseShapeContent, type TxbxBlockParser } from "./parse-shapes.ts";
|
|
9
|
+
import { parseChartSpace } from "./chart/parse-chart-space.ts";
|
|
9
10
|
import {
|
|
10
11
|
type XmlElementNode,
|
|
11
12
|
findFirstChild,
|
|
@@ -203,7 +204,14 @@ function resolveContent(
|
|
|
203
204
|
return { type: "opaque", rawXml };
|
|
204
205
|
}
|
|
205
206
|
if (uri === CHART_GRAPHIC_URI || uri === CHART_GRAPHIC_URI_ALT) {
|
|
206
|
-
|
|
207
|
+
const chartRelId = extractChartRelId(graphicData);
|
|
208
|
+
const chartXml = chartRelId ? opts.chartPartLookup?.(chartRelId) : undefined;
|
|
209
|
+
const parsedData = chartXml ? parseChartSpace(chartXml) : undefined;
|
|
210
|
+
return {
|
|
211
|
+
type: "chart_preview",
|
|
212
|
+
rawXml,
|
|
213
|
+
...(parsedData ? { parsedData } : {}),
|
|
214
|
+
};
|
|
207
215
|
}
|
|
208
216
|
if (uri === SMARTART_GRAPHIC_URI || uri === SMARTART_GRAPHIC_URI_ALT) {
|
|
209
217
|
return { type: "smartart_preview", rawXml };
|
|
@@ -223,6 +231,12 @@ function resolveContent(
|
|
|
223
231
|
return { type: "opaque", rawXml };
|
|
224
232
|
}
|
|
225
233
|
|
|
234
|
+
function extractChartRelId(graphicData: XmlElementNode | undefined): string | null {
|
|
235
|
+
if (!graphicData) return null;
|
|
236
|
+
const chart = findFirstDescendant(graphicData, "chart");
|
|
237
|
+
return chart?.attributes["r:id"] ?? chart?.attributes.id ?? null;
|
|
238
|
+
}
|
|
239
|
+
|
|
226
240
|
// Phase 6 — XML parser helpers imported from ./_mini-xml.ts (previously
|
|
227
241
|
// duplicated inline across four files). See that module for B4 throw-on-
|
|
228
242
|
// unterminated-tag contract and entity-decoding implementation.
|