@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
|
@@ -34,8 +34,10 @@ import {
|
|
|
34
34
|
type DocumentRuntime,
|
|
35
35
|
} from "../runtime/document-runtime.ts";
|
|
36
36
|
import { loadDocxEditorSession } from "../io/docx-session.ts";
|
|
37
|
-
import {
|
|
38
|
-
|
|
37
|
+
import {
|
|
38
|
+
decodePersistedSourcePackageBytes,
|
|
39
|
+
hasValidPersistedSourcePackageDigest,
|
|
40
|
+
} from "../io/source-package-provenance.ts";
|
|
39
41
|
import { deriveCapabilities } from "../runtime/session-capabilities";
|
|
40
42
|
import { TwProseMirrorSurface } from "../ui-tailwind/editor-surface/tw-prosemirror-surface";
|
|
41
43
|
import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace";
|
|
@@ -69,6 +71,15 @@ interface WordReviewEditorRuntime extends DocumentRuntime {
|
|
|
69
71
|
dispose?(): void;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
type PackageBackedDocxSession = ReturnType<typeof loadDocxEditorSession>;
|
|
75
|
+
|
|
76
|
+
interface SnapshotExportBarrier {
|
|
77
|
+
reason:
|
|
78
|
+
| "missing_source_package_provenance"
|
|
79
|
+
| "invalid_source_package_provenance";
|
|
80
|
+
message: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
const VISUALLY_HIDDEN_STYLES: React.CSSProperties = {
|
|
73
84
|
position: "absolute",
|
|
74
85
|
width: "1px",
|
|
@@ -124,6 +135,16 @@ export function __createWordReviewEditorRefBridge(
|
|
|
124
135
|
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
|
|
125
136
|
});
|
|
126
137
|
},
|
|
138
|
+
scrollToComment: (commentId: string) => {
|
|
139
|
+
const comment = runtime.getRenderSnapshot().comments.threads.find(
|
|
140
|
+
(t) => t.commentId === commentId,
|
|
141
|
+
);
|
|
142
|
+
if (!comment || comment.anchor.kind === "detached") return;
|
|
143
|
+
runtime.dispatch({
|
|
144
|
+
type: "selection.set",
|
|
145
|
+
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(comment.anchor)),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
127
148
|
};
|
|
128
149
|
}
|
|
129
150
|
|
|
@@ -217,6 +238,9 @@ function createRuntime(
|
|
|
217
238
|
editorBuild: "dev",
|
|
218
239
|
})
|
|
219
240
|
: undefined;
|
|
241
|
+
const snapshotExportResolution = !args.source.initialDocx
|
|
242
|
+
? resolveSnapshotExportSession(args)
|
|
243
|
+
: undefined;
|
|
220
244
|
const initialSnapshot =
|
|
221
245
|
args.source.initialSnapshot ??
|
|
222
246
|
docxSession?.initialSnapshot ??
|
|
@@ -224,23 +248,36 @@ function createRuntime(
|
|
|
224
248
|
args.documentId,
|
|
225
249
|
args.source.sourceLabel ?? "Generated shell snapshot",
|
|
226
250
|
);
|
|
251
|
+
const runtimeSnapshot = snapshotExportResolution?.barrier
|
|
252
|
+
? applySnapshotExportBarrier(initialSnapshot, snapshotExportResolution.barrier)
|
|
253
|
+
: initialSnapshot;
|
|
227
254
|
|
|
228
255
|
return createDocumentRuntime({
|
|
229
256
|
documentId: args.documentId,
|
|
230
|
-
initialSnapshot,
|
|
257
|
+
initialSnapshot: runtimeSnapshot,
|
|
231
258
|
sourceKind: args.source.source,
|
|
232
259
|
sourceLabel: args.source.sourceLabel,
|
|
233
260
|
readOnly: args.readOnly || docxSession?.readOnly,
|
|
234
|
-
editorBuild:
|
|
261
|
+
editorBuild: runtimeSnapshot.editorBuild,
|
|
235
262
|
fatalError: docxSession?.fatalError,
|
|
236
|
-
exportDocx: async (snapshot, options) =>
|
|
237
|
-
docxSession
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
263
|
+
exportDocx: async (snapshot, options) => {
|
|
264
|
+
if (docxSession) {
|
|
265
|
+
return docxSession.exportDocx(snapshot, options);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (snapshotExportResolution?.session) {
|
|
269
|
+
return snapshotExportResolution.session.exportDocx(snapshot, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw createSnapshotExportBlockedError(
|
|
273
|
+
args.documentId,
|
|
274
|
+
snapshotExportResolution?.barrier ?? {
|
|
275
|
+
reason: "missing_source_package_provenance",
|
|
276
|
+
message:
|
|
277
|
+
"DOCX export is blocked because this snapshot does not carry embedded source package provenance.",
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
},
|
|
244
281
|
onWarning: handlers.onWarning,
|
|
245
282
|
onError: handlers.onError,
|
|
246
283
|
defaultAuthorId: args.currentUserId,
|
|
@@ -520,6 +557,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
520
557
|
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(revision.anchor)),
|
|
521
558
|
});
|
|
522
559
|
},
|
|
560
|
+
scrollToComment: (commentId: string) => {
|
|
561
|
+
const comment = activeRuntime.getRenderSnapshot().comments.threads.find(
|
|
562
|
+
(t) => t.commentId === commentId,
|
|
563
|
+
);
|
|
564
|
+
if (!comment || comment.anchor.kind === "detached") return;
|
|
565
|
+
activeRuntime.dispatch({
|
|
566
|
+
type: "selection.set",
|
|
567
|
+
selection: toRuntimeSelectionSnapshot(createSelectionFromAnchor(comment.anchor)),
|
|
568
|
+
});
|
|
569
|
+
},
|
|
523
570
|
}),
|
|
524
571
|
[activeRuntime, currentUser.userId, documentId, runtime],
|
|
525
572
|
);
|
|
@@ -986,6 +1033,7 @@ function guessSourceLabel(
|
|
|
986
1033
|
return (
|
|
987
1034
|
externalDocSource?.sourceLabel ??
|
|
988
1035
|
initialSourceLabel ??
|
|
1036
|
+
initialSnapshot?.sourcePackage?.sourceLabel ??
|
|
989
1037
|
initialSnapshot?.editorBuild ??
|
|
990
1038
|
undefined
|
|
991
1039
|
);
|
|
@@ -1086,7 +1134,23 @@ async function persistAndExport(input: {
|
|
|
1086
1134
|
lastSavedRevisionTokenRef: input.lastSavedRevisionTokenRef,
|
|
1087
1135
|
});
|
|
1088
1136
|
|
|
1089
|
-
|
|
1137
|
+
let result: ExportResult;
|
|
1138
|
+
try {
|
|
1139
|
+
result = await input.runtime.exportDocx(input.options);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
const normalized = normalizeExportError(error, input.documentId, input.options);
|
|
1142
|
+
input.onError?.(normalized);
|
|
1143
|
+
emitEditorEvent({
|
|
1144
|
+
datastore: input.datastore,
|
|
1145
|
+
onEvent: input.onEvent,
|
|
1146
|
+
event: {
|
|
1147
|
+
type: "error",
|
|
1148
|
+
documentId: input.documentId,
|
|
1149
|
+
error: normalized,
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
throw normalized;
|
|
1153
|
+
}
|
|
1090
1154
|
|
|
1091
1155
|
if (!input.datastore) {
|
|
1092
1156
|
return result;
|
|
@@ -1377,7 +1441,7 @@ function createFallbackPersistedSnapshot(
|
|
|
1377
1441
|
): PersistedEditorSnapshot {
|
|
1378
1442
|
const docId = createCanonicalDocumentId(documentId);
|
|
1379
1443
|
return {
|
|
1380
|
-
snapshotVersion: "persisted-editor-snapshot/
|
|
1444
|
+
snapshotVersion: "persisted-editor-snapshot/2",
|
|
1381
1445
|
schemaVersion: "cds/1.0.0",
|
|
1382
1446
|
documentId,
|
|
1383
1447
|
docId,
|
|
@@ -1438,6 +1502,137 @@ function emptyCompatibilityReport(): CompatibilityReport {
|
|
|
1438
1502
|
};
|
|
1439
1503
|
}
|
|
1440
1504
|
|
|
1505
|
+
function resolveSnapshotExportSession(args: CreateRuntimeArgs): {
|
|
1506
|
+
session?: PackageBackedDocxSession;
|
|
1507
|
+
barrier?: SnapshotExportBarrier;
|
|
1508
|
+
} {
|
|
1509
|
+
const sourcePackage = args.source.initialSnapshot?.sourcePackage;
|
|
1510
|
+
if (!sourcePackage) {
|
|
1511
|
+
return {
|
|
1512
|
+
barrier: {
|
|
1513
|
+
reason: "missing_source_package_provenance",
|
|
1514
|
+
message:
|
|
1515
|
+
"DOCX export is blocked because this snapshot was loaded without embedded source package provenance.",
|
|
1516
|
+
},
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
try {
|
|
1521
|
+
const bytes = decodePersistedSourcePackageBytes(sourcePackage);
|
|
1522
|
+
if (!hasValidPersistedSourcePackageDigest(sourcePackage, bytes)) {
|
|
1523
|
+
return {
|
|
1524
|
+
barrier: {
|
|
1525
|
+
reason: "invalid_source_package_provenance",
|
|
1526
|
+
message:
|
|
1527
|
+
"DOCX export is blocked because the embedded source package provenance failed its integrity check.",
|
|
1528
|
+
},
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const session = loadDocxEditorSession({
|
|
1533
|
+
documentId: args.documentId,
|
|
1534
|
+
sourceLabel: sourcePackage.sourceLabel ?? args.source.sourceLabel,
|
|
1535
|
+
bytes,
|
|
1536
|
+
editorBuild: args.source.initialSnapshot?.editorBuild ?? "dev",
|
|
1537
|
+
});
|
|
1538
|
+
if (session.readOnly || session.fatalError) {
|
|
1539
|
+
return {
|
|
1540
|
+
barrier: {
|
|
1541
|
+
reason: "invalid_source_package_provenance",
|
|
1542
|
+
message:
|
|
1543
|
+
"DOCX export is blocked because the embedded source package provenance is no longer loadable as a valid package-backed session.",
|
|
1544
|
+
},
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return { session };
|
|
1549
|
+
} catch {
|
|
1550
|
+
return {
|
|
1551
|
+
barrier: {
|
|
1552
|
+
reason: "invalid_source_package_provenance",
|
|
1553
|
+
message:
|
|
1554
|
+
"DOCX export is blocked because the embedded source package provenance could not be decoded into a package-backed session.",
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function applySnapshotExportBarrier(
|
|
1561
|
+
snapshot: PersistedEditorSnapshot,
|
|
1562
|
+
barrier: SnapshotExportBarrier,
|
|
1563
|
+
): PersistedEditorSnapshot {
|
|
1564
|
+
const featureEntryId = `feature:source-package-provenance:${barrier.reason}`;
|
|
1565
|
+
const featureEntries = snapshot.compatibility.featureEntries.some(
|
|
1566
|
+
(entry) => entry.featureEntryId === featureEntryId,
|
|
1567
|
+
)
|
|
1568
|
+
? snapshot.compatibility.featureEntries
|
|
1569
|
+
: [
|
|
1570
|
+
...snapshot.compatibility.featureEntries,
|
|
1571
|
+
{
|
|
1572
|
+
featureEntryId,
|
|
1573
|
+
featureKey: "source-package-provenance",
|
|
1574
|
+
featureClass: "unsupported-fatal" as const,
|
|
1575
|
+
message: barrier.message,
|
|
1576
|
+
details: {
|
|
1577
|
+
reason: barrier.reason,
|
|
1578
|
+
},
|
|
1579
|
+
},
|
|
1580
|
+
];
|
|
1581
|
+
|
|
1582
|
+
return {
|
|
1583
|
+
...snapshot,
|
|
1584
|
+
compatibility: {
|
|
1585
|
+
...snapshot.compatibility,
|
|
1586
|
+
blockExport: true,
|
|
1587
|
+
featureEntries,
|
|
1588
|
+
},
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function createSnapshotExportBlockedError(
|
|
1593
|
+
documentId: string,
|
|
1594
|
+
barrier: SnapshotExportBarrier,
|
|
1595
|
+
): EditorError {
|
|
1596
|
+
return {
|
|
1597
|
+
errorId: `${documentId}:export:${barrier.reason}`,
|
|
1598
|
+
code: "export_failed",
|
|
1599
|
+
message: barrier.message,
|
|
1600
|
+
isFatal: false,
|
|
1601
|
+
source: "export",
|
|
1602
|
+
details: {
|
|
1603
|
+
reason: barrier.reason,
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function normalizeExportError(
|
|
1609
|
+
error: unknown,
|
|
1610
|
+
documentId: string,
|
|
1611
|
+
options?: ExportDocxOptions,
|
|
1612
|
+
): EditorError {
|
|
1613
|
+
if (
|
|
1614
|
+
typeof error === "object" &&
|
|
1615
|
+
error !== null &&
|
|
1616
|
+
"errorId" in error &&
|
|
1617
|
+
"code" in error &&
|
|
1618
|
+
"message" in error
|
|
1619
|
+
) {
|
|
1620
|
+
return error as EditorError;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
return {
|
|
1624
|
+
errorId: `${documentId}:export:failed`,
|
|
1625
|
+
code: "export_failed",
|
|
1626
|
+
message:
|
|
1627
|
+
error instanceof Error ? error.message : "DOCX export failed for an unknown reason.",
|
|
1628
|
+
isFatal: false,
|
|
1629
|
+
source: "export",
|
|
1630
|
+
details: {
|
|
1631
|
+
requestedOptions: options ?? {},
|
|
1632
|
+
},
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1441
1636
|
function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
|
|
1442
1637
|
return {
|
|
1443
1638
|
anchor: selection.anchor,
|
|
@@ -6,6 +6,25 @@ import {
|
|
|
6
6
|
tableHeaderCellNodeSpec,
|
|
7
7
|
} from "../../runtime/table-schema.ts";
|
|
8
8
|
|
|
9
|
+
const HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;
|
|
10
|
+
const SAFE_FONT_RE = /^[A-Za-z0-9 ,\-'"]+$/;
|
|
11
|
+
const SAFE_ALIGNMENT = new Set(["left", "center", "right", "justify", "start", "end"]);
|
|
12
|
+
|
|
13
|
+
/** Validate a raw hex color string from OOXML (no leading #). Returns sanitized `#hex` or null. */
|
|
14
|
+
function safeHexColor(raw: string | null | undefined): string | null {
|
|
15
|
+
if (!raw || raw === "auto") return null;
|
|
16
|
+
return HEX_COLOR_RE.test(raw) ? `#${raw}` : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Validate a CSS color value (may already include #). Returns the value or null. */
|
|
20
|
+
function safeCssColor(raw: string | null | undefined): string | null {
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
// Allow #hex, named colors (single word), rgb/rgba functions
|
|
23
|
+
if (/^#[0-9A-Fa-f]{3,8}$/.test(raw)) return raw;
|
|
24
|
+
if (/^[a-zA-Z]+$/.test(raw)) return raw;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
/**
|
|
10
29
|
* ProseMirror schema for the supported live surface slice.
|
|
11
30
|
*
|
|
@@ -27,6 +46,20 @@ export const editorSchema = new Schema({
|
|
|
27
46
|
numberingInstanceId: { default: null },
|
|
28
47
|
numberingLevel: { default: null },
|
|
29
48
|
alignment: { default: null },
|
|
49
|
+
spacingBefore: { default: null },
|
|
50
|
+
spacingAfter: { default: null },
|
|
51
|
+
lineSpacing: { default: null },
|
|
52
|
+
lineRule: { default: null },
|
|
53
|
+
indentLeft: { default: null },
|
|
54
|
+
indentRight: { default: null },
|
|
55
|
+
indentFirstLine: { default: null },
|
|
56
|
+
shadingFill: { default: null },
|
|
57
|
+
borderTop: { default: null },
|
|
58
|
+
borderBottom: { default: null },
|
|
59
|
+
borderLeft: { default: null },
|
|
60
|
+
borderRight: { default: null },
|
|
61
|
+
bidi: { default: null },
|
|
62
|
+
pageBreakBefore: { default: null },
|
|
30
63
|
},
|
|
31
64
|
parseDOM: [{ tag: "p" }],
|
|
32
65
|
toDOM(node) {
|
|
@@ -39,8 +72,41 @@ export const editorSchema = new Schema({
|
|
|
39
72
|
else if (lower === "heading3") classes.push("text-lg font-medium");
|
|
40
73
|
}
|
|
41
74
|
const attrs: Record<string, string> = { class: classes.join(" ") };
|
|
75
|
+
const styles: string[] = [];
|
|
42
76
|
const alignment = node.attrs.alignment as string | null;
|
|
43
|
-
|
|
77
|
+
const safeAlign = alignment === "both" ? "justify" : alignment;
|
|
78
|
+
if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) styles.push(`text-align: ${safeAlign}`);
|
|
79
|
+
const spacingBefore = node.attrs.spacingBefore as number | null;
|
|
80
|
+
if (spacingBefore) styles.push(`margin-top: ${spacingBefore / 20}px`);
|
|
81
|
+
const spacingAfter = node.attrs.spacingAfter as number | null;
|
|
82
|
+
if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}px`);
|
|
83
|
+
const lineSpacing = node.attrs.lineSpacing as number | null;
|
|
84
|
+
const lineRule = node.attrs.lineRule as string | null;
|
|
85
|
+
if (lineSpacing && lineRule === "auto") styles.push(`line-height: ${lineSpacing / 240}`);
|
|
86
|
+
else if (lineSpacing && lineRule === "exact") styles.push(`line-height: ${lineSpacing / 20}px`);
|
|
87
|
+
else if (lineSpacing && lineRule === "atLeast") styles.push(`min-height: ${lineSpacing / 20}px`);
|
|
88
|
+
const indentLeft = node.attrs.indentLeft as number | null;
|
|
89
|
+
if (indentLeft) styles.push(`padding-left: ${indentLeft / 20}px`);
|
|
90
|
+
const indentRight = node.attrs.indentRight as number | null;
|
|
91
|
+
if (indentRight) styles.push(`padding-right: ${indentRight / 20}px`);
|
|
92
|
+
const indentFirstLine = node.attrs.indentFirstLine as number | null;
|
|
93
|
+
if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}px`);
|
|
94
|
+
const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
|
|
95
|
+
if (shadingColor) styles.push(`background-color: ${shadingColor}`);
|
|
96
|
+
for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
|
|
97
|
+
const border = node.attrs[attrName] as { color?: string; sz?: number; val?: string } | null;
|
|
98
|
+
if (border && border.val && border.val !== "none") {
|
|
99
|
+
const width = border.sz ? `${border.sz / 8}px` : "1px";
|
|
100
|
+
const color = safeHexColor(border.color ?? null) ?? "#000000";
|
|
101
|
+
const bStyle = border.val === "dotted" ? "dotted" : border.val === "dashed" ? "dashed" : "solid";
|
|
102
|
+
styles.push(`border-${side}: ${width} ${bStyle} ${color}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const pageBreak = node.attrs.pageBreakBefore as boolean | null;
|
|
106
|
+
if (pageBreak) styles.push("border-top: 2px dashed rgba(0,0,0,0.1); padding-top: 8px; margin-top: 16px");
|
|
107
|
+
const bidi = node.attrs.bidi as boolean | null;
|
|
108
|
+
if (bidi) attrs.dir = "rtl";
|
|
109
|
+
if (styles.length > 0) attrs.style = styles.join("; ");
|
|
44
110
|
return ["p", attrs, 0];
|
|
45
111
|
},
|
|
46
112
|
},
|
|
@@ -64,7 +130,14 @@ export const editorSchema = new Schema({
|
|
|
64
130
|
group: "inline",
|
|
65
131
|
atom: true,
|
|
66
132
|
selectable: false,
|
|
67
|
-
|
|
133
|
+
attrs: {
|
|
134
|
+
tabWidth: { default: null },
|
|
135
|
+
},
|
|
136
|
+
toDOM(node) {
|
|
137
|
+
const width = node.attrs.tabWidth as number | null;
|
|
138
|
+
if (width && width > 0) {
|
|
139
|
+
return ["span", { style: `display: inline-block; width: ${width}px`, "data-node-type": "tab" }, "\u00A0"];
|
|
140
|
+
}
|
|
68
141
|
return ["span", { class: "inline-block w-8", "data-node-type": "tab" }, "\u00A0"];
|
|
69
142
|
},
|
|
70
143
|
},
|
|
@@ -336,6 +409,31 @@ export const editorSchema = new Schema({
|
|
|
336
409
|
return ["s", 0];
|
|
337
410
|
},
|
|
338
411
|
},
|
|
412
|
+
doubleStrikethrough: {
|
|
413
|
+
toDOM() {
|
|
414
|
+
return ["span", { style: "text-decoration: line-through double" }, 0];
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
vanish: {
|
|
418
|
+
toDOM() {
|
|
419
|
+
return ["span", { style: "opacity: 0.3; text-decoration: underline dotted; text-decoration-color: rgba(0,0,0,0.3)" }, 0];
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
emboss: {
|
|
423
|
+
toDOM() {
|
|
424
|
+
return ["span", { style: "text-shadow: 1px -1px 0 rgba(255,255,255,0.6), -1px 1px 0 rgba(0,0,0,0.2)" }, 0];
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
imprint: {
|
|
428
|
+
toDOM() {
|
|
429
|
+
return ["span", { style: "text-shadow: -1px 1px 0 rgba(255,255,255,0.6), 1px -1px 0 rgba(0,0,0,0.2)" }, 0];
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
shadow: {
|
|
433
|
+
toDOM() {
|
|
434
|
+
return ["span", { style: "text-shadow: 1px 1px 2px rgba(0,0,0,0.3)" }, 0];
|
|
435
|
+
},
|
|
436
|
+
},
|
|
339
437
|
superscript: {
|
|
340
438
|
excludes: "subscript",
|
|
341
439
|
parseDOM: [{ tag: "sup" }],
|
|
@@ -372,6 +470,19 @@ export const editorSchema = new Schema({
|
|
|
372
470
|
return ["span", { style: "text-transform: uppercase" }, 0];
|
|
373
471
|
},
|
|
374
472
|
},
|
|
473
|
+
char_spacing: {
|
|
474
|
+
attrs: { value: { default: 0 } },
|
|
475
|
+
toDOM(mark) {
|
|
476
|
+
const twips = mark.attrs.value as number;
|
|
477
|
+
return ["span", { style: `letter-spacing: ${twips / 20}px` }, 0];
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
font_kerning: {
|
|
481
|
+
attrs: { threshold: { default: 0 } },
|
|
482
|
+
toDOM() {
|
|
483
|
+
return ["span", { style: "font-kerning: normal" }, 0];
|
|
484
|
+
},
|
|
485
|
+
},
|
|
375
486
|
font_family: {
|
|
376
487
|
attrs: { family: { default: null } },
|
|
377
488
|
parseDOM: [
|
|
@@ -381,7 +492,9 @@ export const editorSchema = new Schema({
|
|
|
381
492
|
},
|
|
382
493
|
],
|
|
383
494
|
toDOM(mark) {
|
|
384
|
-
|
|
495
|
+
const family = mark.attrs.family as string;
|
|
496
|
+
if (!SAFE_FONT_RE.test(family)) return ["span", 0];
|
|
497
|
+
return ["span", { style: `font-family: ${family}` }, 0];
|
|
385
498
|
},
|
|
386
499
|
},
|
|
387
500
|
font_size: {
|
|
@@ -408,7 +521,8 @@ export const editorSchema = new Schema({
|
|
|
408
521
|
},
|
|
409
522
|
],
|
|
410
523
|
toDOM(mark) {
|
|
411
|
-
const color = mark.attrs.color as string;
|
|
524
|
+
const color = safeCssColor(mark.attrs.color as string);
|
|
525
|
+
if (!color) return ["span", 0];
|
|
412
526
|
return ["span", { style: `color: ${color}` }, 0];
|
|
413
527
|
},
|
|
414
528
|
},
|
|
@@ -421,7 +535,9 @@ export const editorSchema = new Schema({
|
|
|
421
535
|
},
|
|
422
536
|
],
|
|
423
537
|
toDOM(mark) {
|
|
424
|
-
|
|
538
|
+
const color = safeCssColor(mark.attrs.color as string);
|
|
539
|
+
if (!color) return ["mark", 0];
|
|
540
|
+
return ["mark", { style: `background-color: ${color}` }, 0];
|
|
425
541
|
},
|
|
426
542
|
},
|
|
427
543
|
link: {
|
|
@@ -87,10 +87,24 @@ function buildParagraph(
|
|
|
87
87
|
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
88
88
|
): PMNode {
|
|
89
89
|
const content: PMNode[] = [];
|
|
90
|
+
const tabStops = block.tabStops ?? [];
|
|
91
|
+
let tabIndex = 0;
|
|
90
92
|
|
|
91
93
|
for (const segment of block.segments) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
if (segment.kind === "tab" && tabIndex < tabStops.length) {
|
|
95
|
+
const stop = tabStops[tabIndex];
|
|
96
|
+
const stopPos = (stop as { pos?: number }).pos ?? (stop as { position?: number }).position ?? 0;
|
|
97
|
+
const prevStop = tabIndex > 0 ? tabStops[tabIndex - 1] : null;
|
|
98
|
+
const prevPos = prevStop
|
|
99
|
+
? ((prevStop as { pos?: number }).pos ?? (prevStop as { position?: number }).position ?? 0)
|
|
100
|
+
: 0;
|
|
101
|
+
const widthPx = Math.round((stopPos - prevPos) / 15);
|
|
102
|
+
content.push(editorSchema.nodes.tab_char.create({ tabWidth: widthPx > 8 ? widthPx : null }));
|
|
103
|
+
tabIndex++;
|
|
104
|
+
} else {
|
|
105
|
+
const nodes = buildInlineContent(segment);
|
|
106
|
+
content.push(...nodes);
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
return editorSchema.nodes.paragraph.create(
|
|
@@ -98,6 +112,21 @@ function buildParagraph(
|
|
|
98
112
|
styleId: block.styleId ?? null,
|
|
99
113
|
numberingInstanceId: block.numbering?.numberingInstanceId ?? null,
|
|
100
114
|
numberingLevel: block.numbering?.level ?? null,
|
|
115
|
+
alignment: block.alignment ?? null,
|
|
116
|
+
spacingBefore: block.spacing?.before ?? null,
|
|
117
|
+
spacingAfter: block.spacing?.after ?? null,
|
|
118
|
+
lineSpacing: block.spacing?.line ?? null,
|
|
119
|
+
lineRule: block.spacing?.lineRule ?? null,
|
|
120
|
+
indentLeft: block.indentation?.left ?? null,
|
|
121
|
+
indentRight: block.indentation?.right ?? null,
|
|
122
|
+
indentFirstLine: block.indentation?.firstLine ?? null,
|
|
123
|
+
shadingFill: block.shading?.fill ?? null,
|
|
124
|
+
borderTop: (block.borders as Record<string, unknown>)?.top ?? null,
|
|
125
|
+
borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
|
|
126
|
+
borderLeft: (block.borders as Record<string, unknown>)?.left ?? null,
|
|
127
|
+
borderRight: (block.borders as Record<string, unknown>)?.right ?? null,
|
|
128
|
+
bidi: block.bidi ?? null,
|
|
129
|
+
pageBreakBefore: block.pageBreakBefore ?? null,
|
|
101
130
|
},
|
|
102
131
|
content.length > 0 ? Fragment.from(content) : undefined,
|
|
103
132
|
);
|
|
@@ -116,12 +145,47 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
|
|
|
116
145
|
const pmMarks = [];
|
|
117
146
|
if (segment.marks) {
|
|
118
147
|
for (const mark of segment.marks) {
|
|
148
|
+
// Map surface mark names that differ from PM schema mark names
|
|
149
|
+
if (mark === "smallCaps") {
|
|
150
|
+
pmMarks.push(editorSchema.marks.small_caps.create());
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (mark === "allCaps") {
|
|
154
|
+
pmMarks.push(editorSchema.marks.all_caps.create());
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
119
157
|
const pmMark = editorSchema.marks[mark];
|
|
120
158
|
if (pmMark) {
|
|
121
159
|
pmMarks.push(pmMark.create());
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
162
|
}
|
|
163
|
+
if (segment.kind === "text" && segment.markAttrs) {
|
|
164
|
+
if (segment.markAttrs.backgroundColor) {
|
|
165
|
+
pmMarks.push(editorSchema.marks.highlight.create({ color: `#${segment.markAttrs.backgroundColor}` }));
|
|
166
|
+
}
|
|
167
|
+
if (segment.markAttrs.fontFamily) {
|
|
168
|
+
pmMarks.push(editorSchema.marks.font_family.create({ family: segment.markAttrs.fontFamily }));
|
|
169
|
+
}
|
|
170
|
+
if (segment.markAttrs.fontSize) {
|
|
171
|
+
pmMarks.push(editorSchema.marks.font_size.create({ size: segment.markAttrs.fontSize / 2 }));
|
|
172
|
+
}
|
|
173
|
+
if (segment.markAttrs.textColor) {
|
|
174
|
+
pmMarks.push(editorSchema.marks.text_color.create({ color: `#${segment.markAttrs.textColor}` }));
|
|
175
|
+
}
|
|
176
|
+
if (segment.markAttrs.charSpacing) {
|
|
177
|
+
pmMarks.push(editorSchema.marks.char_spacing.create({ value: segment.markAttrs.charSpacing }));
|
|
178
|
+
}
|
|
179
|
+
if (segment.markAttrs.kerning) {
|
|
180
|
+
pmMarks.push(editorSchema.marks.font_kerning.create({ threshold: segment.markAttrs.kerning }));
|
|
181
|
+
}
|
|
182
|
+
if (segment.markAttrs.textFill && !segment.markAttrs.textColor) {
|
|
183
|
+
const colorMatch = segment.markAttrs.textFill.match(/\bval="([0-9A-Fa-f]{6})"/);
|
|
184
|
+
if (colorMatch) {
|
|
185
|
+
pmMarks.push(editorSchema.marks.text_color.create({ color: `#${colorMatch[1]}` }));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
125
189
|
if (segment.hyperlinkHref) {
|
|
126
190
|
pmMarks.push(editorSchema.marks.link.create({ href: segment.hyperlinkHref }));
|
|
127
191
|
}
|