@beyondwork/docx-react-component 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/package.json +6 -11
- package/src/api/public-types.ts +32 -1
- package/src/core/state/editor-state.ts +318 -9
- package/src/io/docx-session.ts +392 -22
- package/src/io/export/export-session.ts +55 -0
- package/src/io/export/serialize-footnotes.ts +5 -20
- package/src/io/export/serialize-headers-footers.ts +5 -31
- package/src/io/export/serialize-main-document.ts +78 -5
- package/src/io/normalize/normalize-text.ts +90 -1
- package/src/io/ooxml/parse-footnotes.ts +68 -5
- package/src/io/ooxml/parse-headers-footers.ts +67 -9
- package/src/io/ooxml/parse-main-document.ts +169 -6
- package/src/io/opc/package-reader.ts +3 -3
- package/src/io/source-package-provenance.ts +241 -0
- package/src/model/canonical-document.ts +450 -2
- package/src/model/cds-1.0.0.ts +5 -2
- package/src/model/snapshot.ts +190 -19
- package/src/preservation/package-preservation.ts +0 -7
- package/src/runtime/document-runtime.ts +7 -1
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
- package/src/runtime/surface-projection.ts +199 -17
- package/src/ui/WordReviewEditor.tsx +209 -14
- package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +66 -2
- package/src/validation/compatibility-engine.ts +208 -0
package/src/model/snapshot.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CDS_SCHEMA_VERSION,
|
|
3
3
|
COMPATIBILITY_REPORT_VERSION,
|
|
4
|
+
LEGACY_PERSISTED_EDITOR_SNAPSHOT_VERSION,
|
|
4
5
|
PERSISTED_EDITOR_SNAPSHOT_VERSION,
|
|
5
6
|
type CompatibilityReportVersion,
|
|
7
|
+
type Base64,
|
|
6
8
|
type ISO8601DateTime,
|
|
7
9
|
type ModelValidationIssue,
|
|
8
10
|
type PersistedEditorSnapshotVersion,
|
|
@@ -87,6 +89,15 @@ export interface CompatibilityReport {
|
|
|
87
89
|
errors: EditorError[];
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
export interface PersistedSourcePackage {
|
|
93
|
+
format: "docx";
|
|
94
|
+
storage: "embedded-base64";
|
|
95
|
+
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
96
|
+
sourceLabel?: string;
|
|
97
|
+
sha256Hex: string;
|
|
98
|
+
bytesBase64: Base64;
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
export interface PersistedEditorSnapshot {
|
|
91
102
|
snapshotVersion: PersistedEditorSnapshotVersion;
|
|
92
103
|
schemaVersion: typeof CDS_SCHEMA_VERSION;
|
|
@@ -99,9 +110,10 @@ export interface PersistedEditorSnapshot {
|
|
|
99
110
|
canonicalDocument: CanonicalDocument;
|
|
100
111
|
compatibility: CompatibilityReport;
|
|
101
112
|
warningLog: EditorWarning[];
|
|
113
|
+
sourcePackage?: PersistedSourcePackage;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
|
-
const
|
|
116
|
+
const SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS = [
|
|
105
117
|
"snapshotVersion",
|
|
106
118
|
"schemaVersion",
|
|
107
119
|
"documentId",
|
|
@@ -115,6 +127,56 @@ const SNAPSHOT_TOP_LEVEL_KEYS = [
|
|
|
115
127
|
"warningLog",
|
|
116
128
|
] as const;
|
|
117
129
|
|
|
130
|
+
const SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS = ["sourcePackage"] as const;
|
|
131
|
+
|
|
132
|
+
const PERSISTED_EDITOR_SNAPSHOT_VERSIONS = new Set<PersistedEditorSnapshotVersion>([
|
|
133
|
+
LEGACY_PERSISTED_EDITOR_SNAPSHOT_VERSION,
|
|
134
|
+
PERSISTED_EDITOR_SNAPSHOT_VERSION,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const DOCX_SOURCE_PACKAGE_MIME_TYPE =
|
|
138
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
139
|
+
const COMPATIBILITY_FEATURE_CLASSES = new Set<CompatibilityFeatureClass>([
|
|
140
|
+
"supported-roundtrip",
|
|
141
|
+
"preserve-only",
|
|
142
|
+
"unsupported-fatal",
|
|
143
|
+
]);
|
|
144
|
+
const EDITOR_WARNING_CODES = new Set<EditorWarningCode>([
|
|
145
|
+
"unsupported_ooxml_preserved",
|
|
146
|
+
"unsupported_ooxml_locked",
|
|
147
|
+
"import_normalized",
|
|
148
|
+
"export_roundtrip_risk",
|
|
149
|
+
"comment_anchor_detached",
|
|
150
|
+
"revision_anchor_detached",
|
|
151
|
+
"large_document_degraded",
|
|
152
|
+
"font_substitution",
|
|
153
|
+
"image_missing",
|
|
154
|
+
]);
|
|
155
|
+
const EDITOR_WARNING_SEVERITIES = new Set<EditorWarning["severity"]>(["info", "warning"]);
|
|
156
|
+
const EDITOR_WARNING_SOURCES = new Set<EditorWarning["source"]>([
|
|
157
|
+
"import",
|
|
158
|
+
"runtime",
|
|
159
|
+
"review",
|
|
160
|
+
"preservation",
|
|
161
|
+
"validation",
|
|
162
|
+
"export",
|
|
163
|
+
]);
|
|
164
|
+
const EDITOR_ERROR_CODES = new Set<EditorErrorCode>([
|
|
165
|
+
"import_failed",
|
|
166
|
+
"export_failed",
|
|
167
|
+
"package_corrupt",
|
|
168
|
+
"validation_failed",
|
|
169
|
+
"datastore_failed",
|
|
170
|
+
"internal_invariant",
|
|
171
|
+
]);
|
|
172
|
+
const EDITOR_ERROR_SOURCES = new Set<EditorError["source"]>([
|
|
173
|
+
"import",
|
|
174
|
+
"runtime",
|
|
175
|
+
"validation",
|
|
176
|
+
"datastore",
|
|
177
|
+
"export",
|
|
178
|
+
]);
|
|
179
|
+
|
|
118
180
|
export function serializePersistedEditorSnapshot(
|
|
119
181
|
snapshot: PersistedEditorSnapshot,
|
|
120
182
|
): string {
|
|
@@ -146,14 +208,15 @@ export function validatePersistedEditorSnapshot(
|
|
|
146
208
|
return issues;
|
|
147
209
|
}
|
|
148
210
|
|
|
149
|
-
validateExactObjectKeys(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"$.snapshotVersion",
|
|
211
|
+
validateExactObjectKeys(
|
|
212
|
+
record,
|
|
213
|
+
SNAPSHOT_REQUIRED_TOP_LEVEL_KEYS,
|
|
214
|
+
SNAPSHOT_OPTIONAL_TOP_LEVEL_KEYS,
|
|
215
|
+
"$",
|
|
155
216
|
issues,
|
|
156
217
|
);
|
|
218
|
+
|
|
219
|
+
validateSnapshotVersion(record.snapshotVersion, "$.snapshotVersion", issues);
|
|
157
220
|
expectExactString(
|
|
158
221
|
record.schemaVersion,
|
|
159
222
|
CDS_SCHEMA_VERSION,
|
|
@@ -193,6 +256,10 @@ export function validatePersistedEditorSnapshot(
|
|
|
193
256
|
});
|
|
194
257
|
}
|
|
195
258
|
|
|
259
|
+
if (record.sourcePackage !== undefined) {
|
|
260
|
+
validatePersistedSourcePackage(record.sourcePackage, "$.sourcePackage", issues);
|
|
261
|
+
}
|
|
262
|
+
|
|
196
263
|
return issues;
|
|
197
264
|
}
|
|
198
265
|
|
|
@@ -280,7 +347,15 @@ function validateCompatibilityFeatureEntry(
|
|
|
280
347
|
|
|
281
348
|
expectString(record.featureEntryId, `${path}.featureEntryId`, issues);
|
|
282
349
|
expectString(record.featureKey, `${path}.featureKey`, issues);
|
|
283
|
-
|
|
350
|
+
if (
|
|
351
|
+
typeof record.featureClass !== "string" ||
|
|
352
|
+
!COMPATIBILITY_FEATURE_CLASSES.has(record.featureClass as CompatibilityFeatureClass)
|
|
353
|
+
) {
|
|
354
|
+
issues.push({
|
|
355
|
+
path: `${path}.featureClass`,
|
|
356
|
+
message: "featureClass must be supported-roundtrip, preserve-only, or unsupported-fatal.",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
284
359
|
expectString(record.message, `${path}.message`, issues);
|
|
285
360
|
}
|
|
286
361
|
|
|
@@ -295,10 +370,31 @@ function validateEditorWarning(
|
|
|
295
370
|
}
|
|
296
371
|
|
|
297
372
|
expectString(record.warningId, `${path}.warningId`, issues);
|
|
298
|
-
|
|
299
|
-
|
|
373
|
+
if (typeof record.code !== "string" || !EDITOR_WARNING_CODES.has(record.code as EditorWarningCode)) {
|
|
374
|
+
issues.push({
|
|
375
|
+
path: `${path}.code`,
|
|
376
|
+
message: `code must be one of ${JSON.stringify([...EDITOR_WARNING_CODES])}.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (
|
|
380
|
+
typeof record.severity !== "string" ||
|
|
381
|
+
!EDITOR_WARNING_SEVERITIES.has(record.severity as EditorWarning["severity"])
|
|
382
|
+
) {
|
|
383
|
+
issues.push({
|
|
384
|
+
path: `${path}.severity`,
|
|
385
|
+
message: "severity must be info or warning.",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
300
388
|
expectString(record.message, `${path}.message`, issues);
|
|
301
|
-
|
|
389
|
+
if (
|
|
390
|
+
typeof record.source !== "string" ||
|
|
391
|
+
!EDITOR_WARNING_SOURCES.has(record.source as EditorWarning["source"])
|
|
392
|
+
) {
|
|
393
|
+
issues.push({
|
|
394
|
+
path: `${path}.source`,
|
|
395
|
+
message: `source must be one of ${JSON.stringify([...EDITOR_WARNING_SOURCES])}.`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
302
398
|
}
|
|
303
399
|
|
|
304
400
|
function validateEditorError(
|
|
@@ -312,9 +408,22 @@ function validateEditorError(
|
|
|
312
408
|
}
|
|
313
409
|
|
|
314
410
|
expectString(record.errorId, `${path}.errorId`, issues);
|
|
315
|
-
|
|
411
|
+
if (typeof record.code !== "string" || !EDITOR_ERROR_CODES.has(record.code as EditorErrorCode)) {
|
|
412
|
+
issues.push({
|
|
413
|
+
path: `${path}.code`,
|
|
414
|
+
message: `code must be one of ${JSON.stringify([...EDITOR_ERROR_CODES])}.`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
316
417
|
expectString(record.message, `${path}.message`, issues);
|
|
317
|
-
|
|
418
|
+
if (
|
|
419
|
+
typeof record.source !== "string" ||
|
|
420
|
+
!EDITOR_ERROR_SOURCES.has(record.source as EditorError["source"])
|
|
421
|
+
) {
|
|
422
|
+
issues.push({
|
|
423
|
+
path: `${path}.source`,
|
|
424
|
+
message: `source must be one of ${JSON.stringify([...EDITOR_ERROR_SOURCES])}.`,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
318
427
|
|
|
319
428
|
if (typeof record.isFatal !== "boolean") {
|
|
320
429
|
issues.push({
|
|
@@ -326,24 +435,25 @@ function validateEditorError(
|
|
|
326
435
|
|
|
327
436
|
function validateExactObjectKeys(
|
|
328
437
|
record: Record<string, unknown>,
|
|
329
|
-
|
|
438
|
+
requiredKeys: readonly string[],
|
|
439
|
+
optionalKeys: readonly string[],
|
|
330
440
|
path: string,
|
|
331
441
|
issues: ModelValidationIssue[],
|
|
332
442
|
): void {
|
|
333
443
|
const actualKeys = new Set(Object.keys(record));
|
|
334
|
-
const
|
|
444
|
+
const allowedKeys = new Set([...requiredKeys, ...optionalKeys]);
|
|
335
445
|
|
|
336
|
-
for (const
|
|
337
|
-
if (!actualKeys.has(
|
|
446
|
+
for (const requiredKey of requiredKeys) {
|
|
447
|
+
if (!actualKeys.has(requiredKey)) {
|
|
338
448
|
issues.push({
|
|
339
449
|
path,
|
|
340
|
-
message: `Missing required key ${JSON.stringify(
|
|
450
|
+
message: `Missing required key ${JSON.stringify(requiredKey)}.`,
|
|
341
451
|
});
|
|
342
452
|
}
|
|
343
453
|
}
|
|
344
454
|
|
|
345
455
|
for (const actualKey of actualKeys) {
|
|
346
|
-
if (!
|
|
456
|
+
if (!allowedKeys.has(actualKey)) {
|
|
347
457
|
issues.push({
|
|
348
458
|
path: `${path}.${actualKey}`,
|
|
349
459
|
message:
|
|
@@ -360,6 +470,65 @@ function prefixIssue(basePath: string) {
|
|
|
360
470
|
});
|
|
361
471
|
}
|
|
362
472
|
|
|
473
|
+
function validateSnapshotVersion(
|
|
474
|
+
value: unknown,
|
|
475
|
+
path: string,
|
|
476
|
+
issues: ModelValidationIssue[],
|
|
477
|
+
): PersistedEditorSnapshotVersion | null {
|
|
478
|
+
if (typeof value !== "string" || !PERSISTED_EDITOR_SNAPSHOT_VERSIONS.has(value as never)) {
|
|
479
|
+
issues.push({
|
|
480
|
+
path,
|
|
481
|
+
message: `Expected one of ${JSON.stringify([...PERSISTED_EDITOR_SNAPSHOT_VERSIONS])}.`,
|
|
482
|
+
});
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return value as PersistedEditorSnapshotVersion;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function validatePersistedSourcePackage(
|
|
490
|
+
sourcePackage: unknown,
|
|
491
|
+
path: string,
|
|
492
|
+
issues: ModelValidationIssue[],
|
|
493
|
+
): void {
|
|
494
|
+
const record = asPlainObject(sourcePackage, path, issues);
|
|
495
|
+
if (!record) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
validateExactObjectKeys(
|
|
500
|
+
record,
|
|
501
|
+
["format", "storage", "mimeType", "sha256Hex", "bytesBase64"],
|
|
502
|
+
["sourceLabel"],
|
|
503
|
+
path,
|
|
504
|
+
issues,
|
|
505
|
+
);
|
|
506
|
+
expectExactString(record.format, "docx", `${path}.format`, issues);
|
|
507
|
+
expectExactString(record.storage, "embedded-base64", `${path}.storage`, issues);
|
|
508
|
+
expectExactString(
|
|
509
|
+
record.mimeType,
|
|
510
|
+
DOCX_SOURCE_PACKAGE_MIME_TYPE,
|
|
511
|
+
`${path}.mimeType`,
|
|
512
|
+
issues,
|
|
513
|
+
);
|
|
514
|
+
expectString(record.sha256Hex, `${path}.sha256Hex`, issues);
|
|
515
|
+
expectString(record.bytesBase64, `${path}.bytesBase64`, issues);
|
|
516
|
+
|
|
517
|
+
if (
|
|
518
|
+
typeof record.sha256Hex === "string" &&
|
|
519
|
+
!/^[0-9a-f]{64}$/u.test(record.sha256Hex)
|
|
520
|
+
) {
|
|
521
|
+
issues.push({
|
|
522
|
+
path: `${path}.sha256Hex`,
|
|
523
|
+
message: "Expected a lowercase SHA-256 hex digest.",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (record.sourceLabel !== undefined) {
|
|
528
|
+
expectString(record.sourceLabel, `${path}.sourceLabel`, issues);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
363
532
|
export function createPersistedEditorSnapshot(params: {
|
|
364
533
|
documentId: string;
|
|
365
534
|
savedAt: ISO8601DateTime;
|
|
@@ -367,6 +536,7 @@ export function createPersistedEditorSnapshot(params: {
|
|
|
367
536
|
canonicalDocument: CanonicalDocument;
|
|
368
537
|
compatibility: CompatibilityReport;
|
|
369
538
|
warningLog?: EditorWarning[];
|
|
539
|
+
sourcePackage?: PersistedSourcePackage;
|
|
370
540
|
}): PersistedEditorSnapshot {
|
|
371
541
|
assertCanonicalDocument(params.canonicalDocument);
|
|
372
542
|
assertCompatibilityReport(params.compatibility);
|
|
@@ -383,6 +553,7 @@ export function createPersistedEditorSnapshot(params: {
|
|
|
383
553
|
canonicalDocument: params.canonicalDocument,
|
|
384
554
|
compatibility: params.compatibility,
|
|
385
555
|
warningLog: params.warningLog ?? params.compatibility.warnings,
|
|
556
|
+
sourcePackage: params.sourcePackage,
|
|
386
557
|
};
|
|
387
558
|
}
|
|
388
559
|
|
|
@@ -5,14 +5,7 @@ import type { OpcPackage } from "../io/opc/package-reader.ts";
|
|
|
5
5
|
const CORE_NON_PRESERVED_PART_PATHS = new Set([
|
|
6
6
|
"/docProps/app.xml",
|
|
7
7
|
"/docProps/core.xml",
|
|
8
|
-
"/docProps/custom.xml",
|
|
9
|
-
"/word/fontTable.xml",
|
|
10
8
|
"/word/numbering.xml",
|
|
11
|
-
"/word/settings.xml",
|
|
12
|
-
"/word/styles.xml",
|
|
13
|
-
"/word/stylesWithEffects.xml",
|
|
14
|
-
"/word/theme/theme1.xml",
|
|
15
|
-
"/word/webSettings.xml",
|
|
16
9
|
]);
|
|
17
10
|
|
|
18
11
|
export function collectPreservedPackageParts(
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type RevisionStore,
|
|
49
49
|
} from "../review/store/revision-store.ts";
|
|
50
50
|
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
51
|
+
import { mergeCompatibilityReports } from "../validation/compatibility-report.ts";
|
|
51
52
|
import { createEditorSurfaceSnapshot } from "./surface-projection.ts";
|
|
52
53
|
|
|
53
54
|
export type Unsubscribe = () => void;
|
|
@@ -724,12 +725,17 @@ function countOpaqueFragments(opaqueFragments: Record<string, unknown>): number
|
|
|
724
725
|
}
|
|
725
726
|
|
|
726
727
|
function createDerivedCompatibility(state: EditorState): InternalCompatibilityReport {
|
|
727
|
-
|
|
728
|
+
const derived = buildCompatibilityReport({
|
|
728
729
|
document: state.document,
|
|
729
730
|
warnings: state.warnings,
|
|
730
731
|
fatalError: state.fatalError,
|
|
731
732
|
generatedAt: state.document.updatedAt,
|
|
732
733
|
});
|
|
734
|
+
|
|
735
|
+
return mergeCompatibilityReports([state.compatibility as never, derived as never], {
|
|
736
|
+
generatedAt: state.document.updatedAt,
|
|
737
|
+
blockExport: state.compatibility.blockExport || derived.blockExport,
|
|
738
|
+
}) as InternalCompatibilityReport;
|
|
733
739
|
}
|
|
734
740
|
|
|
735
741
|
function toPublicCommentSidebarSnapshot(
|
|
@@ -71,7 +71,7 @@ function createDiagnosticsSnapshot(input: {
|
|
|
71
71
|
const diagnosticId = `diagnostic:${sanitizeDiagnosticsToken(input.documentId)}`;
|
|
72
72
|
|
|
73
73
|
return {
|
|
74
|
-
snapshotVersion: "persisted-editor-snapshot/
|
|
74
|
+
snapshotVersion: "persisted-editor-snapshot/2",
|
|
75
75
|
schemaVersion: "cds/1.0.0",
|
|
76
76
|
documentId: input.documentId,
|
|
77
77
|
docId,
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
SurfaceInlineSegment,
|
|
5
5
|
SurfaceTableCellSnapshot,
|
|
6
6
|
SurfaceTableRowSnapshot,
|
|
7
|
+
SurfaceTextMark,
|
|
7
8
|
} from "../api/public-types";
|
|
8
9
|
import type {
|
|
9
10
|
CanonicalDocumentEnvelope,
|
|
@@ -208,10 +209,11 @@ function createTableBlock(
|
|
|
208
209
|
const lockedFragmentIds: string[] = [];
|
|
209
210
|
let innerCursor = cursor;
|
|
210
211
|
const rows: SurfaceTableRowSnapshot[] = [];
|
|
212
|
+
const rowSpans = computeTableRowSpans(table);
|
|
211
213
|
|
|
212
|
-
for (const row of table.rows) {
|
|
214
|
+
for (const [rowIndex, row] of table.rows.entries()) {
|
|
213
215
|
const cells: SurfaceTableCellSnapshot[] = [];
|
|
214
|
-
for (const cell of row.cells) {
|
|
216
|
+
for (const [cellIndex, cell] of row.cells.entries()) {
|
|
215
217
|
const cellContent: SurfaceBlockSnapshot[] = [];
|
|
216
218
|
for (const child of cell.children) {
|
|
217
219
|
const result = createSurfaceBlock(child, document, innerCursor, counters);
|
|
@@ -223,7 +225,7 @@ function createTableBlock(
|
|
|
223
225
|
gridSpan: cell.gridSpan ?? 1,
|
|
224
226
|
verticalMerge: cell.verticalMerge ?? null,
|
|
225
227
|
colspan: cell.gridSpan ?? 1,
|
|
226
|
-
rowspan:
|
|
228
|
+
rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
|
|
227
229
|
content: cellContent,
|
|
228
230
|
});
|
|
229
231
|
}
|
|
@@ -245,6 +247,55 @@ function createTableBlock(
|
|
|
245
247
|
};
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
function computeTableRowSpans(table: TableNode): Map<string, number> {
|
|
251
|
+
const positionedRows = table.rows.map((row) => {
|
|
252
|
+
let startColumn = 0;
|
|
253
|
+
|
|
254
|
+
return row.cells.map((cell, cellIndex) => {
|
|
255
|
+
const width = cell.gridSpan ?? 1;
|
|
256
|
+
const positionedCell = {
|
|
257
|
+
cell,
|
|
258
|
+
cellIndex,
|
|
259
|
+
startColumn,
|
|
260
|
+
endColumn: startColumn + width - 1,
|
|
261
|
+
};
|
|
262
|
+
startColumn += width;
|
|
263
|
+
return positionedCell;
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const rowSpans = new Map<string, number>();
|
|
268
|
+
|
|
269
|
+
for (const [rowIndex, row] of positionedRows.entries()) {
|
|
270
|
+
for (const positionedCell of row) {
|
|
271
|
+
if (positionedCell.cell.verticalMerge !== "restart") {
|
|
272
|
+
rowSpans.set(`${rowIndex}:${positionedCell.cellIndex}`, 1);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let rowspan = 1;
|
|
277
|
+
for (let nextRowIndex = rowIndex + 1; nextRowIndex < positionedRows.length; nextRowIndex += 1) {
|
|
278
|
+
const continuedCell = positionedRows[nextRowIndex]?.find(
|
|
279
|
+
(candidate) =>
|
|
280
|
+
candidate.cell.verticalMerge === "continue" &&
|
|
281
|
+
candidate.startColumn === positionedCell.startColumn &&
|
|
282
|
+
candidate.endColumn === positionedCell.endColumn,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (!continuedCell) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
rowspan += 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
rowSpans.set(`${rowIndex}:${positionedCell.cellIndex}`, rowspan);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return rowSpans;
|
|
297
|
+
}
|
|
298
|
+
|
|
248
299
|
function createSdtBlock(
|
|
249
300
|
sdtIndex: number,
|
|
250
301
|
block: SdtNode,
|
|
@@ -304,6 +355,17 @@ function createParagraphBlock(
|
|
|
304
355
|
to: start,
|
|
305
356
|
...(paragraph.styleId ? { styleId: paragraph.styleId } : {}),
|
|
306
357
|
...(paragraph.numbering ? { numbering: paragraph.numbering } : {}),
|
|
358
|
+
...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
|
|
359
|
+
...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
|
|
360
|
+
...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
|
|
361
|
+
...(paragraph.borders ? { borders: paragraph.borders } : {}),
|
|
362
|
+
...(paragraph.shading ? { shading: paragraph.shading } : {}),
|
|
363
|
+
...(paragraph.tabStops && paragraph.tabStops.length > 0 ? { tabStops: paragraph.tabStops } : {}),
|
|
364
|
+
...(paragraph.keepNext ? { keepNext: true } : {}),
|
|
365
|
+
...(paragraph.keepLines ? { keepLines: true } : {}),
|
|
366
|
+
...(paragraph.pageBreakBefore ? { pageBreakBefore: true } : {}),
|
|
367
|
+
...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
|
|
368
|
+
...(paragraph.bidi ? { bidi: true } : {}),
|
|
307
369
|
segments: [],
|
|
308
370
|
};
|
|
309
371
|
const lockedFragmentIds: string[] = [];
|
|
@@ -339,7 +401,13 @@ function appendInlineSegments(
|
|
|
339
401
|
from: start,
|
|
340
402
|
to: start + Array.from(node.text).length,
|
|
341
403
|
text: node.text,
|
|
342
|
-
...(node.marks ?
|
|
404
|
+
...(node.marks ? (() => {
|
|
405
|
+
const result = cloneMarks(node.marks);
|
|
406
|
+
return {
|
|
407
|
+
...(result.marks.length > 0 ? { marks: result.marks } : {}),
|
|
408
|
+
...(result.markAttrs ? { markAttrs: result.markAttrs } : {}),
|
|
409
|
+
};
|
|
410
|
+
})() : {}),
|
|
343
411
|
...(hyperlinkHref ? { hyperlinkHref } : {}),
|
|
344
412
|
});
|
|
345
413
|
return { nextCursor: start + Array.from(node.text).length, lockedFragmentIds: [] };
|
|
@@ -415,12 +483,66 @@ function appendInlineSegments(
|
|
|
415
483
|
return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
|
|
416
484
|
case "vml_shape":
|
|
417
485
|
return appendComplexPreviewSegment(paragraph, node, start, "VML shape", createVmlDetail(node));
|
|
418
|
-
case "column_break":
|
|
419
486
|
case "symbol":
|
|
420
|
-
|
|
487
|
+
paragraph.segments.push({
|
|
488
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
489
|
+
kind: "text",
|
|
490
|
+
from: start,
|
|
491
|
+
to: start + 1,
|
|
492
|
+
text: node.char ? String.fromCodePoint(parseInt(node.char, 16)) : "\uFFFD",
|
|
493
|
+
});
|
|
494
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
495
|
+
case "column_break":
|
|
496
|
+
paragraph.segments.push({
|
|
497
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
498
|
+
kind: "opaque_inline",
|
|
499
|
+
from: start,
|
|
500
|
+
to: start + 1,
|
|
501
|
+
fragmentId: "",
|
|
502
|
+
warningId: "",
|
|
503
|
+
label: "Column break",
|
|
504
|
+
detail: "Column break marker preserved for export.",
|
|
505
|
+
state: "locked-preserve-only",
|
|
506
|
+
});
|
|
507
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
508
|
+
case "footnote_ref":
|
|
509
|
+
paragraph.segments.push({
|
|
510
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
511
|
+
kind: "text",
|
|
512
|
+
from: start,
|
|
513
|
+
to: start + 1,
|
|
514
|
+
text: node.noteId ?? "*",
|
|
515
|
+
marks: ["superscript" as SurfaceTextMark],
|
|
516
|
+
});
|
|
517
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
518
|
+
case "field": {
|
|
519
|
+
if (node.children && node.children.length > 0) {
|
|
520
|
+
let cursor = start;
|
|
521
|
+
const lockedIds: string[] = [];
|
|
522
|
+
for (const child of node.children) {
|
|
523
|
+
const result = appendInlineSegments(paragraph, child, document, cursor);
|
|
524
|
+
cursor = result.nextCursor;
|
|
525
|
+
lockedIds.push(...result.lockedFragmentIds);
|
|
526
|
+
}
|
|
527
|
+
return { nextCursor: cursor, lockedFragmentIds: lockedIds };
|
|
528
|
+
}
|
|
529
|
+
paragraph.segments.push({
|
|
530
|
+
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
531
|
+
kind: "opaque_inline",
|
|
532
|
+
from: start,
|
|
533
|
+
to: start + 1,
|
|
534
|
+
fragmentId: "",
|
|
535
|
+
warningId: "",
|
|
536
|
+
label: "Field",
|
|
537
|
+
detail: "Field code preserved for export.",
|
|
538
|
+
state: "locked-preserve-only",
|
|
539
|
+
});
|
|
540
|
+
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
541
|
+
}
|
|
421
542
|
case "bookmark_start":
|
|
422
543
|
case "bookmark_end":
|
|
423
|
-
|
|
544
|
+
// Zero-width markers — no visual, no cursor advancement
|
|
545
|
+
return { nextCursor: start, lockedFragmentIds: [] };
|
|
424
546
|
default:
|
|
425
547
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
426
548
|
}
|
|
@@ -527,19 +649,79 @@ function createPlainText(
|
|
|
527
649
|
return text.join("");
|
|
528
650
|
}
|
|
529
651
|
|
|
530
|
-
function cloneMarks(marks: TextMark[]):
|
|
531
|
-
|
|
652
|
+
function cloneMarks(marks: TextMark[]): {
|
|
653
|
+
marks: SurfaceTextMark[];
|
|
654
|
+
markAttrs?: {
|
|
655
|
+
backgroundColor?: string;
|
|
656
|
+
charSpacing?: number;
|
|
657
|
+
kerning?: number;
|
|
658
|
+
textFill?: string;
|
|
659
|
+
fontFamily?: string;
|
|
660
|
+
fontSize?: number;
|
|
661
|
+
textColor?: string;
|
|
662
|
+
};
|
|
663
|
+
} {
|
|
664
|
+
const supported: SurfaceTextMark[] = [];
|
|
665
|
+
const attrs: {
|
|
666
|
+
backgroundColor?: string;
|
|
667
|
+
charSpacing?: number;
|
|
668
|
+
kerning?: number;
|
|
669
|
+
textFill?: string;
|
|
670
|
+
fontFamily?: string;
|
|
671
|
+
fontSize?: number;
|
|
672
|
+
textColor?: string;
|
|
673
|
+
} = {};
|
|
532
674
|
for (const mark of marks) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
675
|
+
switch (mark.type) {
|
|
676
|
+
case "bold":
|
|
677
|
+
case "italic":
|
|
678
|
+
case "underline":
|
|
679
|
+
case "strikethrough":
|
|
680
|
+
case "doubleStrikethrough":
|
|
681
|
+
case "vanish":
|
|
682
|
+
case "emboss":
|
|
683
|
+
case "imprint":
|
|
684
|
+
case "shadow":
|
|
685
|
+
supported.push(mark.type);
|
|
686
|
+
break;
|
|
687
|
+
case "position":
|
|
688
|
+
if (mark.val > 0) supported.push("superscript");
|
|
689
|
+
else if (mark.val < 0) supported.push("subscript");
|
|
690
|
+
break;
|
|
691
|
+
case "backgroundColor":
|
|
692
|
+
attrs.backgroundColor = mark.color;
|
|
693
|
+
break;
|
|
694
|
+
case "charSpacing":
|
|
695
|
+
attrs.charSpacing = mark.val;
|
|
696
|
+
break;
|
|
697
|
+
case "kerning":
|
|
698
|
+
attrs.kerning = mark.val;
|
|
699
|
+
break;
|
|
700
|
+
case "textFill":
|
|
701
|
+
attrs.textFill = mark.xml;
|
|
702
|
+
break;
|
|
703
|
+
case "fontFamily":
|
|
704
|
+
attrs.fontFamily = mark.val;
|
|
705
|
+
break;
|
|
706
|
+
case "fontSize":
|
|
707
|
+
attrs.fontSize = mark.val;
|
|
708
|
+
break;
|
|
709
|
+
case "textColor":
|
|
710
|
+
attrs.textColor = mark.color;
|
|
711
|
+
break;
|
|
712
|
+
case "smallCaps":
|
|
713
|
+
supported.push("smallCaps");
|
|
714
|
+
break;
|
|
715
|
+
case "allCaps":
|
|
716
|
+
supported.push("allCaps");
|
|
717
|
+
break;
|
|
718
|
+
case "lang":
|
|
719
|
+
// no visual effect — skip
|
|
720
|
+
break;
|
|
540
721
|
}
|
|
541
722
|
}
|
|
542
|
-
|
|
723
|
+
const hasAttrs = Object.keys(attrs).length > 0;
|
|
724
|
+
return hasAttrs ? { marks: supported, markAttrs: attrs } : { marks: supported };
|
|
543
725
|
}
|
|
544
726
|
|
|
545
727
|
function normalizeDocumentRoot(content: unknown): DocumentRootNode {
|