@beyondwork/docx-react-component 1.0.11 → 1.0.13

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.
Files changed (40) hide show
  1. package/README.md +8 -2
  2. package/package.json +35 -21
  3. package/src/api/public-types.ts +103 -1
  4. package/src/core/commands/formatting-commands.ts +742 -0
  5. package/src/core/commands/image-commands.ts +84 -2
  6. package/src/core/commands/structural-helpers.ts +309 -0
  7. package/src/core/commands/table-structure-commands.ts +721 -0
  8. package/src/core/commands/text-commands.ts +166 -1
  9. package/src/core/state/editor-state.ts +318 -9
  10. package/src/formats/xlsx/io/parse-sheet.ts +177 -7
  11. package/src/formats/xlsx/io/parse-styles.ts +2 -0
  12. package/src/formats/xlsx/io/xlsx-session.ts +18 -12
  13. package/src/formats/xlsx/model/sheet.ts +81 -1
  14. package/src/formats/xlsx/model/workbook.ts +10 -6
  15. package/src/io/docx-session.ts +392 -22
  16. package/src/io/export/export-session.ts +55 -0
  17. package/src/io/export/serialize-footnotes.ts +5 -20
  18. package/src/io/export/serialize-headers-footers.ts +5 -31
  19. package/src/io/export/serialize-main-document.ts +78 -5
  20. package/src/io/normalize/normalize-text.ts +90 -1
  21. package/src/io/ooxml/parse-footnotes.ts +68 -5
  22. package/src/io/ooxml/parse-headers-footers.ts +67 -9
  23. package/src/io/ooxml/parse-main-document.ts +169 -6
  24. package/src/io/opc/package-reader.ts +3 -3
  25. package/src/io/source-package-provenance.ts +241 -0
  26. package/src/model/canonical-document.ts +450 -2
  27. package/src/model/cds-1.0.0.ts +5 -2
  28. package/src/model/snapshot.ts +190 -19
  29. package/src/preservation/package-preservation.ts +0 -7
  30. package/src/runtime/document-runtime.ts +7 -1
  31. package/src/runtime/read-only-diagnostics-runtime.ts +1 -1
  32. package/src/runtime/surface-projection.ts +200 -17
  33. package/src/runtime/table-commands.ts +79 -0
  34. package/src/runtime/table-schema.ts +9 -0
  35. package/src/ui/WordReviewEditor.tsx +708 -16
  36. package/src/ui-tailwind/editor-surface/pm-schema.ts +121 -5
  37. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +73 -7
  38. package/src/ui-tailwind/editor-surface/search-plugin.ts +76 -16
  39. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +162 -14
  40. package/src/validation/compatibility-engine.ts +208 -0
@@ -1,8 +1,23 @@
1
- import type { CanonicalDocumentEnvelope, SelectionSnapshot } from "../state/editor-state.ts";
1
+ import type { InsertTableOptions } from "../../api/public-types";
2
+ import type { ParagraphNode } from "../../model/canonical-document.ts";
3
+ import {
4
+ createSelectionSnapshot,
5
+ type CanonicalDocumentEnvelope,
6
+ type SelectionSnapshot,
7
+ } from "../state/editor-state.ts";
2
8
  import {
3
9
  applyTextTransaction,
4
10
  type TextTransactionResult,
5
11
  } from "../state/text-transaction.ts";
12
+ import {
13
+ createInsertedTableBlock,
14
+ createNoopStructuralMutation,
15
+ findTableCellParagraphSelection,
16
+ replaceParagraphScope,
17
+ resolveInsertedTableStyleId,
18
+ resolveParagraphScope,
19
+ type StructuralMutationResult,
20
+ } from "./structural-helpers.ts";
6
21
 
7
22
  export interface TextCommandContext {
8
23
  timestamp: string;
@@ -117,3 +132,153 @@ export function splitParagraph(
117
132
  context,
118
133
  );
119
134
  }
135
+
136
+ export function insertPageBreak(
137
+ document: CanonicalDocumentEnvelope,
138
+ selection: SelectionSnapshot,
139
+ context: TextCommandContext,
140
+ ): StructuralMutationResult {
141
+ const scope = resolveParagraphScope(document, selection);
142
+ if (!scope) {
143
+ return createNoopStructuralMutation(document, selection);
144
+ }
145
+
146
+ const localDocument: CanonicalDocumentEnvelope = {
147
+ ...document,
148
+ content: {
149
+ type: "doc",
150
+ children: [scope.paragraph],
151
+ },
152
+ };
153
+ const localSelection = createSelectionSnapshot(
154
+ selection.anchor - scope.paragraphStart,
155
+ selection.head - scope.paragraphStart,
156
+ );
157
+ const splitResult = splitParagraph(localDocument, localSelection, context);
158
+ const splitRoot = splitResult.document.content;
159
+ if (!splitRoot || splitRoot.type !== "doc") {
160
+ return createNoopStructuralMutation(document, selection);
161
+ }
162
+
163
+ const replacementParagraphs = splitRoot.children
164
+ .filter((block): block is ParagraphNode => block.type === "paragraph")
165
+ .map((block, index) =>
166
+ index === 1
167
+ ? {
168
+ ...block,
169
+ pageBreakBefore: true,
170
+ }
171
+ : block,
172
+ );
173
+ if (replacementParagraphs.length < 2) {
174
+ return createNoopStructuralMutation(document, selection);
175
+ }
176
+
177
+ const nextDocument = replaceParagraphScope(document, scope, replacementParagraphs);
178
+ return {
179
+ changed: true,
180
+ document: {
181
+ ...nextDocument,
182
+ updatedAt: context.timestamp,
183
+ },
184
+ selection: createSelectionSnapshot(
185
+ splitResult.selection.anchor + scope.paragraphStart,
186
+ splitResult.selection.head + scope.paragraphStart,
187
+ ),
188
+ mapping: {
189
+ ...splitResult.mapping,
190
+ steps: splitResult.mapping.steps.map((step) => ({
191
+ ...step,
192
+ from: step.from + scope.paragraphStart,
193
+ to: step.to + scope.paragraphStart,
194
+ })),
195
+ },
196
+ };
197
+ }
198
+
199
+ export function insertTable(
200
+ document: CanonicalDocumentEnvelope,
201
+ selection: SelectionSnapshot,
202
+ options: InsertTableOptions,
203
+ context: TextCommandContext,
204
+ ): StructuralMutationResult {
205
+ const scope = resolveParagraphScope(document, selection);
206
+ if (!scope || scope.kind !== "top-level") {
207
+ return createNoopStructuralMutation(document, selection);
208
+ }
209
+
210
+ const safeRows = Math.max(1, Math.floor(options.rows));
211
+ const safeColumns = Math.max(1, Math.floor(options.columns));
212
+ if (!Number.isFinite(safeRows) || !Number.isFinite(safeColumns)) {
213
+ return createNoopStructuralMutation(document, selection);
214
+ }
215
+
216
+ const localDocument: CanonicalDocumentEnvelope = {
217
+ ...document,
218
+ content: {
219
+ type: "doc",
220
+ children: [scope.paragraph],
221
+ },
222
+ };
223
+ const localSelection = createSelectionSnapshot(
224
+ selection.anchor - scope.paragraphStart,
225
+ selection.head - scope.paragraphStart,
226
+ );
227
+ const splitResult = splitParagraph(localDocument, localSelection, context);
228
+ const splitRoot = splitResult.document.content;
229
+ if (!splitRoot || splitRoot.type !== "doc") {
230
+ return createNoopStructuralMutation(document, selection);
231
+ }
232
+
233
+ const replacementParagraphs = splitRoot.children.filter(
234
+ (block): block is ParagraphNode => block.type === "paragraph",
235
+ );
236
+ if (replacementParagraphs.length < 2) {
237
+ return createNoopStructuralMutation(document, selection);
238
+ }
239
+
240
+ const nextRoot = document.content;
241
+ if (!nextRoot || nextRoot.type !== "doc") {
242
+ return createNoopStructuralMutation(document, selection);
243
+ }
244
+
245
+ const insertedTable = createInsertedTableBlock(
246
+ safeRows,
247
+ safeColumns,
248
+ resolveInsertedTableStyleId(document),
249
+ );
250
+ const nextDocument: CanonicalDocumentEnvelope = {
251
+ ...document,
252
+ updatedAt: context.timestamp,
253
+ content: {
254
+ ...nextRoot,
255
+ children: [
256
+ ...nextRoot.children.slice(0, scope.blockIndex),
257
+ replacementParagraphs[0],
258
+ insertedTable,
259
+ replacementParagraphs[1],
260
+ ...nextRoot.children.slice(scope.blockIndex + 1),
261
+ ],
262
+ },
263
+ };
264
+
265
+ return {
266
+ changed: true,
267
+ document: nextDocument,
268
+ selection:
269
+ findTableCellParagraphSelection(
270
+ nextDocument,
271
+ scope.blockIndex + 1,
272
+ 0,
273
+ 0,
274
+ ) ?? selection,
275
+ mapping: {
276
+ ...splitResult.mapping,
277
+ steps: splitResult.mapping.steps.map((step) => ({
278
+ ...step,
279
+ from: step.from + scope.paragraphStart,
280
+ to: step.to + scope.paragraphStart,
281
+ })),
282
+ },
283
+ };
284
+ }
@@ -1,12 +1,15 @@
1
1
  import {
2
2
  DEFAULT_BOUNDARY_ASSOC,
3
3
  createDetachedAnchor,
4
+ createNodeAnchor,
4
5
  createRangeAnchor,
5
6
  type EditorAnchorProjection,
6
7
  } from "../selection/mapping.ts";
8
+ import { isUuid } from "../../model/cds-1.0.0.ts";
7
9
  import {
8
10
  assertPersistedEditorSnapshot as assertModelPersistedEditorSnapshot,
9
11
  createPersistedEditorSnapshot as createModelPersistedEditorSnapshot,
12
+ validatePersistedEditorSnapshot as validateModelPersistedEditorSnapshot,
10
13
  type PersistedEditorSnapshot as ModelPersistedEditorSnapshot,
11
14
  } from "../../model/snapshot.ts";
12
15
  import {
@@ -175,6 +178,7 @@ export interface EditorState {
175
178
  documentId: string;
176
179
  sessionId: string;
177
180
  sourceLabel?: string;
181
+ sourcePackage?: ModelPersistedEditorSnapshot["sourcePackage"];
178
182
  revision: number;
179
183
  revisionToken: string;
180
184
  isDirty: boolean;
@@ -268,29 +272,32 @@ export function createEmptyCompatibilityReport(generatedAt: string): Compatibili
268
272
 
269
273
  export function createEditorState(options: CreateEditorStateOptions): EditorState {
270
274
  const timestamp = new Date(0).toISOString();
271
- if (options.persistedSnapshot) {
272
- assertModelPersistedEditorSnapshot(options.persistedSnapshot as unknown);
273
- }
275
+ const normalizedSnapshot = options.persistedSnapshot
276
+ ? normalizePersistedSnapshotForRuntime(options.persistedSnapshot, options.documentId, timestamp)
277
+ : undefined;
274
278
  if (options.canonicalDocument) {
275
279
  assertCanonicalDocument(options.canonicalDocument as unknown);
276
280
  }
277
281
 
278
- const normalizedDocument = options.persistedSnapshot
279
- ? structuredClone(options.persistedSnapshot.canonicalDocument)
282
+ const normalizedDocument = normalizedSnapshot
283
+ ? structuredClone(normalizedSnapshot.canonicalDocument)
280
284
  : options.canonicalDocument
281
285
  ? structuredClone(options.canonicalDocument)
282
286
  : createDefaultCanonicalDocument(options.documentId, timestamp);
283
- const warnings = options.persistedSnapshot?.warningLog ?? options.warnings ?? [];
287
+ const warnings = normalizedSnapshot
288
+ ? normalizeWarningLog(normalizedSnapshot.warningLog)
289
+ : options.warnings ?? [];
284
290
  const compatibility =
285
- options.persistedSnapshot?.compatibility ??
286
- options.compatibility ??
287
- createEmptyCompatibilityReport(normalizedDocument.updatedAt);
291
+ normalizedSnapshot
292
+ ? normalizeCompatibilityReport(normalizedSnapshot.compatibility)
293
+ : options.compatibility ?? createEmptyCompatibilityReport(normalizedDocument.updatedAt);
288
294
 
289
295
  return {
290
296
  phase: options.fatalError ? "error" : "ready",
291
297
  documentId: options.documentId,
292
298
  sessionId: options.sessionId,
293
299
  sourceLabel: options.sourceLabel,
300
+ sourcePackage: normalizedSnapshot?.sourcePackage,
294
301
  revision: 0,
295
302
  revisionToken: `${options.sessionId}:0`,
296
303
  isDirty: false,
@@ -306,6 +313,265 @@ export function createEditorState(options: CreateEditorStateOptions): EditorStat
306
313
  };
307
314
  }
308
315
 
316
+ function normalizePersistedSnapshotForRuntime(
317
+ snapshot: PersistedEditorSnapshot,
318
+ documentId: string,
319
+ timestamp: string,
320
+ ): PersistedEditorSnapshot {
321
+ const issues = validateModelPersistedEditorSnapshot(snapshot as unknown);
322
+ if (issues.length === 0) {
323
+ return snapshot;
324
+ }
325
+
326
+ if (!isRecord(snapshot) || !isRecord(snapshot.canonicalDocument)) {
327
+ assertModelPersistedEditorSnapshot(snapshot as unknown);
328
+ }
329
+
330
+ const normalizedDocument = normalizeCanonicalDocumentEnvelope(
331
+ snapshot.canonicalDocument,
332
+ documentId,
333
+ timestamp,
334
+ );
335
+ try {
336
+ assertCanonicalDocument(normalizedDocument as unknown);
337
+ } catch {
338
+ assertModelPersistedEditorSnapshot(snapshot as unknown);
339
+ }
340
+ const repairWarning = createSnapshotRepairWarning(issues.length);
341
+ const compatibility = coerceCompatibilityReport(
342
+ snapshot.compatibility,
343
+ normalizedDocument.updatedAt,
344
+ repairWarning,
345
+ );
346
+
347
+ return {
348
+ snapshotVersion:
349
+ snapshot.snapshotVersion === "persisted-editor-snapshot/1" ||
350
+ snapshot.snapshotVersion === "persisted-editor-snapshot/2"
351
+ ? snapshot.snapshotVersion
352
+ : "persisted-editor-snapshot/2",
353
+ schemaVersion: "cds/1.0.0",
354
+ documentId:
355
+ typeof snapshot.documentId === "string" && snapshot.documentId.length > 0
356
+ ? snapshot.documentId
357
+ : documentId,
358
+ docId: normalizedDocument.docId,
359
+ createdAt: normalizedDocument.createdAt,
360
+ updatedAt: normalizedDocument.updatedAt,
361
+ savedAt:
362
+ typeof snapshot.savedAt === "string" && snapshot.savedAt.length > 0
363
+ ? snapshot.savedAt
364
+ : normalizedDocument.updatedAt,
365
+ editorBuild:
366
+ typeof snapshot.editorBuild === "string" && snapshot.editorBuild.length > 0
367
+ ? snapshot.editorBuild
368
+ : "dev",
369
+ canonicalDocument: normalizedDocument,
370
+ compatibility,
371
+ warningLog: dedupeWarnings([
372
+ ...coerceWarnings(snapshot.warningLog),
373
+ repairWarning,
374
+ ]),
375
+ sourcePackage: snapshot.sourcePackage,
376
+ };
377
+ }
378
+
379
+ function normalizeCanonicalDocumentEnvelope(
380
+ value: unknown,
381
+ documentId: string,
382
+ timestamp: string,
383
+ ): CanonicalDocumentEnvelope {
384
+ const base = createDefaultCanonicalDocument(documentId, timestamp);
385
+ const record = isRecord(value) ? value : {};
386
+ const metadata = isRecord(record.metadata) ? record.metadata : {};
387
+
388
+ return {
389
+ ...base,
390
+ ...record,
391
+ schemaVersion: "cds/1.0.0",
392
+ docId: isUuid(record.docId) ? record.docId : createCanonicalDocumentId(documentId),
393
+ createdAt:
394
+ typeof record.createdAt === "string" && record.createdAt.length > 0
395
+ ? record.createdAt
396
+ : base.createdAt,
397
+ updatedAt:
398
+ typeof record.updatedAt === "string" && record.updatedAt.length > 0
399
+ ? record.updatedAt
400
+ : base.updatedAt,
401
+ metadata: {
402
+ ...base.metadata,
403
+ ...metadata,
404
+ customProperties: isRecord(metadata.customProperties)
405
+ ? (Object.fromEntries(
406
+ Object.entries(metadata.customProperties).filter(([, entry]) => typeof entry === "string"),
407
+ ) as Record<string, string>)
408
+ : {},
409
+ },
410
+ styles:
411
+ record.styles === undefined
412
+ ? base.styles
413
+ : (record.styles as CanonicalDocumentEnvelope["styles"]),
414
+ numbering:
415
+ record.numbering === undefined
416
+ ? base.numbering
417
+ : (record.numbering as CanonicalDocumentEnvelope["numbering"]),
418
+ media:
419
+ record.media === undefined
420
+ ? base.media
421
+ : (record.media as CanonicalDocumentEnvelope["media"]),
422
+ content: isRecord(record.content)
423
+ ? (record.content as unknown as CanonicalDocumentEnvelope["content"])
424
+ : base.content,
425
+ review:
426
+ record.review === undefined
427
+ ? base.review
428
+ : (record.review as CanonicalDocumentEnvelope["review"]),
429
+ preservation:
430
+ record.preservation === undefined
431
+ ? base.preservation
432
+ : (record.preservation as CanonicalDocumentEnvelope["preservation"]),
433
+ diagnostics:
434
+ record.diagnostics === undefined
435
+ ? base.diagnostics
436
+ : (record.diagnostics as CanonicalDocumentEnvelope["diagnostics"]),
437
+ ...(isRecord(record.subParts)
438
+ ? { subParts: record.subParts as unknown as CanonicalDocumentEnvelope["subParts"] }
439
+ : {}),
440
+ };
441
+ }
442
+
443
+ function coerceCompatibilityReport(
444
+ value: unknown,
445
+ generatedAt: string,
446
+ repairWarning: EditorWarning,
447
+ ): CompatibilityReport {
448
+ const record = isRecord(value) ? value : {};
449
+ const warnings = dedupeWarnings([
450
+ ...coerceWarnings(record.warnings),
451
+ repairWarning,
452
+ ]);
453
+
454
+ return {
455
+ reportVersion: "compatibility-report/1",
456
+ generatedAt:
457
+ typeof record.generatedAt === "string" && record.generatedAt.length > 0
458
+ ? record.generatedAt
459
+ : generatedAt,
460
+ blockExport: typeof record.blockExport === "boolean" ? record.blockExport : false,
461
+ featureEntries: Array.isArray(record.featureEntries)
462
+ ? record.featureEntries.flatMap((entry) => {
463
+ if (!isRecord(entry)) {
464
+ return [];
465
+ }
466
+ if (
467
+ typeof entry.featureEntryId !== "string" ||
468
+ typeof entry.featureKey !== "string" ||
469
+ typeof entry.featureClass !== "string" ||
470
+ typeof entry.message !== "string"
471
+ ) {
472
+ return [];
473
+ }
474
+ return [{
475
+ featureEntryId: entry.featureEntryId,
476
+ featureKey: entry.featureKey,
477
+ featureClass: entry.featureClass as CompatibilityFeatureEntry["featureClass"],
478
+ message: entry.message,
479
+ ...(entry.affectedAnchor && isRecord(entry.affectedAnchor)
480
+ ? { affectedAnchor: entry.affectedAnchor as unknown as EditorAnchorProjection }
481
+ : {}),
482
+ ...(isRecord(entry.details) ? { details: entry.details } : {}),
483
+ }];
484
+ })
485
+ : [],
486
+ warnings,
487
+ errors: Array.isArray(record.errors)
488
+ ? record.errors.flatMap((entry) => {
489
+ if (!isRecord(entry)) {
490
+ return [];
491
+ }
492
+ if (
493
+ typeof entry.errorId !== "string" ||
494
+ typeof entry.code !== "string" ||
495
+ typeof entry.message !== "string" ||
496
+ typeof entry.source !== "string" ||
497
+ typeof entry.isFatal !== "boolean"
498
+ ) {
499
+ return [];
500
+ }
501
+ return [{
502
+ errorId: entry.errorId,
503
+ code: entry.code as EditorError["code"],
504
+ message: entry.message,
505
+ source: entry.source as EditorError["source"],
506
+ isFatal: entry.isFatal,
507
+ ...(isRecord(entry.details) ? { details: entry.details } : {}),
508
+ }];
509
+ })
510
+ : [],
511
+ };
512
+ }
513
+
514
+ function coerceWarnings(value: unknown): EditorWarning[] {
515
+ if (!Array.isArray(value)) {
516
+ return [];
517
+ }
518
+
519
+ return value.flatMap((warning) => {
520
+ if (!isRecord(warning)) {
521
+ return [];
522
+ }
523
+ if (
524
+ typeof warning.warningId !== "string" ||
525
+ typeof warning.code !== "string" ||
526
+ typeof warning.severity !== "string" ||
527
+ typeof warning.message !== "string" ||
528
+ typeof warning.source !== "string"
529
+ ) {
530
+ return [];
531
+ }
532
+
533
+ return [{
534
+ warningId: warning.warningId,
535
+ code: warning.code as EditorWarning["code"],
536
+ severity: warning.severity as EditorWarning["severity"],
537
+ message: warning.message,
538
+ source: warning.source as EditorWarning["source"],
539
+ ...(warning.affectedAnchor && isRecord(warning.affectedAnchor)
540
+ ? { affectedAnchor: warning.affectedAnchor as unknown as EditorAnchorProjection }
541
+ : {}),
542
+ ...(typeof warning.featureEntryId === "string"
543
+ ? { featureEntryId: warning.featureEntryId }
544
+ : {}),
545
+ ...(isRecord(warning.details) ? { details: warning.details } : {}),
546
+ }];
547
+ });
548
+ }
549
+
550
+ function createSnapshotRepairWarning(issueCount: number): EditorWarning {
551
+ return {
552
+ warningId: `warning:snapshot-repair:${issueCount}`,
553
+ code: "import_normalized",
554
+ severity: "warning",
555
+ message: `Loaded a persisted snapshot after repairing ${issueCount} validation issue${issueCount === 1 ? "" : "s"}.`,
556
+ source: "validation",
557
+ };
558
+ }
559
+
560
+ function dedupeWarnings(warnings: EditorWarning[]): EditorWarning[] {
561
+ const seen = new Set<string>();
562
+ const result: EditorWarning[] = [];
563
+
564
+ for (const warning of warnings) {
565
+ if (!warning.warningId || seen.has(warning.warningId)) {
566
+ continue;
567
+ }
568
+ seen.add(warning.warningId);
569
+ result.push(warning);
570
+ }
571
+
572
+ return result;
573
+ }
574
+
309
575
  export function deriveDocumentStats(state: Pick<EditorState, "document">): DocumentStats {
310
576
  const serializedContent = extractText(state.document.content);
311
577
 
@@ -381,6 +647,7 @@ export function createPersistedEditorSnapshot(
381
647
  canonicalDocument: state.document,
382
648
  compatibility: (options.compatibility ?? state.compatibility) as never,
383
649
  warningLog: state.warnings as never,
650
+ sourcePackage: state.sourcePackage,
384
651
  });
385
652
  return snapshot as PersistedEditorSnapshot;
386
653
  }
@@ -397,6 +664,48 @@ function estimateParagraphCount(content: unknown): number {
397
664
  return extractText(content).length > 0 ? 1 : 0;
398
665
  }
399
666
 
667
+ function normalizeWarningLog(warnings: EditorWarning[]): EditorWarning[] {
668
+ return warnings.map((warning) => ({
669
+ ...warning,
670
+ affectedAnchor: warning.affectedAnchor
671
+ ? normalizeAnchorProjection(warning.affectedAnchor)
672
+ : undefined,
673
+ }));
674
+ }
675
+
676
+ function normalizeCompatibilityReport(report: CompatibilityReport): CompatibilityReport {
677
+ return {
678
+ ...report,
679
+ featureEntries: report.featureEntries.map((entry) => ({
680
+ ...entry,
681
+ affectedAnchor: entry.affectedAnchor
682
+ ? normalizeAnchorProjection(entry.affectedAnchor)
683
+ : undefined,
684
+ })),
685
+ warnings: normalizeWarningLog(report.warnings),
686
+ };
687
+ }
688
+
689
+ function normalizeAnchorProjection(anchor: EditorAnchorProjection): EditorAnchorProjection {
690
+ switch (anchor.kind) {
691
+ case "range": {
692
+ const rangeAnchor = anchor as EditorAnchorProjection & {
693
+ range?: { from: number; to: number };
694
+ from?: number;
695
+ to?: number;
696
+ assoc: { start: -1 | 1; end: -1 | 1 };
697
+ };
698
+ return rangeAnchor.range
699
+ ? createRangeAnchor(rangeAnchor.range.from, rangeAnchor.range.to, rangeAnchor.assoc)
700
+ : createRangeAnchor(rangeAnchor.from ?? 0, rangeAnchor.to ?? 0, rangeAnchor.assoc);
701
+ }
702
+ case "node":
703
+ return createNodeAnchor(anchor.at, anchor.assoc);
704
+ case "detached":
705
+ return createDetachedAnchor(anchor.lastKnownRange, anchor.reason);
706
+ }
707
+ }
708
+
400
709
  function extractText(value: unknown): string {
401
710
  if (typeof value === "string") {
402
711
  return value;