@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.
@@ -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",
@@ -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: initialSnapshot.editorBuild,
261
+ editorBuild: runtimeSnapshot.editorBuild,
235
262
  fatalError: docxSession?.fatalError,
236
- exportDocx: async (snapshot, options) =>
237
- docxSession
238
- ? docxSession.exportDocx(snapshot, options)
239
- : {
240
- bytes: exportSnapshotToMinimalDocx(snapshot),
241
- mimeType: DOCX_MIME_TYPE,
242
- fileName: options?.fileName ?? `${args.documentId}.docx`,
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
- 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
+ }
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/1",
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
- 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
  }