@beyondwork/docx-react-component 1.0.17 → 1.0.19

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 (74) hide show
  1. package/README.md +8 -2
  2. package/package.json +32 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +374 -4
  5. package/src/api/session-state.ts +58 -0
  6. package/src/core/commands/formatting-commands.ts +1 -0
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +5 -1
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +329 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +1 -1
  16. package/src/index.ts +30 -0
  17. package/src/io/docx-session.ts +260 -39
  18. package/src/io/export/serialize-main-document.ts +202 -5
  19. package/src/io/export/serialize-numbering.ts +28 -7
  20. package/src/io/normalize/normalize-text.ts +63 -25
  21. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  22. package/src/io/ooxml/parse-footnotes.ts +212 -20
  23. package/src/io/ooxml/parse-headers-footers.ts +229 -25
  24. package/src/io/ooxml/parse-inline-media.ts +16 -0
  25. package/src/io/ooxml/parse-main-document.ts +411 -6
  26. package/src/io/ooxml/parse-numbering.ts +7 -0
  27. package/src/io/ooxml/parse-settings.ts +184 -0
  28. package/src/io/ooxml/parse-shapes.ts +25 -0
  29. package/src/io/ooxml/parse-styles.ts +463 -0
  30. package/src/io/ooxml/parse-theme.ts +32 -0
  31. package/src/model/canonical-document.ts +133 -3
  32. package/src/model/cds-1.0.0.ts +13 -0
  33. package/src/model/snapshot.ts +2 -1
  34. package/src/runtime/document-layout.ts +332 -0
  35. package/src/runtime/document-navigation.ts +564 -0
  36. package/src/runtime/document-runtime.ts +265 -35
  37. package/src/runtime/document-search.ts +145 -0
  38. package/src/runtime/numbering-prefix.ts +47 -26
  39. package/src/runtime/page-layout-estimation.ts +212 -0
  40. package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
  41. package/src/runtime/session-capabilities.ts +2 -0
  42. package/src/runtime/story-context.ts +164 -0
  43. package/src/runtime/story-targeting.ts +162 -0
  44. package/src/runtime/surface-projection.ts +239 -12
  45. package/src/runtime/table-schema.ts +87 -5
  46. package/src/runtime/view-state.ts +459 -0
  47. package/src/ui/WordReviewEditor.tsx +1902 -312
  48. package/src/ui/browser-export.ts +52 -0
  49. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  50. package/src/ui/headless/selection-helpers.ts +20 -0
  51. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  52. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  53. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  54. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
  57. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  58. package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
  59. package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
  60. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
  61. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  62. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  63. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
  64. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  65. package/src/ui-tailwind/index.ts +2 -1
  66. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
  68. package/src/ui-tailwind/theme/editor-theme.css +123 -0
  69. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  70. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
  71. package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
  72. package/src/validation/compatibility-engine.ts +92 -20
  73. package/src/validation/diagnostics.ts +1 -0
  74. package/src/validation/docx-comment-proof.ts +487 -0
@@ -0,0 +1,107 @@
1
+ export type PerfProbeKind = "typing" | "selection";
2
+
3
+ export interface PerfProbeSample {
4
+ token: string;
5
+ kind: PerfProbeKind;
6
+ durationMs: number;
7
+ recordedAt: number;
8
+ }
9
+
10
+ interface PendingProbe {
11
+ kind: PerfProbeKind;
12
+ startedAt: number;
13
+ }
14
+
15
+ interface PerfProbeState {
16
+ enabled?: boolean;
17
+ nextToken?: number;
18
+ pending?: Record<string, PendingProbe>;
19
+ samples?: PerfProbeSample[];
20
+ maxSamples?: number;
21
+ }
22
+
23
+ export interface PerfProbeSummary {
24
+ samples: PerfProbeSample[];
25
+ latest: {
26
+ typing: PerfProbeSample | null;
27
+ selection: PerfProbeSample | null;
28
+ };
29
+ }
30
+
31
+ declare global {
32
+ interface Window {
33
+ __DOCX_REACT_PERF_PROBE__?: PerfProbeState;
34
+ }
35
+ }
36
+
37
+ export function startPerfProbe(kind: PerfProbeKind): string | null {
38
+ const state = getEnabledState();
39
+ if (!state) {
40
+ return null;
41
+ }
42
+
43
+ const token = `${kind}-${state.nextToken ?? 0}`;
44
+ state.nextToken = (state.nextToken ?? 0) + 1;
45
+ state.pending ??= {};
46
+ state.pending[token] = {
47
+ kind,
48
+ startedAt: performance.now(),
49
+ };
50
+ return token;
51
+ }
52
+
53
+ export function finishPerfProbe(token: string | null | undefined): PerfProbeSample | null {
54
+ if (!token) {
55
+ return null;
56
+ }
57
+ const state = getEnabledState();
58
+ if (!state?.pending?.[token]) {
59
+ return null;
60
+ }
61
+
62
+ const pending = state.pending[token];
63
+ delete state.pending[token];
64
+
65
+ const sample: PerfProbeSample = {
66
+ token,
67
+ kind: pending.kind,
68
+ durationMs: performance.now() - pending.startedAt,
69
+ recordedAt: Date.now(),
70
+ };
71
+
72
+ state.samples ??= [];
73
+ state.samples.push(sample);
74
+ const maxSamples = state.maxSamples ?? 20;
75
+ if (state.samples.length > maxSamples) {
76
+ state.samples.splice(0, state.samples.length - maxSamples);
77
+ }
78
+
79
+ return sample;
80
+ }
81
+
82
+ export function getLatestPerfSummary(): PerfProbeSummary | null {
83
+ const state = getEnabledState();
84
+ const samples = state?.samples ?? [];
85
+ if (!state || samples.length === 0) {
86
+ return null;
87
+ }
88
+
89
+ return {
90
+ samples: [...samples],
91
+ latest: {
92
+ typing: [...samples].reverse().find((sample) => sample.kind === "typing") ?? null,
93
+ selection: [...samples].reverse().find((sample) => sample.kind === "selection") ?? null,
94
+ },
95
+ };
96
+ }
97
+
98
+ function getEnabledState(): PerfProbeState | null {
99
+ if (typeof window === "undefined") {
100
+ return null;
101
+ }
102
+ const state = window.__DOCX_REACT_PERF_PROBE__;
103
+ if (!state?.enabled) {
104
+ return null;
105
+ }
106
+ return state;
107
+ }
@@ -1,9 +1,12 @@
1
- import { Plugin, PluginKey } from "prosemirror-state";
1
+ import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
2
2
  import { keymap } from "prosemirror-keymap";
3
3
  import { columnResizing, goToNextCell, isInTable, tableEditing } from "prosemirror-tables";
4
4
 
5
5
  import type { SelectionSnapshot } from "../../api/public-types";
6
- import { createSelectionSnapshot } from "../../ui/headless/selection-helpers";
6
+ import {
7
+ createNodeSelectionSnapshot,
8
+ createSelectionSnapshot,
9
+ } from "../../ui/headless/selection-helpers";
7
10
  import type { PositionMap } from "./pm-position-map";
8
11
 
9
12
  export interface CommandBridgeCallbacks {
@@ -13,10 +16,12 @@ export interface CommandBridgeCallbacks {
13
16
  onSplitParagraph: () => void;
14
17
  onInsertHardBreak: () => void;
15
18
  onInsertTab: () => void;
19
+ onOutdentTab?: () => void;
16
20
  onUndo: () => void;
17
21
  onRedo: () => void;
18
22
  onSelectionChange: (selection: SelectionSnapshot) => void;
19
23
  getPositionMap: () => PositionMap | null;
24
+ isSelectionSyncSuppressed?: () => boolean;
20
25
  }
21
26
 
22
27
  const bridgeKey = new PluginKey("command-bridge");
@@ -31,6 +36,8 @@ const bridgeKey = new PluginKey("command-bridge");
31
36
  export function createCommandBridgePlugins(
32
37
  callbacks: CommandBridgeCallbacks,
33
38
  ): Plugin[] {
39
+ let isComposing = false;
40
+
34
41
  // Transaction filter: block ALL doc-changing transactions.
35
42
  // The runtime is the sole authority for document mutations.
36
43
  const filterPlugin = new Plugin({
@@ -48,15 +55,26 @@ export function createCommandBridgePlugins(
48
55
  view() {
49
56
  return {
50
57
  update(view, prevState) {
58
+ if (callbacks.isSelectionSyncSuppressed?.()) {
59
+ return;
60
+ }
61
+ if (isComposing) {
62
+ return;
63
+ }
51
64
  if (!view.state.selection.eq(prevState.selection)) {
52
65
  const posMap = callbacks.getPositionMap();
53
66
  if (!posMap) return;
54
67
 
68
+ if (view.state.selection instanceof NodeSelection) {
69
+ callbacks.onSelectionChange(
70
+ createNodeSelectionSnapshot(posMap.pmToRuntime(view.state.selection.from), 1),
71
+ );
72
+ return;
73
+ }
74
+
55
75
  const { from, to } = view.state.selection;
56
- const runtimeAnchor = posMap.pmToRuntime(from);
57
- const runtimeHead = posMap.pmToRuntime(to);
58
76
  callbacks.onSelectionChange(
59
- createSelectionSnapshot(runtimeAnchor, runtimeHead),
77
+ createSelectionSnapshot(posMap.pmToRuntime(from), posMap.pmToRuntime(to)),
60
78
  );
61
79
  }
62
80
  },
@@ -67,6 +85,20 @@ export function createCommandBridgePlugins(
67
85
  // Text input hook: intercept typed characters.
68
86
  const inputPlugin = new Plugin({
69
87
  props: {
88
+ handleDOMEvents: {
89
+ blur() {
90
+ isComposing = false;
91
+ return false;
92
+ },
93
+ compositionstart() {
94
+ isComposing = true;
95
+ return false;
96
+ },
97
+ compositionend() {
98
+ isComposing = false;
99
+ return false;
100
+ },
101
+ },
70
102
  handleTextInput(_view, _from, _to, text) {
71
103
  callbacks.onInsertText(text);
72
104
  return true; // Block PM from processing
@@ -87,22 +119,27 @@ export function createCommandBridgePlugins(
87
119
  // Keymap: intercept editing keys and dispatch runtime commands.
88
120
  const keymapPlugin = keymap({
89
121
  Backspace: () => {
122
+ if (isComposing) return false;
90
123
  callbacks.onDeleteBackward();
91
124
  return true;
92
125
  },
93
126
  Delete: () => {
127
+ if (isComposing) return false;
94
128
  callbacks.onDeleteForward();
95
129
  return true;
96
130
  },
97
131
  Enter: () => {
132
+ if (isComposing) return false;
98
133
  callbacks.onSplitParagraph();
99
134
  return true;
100
135
  },
101
136
  "Shift-Enter": () => {
137
+ if (isComposing) return false;
102
138
  callbacks.onInsertHardBreak();
103
139
  return true;
104
140
  },
105
141
  Tab: (state, dispatch, view) => {
142
+ if (isComposing) return false;
106
143
  if (isInTable(state)) {
107
144
  return goToNextCell(1)(state, dispatch, view);
108
145
  }
@@ -110,10 +147,12 @@ export function createCommandBridgePlugins(
110
147
  return true;
111
148
  },
112
149
  "Shift-Tab": (state, dispatch, view) => {
150
+ if (isComposing) return false;
113
151
  if (isInTable(state)) {
114
152
  return goToNextCell(-1)(state, dispatch, view);
115
153
  }
116
- return false;
154
+ callbacks.onOutdentTab?.();
155
+ return true;
117
156
  },
118
157
  "Mod-z": () => {
119
158
  callbacks.onUndo();
@@ -0,0 +1,31 @@
1
+ import { Plugin } from "prosemirror-state";
2
+
3
+ export interface ContextualInteractionCallbacks {
4
+ onCommentActivated?: (commentId: string) => void;
5
+ onRevisionActivated?: (revisionId: string) => void;
6
+ }
7
+
8
+ export function createContextualInteractionPlugin(
9
+ callbacks: ContextualInteractionCallbacks,
10
+ ): Plugin {
11
+ return new Plugin({
12
+ props: {
13
+ handleClick(_view, _pos, event) {
14
+ const target = event.target as HTMLElement | null;
15
+ const commentId = target?.closest?.("[data-comment-id]")?.getAttribute("data-comment-id");
16
+ if (commentId) {
17
+ callbacks.onCommentActivated?.(commentId);
18
+ return true;
19
+ }
20
+
21
+ const revisionId = target?.closest?.("[data-revision-id]")?.getAttribute("data-revision-id");
22
+ if (revisionId) {
23
+ callbacks.onRevisionActivated?.(revisionId);
24
+ return true;
25
+ }
26
+
27
+ return false;
28
+ },
29
+ },
30
+ });
31
+ }
@@ -25,7 +25,7 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
25
25
  return entries[0]?.pmStart ?? 1;
26
26
  }
27
27
  if (runtimePos >= runtimeStorySize) {
28
- return pmDocSize - 1;
28
+ return entries[entries.length - 1]?.pmEnd ?? pmDocSize - 1;
29
29
  }
30
30
 
31
31
  for (const entry of entries) {
@@ -75,7 +75,7 @@ function walkBlocks(
75
75
  for (const block of blocks) {
76
76
  switch (block.kind) {
77
77
  case "paragraph": {
78
- const pmContentStart = nextPmCursor + 1;
78
+ const pmContentStart = nextPmCursor;
79
79
  const runtimeLength = block.to - block.from;
80
80
  entries.push({
81
81
  runtimeStart: block.from,
@@ -95,6 +95,7 @@ export const editorSchema = new Schema({
95
95
  numberingInstanceId: { default: null },
96
96
  numberingLevel: { default: null },
97
97
  numberingPrefix: { default: null },
98
+ numberingSuffix: { default: null },
98
99
  alignment: { default: null },
99
100
  spacingBefore: { default: null },
100
101
  spacingAfter: { default: null },
@@ -162,6 +163,7 @@ export const editorSchema = new Schema({
162
163
  if (styles.length > 0) attrs.style = styles.join("; ");
163
164
  const numberingPrefix = node.attrs.numberingPrefix as string | null;
164
165
  const numberingLevel = node.attrs.numberingLevel as number | null;
166
+ const numberingSuffix = node.attrs.numberingSuffix as "tab" | "space" | "nothing" | null;
165
167
  const children: Array<string | number | readonly unknown[]> = [];
166
168
  if (pageBreak) {
167
169
  children.push([
@@ -177,6 +179,7 @@ export const editorSchema = new Schema({
177
179
  }
178
180
  if (numberingPrefix) {
179
181
  const minWidth = Math.min(Math.max(numberingPrefix.length + 1, 4), 14);
182
+ const marginRight = numberingSuffix === "nothing" ? "0.25rem" : numberingSuffix === "space" ? "0.5rem" : "0.75rem";
180
183
  children.push([
181
184
  "span",
182
185
  {
@@ -187,7 +190,8 @@ export const editorSchema = new Schema({
187
190
  ...(typeof numberingLevel === "number"
188
191
  ? { "data-numbering-level": String(numberingLevel) }
189
192
  : {}),
190
- style: `min-width: ${minWidth}ch; margin-right: 0.75rem; font-variant-numeric: tabular-nums;`,
193
+ ...(numberingSuffix ? { "data-numbering-suffix": numberingSuffix } : {}),
194
+ style: `min-width: ${minWidth}ch; margin-right: ${marginRight}; font-variant-numeric: tabular-nums;`,
191
195
  },
192
196
  numberingPrefix,
193
197
  ]);
@@ -300,14 +304,28 @@ export const editorSchema = new Schema({
300
304
  alias: { default: null },
301
305
  tag: { default: null },
302
306
  lock: { default: null },
307
+ checkboxChecked: { default: null },
308
+ dateValue: { default: null },
309
+ dropdownItems: { default: null },
310
+ comboBoxItems: { default: null },
311
+ showingPlcHdr: { default: false },
303
312
  },
304
313
  toDOM(node) {
305
- const meta = [node.attrs.alias, node.attrs.tag, node.attrs.sdtType].filter(Boolean).join(" · ");
314
+ const sdtType = node.attrs.sdtType as string | null;
315
+ const typeLabel = sdtType === "checkbox" ? "\u2611 Checkbox"
316
+ : sdtType === "date" ? "\uD83D\uDCC5 Date"
317
+ : sdtType === "dropDownList" ? "\u25BE Dropdown"
318
+ : sdtType === "comboBox" ? "\u25BE Combo box"
319
+ : sdtType === "plainText" ? "\u270E Plain text"
320
+ : sdtType === "richText" ? "\u270E Rich text"
321
+ : sdtType ?? undefined;
322
+ const meta = [node.attrs.alias, node.attrs.tag, typeLabel].filter(Boolean).join(" \u00B7 ");
306
323
  return [
307
324
  "section",
308
325
  {
309
326
  class: "my-2 rounded-xl border border-primary/15 bg-surface-raised/60 px-3 py-2",
310
327
  "data-node-type": "sdt_block",
328
+ ...(sdtType ? { "data-sdt-type": sdtType } : {}),
311
329
  },
312
330
  [
313
331
  "div",
@@ -332,13 +350,29 @@ export const editorSchema = new Schema({
332
350
  warningId: { default: "" },
333
351
  label: { default: "Locked" },
334
352
  detail: { default: "" },
353
+ presentation: { default: "inline-chip" },
335
354
  },
336
355
  toDOM(node) {
356
+ const presentation = node.attrs.presentation as string;
357
+ if (presentation === "quiet-marker") {
358
+ return [
359
+ "span",
360
+ {
361
+ class: "inline-block h-0 w-0 overflow-hidden align-baseline",
362
+ "data-node-type": "opaque_inline",
363
+ "data-inline-presentation": "quiet-marker",
364
+ contenteditable: "false",
365
+ title: node.attrs.detail as string,
366
+ "aria-label": node.attrs.label as string,
367
+ },
368
+ ];
369
+ }
337
370
  return [
338
371
  "span",
339
372
  {
340
373
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-comment bg-warning-soft",
341
374
  "data-node-type": "opaque_inline",
375
+ "data-inline-presentation": "inline-chip",
342
376
  title: node.attrs.detail as string,
343
377
  },
344
378
  "\uD83D\uDD12 " + (node.attrs.label as string),
@@ -403,7 +437,7 @@ export const editorSchema = new Schema({
403
437
  inline: true,
404
438
  group: "inline",
405
439
  atom: true,
406
- selectable: false,
440
+ selectable: true,
407
441
  attrs: {
408
442
  text: { default: null },
409
443
  geometry: { default: null },
@@ -454,7 +488,7 @@ export const editorSchema = new Schema({
454
488
  inline: true,
455
489
  group: "inline",
456
490
  atom: true,
457
- selectable: false,
491
+ selectable: true,
458
492
  attrs: {
459
493
  text: { default: null },
460
494
  shapeType: { default: null },
@@ -551,7 +585,15 @@ export const editorSchema = new Schema({
551
585
  },
552
586
  vanish: {
553
587
  toDOM() {
554
- return ["span", { style: "opacity: 0.3; text-decoration: underline dotted; text-decoration-color: rgba(0,0,0,0.3)" }, 0];
588
+ return [
589
+ "span",
590
+ {
591
+ style: "display: none",
592
+ "data-hidden-text": "true",
593
+ "aria-hidden": "true",
594
+ },
595
+ 0,
596
+ ];
555
597
  },
556
598
  },
557
599
  emboss: {
@@ -1,5 +1,5 @@
1
1
  import { Fragment, type Node as PMNode } from "prosemirror-model";
2
- import { EditorState, type Plugin, Selection, TextSelection } from "prosemirror-state";
2
+ import { EditorState, NodeSelection, type Plugin, Selection, TextSelection } from "prosemirror-state";
3
3
 
4
4
  import type {
5
5
  EditorSurfaceSnapshot,
@@ -30,35 +30,72 @@ export function createPMStateFromSnapshot(
30
30
  ): PMStateResult {
31
31
  const doc = buildPMDoc(surface);
32
32
  const positionMap = buildPositionMap(surface);
33
+ const pmSelection = createPMSelectionFromSnapshot(doc, positionMap, selection);
33
34
 
34
- // Convert runtime selection to PM selection
35
- const pmAnchor = clamp(
36
- positionMap.runtimeToPm(selection.anchor),
37
- 1,
38
- positionMap.pmDocSize - 1,
39
- );
40
- const pmHead = clamp(
41
- positionMap.runtimeToPm(selection.head),
42
- 1,
43
- positionMap.pmDocSize - 1,
44
- );
35
+ const state = EditorState.create({
36
+ doc,
37
+ selection: pmSelection,
38
+ plugins,
39
+ });
40
+
41
+ return { state, positionMap };
42
+ }
45
43
 
46
- let pmSelection: Selection;
44
+ export function createPMSelectionFromSnapshot(
45
+ doc: PMNode,
46
+ positionMap: PositionMap,
47
+ selection: SelectionSnapshot,
48
+ ): Selection {
49
+ const pmAnchor = clamp(positionMap.runtimeToPm(selection.anchor), 1, positionMap.pmDocSize - 1);
50
+ const pmHead = clamp(positionMap.runtimeToPm(selection.head), 1, positionMap.pmDocSize - 1);
47
51
  try {
48
- pmSelection = TextSelection.between(doc.resolve(pmAnchor), doc.resolve(pmHead));
52
+ if (selection.activeRange.kind === "node") {
53
+ return NodeSelection.create(doc, pmAnchor);
54
+ }
55
+
56
+ const forward = selection.head >= selection.anchor;
57
+ const resolvedAnchor = resolveInlineBoundary(doc, pmAnchor, forward ? 1 : -1);
58
+ const resolvedHead = resolveInlineBoundary(doc, pmHead, forward ? -1 : 1);
59
+ if (resolvedAnchor !== null && resolvedHead !== null) {
60
+ return TextSelection.create(doc, resolvedAnchor, resolvedHead);
61
+ }
62
+
63
+ const $anchor = doc.resolve(pmAnchor);
64
+ const $head = doc.resolve(pmHead);
65
+ return (
66
+ Selection.findFrom($anchor, 1, true) ??
67
+ Selection.findFrom($anchor, -1, true) ??
68
+ Selection.findFrom($head, 1, true) ??
69
+ Selection.findFrom($head, -1, true) ??
70
+ Selection.near($anchor, 1)
71
+ );
49
72
  } catch {
50
73
  // If the mapped runtime selection is invalid or lands in a non-text block,
51
74
  // let ProseMirror choose the nearest valid starting selection.
52
- pmSelection = Selection.atStart(doc);
75
+ return Selection.atStart(doc);
53
76
  }
77
+ }
54
78
 
55
- const state = EditorState.create({
56
- doc,
57
- selection: pmSelection,
58
- plugins,
59
- });
79
+ function resolveInlineBoundary(
80
+ doc: PMNode,
81
+ pos: number,
82
+ bias: -1 | 1,
83
+ ): number | null {
84
+ const $pos = doc.resolve(pos);
85
+ if ($pos.parent.inlineContent) {
86
+ return pos;
87
+ }
60
88
 
61
- return { state, positionMap };
89
+ const candidate =
90
+ Selection.findFrom($pos, bias, true) ??
91
+ Selection.findFrom($pos, -bias, true) ??
92
+ Selection.near($pos, bias);
93
+
94
+ if (candidate.$from.parent.inlineContent || candidate.$to.parent.inlineContent) {
95
+ return bias < 0 ? candidate.to : candidate.from;
96
+ }
97
+
98
+ return null;
62
99
  }
63
100
 
64
101
  function buildPMDoc(surface: EditorSurfaceSnapshot): PMNode {
@@ -124,6 +161,9 @@ function buildParagraph(
124
161
  numberingPrefix:
125
162
  (block as typeof block & { numberingPrefix?: string }).numberingPrefix ??
126
163
  null,
164
+ numberingSuffix:
165
+ (block as typeof block & { numberingSuffix?: string }).numberingSuffix ??
166
+ null,
127
167
  alignment: block.alignment ?? null,
128
168
  spacingBefore: block.spacing?.before ?? null,
129
169
  spacingAfter: block.spacing?.after ?? null,
@@ -226,6 +266,14 @@ function buildInlineContent(segment: SurfaceInlineSegment): PMNode[] {
226
266
  case "opaque_inline":
227
267
  return [buildOpaqueInlineOrComplexAtom(segment)];
228
268
 
269
+ case "note_ref": {
270
+ const text = editorSchema.text(
271
+ segment.label,
272
+ [editorSchema.marks.superscript.create()],
273
+ );
274
+ return [text];
275
+ }
276
+
229
277
  default:
230
278
  return [];
231
279
  }
@@ -262,17 +310,36 @@ function buildTable(
262
310
  gridSpan: cell.gridSpan,
263
311
  verticalMerge: cell.verticalMerge,
264
312
  backgroundColor: cell.backgroundColor ?? null,
313
+ verticalAlign: cell.verticalAlign ?? null,
314
+ borderTop: cell.borderTop ?? null,
315
+ borderRight: cell.borderRight ?? null,
316
+ borderBottom: cell.borderBottom ?? null,
317
+ borderLeft: cell.borderLeft ?? null,
265
318
  },
266
319
  Fragment.from(cellContent),
267
320
  ),
268
321
  );
269
322
  }
270
- rows.push(editorSchema.nodes.table_row.create(null, Fragment.from(cells)));
323
+ rows.push(editorSchema.nodes.table_row.create(
324
+ {
325
+ height: row.height ?? null,
326
+ heightRule: row.heightRule ?? null,
327
+ isHeader: row.isHeader ?? false,
328
+ },
329
+ Fragment.from(cells),
330
+ ));
271
331
  }
272
332
  return editorSchema.nodes.table.create(
273
333
  {
274
334
  styleId: block.styleId ?? null,
275
335
  gridColumns: block.gridColumns,
336
+ alignment: block.alignment ?? null,
337
+ tblLookFirstRow: block.tblLook?.firstRow ?? false,
338
+ tblLookLastRow: block.tblLook?.lastRow ?? false,
339
+ tblLookFirstColumn: block.tblLook?.firstColumn ?? false,
340
+ tblLookLastColumn: block.tblLook?.lastColumn ?? false,
341
+ tblLookNoHBand: block.tblLook?.noHBand ?? false,
342
+ tblLookNoVBand: block.tblLook?.noVBand ?? false,
276
343
  },
277
344
  Fragment.from(rows),
278
345
  );
@@ -304,6 +371,11 @@ function buildSdtBlock(
304
371
  alias: block.alias ?? null,
305
372
  tag: block.tag ?? null,
306
373
  lock: block.lock ?? null,
374
+ checkboxChecked: block.checkboxChecked ?? null,
375
+ dateValue: block.dateValue ?? null,
376
+ dropdownItems: block.dropdownItems ?? null,
377
+ comboBoxItems: block.comboBoxItems ?? null,
378
+ showingPlcHdr: block.showingPlcHdr ?? false,
307
379
  },
308
380
  Fragment.from(children),
309
381
  );
@@ -360,6 +432,7 @@ function buildOpaqueInlineOrComplexAtom(
360
432
  warningId: segment.warningId,
361
433
  label,
362
434
  detail,
435
+ presentation: segment.presentation ?? "inline-chip",
363
436
  });
364
437
  }
365
438