@stesura/react-wordgard 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +35 -0
- package/LICENSE.txt +21 -0
- package/README.md +450 -2
- package/THIRD_PARTY_LICENSES.txt +93 -0
- package/dist/ReactWordgard.d.ts +13 -0
- package/dist/ReactWordgard.js +96 -0
- package/dist/changes.d.ts +14 -0
- package/dist/changes.js +163 -0
- package/dist/components/ChildNodeViews.d.ts +8 -0
- package/dist/components/ChildNodeViews.js +38 -0
- package/dist/components/DecorationRoot.d.ts +7 -0
- package/dist/components/DecorationRoot.js +28 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.js +16 -0
- package/dist/components/SeparatorHackView.d.ts +2 -0
- package/dist/components/SeparatorHackView.js +5 -0
- package/dist/components/ShapeView.d.ts +12 -0
- package/dist/components/ShapeView.js +26 -0
- package/dist/components/TextNodeView.d.ts +5 -0
- package/dist/components/TextNodeView.js +5 -0
- package/dist/components/TrailingHackView.d.ts +2 -0
- package/dist/components/TrailingHackView.js +5 -0
- package/dist/components/WidgetView.d.ts +10 -0
- package/dist/components/WidgetView.js +35 -0
- package/dist/components/WordgardDoc.d.ts +4 -0
- package/dist/components/WordgardDoc.js +34 -0
- package/dist/components/WordgardEditor.d.ts +7 -0
- package/dist/components/WordgardEditor.js +13 -0
- package/dist/components/WrapperView.d.ts +12 -0
- package/dist/components/WrapperView.js +26 -0
- package/dist/components/marks/MarkView.d.ts +11 -0
- package/dist/components/marks/MarkView.js +13 -0
- package/dist/components/marks/ReactMarkView.d.ts +10 -0
- package/dist/components/marks/ReactMarkView.js +32 -0
- package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
- package/dist/components/nodes/DefaultNodeView.js +116 -0
- package/dist/components/nodes/DocNodeView.d.ts +7 -0
- package/dist/components/nodes/DocNodeView.js +6 -0
- package/dist/composition.d.ts +23 -0
- package/dist/composition.js +97 -0
- package/dist/contexts/DecorationContext.d.ts +15 -0
- package/dist/contexts/DecorationContext.js +38 -0
- package/dist/contexts/DecorationRootContext.d.ts +6 -0
- package/dist/contexts/DecorationRootContext.js +6 -0
- package/dist/contexts/EditorContext.d.ts +26 -0
- package/dist/contexts/EditorContext.js +87 -0
- package/dist/contexts/RenderStoreContext.d.ts +15 -0
- package/dist/contexts/RenderStoreContext.js +43 -0
- package/dist/contexts/editorEvents.d.ts +12 -0
- package/dist/contexts/editorEvents.js +95 -0
- package/dist/decorations/DecoIterator.d.ts +43 -0
- package/dist/decorations/DecoIterator.js +462 -0
- package/dist/decorations/ReactMarkType.d.ts +30 -0
- package/dist/decorations/ReactMarkType.js +48 -0
- package/dist/decorations/ReactShapeType.d.ts +49 -0
- package/dist/decorations/ReactShapeType.js +204 -0
- package/dist/decorations/ReactWidgetType.d.ts +19 -0
- package/dist/decorations/ReactWidgetType.js +69 -0
- package/dist/decorations/ReactWrapperType.d.ts +22 -0
- package/dist/decorations/ReactWrapperType.js +44 -0
- package/dist/decorations/iterDeco.d.ts +82 -0
- package/dist/decorations/iterDeco.js +409 -0
- package/dist/helpers/attributes.d.ts +2 -0
- package/dist/helpers/attributes.js +11 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/props.d.ts +8 -0
- package/dist/helpers/props.js +248 -0
- package/dist/helpers/refs.d.ts +6 -0
- package/dist/helpers/refs.js +20 -0
- package/dist/hooks/useEditorEffect.d.ts +3 -0
- package/dist/hooks/useEditorEffect.js +24 -0
- package/dist/hooks/useEditorEventCallback.d.ts +2 -0
- package/dist/hooks/useEditorEventCallback.js +13 -0
- package/dist/hooks/useEditorEventListener.d.ts +3 -0
- package/dist/hooks/useEditorEventListener.js +26 -0
- package/dist/hooks/useEditorLayoutEffect.d.ts +6 -0
- package/dist/hooks/useEditorLayoutEffect.js +38 -0
- package/dist/hooks/useEditorState.d.ts +1 -0
- package/dist/hooks/useEditorState.js +10 -0
- package/dist/hooks/useEditorStateSelector.d.ts +2 -0
- package/dist/hooks/useEditorStateSelector.js +6 -0
- package/dist/hooks/useIgnoreMutation.d.ts +2 -0
- package/dist/hooks/useIgnoreMutation.js +18 -0
- package/dist/hooks/useIsNodeSelected.d.ts +1 -0
- package/dist/hooks/useIsNodeSelected.js +6 -0
- package/dist/hooks/useMergedDOMRefs.d.ts +2 -0
- package/dist/hooks/useMergedDOMRefs.js +17 -0
- package/dist/hooks/useNodePos.d.ts +1 -0
- package/dist/hooks/useNodePos.js +8 -0
- package/dist/hooks/useSelectNode.d.ts +2 -0
- package/dist/hooks/useSelectNode.js +24 -0
- package/dist/hooks/useStopEvent.d.ts +2 -0
- package/dist/hooks/useStopEvent.js +20 -0
- package/dist/hooks/useWordgard.d.ts +1 -0
- package/dist/hooks/useWordgard.js +4 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/reactKeys.d.ts +39 -0
- package/dist/reactKeys.js +188 -0
- package/dist/tile.d.ts +32 -0
- package/dist/tile.js +1324 -0
- package/dist/viewRoots.d.ts +2 -0
- package/dist/viewRoots.js +8 -0
- package/dist/wordgardInternals.d.ts +211 -0
- package/dist/wordgardInternals.js +85 -0
- package/package.json +119 -4
- package/skills/react-wordgard/SKILL.md +397 -0
- package/skills/react-wordgard/references/wordgard.md +189 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Adapted from react-prosemirror's ReactEditorView, to fit Wordgard: it
|
|
2
|
+
// extends Wordgard instead of EditorView and, where ReactEditorView replaces
|
|
3
|
+
// the docView ProseMirror's constructor builds, has the base constructor build
|
|
4
|
+
// a tile tree React renders.
|
|
5
|
+
import { Wordgard } from "wordgard/editor";
|
|
6
|
+
import { GardState } from "wordgard/state";
|
|
7
|
+
import { editorOf } from "./contexts/EditorContext.js";
|
|
8
|
+
import { editorEventHandlers, hasEditorEventHandlers } from "./contexts/editorEvents.js";
|
|
9
|
+
import { ReactDocTile, RefusedUpdate, observeShapes } from "./tile.js";
|
|
10
|
+
import { DocTile } from "./wordgardInternals.js";
|
|
11
|
+
// Asks Wordgard for a frame: a no-op read schedules a flush.
|
|
12
|
+
const retry = () => { };
|
|
13
|
+
export class ReactWordgard
|
|
14
|
+
// @ts-expect-error Wordgard's constructor is private in its typings.
|
|
15
|
+
extends Wordgard {
|
|
16
|
+
// An editor with a React root of its own.
|
|
17
|
+
static create = (spec) => new ReactWordgard(spec, false);
|
|
18
|
+
// An editor WordgardEditor hosts: its document renders through
|
|
19
|
+
// WordgardDoc's portal, in the app's React tree.
|
|
20
|
+
static host = (spec) => new ReactWordgard(spec, true);
|
|
21
|
+
// Wordgard's own tests build tile trees through `Wordgard.DocTile`.
|
|
22
|
+
static DocTile = ReactDocTile;
|
|
23
|
+
constructor(spec, hosted = false) {
|
|
24
|
+
// Wordgard's constructor draws the document with `DocTile.create`, before
|
|
25
|
+
// this one can run: React draws it there instead, so it's drawn once. Put
|
|
26
|
+
// back at once, for any editor React's render creates; other editors
|
|
27
|
+
// created first (by a plugin) draw as before.
|
|
28
|
+
const create = DocTile.create;
|
|
29
|
+
const self = { editor: null };
|
|
30
|
+
DocTile.create = (state, dom, wg) => {
|
|
31
|
+
if (!(wg instanceof ReactWordgard))
|
|
32
|
+
return create(state, dom, wg);
|
|
33
|
+
DocTile.create = create;
|
|
34
|
+
if (!hosted)
|
|
35
|
+
return ReactDocTile.create(state, dom, wg);
|
|
36
|
+
return ReactDocTile.host(state, dom, wg, () => {
|
|
37
|
+
const { editor } = self;
|
|
38
|
+
if (editor)
|
|
39
|
+
editor.docTile = editor.observer.ignore(() => ReactDocTile.mount(editor.contentDOM, editor, editor.docTile));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
// Given a `parent`, it would also put the editor in the page, where
|
|
43
|
+
// connecting measures it: that's left until it's built.
|
|
44
|
+
// useEditorEventListener's handlers come first; Wordgard keeps one copy of
|
|
45
|
+
// an extension, so a state taken from another ReactWordgard is fine.
|
|
46
|
+
const events = spec.state
|
|
47
|
+
? {
|
|
48
|
+
state: spec.state.update({ effects: GardState.appendConfig.of(editorEventHandlers) })
|
|
49
|
+
.state,
|
|
50
|
+
}
|
|
51
|
+
: { config: [editorEventHandlers, spec.config ?? []] };
|
|
52
|
+
try {
|
|
53
|
+
// @ts-expect-error The typings drop the private constructor's parameter.
|
|
54
|
+
super({ ...spec, ...events, parent: undefined });
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
DocTile.create = create;
|
|
58
|
+
}
|
|
59
|
+
self.editor = this;
|
|
60
|
+
// React shapes' own commits are sorted out of what Wordgard's DOM
|
|
61
|
+
// observer reads; replaced before the editor is connected and observes.
|
|
62
|
+
this.observer.observer = observeShapes(this, (records) => {
|
|
63
|
+
this.observer.queue.push(...records);
|
|
64
|
+
this.scheduleFlush();
|
|
65
|
+
});
|
|
66
|
+
spec.parent?.appendChild(this.dom);
|
|
67
|
+
}
|
|
68
|
+
// Wordgard's flush runs from an animation frame and from the methods that
|
|
69
|
+
// need an up-to-date DOM; useEditorEffect's effects run after it. A refused
|
|
70
|
+
// update leaves Wordgard's transactions pending with no frame requested for
|
|
71
|
+
// them, so one is.
|
|
72
|
+
flush() {
|
|
73
|
+
const editor = editorOf(this);
|
|
74
|
+
// React shapes that committed since their records were delivered get
|
|
75
|
+
// their tiles first (taking the records rebuilds them): Wordgard reads
|
|
76
|
+
// them before the build.
|
|
77
|
+
this.observer.queue.push(...this.observer.observer.takeRecords());
|
|
78
|
+
editor.flushing = true;
|
|
79
|
+
try {
|
|
80
|
+
super.flush();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof RefusedUpdate)
|
|
84
|
+
this.scheduleDOMRead(retry);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
editor.flushing = false;
|
|
89
|
+
}
|
|
90
|
+
editor.runEffects();
|
|
91
|
+
// A reconfiguration (GardState.reconfigure) dropped useEditorEventListener's
|
|
92
|
+
// handlers: they come back with a transaction of their own.
|
|
93
|
+
if (!hasEditorEventHandlers(this.state))
|
|
94
|
+
this.dispatch({ effects: GardState.appendConfig.of(editorEventHandlers) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -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
|
+
};
|
package/dist/changes.js
ADDED
|
@@ -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,38 @@
|
|
|
1
|
+
// Named after react-prosemirror's ChildNodeViews, rewritten for Wordgard: draws
|
|
2
|
+
// what iterDeco lays out: nodes, text, mark wrappers, Wordgard's hack nodes and
|
|
3
|
+
// React widgets. Native widgets aren't React's: ReactDocTile puts their DOM in
|
|
4
|
+
// place after the commit.
|
|
5
|
+
import React, { memo } from "react";
|
|
6
|
+
import { cellOf } from "../contexts/RenderStoreContext.js";
|
|
7
|
+
import { isReactWidget } from "../decorations/ReactWidgetType.js";
|
|
8
|
+
import { shapeOf } from "../decorations/iterDeco.js";
|
|
9
|
+
import { childViewKeys } from "../reactKeys.js";
|
|
10
|
+
import { SeparatorHackView } from "./SeparatorHackView.js";
|
|
11
|
+
import { TextNodeView } from "./TextNodeView.js";
|
|
12
|
+
import { TrailingHackView } from "./TrailingHackView.js";
|
|
13
|
+
import { WidgetView } from "./WidgetView.js";
|
|
14
|
+
import { MarkView } from "./marks/MarkView.js";
|
|
15
|
+
import { DefaultNodeView } from "./nodes/DefaultNodeView.js";
|
|
16
|
+
export const renderChildViews = (views) => childViewKeys(views).map(({ view, key }) => {
|
|
17
|
+
switch (view.type) {
|
|
18
|
+
case "node":
|
|
19
|
+
return React.createElement(DefaultNodeView, { key: key, cell: cellOf(view) });
|
|
20
|
+
case "text":
|
|
21
|
+
return React.createElement(TextNodeView, { key: key, text: view.text });
|
|
22
|
+
case "wrapper":
|
|
23
|
+
return React.createElement(MarkView, { key: key, elt: view.elt, views: view.children, mark: view.mark });
|
|
24
|
+
case "buffer":
|
|
25
|
+
return React.createElement(SeparatorHackView, { key: key });
|
|
26
|
+
case "composition":
|
|
27
|
+
// React keeps what it last committed in its own node, so it never
|
|
28
|
+
// writes to it; the browser's own node isn't React's to draw.
|
|
29
|
+
return view.rendered === null ? null : React.createElement(TextNodeView, { key: key, text: view.rendered });
|
|
30
|
+
case "trailingHack":
|
|
31
|
+
return React.createElement(TrailingHackView, { key: key });
|
|
32
|
+
case "widget":
|
|
33
|
+
return isReactWidget(view.widget) ? React.createElement(WidgetView, { key: key, widget: view.widget }) : null;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// Skipped when a plot's content keeps its shape (iterDeco's shapeOf): the
|
|
37
|
+
// nodes that changed in it get their views from their cells.
|
|
38
|
+
export const ChildNodeViews = memo(({ views }) => React.createElement(React.Fragment, null, renderChildViews(views)), (a, b) => shapeOf(a.views) == shapeOf(b.views));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AllHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
export type DecorationRootProps = AllHTMLAttributes<HTMLElement> & {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
};
|
|
5
|
+
export declare const DecorationRoot: import("react").ForwardRefExoticComponent<AllHTMLAttributes<HTMLElement> & {
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
} & import("react").RefAttributes<Element>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A React shape's, wrapper's or mark's root, drawn for its component: the tag
|
|
2
|
+
// its Elt says, Wordgard's attributes and the root's ref, so the component
|
|
3
|
+
// can't get them wrong. The component's own props win, but `className` joins
|
|
4
|
+
// Wordgard's and `style` merges, its keys winning. Not in react-prosemirror.
|
|
5
|
+
import { createElement, forwardRef, useContext, useMemo, } from "react";
|
|
6
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
7
|
+
import { cssToStyle, htmlAttrsToReactProps } from "../helpers/props.js";
|
|
8
|
+
import { mergeRefs, toCallbackRef } from "../helpers/refs.js";
|
|
9
|
+
export const DecorationRoot = forwardRef(({ className, style, ...props }, ref) => {
|
|
10
|
+
const root = useContext(DecorationRootContext);
|
|
11
|
+
const rootRef = root?.ref;
|
|
12
|
+
const merged = useMemo(() => mergeRefs(rootRef, toCallbackRef(ref)), [rootRef, ref]);
|
|
13
|
+
if (!root)
|
|
14
|
+
throw new Error("DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their component");
|
|
15
|
+
// Wordgard's style is a string, which React can't merge with an object.
|
|
16
|
+
const { style: css, ...attrs } = root.attrs;
|
|
17
|
+
const own = htmlAttrsToReactProps(style ? attrs : root.attrs);
|
|
18
|
+
const classes = [own.className, className].filter(Boolean).join(" ");
|
|
19
|
+
return createElement(root.tagName, {
|
|
20
|
+
...own,
|
|
21
|
+
...props,
|
|
22
|
+
className: classes || undefined,
|
|
23
|
+
...(style && { style: { ...cssToStyle(css), ...style } }),
|
|
24
|
+
ref: merged,
|
|
25
|
+
suppressContentEditableWarning: true,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
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,16 @@
|
|
|
1
|
+
// Not in react-prosemirror: catches an error thrown while React renders the
|
|
2
|
+
// document, for ReactDocTile to throw it from the update. React 18 rethrows it
|
|
3
|
+
// from flushSync, React 19 reports it as uncaught and commits nothing, and the
|
|
4
|
+
// tiles can't be built over what didn't commit. A class: React has no hook for
|
|
5
|
+
// this.
|
|
6
|
+
import { Component } from "react";
|
|
7
|
+
export class ErrorBoundary extends Component {
|
|
8
|
+
state = { failed: false };
|
|
9
|
+
static getDerivedStateFromError = () => ({ failed: true });
|
|
10
|
+
componentDidCatch(error) {
|
|
11
|
+
this.props.onError(error);
|
|
12
|
+
}
|
|
13
|
+
render() {
|
|
14
|
+
return this.state.failed ? null : this.props.children;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Named after react-prosemirror's SeparatorHackView, rewritten for Wordgard: the
|
|
2
|
+
// `<img class="wg-buffer">` Wordgard puts at the start and end of an inline plot
|
|
3
|
+
// (`Widget.img`), which gives the cursor somewhere to go.
|
|
4
|
+
import React from "react";
|
|
5
|
+
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,26 @@
|
|
|
1
|
+
// Named after react-prosemirror's ReactNodeView, for Wordgard: draws a React
|
|
2
|
+
// shape's component, with the node's content as its children. ReactDocTile
|
|
3
|
+
// builds the tiles over what it commits; the component's own commits are
|
|
4
|
+
// sorted out of Wordgard's DOM observer (`sortRecords`).
|
|
5
|
+
import React, { useMemo, useState } from "react";
|
|
6
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
7
|
+
import { atomRef, createShapeHandle, drawShape, nodePos, shapeAttributes, } from "../decorations/ReactShapeType.js";
|
|
8
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
9
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
10
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
11
|
+
export const ShapeView = ({ elt, node, content, selected, outerRef }) => {
|
|
12
|
+
const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
|
|
13
|
+
const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
|
|
14
|
+
const atom = !elt.hasContent;
|
|
15
|
+
const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers), atomRef(handle, atom)), [ref, outerRef, handlers, handle, atom]);
|
|
16
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
17
|
+
const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
|
|
18
|
+
const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
|
|
19
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
20
|
+
React.createElement(DecorationRootContext.Provider, { value: root }, drawShape(elt, {
|
|
21
|
+
attrs: componentProps(attrs),
|
|
22
|
+
nodeProps: { node, getPos: handlers.getPos, contentDOMRef },
|
|
23
|
+
children: content,
|
|
24
|
+
ref: rootRef,
|
|
25
|
+
}))));
|
|
26
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Named after react-prosemirror's TextNodeView, rewritten for Wordgard: a text
|
|
2
|
+
// leaf renders as a bare text node, its tile is built after the commit
|
|
3
|
+
// (ReactDocTile). Skipped while its text is the same.
|
|
4
|
+
import { memo } from "react";
|
|
5
|
+
export const TextNodeView = memo(({ text }) => text);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Named after react-prosemirror's TrailingHackView, rewritten for Wordgard: the
|
|
2
|
+
// `<br>` Wordgard's own renderer adds to a textblock that needs one
|
|
3
|
+
// (`Widget.br`).
|
|
4
|
+
import React from "react";
|
|
5
|
+
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,35 @@
|
|
|
1
|
+
// Named after react-prosemirror's WidgetView, rewritten for Wordgard: draws a
|
|
2
|
+
// React widget's component, whose root element becomes the widget's DOM.
|
|
3
|
+
// ReactDocTile builds its tile after the commit, so there's no view
|
|
4
|
+
// description to register here. Skipped while the widget is `eq`, so the
|
|
5
|
+
// component keeps its DOM, as Wordgard keeps an equal widget's.
|
|
6
|
+
import React, { memo, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
8
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
9
|
+
import { drawWidget, widgetElement, widgetRoot } from "../decorations/ReactWidgetType.js";
|
|
10
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
11
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
12
|
+
export const WidgetView = memo(({ widget, attrs = none, selected = false, outerRef }) => {
|
|
13
|
+
const root = useRef(null);
|
|
14
|
+
const [handlers] = useState(() => createDecorationHandlers(() => {
|
|
15
|
+
// A removed element keeps its last tile.
|
|
16
|
+
const tile = root.current?.isConnected && root.current.wgTile;
|
|
17
|
+
if (!tile)
|
|
18
|
+
throw new Error("This React widget isn't drawn");
|
|
19
|
+
return tile.posBefore;
|
|
20
|
+
}));
|
|
21
|
+
const ref = useMemo(() => mergeRefs((dom) => {
|
|
22
|
+
root.current = dom;
|
|
23
|
+
}, widgetRoot, decorationRoot(handlers), outerRef), [root, handlers, outerRef]);
|
|
24
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
25
|
+
const record = useMemo(() => attrsToRecord(attrs), [attrs]);
|
|
26
|
+
const tagName = widgetElement(widget);
|
|
27
|
+
const decorationRootValue = useMemo(() => ({ tagName, attrs: record, ref }), [tagName, record, ref]);
|
|
28
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
29
|
+
React.createElement(DecorationRootContext.Provider, { value: decorationRootValue }, drawWidget(widget, ref, componentProps(record)))));
|
|
30
|
+
}, (a, b) => a.widget.eq(b.widget) &&
|
|
31
|
+
a.outerRef == b.outerRef &&
|
|
32
|
+
a.selected == b.selected &&
|
|
33
|
+
sameAttrs(a.attrs ?? none, b.attrs ?? none));
|
|
34
|
+
const none = [];
|
|
35
|
+
const sameAttrs = (a, b) => a.length == b.length && a.every((value, i) => value == b[i]);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Named after react-prosemirror's ProseMirrorDoc, rewritten for Wordgard:
|
|
2
|
+
// puts the editor's element in the page, inside a `div` taking the props, and
|
|
3
|
+
// renders its document through a portal into Wordgard's content element, so
|
|
4
|
+
// the document shares the app's React tree and contexts. The portal commits
|
|
5
|
+
// first and builds the tiles; attaching the element then connects the editor,
|
|
6
|
+
// as Wordgard's own editor is built before it's connected.
|
|
7
|
+
import React, { useCallback, useInsertionEffect, useLayoutEffect, useSyncExternalStore, } from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { editorOf, useEditorContext } from "../contexts/EditorContext.js";
|
|
10
|
+
import { docPortal } from "../tile.js";
|
|
11
|
+
const DocPortal = ({ wg }) => {
|
|
12
|
+
const portal = docPortal(wg.contentDOM);
|
|
13
|
+
const element = useSyncExternalStore(portal.subscribe, portal.get);
|
|
14
|
+
// Insertion effects all run before any layout effect, so a second portal
|
|
15
|
+
// committed alongside this one has claimed the editor by the time `mount`
|
|
16
|
+
// builds the tiles. StrictMode doesn't run them twice.
|
|
17
|
+
useInsertionEffect(() => portal.claim(), [portal]);
|
|
18
|
+
useLayoutEffect(() => portal.committed(element));
|
|
19
|
+
useLayoutEffect(() => portal.mount(), [portal]);
|
|
20
|
+
return createPortal(element, wg.contentDOM);
|
|
21
|
+
};
|
|
22
|
+
export const WordgardDoc = (props) => {
|
|
23
|
+
const { wg } = useEditorContext();
|
|
24
|
+
const attach = useCallback((dom) => {
|
|
25
|
+
if (!dom)
|
|
26
|
+
return;
|
|
27
|
+
dom.appendChild(wg.dom);
|
|
28
|
+
// Effects waiting for the editor to be in the page.
|
|
29
|
+
editorOf(wg).requestEffects();
|
|
30
|
+
}, [wg]);
|
|
31
|
+
return (React.createElement(React.Fragment, null,
|
|
32
|
+
React.createElement(DocPortal, { wg: wg }),
|
|
33
|
+
React.createElement("div", { ...props, ref: attach })));
|
|
34
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Wordgard } from "wordgard/editor";
|
|
3
|
+
type Props = Omit<Wordgard.Spec, "parent"> & {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export declare const WordgardEditor: ({ children, ...spec }: Props) => React.JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Named after react-prosemirror's ProseMirror component, rewritten for
|
|
2
|
+
// Wordgard: creates the editor and provides it to what it holds, WordgardDoc
|
|
3
|
+
// and the app's own components (menus, hooks). The spec is read once, as
|
|
4
|
+
// react-prosemirror's `defaultState`: Wordgard's editor owns its state, so
|
|
5
|
+
// changes go through `dispatch`, and configuration through Wordgard's
|
|
6
|
+
// compartments or `GardState.appendConfig`.
|
|
7
|
+
import React, { useState } from "react";
|
|
8
|
+
import { ReactWordgard } from "../ReactWordgard.js";
|
|
9
|
+
import { EditorContext, editorOf } from "../contexts/EditorContext.js";
|
|
10
|
+
export const WordgardEditor = ({ children, ...spec }) => {
|
|
11
|
+
const [editor] = useState(() => editorOf(ReactWordgard.host(spec)));
|
|
12
|
+
return React.createElement(EditorContext.Provider, { value: editor }, children);
|
|
13
|
+
};
|
|
@@ -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
|
+
wrapped: (ref: (dom: Element | null) => void) => ReactNode;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
outerRef?: ((dom: Element | null) => void) | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare const WrapperView: ({ elt, node, wrapped, selected, outerRef }: Props) => React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Draws a React wrapper's component, with what it wraps (the node's shape, or
|
|
2
|
+
// the element its `target` picks) as its children. Like ShapeView, it has no
|
|
3
|
+
// effects: ReactDocTile builds the tiles over what it commits, and the
|
|
4
|
+
// component's own commits are sorted out of Wordgard's DOM observer.
|
|
5
|
+
import React, { useMemo, useState } from "react";
|
|
6
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
7
|
+
import { createWrapperHandle, nodePos, shapeAttributes } from "../decorations/ReactShapeType.js";
|
|
8
|
+
import { drawWrapper } from "../decorations/ReactWrapperType.js";
|
|
9
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
10
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
11
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
12
|
+
export const WrapperView = ({ elt, node, wrapped, selected, outerRef }) => {
|
|
13
|
+
const [{ handle, ref, wrappedRef }] = useState(createWrapperHandle);
|
|
14
|
+
const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
|
|
15
|
+
const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers)), [ref, outerRef, handlers]);
|
|
16
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
17
|
+
const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
|
|
18
|
+
const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
|
|
19
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
20
|
+
React.createElement(DecorationRootContext.Provider, { value: root }, drawWrapper(elt, {
|
|
21
|
+
attrs: componentProps(attrs),
|
|
22
|
+
nodeProps: { node, getPos: handlers.getPos },
|
|
23
|
+
children: wrapped(wrappedRef),
|
|
24
|
+
ref: rootRef,
|
|
25
|
+
}))));
|
|
26
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Mark } from "wordgard/doc";
|
|
3
|
+
import { type ChildView } from "../../decorations/iterDeco.js";
|
|
4
|
+
import type { DecoElt } from "../../wordgardInternals.js";
|
|
5
|
+
type Props = {
|
|
6
|
+
elt: DecoElt;
|
|
7
|
+
views: readonly ChildView[];
|
|
8
|
+
mark?: Mark | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare const MarkView: React.MemoExoticComponent<({ elt, views, mark }: Props) => React.JSX.Element>;
|
|
11
|
+
export {};
|