@stesura/react-wordgard 0.0.0-stage → 0.2.0

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 (111) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +912 -2
  4. package/THIRD_PARTY_LICENSES.txt +93 -0
  5. package/dist/ReactWordgard.d.ts +13 -0
  6. package/dist/ReactWordgard.js +94 -0
  7. package/dist/changes.d.ts +14 -0
  8. package/dist/changes.js +163 -0
  9. package/dist/components/ChildNodeViews.d.ts +8 -0
  10. package/dist/components/ChildNodeViews.js +36 -0
  11. package/dist/components/DecorationRoot.d.ts +13 -0
  12. package/dist/components/DecorationRoot.js +31 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.js +14 -0
  15. package/dist/components/SeparatorHackView.d.ts +2 -0
  16. package/dist/components/SeparatorHackView.js +4 -0
  17. package/dist/components/ShapeView.d.ts +12 -0
  18. package/dist/components/ShapeView.js +24 -0
  19. package/dist/components/TextNodeView.d.ts +5 -0
  20. package/dist/components/TextNodeView.js +4 -0
  21. package/dist/components/TrailingHackView.d.ts +2 -0
  22. package/dist/components/TrailingHackView.js +4 -0
  23. package/dist/components/WidgetView.d.ts +10 -0
  24. package/dist/components/WidgetView.js +32 -0
  25. package/dist/components/WordgardDoc.d.ts +9 -0
  26. package/dist/components/WordgardDoc.js +36 -0
  27. package/dist/components/WordgardEditor.d.ts +15 -0
  28. package/dist/components/WordgardEditor.js +17 -0
  29. package/dist/components/WordgardScope.d.ts +14 -0
  30. package/dist/components/WordgardScope.js +14 -0
  31. package/dist/components/WrapperView.d.ts +12 -0
  32. package/dist/components/WrapperView.js +23 -0
  33. package/dist/components/marks/MarkView.d.ts +11 -0
  34. package/dist/components/marks/MarkView.js +10 -0
  35. package/dist/components/marks/ReactMarkView.d.ts +10 -0
  36. package/dist/components/marks/ReactMarkView.js +30 -0
  37. package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
  38. package/dist/components/nodes/DefaultNodeView.js +112 -0
  39. package/dist/components/nodes/DocNodeView.d.ts +7 -0
  40. package/dist/components/nodes/DocNodeView.js +5 -0
  41. package/dist/composition.d.ts +23 -0
  42. package/dist/composition.js +95 -0
  43. package/dist/contexts/DecorationContext.d.ts +15 -0
  44. package/dist/contexts/DecorationContext.js +35 -0
  45. package/dist/contexts/DecorationRootContext.d.ts +6 -0
  46. package/dist/contexts/DecorationRootContext.js +4 -0
  47. package/dist/contexts/EditorContext.d.ts +27 -0
  48. package/dist/contexts/EditorContext.js +87 -0
  49. package/dist/contexts/RenderStoreContext.d.ts +15 -0
  50. package/dist/contexts/RenderStoreContext.js +41 -0
  51. package/dist/contexts/editorEvents.d.ts +12 -0
  52. package/dist/contexts/editorEvents.js +92 -0
  53. package/dist/decorations/DecoIterator.d.ts +43 -0
  54. package/dist/decorations/DecoIterator.js +462 -0
  55. package/dist/decorations/ReactMarkType.d.ts +41 -0
  56. package/dist/decorations/ReactMarkType.js +47 -0
  57. package/dist/decorations/ReactShapeType.d.ts +65 -0
  58. package/dist/decorations/ReactShapeType.js +202 -0
  59. package/dist/decorations/ReactWidgetType.d.ts +29 -0
  60. package/dist/decorations/ReactWidgetType.js +67 -0
  61. package/dist/decorations/ReactWrapperType.d.ts +31 -0
  62. package/dist/decorations/ReactWrapperType.js +41 -0
  63. package/dist/decorations/iterDeco.d.ts +82 -0
  64. package/dist/decorations/iterDeco.js +406 -0
  65. package/dist/helpers/attributes.d.ts +2 -0
  66. package/dist/helpers/attributes.js +11 -0
  67. package/dist/helpers/index.d.ts +2 -0
  68. package/dist/helpers/index.js +2 -0
  69. package/dist/helpers/props.d.ts +8 -0
  70. package/dist/helpers/props.js +248 -0
  71. package/dist/helpers/refs.d.ts +6 -0
  72. package/dist/helpers/refs.js +20 -0
  73. package/dist/hooks/useEditorEffect.d.ts +10 -0
  74. package/dist/hooks/useEditorEffect.js +28 -0
  75. package/dist/hooks/useEditorEventCallback.d.ts +8 -0
  76. package/dist/hooks/useEditorEventCallback.js +17 -0
  77. package/dist/hooks/useEditorEventListener.d.ts +10 -0
  78. package/dist/hooks/useEditorEventListener.js +26 -0
  79. package/dist/hooks/useEditorLayoutEffect.d.ts +15 -0
  80. package/dist/hooks/useEditorLayoutEffect.js +34 -0
  81. package/dist/hooks/useEditorState.d.ts +6 -0
  82. package/dist/hooks/useEditorState.js +12 -0
  83. package/dist/hooks/useEditorStateSelector.d.ts +7 -0
  84. package/dist/hooks/useEditorStateSelector.js +11 -0
  85. package/dist/hooks/useIgnoreMutation.d.ts +6 -0
  86. package/dist/hooks/useIgnoreMutation.js +19 -0
  87. package/dist/hooks/useIsNodeSelected.d.ts +2 -0
  88. package/dist/hooks/useIsNodeSelected.js +5 -0
  89. package/dist/hooks/useMergedDOMRefs.d.ts +6 -0
  90. package/dist/hooks/useMergedDOMRefs.js +18 -0
  91. package/dist/hooks/useNodePos.d.ts +4 -0
  92. package/dist/hooks/useNodePos.js +7 -0
  93. package/dist/hooks/useSelectNode.d.ts +6 -0
  94. package/dist/hooks/useSelectNode.js +25 -0
  95. package/dist/hooks/useStopEvent.d.ts +8 -0
  96. package/dist/hooks/useStopEvent.js +21 -0
  97. package/dist/hooks/useWordgard.d.ts +2 -0
  98. package/dist/hooks/useWordgard.js +3 -0
  99. package/dist/index.d.ts +21 -0
  100. package/dist/index.js +21 -0
  101. package/dist/reactKeys.d.ts +39 -0
  102. package/dist/reactKeys.js +188 -0
  103. package/dist/tile.d.ts +32 -0
  104. package/dist/tile.js +1319 -0
  105. package/dist/viewRoots.d.ts +2 -0
  106. package/dist/viewRoots.js +7 -0
  107. package/dist/wordgardInternals.d.ts +211 -0
  108. package/dist/wordgardInternals.js +82 -0
  109. package/package.json +119 -4
  110. package/skills/react-wordgard/SKILL.md +525 -0
  111. package/skills/react-wordgard/references/wordgard.md +189 -0
@@ -0,0 +1,93 @@
1
+ react-wordgard includes code from the projects below. Their copyright and
2
+ license notices are reproduced here, as their licenses require.
3
+
4
+
5
+ ================================================================================
6
+ react-prosemirror
7
+ https://github.com/handlewithcarecollective/react-prosemirror
8
+ ================================================================================
9
+
10
+ react-wordgard is derived from react-prosemirror, maintained by Handle with
11
+ Care Collective and originally created at The New York Times Company. Files
12
+ that came from it keep their "Copyright (c) The New York Times Company"
13
+ headers.
14
+
15
+ react-prosemirror was released under the Apache License 2.0 until 2026-10-02
16
+ (commit 1827d3a) and under the MIT License since then. Both notices follow.
17
+
18
+ --- MIT License (since 2026-10-02) ---------------------------------------------
19
+
20
+ MIT License
21
+
22
+ Copyright (c) 2026 Handle with Care Collective
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in all
32
+ copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
+ SOFTWARE.
41
+
42
+ --- Apache License 2.0 (until 2026-10-02) --------------------------------------
43
+
44
+ Copyright (c) 2022 The New York Times Company
45
+ Copyright (c) 2025 Handle with Care Collective
46
+
47
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
48
+ this library except in compliance with the License. You may obtain a copy of the
49
+ License at
50
+
51
+ www.apache.org/licenses/LICENSE-2.0
52
+
53
+ Unless required by applicable law or agreed to in writing, software distributed
54
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
55
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
56
+ specific language governing permissions and limitations under the License.
57
+
58
+
59
+ ================================================================================
60
+ Wordgard
61
+ https://wordgard.net
62
+ ================================================================================
63
+
64
+ test/wordgard/ holds copies of Wordgard's browser tests and their helpers;
65
+ demo/main.tsx is adapted from Wordgard's demo (demo/demo.ts), and the
66
+ IME emulation in src/__tests__/tile.browser.test.ts from its `compose` test
67
+ helper (test/webtest-composition.ts). src/decorations/DecoIterator.ts copies
68
+ Wordgard's decoration walker (src/editor/decoration.ts), with changes noted in
69
+ the file. src/tile.ts copies `setUneditable` from Wordgard's tile tree
70
+ (src/editor/tile.ts) and follows its tile builder there.
71
+
72
+ MIT License
73
+
74
+ Copyright (C) 2026 by Marijn Haverbeke <marijn@haverbeke.berlin>.
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining
77
+ a copy of this software and associated documentation files (the
78
+ "Software"), to deal in the Software without restriction, including
79
+ without limitation the rights to use, copy, modify, merge, publish,
80
+ distribute, sublicense, and/or sell copies of the Software, and to
81
+ permit persons to whom the Software is furnished to do so, subject to
82
+ the following conditions:
83
+
84
+ The above copyright notice and this permission notice shall be
85
+ included in all copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
89
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
90
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
91
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
92
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
93
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ import { Wordgard } from "wordgard/editor";
2
+ import { ReactDocTile } from "./tile.js";
3
+ import { type DOMObserver, DocTile } from "./wordgardInternals.js";
4
+ export declare class ReactWordgard extends Wordgard {
5
+ docTile: DocTile;
6
+ observer: DOMObserver;
7
+ scheduleFlush: () => void;
8
+ static create: (spec: Wordgard.Spec) => ReactWordgard;
9
+ static host: (spec: Wordgard.Spec) => ReactWordgard;
10
+ static DocTile: typeof ReactDocTile;
11
+ constructor(spec: Wordgard.Spec, hosted?: boolean);
12
+ flush(): void;
13
+ }
@@ -0,0 +1,94 @@
1
+ // Adapted from react-prosemirror's ReactEditorView, to fit Wordgard: it extends Wordgard, and its
2
+ // base constructor builds a tile tree React renders, where ReactEditorView replaces the docView.
3
+ import { Wordgard } from "wordgard/editor";
4
+ import { GardState } from "wordgard/state";
5
+ import { editorOf } from "./contexts/EditorContext.js";
6
+ import { editorEventHandlers, hasEditorEventHandlers } from "./contexts/editorEvents.js";
7
+ import { ReactDocTile, RefusedUpdate, observeShapes } from "./tile.js";
8
+ import { DocTile } from "./wordgardInternals.js";
9
+ // Asks Wordgard for a frame: a no-op read schedules a flush.
10
+ const retry = () => { };
11
+ export class ReactWordgard
12
+ // @ts-expect-error Wordgard's constructor is private in its typings.
13
+ extends Wordgard {
14
+ // An editor with a React root of its own.
15
+ static create = (spec) => new ReactWordgard(spec, false);
16
+ // An editor WordgardEditor hosts: its document renders through
17
+ // WordgardDoc's portal, in the app's React tree.
18
+ static host = (spec) => new ReactWordgard(spec, true);
19
+ // Wordgard's own tests build tile trees through `Wordgard.DocTile`.
20
+ static DocTile = ReactDocTile;
21
+ constructor(spec, hosted = false) {
22
+ // Wordgard's constructor draws the document with `DocTile.create`, before
23
+ // this one can run: React draws it there instead, so it's drawn once. Put
24
+ // back at once, for any editor React's render creates; other editors
25
+ // created first (by a plugin) draw as before.
26
+ const create = DocTile.create;
27
+ const self = { editor: null };
28
+ DocTile.create = (state, dom, wg) => {
29
+ if (!(wg instanceof ReactWordgard))
30
+ return create(state, dom, wg);
31
+ DocTile.create = create;
32
+ if (!hosted)
33
+ return ReactDocTile.create(state, dom, wg);
34
+ return ReactDocTile.host(state, dom, wg, () => {
35
+ const { editor } = self;
36
+ if (editor)
37
+ editor.docTile = editor.observer.ignore(() => ReactDocTile.mount(editor.contentDOM, editor, editor.docTile));
38
+ });
39
+ };
40
+ // Given a `parent`, it would also put the editor in the page, where
41
+ // connecting measures it: that's left until it's built.
42
+ // useEditorEventListener's handlers come first; Wordgard keeps one copy of
43
+ // an extension, so a state taken from another ReactWordgard is fine.
44
+ const events = spec.state
45
+ ? {
46
+ state: spec.state.update({ effects: GardState.appendConfig.of(editorEventHandlers) })
47
+ .state,
48
+ }
49
+ : { config: [editorEventHandlers, spec.config ?? []] };
50
+ try {
51
+ // @ts-expect-error The typings drop the private constructor's parameter.
52
+ super({ ...spec, ...events, parent: undefined });
53
+ }
54
+ finally {
55
+ DocTile.create = create;
56
+ }
57
+ self.editor = this;
58
+ // React shapes' own commits are sorted out of what Wordgard's DOM
59
+ // observer reads; replaced before the editor is connected and observes.
60
+ this.observer.observer = observeShapes(this, (records) => {
61
+ this.observer.queue.push(...records);
62
+ this.scheduleFlush();
63
+ });
64
+ spec.parent?.appendChild(this.dom);
65
+ }
66
+ // Wordgard's flush runs from an animation frame and from the methods that
67
+ // need an up-to-date DOM; useEditorEffect's effects run after it. A refused
68
+ // update leaves Wordgard's transactions pending with no frame requested for
69
+ // them, so one is.
70
+ flush() {
71
+ const editor = editorOf(this);
72
+ // React shapes that committed since their records were delivered get
73
+ // their tiles first (taking the records rebuilds them): Wordgard reads
74
+ // them before the build.
75
+ this.observer.queue.push(...this.observer.observer.takeRecords());
76
+ editor.flushing = true;
77
+ try {
78
+ super.flush();
79
+ }
80
+ catch (error) {
81
+ if (error instanceof RefusedUpdate)
82
+ this.scheduleDOMRead(retry);
83
+ throw error;
84
+ }
85
+ finally {
86
+ editor.flushing = false;
87
+ }
88
+ editor.runEffects();
89
+ // A reconfiguration (GardState.reconfigure) dropped useEditorEventListener's
90
+ // handlers: they come back with a transaction of their own.
91
+ if (!hasEditorEventHandlers(this.state))
92
+ this.dispatch({ effects: GardState.appendConfig.of(editorEventHandlers) });
93
+ }
94
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plot } from "wordgard/doc";
2
+ export declare const mapNodePos: (changes: readonly number[], pos: number, leaf?: boolean) => {
3
+ pos: number;
4
+ retagged: boolean;
5
+ } | null;
6
+ export declare const mapKept: (changes: readonly number[], pos: number) => number | null;
7
+ export declare const samePlace: (changes: readonly number[], oldStart: number, start: number) => boolean;
8
+ export declare const redrawn: (changes: readonly number[] | null, pos: number) => boolean;
9
+ export declare const isKept: (changes: readonly number[] | null, from: number, to: number) => boolean;
10
+ export declare const pointMapper: (changes: readonly number[]) => (pos: number) => number[];
11
+ export declare const unchangedEnds: (changes: readonly number[], oldPlot: Plot, oldStart: number, plot: Plot) => {
12
+ head: number;
13
+ tail: number;
14
+ };
@@ -0,0 +1,163 @@
1
+ const indexes = new WeakMap();
2
+ const indexOf = (changes) => {
3
+ const found = indexes.get(changes);
4
+ if (found)
5
+ return found;
6
+ const index = {
7
+ startA: [],
8
+ endA: [],
9
+ startB: [],
10
+ endB: [],
11
+ ins: [],
12
+ first: Infinity,
13
+ last: -Infinity,
14
+ };
15
+ for (let i = 0, posA = 0, posB = 0; i < changes.length; i += 2) {
16
+ const len = changes[i] ?? 0;
17
+ const ins = changes[i + 1] ?? 0;
18
+ const endB = posB + (ins < 0 ? len : ins);
19
+ index.startA.push(posA);
20
+ index.endA.push(posA + len);
21
+ index.startB.push(posB);
22
+ index.endB.push(endB);
23
+ index.ins.push(ins);
24
+ if (ins != -1) {
25
+ index.first = Math.min(index.first, posA);
26
+ index.last = posA + len;
27
+ }
28
+ posA += len;
29
+ posB = endB;
30
+ }
31
+ indexes.set(changes, index);
32
+ return index;
33
+ };
34
+ // The first section whose end (`ends` is ascending) is past `pos`, or reaches
35
+ // it when `inclusive`; the section count when none is.
36
+ const sectionAt = (ends, pos, inclusive = false) => {
37
+ let low = 0;
38
+ let high = ends.length;
39
+ while (low < high) {
40
+ const mid = (low + high) >> 1;
41
+ const end = ends[mid] ?? 0;
42
+ if (inclusive ? end >= pos : end > pos)
43
+ high = mid;
44
+ else
45
+ low = mid + 1;
46
+ }
47
+ return low;
48
+ };
49
+ // Wordgard's ChangeSet.mapPos with assoc 1 and track "after": where the node
50
+ // starting at `pos` went, or null when its first token was replaced.
51
+ // `retagged` marks a replacement of that token alone, by one token (a tag or
52
+ // parameter change), which keeps the key when the node type stays the same. A
53
+ // leaf is its token, and Wordgard's update merges adjacent replaced sections:
54
+ // a leaf in a replaced section of unchanged length is retagged, at the same
55
+ // offset in it. A copy, since `update` gets bare sections and Wordgard can't
56
+ // map over those (docs/wordgard-upstream-bugs.md, "Requests").
57
+ export const mapNodePos = (changes, pos, leaf = false) => {
58
+ const { startA, endA, startB, endB, ins } = indexOf(changes);
59
+ const k = sectionAt(endA, pos);
60
+ const fromA = startA[k] ?? 0;
61
+ const fromB = startB[k] ?? 0;
62
+ if (k == endA.length) {
63
+ const totalA = endA.at(-1) ?? 0;
64
+ return { pos: (endB.at(-1) ?? 0) + pos - totalA, retagged: false };
65
+ }
66
+ if ((ins[k] ?? 0) < 0)
67
+ return { pos: fromB + pos - fromA, retagged: false };
68
+ const lenA = (endA[k] ?? 0) - fromA;
69
+ if (leaf && lenA == ins[k])
70
+ return { pos: fromB + pos - fromA, retagged: true };
71
+ return fromA == pos && lenA == 1 && ins[k] == 1 ? { pos: fromB, retagged: true } : null;
72
+ };
73
+ // Where `pos` in the old document went, or null when it was replaced.
74
+ export const mapKept = (changes, pos) => {
75
+ const { startA, endA, startB, ins } = indexOf(changes);
76
+ const k = sectionAt(endA, pos);
77
+ if (k == endA.length || (ins[k] ?? 0) >= 0)
78
+ return null;
79
+ return (startB[k] ?? 0) + pos - (startA[k] ?? 0);
80
+ };
81
+ // Whether a plot's content, which started at `oldStart`, starts at `start`
82
+ // now, the plot kept in its place: mapped through its opening token (the doc
83
+ // has none, and starts at 0), kept or replaced by one (a parameter changed),
84
+ // as its key is, so an insertion at the start of the content doesn't count as
85
+ // a move.
86
+ export const samePlace = (changes, oldStart, start) => oldStart == 0 ? start == 0 : mapNodePos(changes, oldStart - 1)?.pos == start - 1;
87
+ // Whether the token at `pos` in the new document is in a range the update
88
+ // redraws or replaces.
89
+ export const redrawn = (changes, pos) => {
90
+ if (!changes)
91
+ return false;
92
+ const { endB, ins } = indexOf(changes);
93
+ const k = sectionAt(endB, pos);
94
+ return k < endB.length && ins[k] != -1;
95
+ };
96
+ // Whether [from, to] in the new document lies in kept sections only, touching
97
+ // no other (zero-length ones included).
98
+ export const isKept = (changes, from, to) => {
99
+ if (!changes)
100
+ return false;
101
+ const { startB, endB, ins } = indexOf(changes);
102
+ for (let k = sectionAt(endB, from, true); k < endB.length; k++) {
103
+ const start = startB[k] ?? 0;
104
+ const end = endB[k] ?? 0;
105
+ if (start > to)
106
+ break;
107
+ if (ins[k] != -1 && (end == start ? start >= from && start <= to : start < to && end > from))
108
+ return false;
109
+ }
110
+ return true;
111
+ };
112
+ // Where an old position is in the new document, for Wordgard's update to look
113
+ // for old widgets there (TilePointer.matchingWidget): along kept and redrawn
114
+ // ranges, and at both edges of a replaced one, which meet in the old document
115
+ // when it's an insertion. Nowhere inside one.
116
+ export const pointMapper = (changes) => {
117
+ const { startA, endA, startB, ins } = indexOf(changes);
118
+ return (pos) => {
119
+ const found = [];
120
+ for (let k = sectionAt(endA, pos, true); k < endA.length; k++) {
121
+ const a = startA[k] ?? 0;
122
+ if (a > pos)
123
+ break;
124
+ const b = startB[k] ?? 0;
125
+ const inserted = ins[k] ?? 0;
126
+ if (inserted < 0)
127
+ found.push(b + pos - a);
128
+ else {
129
+ if (pos == a)
130
+ found.push(b);
131
+ if (pos == (endA[k] ?? 0))
132
+ found.push(b + inserted);
133
+ }
134
+ }
135
+ return found;
136
+ };
137
+ };
138
+ // How many of a plot's first and last children an update leaves alone: the
139
+ // same node objects, wholly outside the ranges it redraws or replaces, without
140
+ // touching one (zero-length ones included). `oldStart` is where the old plot's
141
+ // content started. Laying out, keying and checking a re-laid plot then only
142
+ // need its middle.
143
+ export const unchangedEnds = (changes, oldPlot, oldStart, plot) => {
144
+ const { first, last } = indexOf(changes);
145
+ const before = oldPlot.content;
146
+ const after = plot.content;
147
+ const both = Math.min(before.length, after.length);
148
+ let head = 0;
149
+ for (let pos = oldStart; head < both; head++) {
150
+ const child = before[head];
151
+ if (!child || child !== after[head] || pos + child.length >= first)
152
+ break;
153
+ pos += child.length;
154
+ }
155
+ let tail = 0;
156
+ for (let pos = oldStart + oldPlot.contentLength; tail < both - head; tail++) {
157
+ const child = before[before.length - 1 - tail];
158
+ if (!child || child !== after[after.length - 1 - tail] || pos - child.length <= last)
159
+ break;
160
+ pos -= child.length;
161
+ }
162
+ return { head, tail };
163
+ };
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import { type ChildView } from "../decorations/iterDeco.js";
3
+ type Props = {
4
+ views: readonly ChildView[];
5
+ };
6
+ export declare const renderChildViews: (views: readonly ChildView[]) => (React.JSX.Element | null)[];
7
+ export declare const ChildNodeViews: React.MemoExoticComponent<({ views }: Props) => React.JSX.Element>;
8
+ export {};
@@ -0,0 +1,36 @@
1
+ // Named after react-prosemirror's ChildNodeViews, rewritten for Wordgard: draws what iterDeco lays
2
+ // out. Native widgets aren't React's: ReactDocTile puts their DOM in place after the commit.
3
+ import React, { memo } from "react";
4
+ import { cellOf } from "../contexts/RenderStoreContext.js";
5
+ import { isReactWidget } from "../decorations/ReactWidgetType.js";
6
+ import { shapeOf } from "../decorations/iterDeco.js";
7
+ import { childViewKeys } from "../reactKeys.js";
8
+ import { SeparatorHackView } from "./SeparatorHackView.js";
9
+ import { TextNodeView } from "./TextNodeView.js";
10
+ import { TrailingHackView } from "./TrailingHackView.js";
11
+ import { WidgetView } from "./WidgetView.js";
12
+ import { MarkView } from "./marks/MarkView.js";
13
+ import { DefaultNodeView } from "./nodes/DefaultNodeView.js";
14
+ export const renderChildViews = (views) => childViewKeys(views).map(({ view, key }) => {
15
+ switch (view.type) {
16
+ case "node":
17
+ return React.createElement(DefaultNodeView, { key: key, cell: cellOf(view) });
18
+ case "text":
19
+ return React.createElement(TextNodeView, { key: key, text: view.text });
20
+ case "wrapper":
21
+ return React.createElement(MarkView, { key: key, elt: view.elt, views: view.children, mark: view.mark });
22
+ case "buffer":
23
+ return React.createElement(SeparatorHackView, { key: key });
24
+ case "composition":
25
+ // React keeps what it last committed in its own node, so it never
26
+ // writes to it; the browser's own node isn't React's to draw.
27
+ return view.rendered === null ? null : React.createElement(TextNodeView, { key: key, text: view.rendered });
28
+ case "trailingHack":
29
+ return React.createElement(TrailingHackView, { key: key });
30
+ case "widget":
31
+ return isReactWidget(view.widget) ? React.createElement(WidgetView, { key: key, widget: view.widget }) : null;
32
+ }
33
+ });
34
+ // Skipped when a plot's content keeps its shape (iterDeco's shapeOf): the
35
+ // nodes that changed in it get their views from their cells.
36
+ export const ChildNodeViews = memo(({ views }) => React.createElement(React.Fragment, null, renderChildViews(views)), (a, b) => shapeOf(a.views) == shapeOf(b.views));
@@ -0,0 +1,13 @@
1
+ import { type AllHTMLAttributes, type ReactNode } from "react";
2
+ /** `DecorationRoot`'s props: its root's attributes, and its children. */
3
+ export type DecorationRootProps = AllHTMLAttributes<HTMLElement> & {
4
+ children?: ReactNode;
5
+ };
6
+ /**
7
+ * The root element of a React shape, wrapper, mark or widget: the tag the decoration says, with
8
+ * Wordgard's attributes merged with the props. See [React
9
+ * decorations](https://github.com/stesura/react-wordgard#react-decorations).
10
+ */
11
+ export declare const DecorationRoot: import("react").ForwardRefExoticComponent<AllHTMLAttributes<HTMLElement> & {
12
+ children?: ReactNode;
13
+ } & import("react").RefAttributes<Element>>;
@@ -0,0 +1,31 @@
1
+ // Not in react-prosemirror. The component's own props win, but `className` joins Wordgard's and
2
+ // `style` merges, its keys winning.
3
+ import { createElement, forwardRef, useContext, useMemo, } from "react";
4
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
5
+ import { cssToStyle, htmlAttrsToReactProps } from "../helpers/props.js";
6
+ import { mergeRefs, toCallbackRef } from "../helpers/refs.js";
7
+ /**
8
+ * The root element of a React shape, wrapper, mark or widget: the tag the decoration says, with
9
+ * Wordgard's attributes merged with the props. See [React
10
+ * decorations](https://github.com/stesura/react-wordgard#react-decorations).
11
+ */
12
+ export const DecorationRoot = forwardRef(({ className, style, ...props }, ref) => {
13
+ const root = useContext(DecorationRootContext);
14
+ const rootRef = root?.ref;
15
+ const merged = useMemo(() => mergeRefs(rootRef, toCallbackRef(ref)), [rootRef, ref]);
16
+ if (!root)
17
+ throw new Error("DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their component");
18
+ // Wordgard's style is a string, which React can't merge with an object.
19
+ const { style: css, ...attrs } = root.attrs;
20
+ const own = htmlAttrsToReactProps(style ? attrs : root.attrs);
21
+ const classes = [own.className, className].filter(Boolean).join(" ");
22
+ return createElement(root.tagName, {
23
+ ...own,
24
+ ...props,
25
+ className: classes || undefined,
26
+ ...(style && { style: { ...cssToStyle(css), ...style } }),
27
+ ref: merged,
28
+ suppressContentEditableWarning: true,
29
+ });
30
+ });
31
+ DecorationRoot.displayName = "DecorationRoot";
@@ -0,0 +1,18 @@
1
+ import { Component, type ReactNode } from "react";
2
+ type Props = {
3
+ onError: (error: unknown) => void;
4
+ children?: ReactNode;
5
+ };
6
+ export declare class ErrorBoundary extends Component<Props, {
7
+ failed: boolean;
8
+ }> {
9
+ state: {
10
+ failed: boolean;
11
+ };
12
+ static getDerivedStateFromError: () => {
13
+ failed: boolean;
14
+ };
15
+ componentDidCatch(error: unknown): void;
16
+ render(): ReactNode;
17
+ }
18
+ export {};
@@ -0,0 +1,14 @@
1
+ // Not in react-prosemirror: catches a render error for ReactDocTile to throw from the update. React
2
+ // 18 rethrows it from flushSync, React 19 reports it and commits nothing. A class: React has no
3
+ // hook for this.
4
+ import { Component } from "react";
5
+ export class ErrorBoundary extends Component {
6
+ state = { failed: false };
7
+ static getDerivedStateFromError = () => ({ failed: true });
8
+ componentDidCatch(error) {
9
+ this.props.onError(error);
10
+ }
11
+ render() {
12
+ return this.state.failed ? null : this.props.children;
13
+ }
14
+ }
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const SeparatorHackView: () => React.JSX.Element;
@@ -0,0 +1,4 @@
1
+ // Named after react-prosemirror's SeparatorHackView, rewritten for Wordgard: the
2
+ // `<img class="wg-buffer">` (`Widget.img`) at the ends of an inline plot.
3
+ import React from "react";
4
+ export const SeparatorHackView = () => React.createElement("img", { className: "wg-buffer" });
@@ -0,0 +1,12 @@
1
+ import React, { type ReactNode } from "react";
2
+ import type { Node } from "wordgard/doc";
3
+ import type { DecoElt } from "../wordgardInternals.js";
4
+ type Props = {
5
+ elt: DecoElt;
6
+ node: Node;
7
+ content: ReactNode;
8
+ selected: boolean;
9
+ outerRef?: ((dom: Element | null) => void) | undefined;
10
+ };
11
+ export declare const ShapeView: ({ elt, node, content, selected, outerRef }: Props) => React.JSX.Element;
12
+ export {};
@@ -0,0 +1,24 @@
1
+ // Named after react-prosemirror's ReactNodeView, for Wordgard: draws a React shape's component. Its
2
+ // own commits are sorted out of Wordgard's DOM observer (`sortRecords`).
3
+ import React, { useMemo, useState } from "react";
4
+ import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
5
+ import { atomRef, createShapeHandle, drawShape, nodePos, shapeAttributes, } from "../decorations/ReactShapeType.js";
6
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
7
+ import { attrsToRecord, componentProps } from "../helpers/index.js";
8
+ import { mergeRefs } from "../helpers/refs.js";
9
+ export const ShapeView = ({ elt, node, content, selected, outerRef }) => {
10
+ const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
11
+ const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
12
+ const atom = !elt.hasContent;
13
+ const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers), atomRef(handle, atom)), [ref, outerRef, handlers, handle, atom]);
14
+ const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
15
+ const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
16
+ const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
17
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
18
+ React.createElement(DecorationRootContext.Provider, { value: root }, drawShape(elt, {
19
+ attrs: componentProps(attrs),
20
+ nodeProps: { node, getPos: handlers.getPos, contentDOMRef },
21
+ children: content,
22
+ ref: rootRef,
23
+ }))));
24
+ };
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ text: string;
3
+ };
4
+ export declare const TextNodeView: import("react").MemoExoticComponent<({ text }: Props) => string>;
5
+ export {};
@@ -0,0 +1,4 @@
1
+ // Named after react-prosemirror's TextNodeView, rewritten for Wordgard: a bare text node, whose
2
+ // tile ReactDocTile builds after the commit.
3
+ import { memo } from "react";
4
+ export const TextNodeView = memo(({ text }) => text);
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const TrailingHackView: () => React.JSX.Element;
@@ -0,0 +1,4 @@
1
+ // Named after react-prosemirror's TrailingHackView, rewritten for Wordgard: the `<br>` a textblock
2
+ // may need (`Widget.br`).
3
+ import React from "react";
4
+ export const TrailingHackView = () => React.createElement("br", null);
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { Widget } from "wordgard/editor";
3
+ type Props = {
4
+ widget: Widget;
5
+ attrs?: readonly string[];
6
+ selected?: boolean;
7
+ outerRef?: ((dom: Element | null) => void) | undefined;
8
+ };
9
+ export declare const WidgetView: React.MemoExoticComponent<({ widget, attrs, selected, outerRef }: Props) => React.JSX.Element>;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ // Named after react-prosemirror's WidgetView, rewritten for Wordgard. Skipped while the widget is
2
+ // `eq`, so the component keeps its DOM, as Wordgard keeps an equal widget's.
3
+ import React, { memo, useMemo, useRef, useState } from "react";
4
+ import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
5
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
6
+ import { drawWidget, widgetElement, widgetRoot } from "../decorations/ReactWidgetType.js";
7
+ import { attrsToRecord, componentProps } from "../helpers/index.js";
8
+ import { mergeRefs } from "../helpers/refs.js";
9
+ export const WidgetView = memo(({ widget, attrs = none, selected = false, outerRef }) => {
10
+ const root = useRef(null);
11
+ const [handlers] = useState(() => createDecorationHandlers(() => {
12
+ // A removed element keeps its last tile.
13
+ const tile = root.current?.isConnected && root.current.wgTile;
14
+ if (!tile)
15
+ throw new Error("This React widget isn't drawn");
16
+ return tile.posBefore;
17
+ }));
18
+ const ref = useMemo(() => mergeRefs((dom) => {
19
+ root.current = dom;
20
+ }, widgetRoot, decorationRoot(handlers), outerRef), [root, handlers, outerRef]);
21
+ const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
22
+ const record = useMemo(() => attrsToRecord(attrs), [attrs]);
23
+ const tagName = widgetElement(widget);
24
+ const decorationRootValue = useMemo(() => ({ tagName, attrs: record, ref }), [tagName, record, ref]);
25
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
26
+ React.createElement(DecorationRootContext.Provider, { value: decorationRootValue }, drawWidget(widget, ref, componentProps(record)))));
27
+ }, (a, b) => a.widget.eq(b.widget) &&
28
+ a.outerRef == b.outerRef &&
29
+ a.selected == b.selected &&
30
+ sameAttrs(a.attrs ?? none, b.attrs ?? none));
31
+ const none = [];
32
+ const sameAttrs = (a, b) => a.length == b.length && a.every((value, i) => value == b[i]);
@@ -0,0 +1,9 @@
1
+ import React, { type HTMLAttributes } from "react";
2
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "children">;
3
+ /**
4
+ * Puts the editor in the page, inside a `div` taking the props, and renders the document through a
5
+ * portal, in your React tree. It goes under its `<WordgardEditor>`, inside any other components,
6
+ * with other children before or after it. One per editor: a second throws.
7
+ */
8
+ export declare const WordgardDoc: (props: Props) => React.JSX.Element;
9
+ export {};