@beyondwork/docx-react-component 1.0.11 → 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 +31 -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 +189 -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",
|
|
@@ -227,6 +238,9 @@ function createRuntime(
|
|
|
227
238
|
editorBuild: "dev",
|
|
228
239
|
})
|
|
229
240
|
: undefined;
|
|
241
|
+
const snapshotExportResolution = !args.source.initialDocx
|
|
242
|
+
? resolveSnapshotExportSession(args)
|
|
243
|
+
: undefined;
|
|
230
244
|
const initialSnapshot =
|
|
231
245
|
args.source.initialSnapshot ??
|
|
232
246
|
docxSession?.initialSnapshot ??
|
|
@@ -234,23 +248,36 @@ function createRuntime(
|
|
|
234
248
|
args.documentId,
|
|
235
249
|
args.source.sourceLabel ?? "Generated shell snapshot",
|
|
236
250
|
);
|
|
251
|
+
const runtimeSnapshot = snapshotExportResolution?.barrier
|
|
252
|
+
? applySnapshotExportBarrier(initialSnapshot, snapshotExportResolution.barrier)
|
|
253
|
+
: initialSnapshot;
|
|
237
254
|
|
|
238
255
|
return createDocumentRuntime({
|
|
239
256
|
documentId: args.documentId,
|
|
240
|
-
initialSnapshot,
|
|
257
|
+
initialSnapshot: runtimeSnapshot,
|
|
241
258
|
sourceKind: args.source.source,
|
|
242
259
|
sourceLabel: args.source.sourceLabel,
|
|
243
260
|
readOnly: args.readOnly || docxSession?.readOnly,
|
|
244
|
-
editorBuild:
|
|
261
|
+
editorBuild: runtimeSnapshot.editorBuild,
|
|
245
262
|
fatalError: docxSession?.fatalError,
|
|
246
|
-
exportDocx: async (snapshot, options) =>
|
|
247
|
-
docxSession
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
},
|
|
254
281
|
onWarning: handlers.onWarning,
|
|
255
282
|
onError: handlers.onError,
|
|
256
283
|
defaultAuthorId: args.currentUserId,
|
|
@@ -1006,6 +1033,7 @@ function guessSourceLabel(
|
|
|
1006
1033
|
return (
|
|
1007
1034
|
externalDocSource?.sourceLabel ??
|
|
1008
1035
|
initialSourceLabel ??
|
|
1036
|
+
initialSnapshot?.sourcePackage?.sourceLabel ??
|
|
1009
1037
|
initialSnapshot?.editorBuild ??
|
|
1010
1038
|
undefined
|
|
1011
1039
|
);
|
|
@@ -1106,7 +1134,23 @@ async function persistAndExport(input: {
|
|
|
1106
1134
|
lastSavedRevisionTokenRef: input.lastSavedRevisionTokenRef,
|
|
1107
1135
|
});
|
|
1108
1136
|
|
|
1109
|
-
|
|
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
|
+
}
|
|
1110
1154
|
|
|
1111
1155
|
if (!input.datastore) {
|
|
1112
1156
|
return result;
|
|
@@ -1397,7 +1441,7 @@ function createFallbackPersistedSnapshot(
|
|
|
1397
1441
|
): PersistedEditorSnapshot {
|
|
1398
1442
|
const docId = createCanonicalDocumentId(documentId);
|
|
1399
1443
|
return {
|
|
1400
|
-
snapshotVersion: "persisted-editor-snapshot/
|
|
1444
|
+
snapshotVersion: "persisted-editor-snapshot/2",
|
|
1401
1445
|
schemaVersion: "cds/1.0.0",
|
|
1402
1446
|
documentId,
|
|
1403
1447
|
docId,
|
|
@@ -1458,6 +1502,137 @@ function emptyCompatibilityReport(): CompatibilityReport {
|
|
|
1458
1502
|
};
|
|
1459
1503
|
}
|
|
1460
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
|
+
|
|
1461
1636
|
function toRuntimeSelectionSnapshot(selection: PublicSelectionSnapshot) {
|
|
1462
1637
|
return {
|
|
1463
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
|
}
|