@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.
@@ -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 { exportSnapshotToMinimalDocx } from "../io/export/minimal-docx.ts";
38
- import { DOCX_MIME_TYPE } from "../io/opc/docx-package.ts";
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: initialSnapshot.editorBuild,
261
+ editorBuild: runtimeSnapshot.editorBuild,
245
262
  fatalError: docxSession?.fatalError,
246
- exportDocx: async (snapshot, options) =>
247
- docxSession
248
- ? docxSession.exportDocx(snapshot, options)
249
- : {
250
- bytes: exportSnapshotToMinimalDocx(snapshot),
251
- mimeType: DOCX_MIME_TYPE,
252
- fileName: options?.fileName ?? `${args.documentId}.docx`,
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
- const result = await input.runtime.exportDocx(input.options);
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/1",
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
- if (alignment) attrs.style = `text-align: ${alignment}`;
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
- toDOM() {
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
- return ["span", { style: `font-family: ${mark.attrs.family as string}` }, 0];
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
- return ["mark", { style: `background-color: ${mark.attrs.color as string}` }, 0];
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
- const nodes = buildInlineContent(segment);
93
- content.push(...nodes);
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
  }