@beyondwork/docx-react-component 1.0.46 → 1.0.48
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 +115 -1
- package/src/compare/diff-engine.ts +4 -0
- package/src/core/commands/add-scope.ts +257 -0
- package/src/core/commands/formatting-commands.ts +2 -0
- package/src/core/commands/index.ts +120 -1
- package/src/core/schema/text-schema.ts +95 -1
- package/src/core/state/text-transaction.ts +17 -5
- package/src/io/chart-preview-resolver.ts +27 -0
- package/src/io/docx-session.ts +219 -2
- package/src/io/export/serialize-main-document.ts +37 -0
- package/src/io/export/serialize-settings.ts +421 -0
- package/src/io/export/serialize-styles.ts +10 -0
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/chart/parse-axis.ts +277 -0
- package/src/io/ooxml/chart/parse-chart-space.ts +813 -0
- package/src/io/ooxml/chart/parse-series.ts +570 -0
- package/src/io/ooxml/chart/resolve-color.ts +251 -0
- package/src/io/ooxml/chart/types.ts +420 -0
- package/src/io/ooxml/parse-block-structure.ts +99 -0
- package/src/io/ooxml/parse-complex-content.ts +87 -2
- package/src/io/ooxml/parse-main-document.ts +115 -1
- package/src/io/ooxml/parse-scope-markers.ts +184 -0
- package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
- package/src/io/ooxml/parse-settings.ts +97 -1
- package/src/io/ooxml/parse-styles.ts +65 -0
- package/src/io/ooxml/parse-theme.ts +2 -127
- package/src/io/ooxml/workflow-payload.ts +27 -0
- package/src/io/ooxml/xml-attr-helpers.ts +59 -1
- package/src/io/ooxml/xml-parser.ts +142 -0
- package/src/model/canonical-document.ts +94 -0
- package/src/model/scope-markers.ts +144 -0
- package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
- package/src/runtime/collab/checkpoint-election.ts +75 -0
- package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
- package/src/runtime/collab/checkpoint-store.ts +115 -0
- package/src/runtime/collab/event-types.ts +37 -5
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
- package/src/runtime/collab/runtime-collab-sync.ts +404 -1
- package/src/runtime/document-runtime.ts +221 -16
- package/src/runtime/editor-surface/capabilities.ts +63 -50
- package/src/runtime/layout/layout-engine-version.ts +27 -2
- package/src/runtime/prerender/cache-envelope.ts +19 -7
- package/src/runtime/prerender/cache-key.ts +25 -14
- package/src/runtime/prerender/canonical-document-hash.ts +63 -0
- package/src/runtime/prerender/customxml-cache.ts +211 -0
- package/src/runtime/prerender/customxml-probe.ts +78 -0
- package/src/runtime/prerender/prerender-document.ts +74 -7
- package/src/runtime/scope-resolver.ts +148 -0
- package/src/runtime/scope-tag-registry.ts +10 -0
- package/src/runtime/surface-projection.ts +8 -1
- package/src/runtime/text-ack-range.ts +3 -3
- package/src/ui/WordReviewEditor.tsx +30 -0
- package/src/ui/editor-runtime-boundary.ts +6 -1
- package/src/ui/runtime-shortcut-dispatch.ts +12 -7
|
@@ -26,6 +26,7 @@ export type StoryUnit =
|
|
|
26
26
|
| ImageUnit
|
|
27
27
|
| OpaqueInlineUnit
|
|
28
28
|
| OpaqueBlockUnit
|
|
29
|
+
| ScopeMarkerUnit
|
|
29
30
|
| ParagraphBreakUnit;
|
|
30
31
|
|
|
31
32
|
export interface TextCharacterUnit {
|
|
@@ -69,6 +70,18 @@ export interface ParagraphBreakUnit {
|
|
|
69
70
|
nextParagraph: ParagraphProperties;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Zero-width inline unit that preserves S1 scope-marker nodes through text
|
|
75
|
+
* transactions. Without this unit, scope markers would be silently dropped
|
|
76
|
+
* during `parseTextStory` / `serializeTextStory`, and any `text.insert` /
|
|
77
|
+
* `text.delete-*` dispatch would vaporize the structural scope anchors.
|
|
78
|
+
*/
|
|
79
|
+
export interface ScopeMarkerUnit {
|
|
80
|
+
kind: "scope_marker";
|
|
81
|
+
boundary: "start" | "end";
|
|
82
|
+
scopeId: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
export function parseTextStory(content: unknown): TextStory {
|
|
73
86
|
const root = normalizeDocumentRoot(content);
|
|
74
87
|
const firstParagraphNode = root.children.find(isParagraphNode);
|
|
@@ -111,10 +124,60 @@ export function parseTextStory(content: unknown): TextStory {
|
|
|
111
124
|
return {
|
|
112
125
|
firstParagraph,
|
|
113
126
|
units,
|
|
114
|
-
size: units
|
|
127
|
+
size: countLogicalPositions(units),
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Story positions are logical — scope-marker units are preserved in the
|
|
133
|
+
* `units` array for round-trip fidelity but they do NOT consume a position.
|
|
134
|
+
* This matches the surface-projection treatment (markers = 0 width, same as
|
|
135
|
+
* bookmark_start / bookmark_end) so a position 3 set via `selection.set`
|
|
136
|
+
* resolves to the same character in both views.
|
|
137
|
+
*/
|
|
138
|
+
export function countLogicalPositions(units: StoryUnit[]): number {
|
|
139
|
+
let size = 0;
|
|
140
|
+
for (const unit of units) {
|
|
141
|
+
if (unit.kind !== "scope_marker") size += 1;
|
|
142
|
+
}
|
|
143
|
+
return size;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Translate a logical (scope-marker-skipping) position into a unit-array
|
|
148
|
+
* index. Walks units and increments the unit cursor once per non-marker unit;
|
|
149
|
+
* scope markers are passed over transparently. Returns `units.length` when
|
|
150
|
+
* the logical position is at or beyond end-of-story.
|
|
151
|
+
*
|
|
152
|
+
* When `startBias === "after"` (default), the returned unit index is the
|
|
153
|
+
* first position AFTER any scope markers that sit exactly at the logical
|
|
154
|
+
* boundary — useful when slicing units as "...before this cursor". When
|
|
155
|
+
* `startBias === "before"`, markers at the boundary are included in the
|
|
156
|
+
* "after" slice.
|
|
157
|
+
*/
|
|
158
|
+
export function logicalPositionToUnitIndex(
|
|
159
|
+
units: StoryUnit[],
|
|
160
|
+
logicalPos: number,
|
|
161
|
+
startBias: "before" | "after" = "after",
|
|
162
|
+
): number {
|
|
163
|
+
let logicalCursor = 0;
|
|
164
|
+
let unitIndex = 0;
|
|
165
|
+
while (unitIndex < units.length) {
|
|
166
|
+
if (logicalCursor === logicalPos && startBias === "before") {
|
|
167
|
+
return unitIndex;
|
|
168
|
+
}
|
|
169
|
+
const unit = units[unitIndex]!;
|
|
170
|
+
if (unit.kind !== "scope_marker") {
|
|
171
|
+
if (logicalCursor === logicalPos && startBias === "after") {
|
|
172
|
+
return unitIndex;
|
|
173
|
+
}
|
|
174
|
+
logicalCursor += 1;
|
|
175
|
+
}
|
|
176
|
+
unitIndex += 1;
|
|
177
|
+
}
|
|
178
|
+
return unitIndex;
|
|
179
|
+
}
|
|
180
|
+
|
|
118
181
|
export function serializeTextStory(story: TextStory): DocumentRootNode {
|
|
119
182
|
const blocks: Array<ParagraphNode | OpaqueBlockNode> = [];
|
|
120
183
|
let currentParagraph: ParagraphNode | undefined = createParagraph(story.firstParagraph);
|
|
@@ -272,6 +335,15 @@ export function serializeTextStory(story: TextStory): DocumentRootNode {
|
|
|
272
335
|
warningId: unit.warningId,
|
|
273
336
|
});
|
|
274
337
|
break;
|
|
338
|
+
case "scope_marker":
|
|
339
|
+
pushInlineNode({
|
|
340
|
+
type:
|
|
341
|
+
unit.boundary === "start"
|
|
342
|
+
? "scope_marker_start"
|
|
343
|
+
: "scope_marker_end",
|
|
344
|
+
scopeId: unit.scopeId,
|
|
345
|
+
});
|
|
346
|
+
break;
|
|
275
347
|
}
|
|
276
348
|
}
|
|
277
349
|
|
|
@@ -305,6 +377,8 @@ export function createPlainText(story: TextStory): string {
|
|
|
305
377
|
return "\uFFF9";
|
|
306
378
|
case "opaque_block":
|
|
307
379
|
return "\uFFFA";
|
|
380
|
+
case "scope_marker":
|
|
381
|
+
return "";
|
|
308
382
|
}
|
|
309
383
|
})
|
|
310
384
|
.join("");
|
|
@@ -355,6 +429,12 @@ export function cloneStoryUnit(unit: StoryUnit): StoryUnit {
|
|
|
355
429
|
kind: "paragraph_break",
|
|
356
430
|
nextParagraph: cloneParagraphProperties(unit.nextParagraph),
|
|
357
431
|
};
|
|
432
|
+
case "scope_marker":
|
|
433
|
+
return {
|
|
434
|
+
kind: "scope_marker",
|
|
435
|
+
boundary: unit.boundary,
|
|
436
|
+
scopeId: unit.scopeId,
|
|
437
|
+
};
|
|
358
438
|
}
|
|
359
439
|
}
|
|
360
440
|
|
|
@@ -442,6 +522,20 @@ function flattenInlineNodes(
|
|
|
442
522
|
warningId: node.warningId,
|
|
443
523
|
});
|
|
444
524
|
break;
|
|
525
|
+
case "scope_marker_start":
|
|
526
|
+
units.push({
|
|
527
|
+
kind: "scope_marker",
|
|
528
|
+
boundary: "start",
|
|
529
|
+
scopeId: node.scopeId,
|
|
530
|
+
});
|
|
531
|
+
break;
|
|
532
|
+
case "scope_marker_end":
|
|
533
|
+
units.push({
|
|
534
|
+
kind: "scope_marker",
|
|
535
|
+
boundary: "end",
|
|
536
|
+
scopeId: node.scopeId,
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
445
539
|
}
|
|
446
540
|
}
|
|
447
541
|
|
|
@@ -3,7 +3,9 @@ import type { TransactionMapping } from "../selection/mapping.ts";
|
|
|
3
3
|
import {
|
|
4
4
|
cloneParagraphProperties,
|
|
5
5
|
cloneStoryUnit,
|
|
6
|
+
countLogicalPositions,
|
|
6
7
|
createPlainText,
|
|
8
|
+
logicalPositionToUnitIndex,
|
|
7
9
|
parseTextStory,
|
|
8
10
|
serializeTextStory,
|
|
9
11
|
type ParagraphProperties,
|
|
@@ -117,12 +119,18 @@ function applyLinearTextTransaction(
|
|
|
117
119
|
const normalizedRange = resolveRange(selection, story.size, intent);
|
|
118
120
|
const insertionUnits = createInsertionUnits(intent, story, normalizedRange.from);
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
// `normalizedRange.{from,to}` are logical positions (scope markers = 0 width,
|
|
123
|
+
// matching surface-projection). Translate to unit-array indices so scope
|
|
124
|
+
// marker units preserved at the boundary stay intact on either side.
|
|
125
|
+
const unitFrom = logicalPositionToUnitIndex(story.units, normalizedRange.from, "before");
|
|
126
|
+
const unitTo = logicalPositionToUnitIndex(story.units, normalizedRange.to, "after");
|
|
127
|
+
|
|
128
|
+
ensureEditableRange(story.units.slice(unitFrom, unitTo));
|
|
121
129
|
|
|
122
130
|
const nextUnits = [
|
|
123
|
-
...story.units.slice(0,
|
|
131
|
+
...story.units.slice(0, unitFrom).map(cloneStoryUnit),
|
|
124
132
|
...insertionUnits.map(cloneStoryUnit),
|
|
125
|
-
...story.units.slice(
|
|
133
|
+
...story.units.slice(unitTo).map(cloneStoryUnit),
|
|
126
134
|
];
|
|
127
135
|
|
|
128
136
|
const nextStory: TextStory = {
|
|
@@ -130,9 +138,13 @@ function applyLinearTextTransaction(
|
|
|
130
138
|
units: normalizeStoryUnits(nextUnits),
|
|
131
139
|
size: 0,
|
|
132
140
|
};
|
|
133
|
-
nextStory.size = nextStory.units
|
|
141
|
+
nextStory.size = countLogicalPositions(nextStory.units);
|
|
134
142
|
|
|
135
|
-
|
|
143
|
+
// `normalizedRange.from` is the logical insertion point; count the logical
|
|
144
|
+
// positions added by `insertionUnits` (skipping any scope markers) to derive
|
|
145
|
+
// the post-insert caret.
|
|
146
|
+
const logicalInsertionSize = countLogicalPositions(insertionUnits);
|
|
147
|
+
const caret = normalizedRange.from + logicalInsertionSize;
|
|
136
148
|
|
|
137
149
|
return {
|
|
138
150
|
document: {
|
|
@@ -214,6 +214,33 @@ function extractPartTextFromPackage(pkg: OpcPackage, path: string): string | und
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Build a chart-part lookup callback suitable for
|
|
219
|
+
* `parseMainDocumentXml(..., chartPartLookup)`.
|
|
220
|
+
*
|
|
221
|
+
* The callback is called synchronously during parsing with a chart
|
|
222
|
+
* relationship id (the `r:id` on a `<c:chart>` reference). It resolves
|
|
223
|
+
* the id to a chart-part target path via the document's relationship
|
|
224
|
+
* table, then decodes the matching package part's bytes as UTF-8. Unknown
|
|
225
|
+
* ids and missing parts return undefined, in which case the parser
|
|
226
|
+
* proceeds without a typed `ChartModel` (the drawing still produces a
|
|
227
|
+
* `ChartPreviewNode` with `rawXml`).
|
|
228
|
+
*/
|
|
229
|
+
export function createChartPartLookup(
|
|
230
|
+
pkg: OpcPackage,
|
|
231
|
+
documentPartPath: string,
|
|
232
|
+
documentRelationships: readonly import("./ooxml/part-manifest.ts").OpcRelationship[],
|
|
233
|
+
): (rId: string) => string | undefined {
|
|
234
|
+
const relById = new Map(documentRelationships.map((r) => [r.id, r]));
|
|
235
|
+
return (rId: string): string | undefined => {
|
|
236
|
+
const rel = relById.get(rId);
|
|
237
|
+
if (!rel) return undefined;
|
|
238
|
+
const target = resolveRelationshipTarget(documentPartPath, rel);
|
|
239
|
+
if (!target) return undefined;
|
|
240
|
+
return extractPartTextFromPackage(pkg, normalizePartPath(target));
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
/**
|
|
218
245
|
* Produce a new CanonicalDocument with the resolved chart_preview
|
|
219
246
|
* nodes carrying previewMediaId + corresponding MediaCatalog entries.
|
package/src/io/docx-session.ts
CHANGED
|
@@ -44,8 +44,9 @@ import {
|
|
|
44
44
|
normalizeParsedTextDocument,
|
|
45
45
|
normalizeParsedTextDocumentAsync,
|
|
46
46
|
} from "./normalize/normalize-text.ts";
|
|
47
|
-
import { resolveChartPreviewsForDocument } from "./chart-preview-resolver.ts";
|
|
47
|
+
import { createChartPartLookup, resolveChartPreviewsForDocument } from "./chart-preview-resolver.ts";
|
|
48
48
|
import { type LoadScheduler } from "./load-scheduler.ts";
|
|
49
|
+
import type { CacheEnvelope } from "../runtime/prerender/cache-envelope.ts";
|
|
49
50
|
import {
|
|
50
51
|
CONTENT_TYPES_PATH,
|
|
51
52
|
PACKAGE_RELATIONSHIPS_PATH,
|
|
@@ -76,6 +77,10 @@ import {
|
|
|
76
77
|
import { buildAppPropertiesXml } from "./export/build-app-properties-xml.ts";
|
|
77
78
|
import { createExportSession } from "./export/export-session.ts";
|
|
78
79
|
import { serializeMainDocument } from "./export/serialize-main-document.ts";
|
|
80
|
+
import {
|
|
81
|
+
serializeSettingsXml,
|
|
82
|
+
WORD_SETTINGS_CONTENT_TYPE,
|
|
83
|
+
} from "./export/serialize-settings.ts";
|
|
79
84
|
import {
|
|
80
85
|
parseRevisionsFromDocumentXml,
|
|
81
86
|
parseRevisionsFromStoryXml,
|
|
@@ -264,6 +269,21 @@ interface ImportedDocxState {
|
|
|
264
269
|
sourceDocumentAttributes: Record<string, string>;
|
|
265
270
|
sourceNumberingPartPath?: string;
|
|
266
271
|
sourceNumberingRelationshipId?: string;
|
|
272
|
+
/**
|
|
273
|
+
* Resolved `/word/settings.xml` part path when the source package carried
|
|
274
|
+
* one. Threaded through to the export path so the settings serializer can
|
|
275
|
+
* call `replaceOwnedPart` with the right relationship target.
|
|
276
|
+
*/
|
|
277
|
+
sourceSettingsPartPath?: string;
|
|
278
|
+
/**
|
|
279
|
+
* Original settings.xml bytes decoded as UTF-8. Passed to
|
|
280
|
+
* `serializeSettingsXml(settings, sourceXml)` as the graft source so
|
|
281
|
+
* unmodelled top-level children (`<w:defaultTabStop>`,
|
|
282
|
+
* `<w:documentProtection>`, mail-merge state, etc.) survive verbatim
|
|
283
|
+
* through round-trip. Undefined when the source package lacked a
|
|
284
|
+
* settings part.
|
|
285
|
+
*/
|
|
286
|
+
sourceSettingsXml?: string;
|
|
267
287
|
sourceCommentsPartPath?: string;
|
|
268
288
|
sourceCommentsRelationshipId?: string;
|
|
269
289
|
sourceCommentsRootTag?: string;
|
|
@@ -428,11 +448,17 @@ export function loadDocxEditorSession(
|
|
|
428
448
|
)
|
|
429
449
|
: createEmptyNumberingCatalog();
|
|
430
450
|
const mediaParts = collectInlineMediaParts(sourcePackage);
|
|
451
|
+
const chartPartLookup = createChartPartLookup(
|
|
452
|
+
sourcePackage,
|
|
453
|
+
mainDocumentPath,
|
|
454
|
+
documentPart.relationships,
|
|
455
|
+
);
|
|
431
456
|
const parsedDocument = parseMainDocumentXml(
|
|
432
457
|
sourceDocumentXml,
|
|
433
458
|
documentPart.relationships,
|
|
434
459
|
mediaParts,
|
|
435
460
|
mainDocumentPath,
|
|
461
|
+
chartPartLookup,
|
|
436
462
|
);
|
|
437
463
|
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
438
464
|
const normalizedDocument = normalizeParsedTextDocument(
|
|
@@ -831,6 +857,9 @@ export function loadDocxEditorSession(
|
|
|
831
857
|
relationship.type === NUMBERING_RELATIONSHIP_TYPE &&
|
|
832
858
|
relationship.targetMode === "internal",
|
|
833
859
|
)?.id,
|
|
860
|
+
sourceSettingsPartPath: settingsPartPath,
|
|
861
|
+
sourceSettingsXml:
|
|
862
|
+
settingsXmlForProtection.length > 0 ? settingsXmlForProtection : undefined,
|
|
834
863
|
sourceCommentsPartPath: commentsPartPath,
|
|
835
864
|
sourceCommentsRelationshipId: documentPart.relationships.find(
|
|
836
865
|
(relationship) =>
|
|
@@ -898,7 +927,7 @@ export function loadDocxEditorSession(
|
|
|
898
927
|
}
|
|
899
928
|
}
|
|
900
929
|
|
|
901
|
-
interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSessionOptions {
|
|
930
|
+
export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSessionOptions {
|
|
902
931
|
/**
|
|
903
932
|
* Scheduler that the async loader awaits between parse stages. Callers
|
|
904
933
|
* in DOM environments should construct this with `createLoadScheduler()`
|
|
@@ -909,6 +938,27 @@ interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSessionOptions
|
|
|
909
938
|
* behaves like the sync path from the test harness POV.
|
|
910
939
|
*/
|
|
911
940
|
scheduler: LoadScheduler;
|
|
941
|
+
/**
|
|
942
|
+
* L7 Phase 2.5 Plan B B.6b — optional laycache envelope. When supplied,
|
|
943
|
+
* `loadDocxEditorSessionAsync` still performs the cheap OPC read +
|
|
944
|
+
* workflow-payload parse (needed for `initialEditorStatePayload`,
|
|
945
|
+
* `workflowOverlay`, and `workflowMetadata`), then skips the five
|
|
946
|
+
* expensive stages — `parseMainDocumentXml`,
|
|
947
|
+
* `normalizeParsedTextDocumentAsync`, `parseCommentsFromOoxml`,
|
|
948
|
+
* `parseStylesXml`, and `createImportedCanonicalDocument` — and uses
|
|
949
|
+
* `envelope.canonicalDocument` directly (reference-equal, no clone).
|
|
950
|
+
*
|
|
951
|
+
* Callers obtain a validated envelope by calling
|
|
952
|
+
* `tryReadLaycacheEnvelope(bytes)` before invoking this function; when
|
|
953
|
+
* the probe returns `null`, omit this field and the loader runs the
|
|
954
|
+
* full parse.
|
|
955
|
+
*
|
|
956
|
+
* Async-only — the sync `loadDocxEditorSession` does not honor this
|
|
957
|
+
* option. `buildCompatibilityReport` and
|
|
958
|
+
* `resolveChartPreviewsForDocument` still run on the short-circuit
|
|
959
|
+
* path because their outputs are required by downstream consumers.
|
|
960
|
+
*/
|
|
961
|
+
laycacheEnvelope?: CacheEnvelope;
|
|
912
962
|
}
|
|
913
963
|
|
|
914
964
|
/**
|
|
@@ -1008,6 +1058,137 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1008
1058
|
}
|
|
1009
1059
|
|
|
1010
1060
|
try {
|
|
1061
|
+
// L7 Phase 2.5 Plan B B.6b — loader short-circuit. Hand
|
|
1062
|
+
// `envelope.canonicalDocument` through reference-equal and skip the
|
|
1063
|
+
// five expensive parse stages. The four `onLoadStage` callbacks still
|
|
1064
|
+
// fire in order — `body` and `styles-numbering-comments` emit with
|
|
1065
|
+
// near-zero duration — so host progress bars are unaffected.
|
|
1066
|
+
if (options.laycacheEnvelope) {
|
|
1067
|
+
stages.emit("body");
|
|
1068
|
+
stages.emit("styles-numbering-comments");
|
|
1069
|
+
|
|
1070
|
+
const canonicalDocument = options.laycacheEnvelope.canonicalDocument;
|
|
1071
|
+
|
|
1072
|
+
// `extractProtectionRanges` needs `parsedDocument.blocks` (which we
|
|
1073
|
+
// are skipping), so the short-circuit uses an empty ranges list;
|
|
1074
|
+
// document-level `editType` / `enforcement` still come from
|
|
1075
|
+
// settings.xml so read-only docs stay read-only.
|
|
1076
|
+
const settingsPartPath = resolveDocumentRelatedPartPath(
|
|
1077
|
+
sourcePackage,
|
|
1078
|
+
mainDocumentPath,
|
|
1079
|
+
documentPart.relationships,
|
|
1080
|
+
SETTINGS_RELATIONSHIP_TYPE,
|
|
1081
|
+
SETTINGS_PART_PATH,
|
|
1082
|
+
);
|
|
1083
|
+
const settingsXml =
|
|
1084
|
+
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
1085
|
+
? decodeUtf8(
|
|
1086
|
+
sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array(),
|
|
1087
|
+
)
|
|
1088
|
+
: "";
|
|
1089
|
+
const documentProtection = extractDocumentProtection(settingsXml);
|
|
1090
|
+
const protectionSnapshot = buildProtectionSnapshot(documentProtection, []);
|
|
1091
|
+
|
|
1092
|
+
// Chart previews (`previewMediaId` is host-dependent) aren't cached
|
|
1093
|
+
// in the envelope, so we still resolve them on the short-circuit.
|
|
1094
|
+
const documentWithChartPreviews = (await resolveChartPreviewsForDocument(
|
|
1095
|
+
canonicalDocument,
|
|
1096
|
+
sourcePackage,
|
|
1097
|
+
options.hostAdapter,
|
|
1098
|
+
)) as CanonicalDocumentEnvelope;
|
|
1099
|
+
|
|
1100
|
+
const timestamp = new Date().toISOString();
|
|
1101
|
+
const compatibility = buildCompatibilityReport({
|
|
1102
|
+
document: documentWithChartPreviews,
|
|
1103
|
+
generatedAt: timestamp,
|
|
1104
|
+
});
|
|
1105
|
+
await scheduler.yield();
|
|
1106
|
+
|
|
1107
|
+
const snapshot = createImportedSnapshot({
|
|
1108
|
+
documentId: options.documentId,
|
|
1109
|
+
editorBuild,
|
|
1110
|
+
timestamp,
|
|
1111
|
+
document: documentWithChartPreviews,
|
|
1112
|
+
compatibility: toPublicCompatibilityReport(compatibility),
|
|
1113
|
+
protectionSnapshot,
|
|
1114
|
+
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
1115
|
+
workflowOverlay: embeddedWorkflowOverlay,
|
|
1116
|
+
workflowMetadata: embeddedWorkflowMetadata,
|
|
1117
|
+
});
|
|
1118
|
+
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
1119
|
+
if (snapshotIssues.length > 0) {
|
|
1120
|
+
const firstIssue = snapshotIssues[0];
|
|
1121
|
+
return createDiagnosticsSession(
|
|
1122
|
+
options,
|
|
1123
|
+
createValidationImportDiagnostics({
|
|
1124
|
+
message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
|
|
1125
|
+
source: "import",
|
|
1126
|
+
details: {
|
|
1127
|
+
issueCount: snapshotIssues.length,
|
|
1128
|
+
firstIssuePath: firstIssue?.path,
|
|
1129
|
+
},
|
|
1130
|
+
}),
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Build `initialSessionState` inline — bypassing
|
|
1135
|
+
// `editorSessionStateFromPersistedSnapshot`'s structuredClone so
|
|
1136
|
+
// `session.initialSessionState.canonicalDocument` is reference-equal
|
|
1137
|
+
// to `envelope.canonicalDocument` (cloning a large canonical document
|
|
1138
|
+
// defeats part of the cache gain).
|
|
1139
|
+
const sessionState: EditorSessionState = {
|
|
1140
|
+
sessionVersion: "editor-session-state/1",
|
|
1141
|
+
schemaVersion: snapshot.schemaVersion,
|
|
1142
|
+
documentId: snapshot.documentId,
|
|
1143
|
+
docId: snapshot.docId,
|
|
1144
|
+
createdAt: snapshot.createdAt,
|
|
1145
|
+
updatedAt: snapshot.updatedAt,
|
|
1146
|
+
editorBuild: snapshot.editorBuild,
|
|
1147
|
+
canonicalDocument: snapshot.canonicalDocument,
|
|
1148
|
+
compatibility: snapshot.compatibility,
|
|
1149
|
+
warningLog: snapshot.warningLog,
|
|
1150
|
+
protectionSnapshot: snapshot.protectionSnapshot,
|
|
1151
|
+
sourcePackage: snapshot.sourcePackage,
|
|
1152
|
+
workflowOverlay: snapshot.workflowOverlay,
|
|
1153
|
+
workflowMetadata: snapshot.workflowMetadata,
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// The short-circuit path does not carry an `ImportedDocxState`, so
|
|
1157
|
+
// `exportDocx` lazily re-runs the cold path on first invocation and
|
|
1158
|
+
// memoizes. Keeps the warm-load fast while preserving byte-exact
|
|
1159
|
+
// export correctness.
|
|
1160
|
+
let lazyColdExport: LoadedDocxEditorSession["exportDocx"] | undefined;
|
|
1161
|
+
const exportDocx: LoadedDocxEditorSession["exportDocx"] = async (
|
|
1162
|
+
nextSessionStateOrSnapshot,
|
|
1163
|
+
exportOptions,
|
|
1164
|
+
) => {
|
|
1165
|
+
if (!lazyColdExport) {
|
|
1166
|
+
const { laycacheEnvelope: _unused, ...coldOptions } = options;
|
|
1167
|
+
void _unused;
|
|
1168
|
+
const coldSession = await loadDocxEditorSessionAsync(coldOptions);
|
|
1169
|
+
if (coldSession.fatalError) {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`DOCX export via short-circuit fallback failed cold load: ${coldSession.fatalError.message ?? "fatal error"}`,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
lazyColdExport = coldSession.exportDocx;
|
|
1175
|
+
}
|
|
1176
|
+
return lazyColdExport(nextSessionStateOrSnapshot, exportOptions);
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
stages.emit("skeleton-ready");
|
|
1180
|
+
return {
|
|
1181
|
+
initialSessionState: sessionState,
|
|
1182
|
+
initialSnapshot: snapshot,
|
|
1183
|
+
readOnly: false,
|
|
1184
|
+
protectionSnapshot,
|
|
1185
|
+
exportDocx,
|
|
1186
|
+
...(embeddedWorkflowPayload?.editorState
|
|
1187
|
+
? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
|
|
1188
|
+
: {}),
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1011
1192
|
const sourceDocumentXml = decodeUtf8(documentPart.bytes);
|
|
1012
1193
|
const importedRevisions = parseRevisionsFromDocumentXml(sourceDocumentXml);
|
|
1013
1194
|
const numberingPartPath = resolveDocumentRelatedPartPath(
|
|
@@ -1023,11 +1204,17 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1023
1204
|
)
|
|
1024
1205
|
: createEmptyNumberingCatalog();
|
|
1025
1206
|
const mediaParts = collectInlineMediaParts(sourcePackage);
|
|
1207
|
+
const chartPartLookup = createChartPartLookup(
|
|
1208
|
+
sourcePackage,
|
|
1209
|
+
mainDocumentPath,
|
|
1210
|
+
documentPart.relationships,
|
|
1211
|
+
);
|
|
1026
1212
|
const parsedDocument = parseMainDocumentXml(
|
|
1027
1213
|
sourceDocumentXml,
|
|
1028
1214
|
documentPart.relationships,
|
|
1029
1215
|
mediaParts,
|
|
1030
1216
|
mainDocumentPath,
|
|
1217
|
+
chartPartLookup,
|
|
1031
1218
|
);
|
|
1032
1219
|
await scheduler.yield();
|
|
1033
1220
|
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
@@ -1442,6 +1629,9 @@ export async function loadDocxEditorSessionAsync(
|
|
|
1442
1629
|
relationship.type === NUMBERING_RELATIONSHIP_TYPE &&
|
|
1443
1630
|
relationship.targetMode === "internal",
|
|
1444
1631
|
)?.id,
|
|
1632
|
+
sourceSettingsPartPath: settingsPartPath,
|
|
1633
|
+
sourceSettingsXml:
|
|
1634
|
+
settingsXmlForProtection.length > 0 ? settingsXmlForProtection : undefined,
|
|
1445
1635
|
sourceCommentsPartPath: commentsPartPath,
|
|
1446
1636
|
sourceCommentsRelationshipId: documentPart.relationships.find(
|
|
1447
1637
|
(relationship) =>
|
|
@@ -1736,6 +1926,16 @@ function exportDocxEditorSession(
|
|
|
1736
1926
|
}
|
|
1737
1927
|
}
|
|
1738
1928
|
|
|
1929
|
+
// Settings.xml is owned when the source package carried one OR the canonical
|
|
1930
|
+
// model carries settings we need to re-emit. The `canReuse && signatureMatch`
|
|
1931
|
+
// short-circuit above already skips re-export entirely for no-edit sessions,
|
|
1932
|
+
// so every path that reaches here is willing to emit a rebuilt settings.xml.
|
|
1933
|
+
const settingsPartPath =
|
|
1934
|
+
state.sourceSettingsPartPath ?? SETTINGS_PART_PATH;
|
|
1935
|
+
const hasSettingsSurface =
|
|
1936
|
+
Boolean(state.sourceSettingsPartPath) ||
|
|
1937
|
+
exportedSubParts?.settings !== undefined;
|
|
1938
|
+
|
|
1739
1939
|
const exportSession = createExportSession(state.sourcePackage, [
|
|
1740
1940
|
state.sourceDocumentPartPath,
|
|
1741
1941
|
APP_PROPERTIES_PART_PATH,
|
|
@@ -1748,6 +1948,7 @@ function exportDocxEditorSession(
|
|
|
1748
1948
|
commentsExtendedPartPath,
|
|
1749
1949
|
commentsIdsPartPath,
|
|
1750
1950
|
peoplePartPath,
|
|
1951
|
+
...(hasSettingsSurface ? [settingsPartPath] : []),
|
|
1751
1952
|
...subPartOwnedPaths,
|
|
1752
1953
|
]);
|
|
1753
1954
|
|
|
@@ -1781,6 +1982,22 @@ function exportDocxEditorSession(
|
|
|
1781
1982
|
});
|
|
1782
1983
|
}
|
|
1783
1984
|
|
|
1985
|
+
if (hasSettingsSurface) {
|
|
1986
|
+
// Canonical settings ∅ + no source settings → omit the owned-part write
|
|
1987
|
+
// (hasSettingsSurface is already false in that case). Otherwise route
|
|
1988
|
+
// through the graft serializer so unmodelled children round-trip via
|
|
1989
|
+
// source bytes while canonical mutations land.
|
|
1990
|
+
const canonicalSettings = exportedSubParts?.settings ?? {};
|
|
1991
|
+
const settingsXml = serializeSettingsXml(canonicalSettings, state.sourceSettingsXml);
|
|
1992
|
+
exportSession.replaceOwnedPart({
|
|
1993
|
+
path: settingsPartPath,
|
|
1994
|
+
bytes: new TextEncoder().encode(settingsXml),
|
|
1995
|
+
contentType:
|
|
1996
|
+
state.sourcePackage.parts.get(settingsPartPath)?.contentType ??
|
|
1997
|
+
WORD_SETTINGS_CONTENT_TYPE,
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1784
2001
|
if (serializedComments.serializedCommentIds.length > 0 || state.sourceCommentsPartPath) {
|
|
1785
2002
|
exportSession.replaceOwnedPart({
|
|
1786
2003
|
path: commentsPartPath,
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
} from "../../model/canonical-document.ts";
|
|
16
16
|
import type { OpcRelationship } from "../ooxml/part-manifest.ts";
|
|
17
17
|
import type { RevisionParagraphBoundary } from "../ooxml/revision-boundaries.ts";
|
|
18
|
+
import { SCOPE_MARKER_BOOKMARK_PREFIX } from "../ooxml/parse-scope-markers.ts";
|
|
18
19
|
import { getOpaqueFragment } from "../../preservation/store.ts";
|
|
19
20
|
import { retainRelationshipsForFragment } from "../../preservation/relationship-retention.ts";
|
|
20
21
|
import { serializeParagraphNumberingProperties } from "./serialize-numbering.ts";
|
|
@@ -571,6 +572,21 @@ function serializeTableInlineNode(
|
|
|
571
572
|
);
|
|
572
573
|
case "bookmark_end":
|
|
573
574
|
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(node.bookmarkId)}"/>`;
|
|
575
|
+
case "scope_marker_start": {
|
|
576
|
+
// S1 — scope markers export as w:bookmarkStart with the reserved
|
|
577
|
+
// `bw:scope:` name prefix. The synthetic w:id is keyed on scopeId so
|
|
578
|
+
// the matching end element references the same id.
|
|
579
|
+
const bkId = `scope-${node.scopeId}`;
|
|
580
|
+
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
581
|
+
return (
|
|
582
|
+
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
583
|
+
` w:name="${escapeXmlAttribute(name)}"/>`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
case "scope_marker_end": {
|
|
587
|
+
const bkId = `scope-${node.scopeId}`;
|
|
588
|
+
return `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
589
|
+
}
|
|
574
590
|
case "footnote_ref": {
|
|
575
591
|
const refElement =
|
|
576
592
|
node.noteKind === "footnote"
|
|
@@ -1060,6 +1076,27 @@ function serializeInlineNode(
|
|
|
1060
1076
|
boundaries.set(cursor, xmlOffset + xml.length);
|
|
1061
1077
|
return { xml, cursor, boundaries };
|
|
1062
1078
|
}
|
|
1079
|
+
case "scope_marker_start": {
|
|
1080
|
+
// S1 — mirror the bookmark_start shape with the reserved `bw:scope:`
|
|
1081
|
+
// name prefix. See serializeInline() above for the same convention.
|
|
1082
|
+
const bkId = `scope-${node.scopeId}`;
|
|
1083
|
+
const name = `${SCOPE_MARKER_BOOKMARK_PREFIX}${node.scopeId}`;
|
|
1084
|
+
const xml =
|
|
1085
|
+
`<w:bookmarkStart w:id="${escapeXmlAttribute(bkId)}"` +
|
|
1086
|
+
` w:name="${escapeXmlAttribute(name)}"/>`;
|
|
1087
|
+
const boundaries = new Map<number, number>();
|
|
1088
|
+
boundaries.set(cursor, xmlOffset);
|
|
1089
|
+
boundaries.set(cursor, xmlOffset + xml.length);
|
|
1090
|
+
return { xml, cursor, boundaries };
|
|
1091
|
+
}
|
|
1092
|
+
case "scope_marker_end": {
|
|
1093
|
+
const bkId = `scope-${node.scopeId}`;
|
|
1094
|
+
const xml = `<w:bookmarkEnd w:id="${escapeXmlAttribute(bkId)}"/>`;
|
|
1095
|
+
const boundaries = new Map<number, number>();
|
|
1096
|
+
boundaries.set(cursor, xmlOffset);
|
|
1097
|
+
boundaries.set(cursor, xmlOffset + xml.length);
|
|
1098
|
+
return { xml, cursor, boundaries };
|
|
1099
|
+
}
|
|
1063
1100
|
case "footnote_ref": {
|
|
1064
1101
|
const refElement =
|
|
1065
1102
|
node.noteKind === "footnote"
|