@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.
- package/CHANGELOG.md +41 -0
- package/LICENSE.txt +21 -0
- package/README.md +912 -2
- package/THIRD_PARTY_LICENSES.txt +93 -0
- package/dist/ReactWordgard.d.ts +13 -0
- package/dist/ReactWordgard.js +94 -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 +36 -0
- package/dist/components/DecorationRoot.d.ts +13 -0
- package/dist/components/DecorationRoot.js +31 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.js +14 -0
- package/dist/components/SeparatorHackView.d.ts +2 -0
- package/dist/components/SeparatorHackView.js +4 -0
- package/dist/components/ShapeView.d.ts +12 -0
- package/dist/components/ShapeView.js +24 -0
- package/dist/components/TextNodeView.d.ts +5 -0
- package/dist/components/TextNodeView.js +4 -0
- package/dist/components/TrailingHackView.d.ts +2 -0
- package/dist/components/TrailingHackView.js +4 -0
- package/dist/components/WidgetView.d.ts +10 -0
- package/dist/components/WidgetView.js +32 -0
- package/dist/components/WordgardDoc.d.ts +9 -0
- package/dist/components/WordgardDoc.js +36 -0
- package/dist/components/WordgardEditor.d.ts +15 -0
- package/dist/components/WordgardEditor.js +17 -0
- package/dist/components/WordgardScope.d.ts +14 -0
- package/dist/components/WordgardScope.js +14 -0
- package/dist/components/WrapperView.d.ts +12 -0
- package/dist/components/WrapperView.js +23 -0
- package/dist/components/marks/MarkView.d.ts +11 -0
- package/dist/components/marks/MarkView.js +10 -0
- package/dist/components/marks/ReactMarkView.d.ts +10 -0
- package/dist/components/marks/ReactMarkView.js +30 -0
- package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
- package/dist/components/nodes/DefaultNodeView.js +112 -0
- package/dist/components/nodes/DocNodeView.d.ts +7 -0
- package/dist/components/nodes/DocNodeView.js +5 -0
- package/dist/composition.d.ts +23 -0
- package/dist/composition.js +95 -0
- package/dist/contexts/DecorationContext.d.ts +15 -0
- package/dist/contexts/DecorationContext.js +35 -0
- package/dist/contexts/DecorationRootContext.d.ts +6 -0
- package/dist/contexts/DecorationRootContext.js +4 -0
- package/dist/contexts/EditorContext.d.ts +27 -0
- package/dist/contexts/EditorContext.js +87 -0
- package/dist/contexts/RenderStoreContext.d.ts +15 -0
- package/dist/contexts/RenderStoreContext.js +41 -0
- package/dist/contexts/editorEvents.d.ts +12 -0
- package/dist/contexts/editorEvents.js +92 -0
- package/dist/decorations/DecoIterator.d.ts +43 -0
- package/dist/decorations/DecoIterator.js +462 -0
- package/dist/decorations/ReactMarkType.d.ts +41 -0
- package/dist/decorations/ReactMarkType.js +47 -0
- package/dist/decorations/ReactShapeType.d.ts +65 -0
- package/dist/decorations/ReactShapeType.js +202 -0
- package/dist/decorations/ReactWidgetType.d.ts +29 -0
- package/dist/decorations/ReactWidgetType.js +67 -0
- package/dist/decorations/ReactWrapperType.d.ts +31 -0
- package/dist/decorations/ReactWrapperType.js +41 -0
- package/dist/decorations/iterDeco.d.ts +82 -0
- package/dist/decorations/iterDeco.js +406 -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 +10 -0
- package/dist/hooks/useEditorEffect.js +28 -0
- package/dist/hooks/useEditorEventCallback.d.ts +8 -0
- package/dist/hooks/useEditorEventCallback.js +17 -0
- package/dist/hooks/useEditorEventListener.d.ts +10 -0
- package/dist/hooks/useEditorEventListener.js +26 -0
- package/dist/hooks/useEditorLayoutEffect.d.ts +15 -0
- package/dist/hooks/useEditorLayoutEffect.js +34 -0
- package/dist/hooks/useEditorState.d.ts +6 -0
- package/dist/hooks/useEditorState.js +12 -0
- package/dist/hooks/useEditorStateSelector.d.ts +7 -0
- package/dist/hooks/useEditorStateSelector.js +11 -0
- package/dist/hooks/useIgnoreMutation.d.ts +6 -0
- package/dist/hooks/useIgnoreMutation.js +19 -0
- package/dist/hooks/useIsNodeSelected.d.ts +2 -0
- package/dist/hooks/useIsNodeSelected.js +5 -0
- package/dist/hooks/useMergedDOMRefs.d.ts +6 -0
- package/dist/hooks/useMergedDOMRefs.js +18 -0
- package/dist/hooks/useNodePos.d.ts +4 -0
- package/dist/hooks/useNodePos.js +7 -0
- package/dist/hooks/useSelectNode.d.ts +6 -0
- package/dist/hooks/useSelectNode.js +25 -0
- package/dist/hooks/useStopEvent.d.ts +8 -0
- package/dist/hooks/useStopEvent.js +21 -0
- package/dist/hooks/useWordgard.d.ts +2 -0
- package/dist/hooks/useWordgard.js +3 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -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 +1319 -0
- package/dist/viewRoots.d.ts +2 -0
- package/dist/viewRoots.js +7 -0
- package/dist/wordgardInternals.d.ts +211 -0
- package/dist/wordgardInternals.js +82 -0
- package/package.json +119 -4
- package/skills/react-wordgard/SKILL.md +525 -0
- package/skills/react-wordgard/references/wordgard.md +189 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Named after react-prosemirror's iterDeco, rewritten for Wordgard: lays out a plot's content from
|
|
2
|
+
// DecoIterator, as Wordgard's ContentUpdate.build does. A plot that kept its identity outside the
|
|
3
|
+
// redrawn ranges keeps its previous layout.
|
|
4
|
+
import { Attributes, Elt, Leaf, Mark } from "wordgard/doc";
|
|
5
|
+
import { Widget } from "wordgard/editor";
|
|
6
|
+
import { isKept, pointMapper, samePlace, unchangedEnds } from "../changes.js";
|
|
7
|
+
import { moveCell } from "../contexts/RenderStoreContext.js";
|
|
8
|
+
import { childViewKeys, createNodeKey } from "../reactKeys.js";
|
|
9
|
+
import { documentMark, renderMarkWrapper, renderWrapper, spanningOf, } from "./DecoIterator.js";
|
|
10
|
+
import { isReactMark } from "./ReactMarkType.js";
|
|
11
|
+
import { isReactWidget } from "./ReactWidgetType.js";
|
|
12
|
+
import { isReactWrapper } from "./ReactWrapperType.js";
|
|
13
|
+
// The wrappers of the marks the cursor types with (ContentUpdate.composition
|
|
14
|
+
// opens only those with an element).
|
|
15
|
+
export const cursorWrappers = (marks) => marks
|
|
16
|
+
.filter((mark) => mark.type.element)
|
|
17
|
+
.map((mark) => wrapperView(renderMarkWrapper(mark), mark.type.spanning, mark));
|
|
18
|
+
// A React mark's component gets its mark, and draws only runs of it.
|
|
19
|
+
const wrapperView = (elt, spanning, mark) => mark && isReactMark(elt) ? { elt, spanning, mark: documentMark(mark) } : { elt, spanning };
|
|
20
|
+
const sameMark = (a, b) => a == b || (!!a && !!b && a.eq(b));
|
|
21
|
+
// buildNodeShape's rule: in editable content, the outermost element without a
|
|
22
|
+
// content hole becomes uneditable, unless it's a void element, and so does
|
|
23
|
+
// what it holds.
|
|
24
|
+
export const uneditable = (shape, inEditable) => {
|
|
25
|
+
if (!inEditable || shape.hasContent)
|
|
26
|
+
return { shape, inEditable };
|
|
27
|
+
if (Attributes.get(shape.attrs, "contenteditable") == null &&
|
|
28
|
+
!/^(br|hr|img|input|wbr)$/i.test(shape.tagName))
|
|
29
|
+
shape = Elt.create(shape.tagName, Attributes.merge(shape.attrs, ["contenteditable", "false"]), shape.children);
|
|
30
|
+
return { shape, inEditable: false };
|
|
31
|
+
};
|
|
32
|
+
// Whether React can skip a node view: it would draw the same thing.
|
|
33
|
+
export const sameNodeView = (a, b) => a == b || (a.node == b.node && a.children == b.children && a.shape.eq(b.shape));
|
|
34
|
+
// Whether two views draw the same thing.
|
|
35
|
+
const sameView = (view, other) => {
|
|
36
|
+
if (view == other)
|
|
37
|
+
return true;
|
|
38
|
+
switch (view.type) {
|
|
39
|
+
case "node":
|
|
40
|
+
return other.type == "node" && view.key == other.key && sameNodeView(view, other);
|
|
41
|
+
case "text":
|
|
42
|
+
return other.type == "text" && view.text == other.text;
|
|
43
|
+
case "wrapper":
|
|
44
|
+
return (other.type == "wrapper" &&
|
|
45
|
+
view.key == other.key &&
|
|
46
|
+
view.spanning == other.spanning &&
|
|
47
|
+
view.elt.eq(other.elt) &&
|
|
48
|
+
sameMark(view.mark, other.mark) &&
|
|
49
|
+
view.children.length == other.children.length &&
|
|
50
|
+
view.children.every((child, i) => {
|
|
51
|
+
const match = other.children[i];
|
|
52
|
+
return !!match && sameView(child, match);
|
|
53
|
+
}));
|
|
54
|
+
case "buffer":
|
|
55
|
+
return other.type == "buffer" && view.side == other.side;
|
|
56
|
+
case "widget":
|
|
57
|
+
return (other.type == "widget" &&
|
|
58
|
+
view.side == other.side &&
|
|
59
|
+
view.key == other.key &&
|
|
60
|
+
view.offset == other.offset &&
|
|
61
|
+
view.widget.eq(other.widget));
|
|
62
|
+
case "composition":
|
|
63
|
+
return (other.type == "composition" && view.key == other.key && view.rendered == other.rendered);
|
|
64
|
+
case "trailingHack":
|
|
65
|
+
return other.type == "trailingHack";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// What makes a list draw as before: the same keys, each for the same view or
|
|
69
|
+
// for a node that kept its cell (moveCell). A list whose shape is another's
|
|
70
|
+
// last one's shares it.
|
|
71
|
+
const shapes = new WeakMap();
|
|
72
|
+
export const shapeOf = (views) => shapes.get(views) ?? views;
|
|
73
|
+
// A plot's content laid out again, keeping the old view of each child that
|
|
74
|
+
// draws the same, matched by its React key (which React keeps the component
|
|
75
|
+
// for), and the old list when they all do. A wrapper that changed keeps the
|
|
76
|
+
// children that didn't. React then skips what's kept (the views' memos): a
|
|
77
|
+
// plot can change while its content doesn't (a paragraph's alignment, a mark
|
|
78
|
+
// on its tag), and typing changes one text run of a paragraph. A node that
|
|
79
|
+
// keeps its key keeps its cell, so a list whose shape is the same isn't drawn
|
|
80
|
+
// again for it. `after` holds `before`'s first `head` and last `tail` views
|
|
81
|
+
// (iterDeco's unchanged ends, all nodes): only the middle is matched. Writes
|
|
82
|
+
// what it keeps into `after`.
|
|
83
|
+
const keepSame = (store, before, after, head = 0, tail = 0) => {
|
|
84
|
+
const whole = !head && !tail;
|
|
85
|
+
const old = new Map(childViewKeys(whole ? before : before.slice(head, before.length - tail)).map(({ view, key }) => [key, view]));
|
|
86
|
+
let changed = before.length != after.length;
|
|
87
|
+
let sameShape = !changed;
|
|
88
|
+
childViewKeys(whole ? after : after.slice(head, after.length - tail)).forEach(({ view, key }, at) => {
|
|
89
|
+
const i = head + at;
|
|
90
|
+
const match = old.get(key);
|
|
91
|
+
let found = view;
|
|
92
|
+
if (match && sameView(match, view))
|
|
93
|
+
found = match;
|
|
94
|
+
else if (match?.type == "wrapper" &&
|
|
95
|
+
view.type == "wrapper" &&
|
|
96
|
+
match.elt.eq(view.elt) &&
|
|
97
|
+
sameMark(match.mark, view.mark))
|
|
98
|
+
keepSame(store, match.children, view.children);
|
|
99
|
+
if (match?.type == "node" && found.type == "node")
|
|
100
|
+
moveCell(store, match, found);
|
|
101
|
+
if (found != before[i]) {
|
|
102
|
+
changed = true;
|
|
103
|
+
if (found.type != "node" || match != before[i])
|
|
104
|
+
sameShape = false;
|
|
105
|
+
}
|
|
106
|
+
after[i] = found;
|
|
107
|
+
});
|
|
108
|
+
if (!changed) {
|
|
109
|
+
shapes.set(after, shapeOf(before));
|
|
110
|
+
return before;
|
|
111
|
+
}
|
|
112
|
+
if (sameShape)
|
|
113
|
+
shapes.set(after, shapeOf(before));
|
|
114
|
+
return after;
|
|
115
|
+
};
|
|
116
|
+
const isPoint = (view) => view?.type == "buffer" || view?.type == "widget";
|
|
117
|
+
// A spanning wrapper continues the previous sibling when it draws the same
|
|
118
|
+
// element, taking in the points between them (ContentUpdate.enterSpanning,
|
|
119
|
+
// minus a Wordgard bug: docs/wordgard-upstream-bugs.md).
|
|
120
|
+
const openWrapper = (views, { elt, spanning, mark }) => {
|
|
121
|
+
let i = views.length - 1;
|
|
122
|
+
while (isPoint(views[i]))
|
|
123
|
+
i--;
|
|
124
|
+
const prev = views[i];
|
|
125
|
+
if (spanning &&
|
|
126
|
+
prev?.type == "wrapper" &&
|
|
127
|
+
prev.spanning &&
|
|
128
|
+
prev.elt.eq(elt) &&
|
|
129
|
+
sameMark(prev.mark, mark)) {
|
|
130
|
+
prev.children.push(...views.splice(i + 1));
|
|
131
|
+
return prev;
|
|
132
|
+
}
|
|
133
|
+
const wrapper = { type: "wrapper", elt, spanning, children: [] };
|
|
134
|
+
if (mark)
|
|
135
|
+
wrapper.mark = mark;
|
|
136
|
+
views.push(wrapper);
|
|
137
|
+
return wrapper;
|
|
138
|
+
};
|
|
139
|
+
// Adjacent text in the same wrapper is one text node (ContentUpdate.addText).
|
|
140
|
+
const addText = (views, text) => {
|
|
141
|
+
const last = views.at(-1);
|
|
142
|
+
if (last?.type == "text")
|
|
143
|
+
last.text += text;
|
|
144
|
+
else
|
|
145
|
+
views.push({ type: "text", text });
|
|
146
|
+
};
|
|
147
|
+
// ensureHackNode: a textblock or inline plot needs a trailing <br> when it ends,
|
|
148
|
+
// looking inside wrappers and past widgets out of the flow, in nothing or in a
|
|
149
|
+
// <br>. Undefined when `views` holds nothing to decide on. Unlike Wordgard, a
|
|
150
|
+
// widget rendering a <br> counts as content: its DOM doesn't exist yet.
|
|
151
|
+
const needsTrailingHack = (views) => {
|
|
152
|
+
for (let i = views.length - 1; i >= 0; i--) {
|
|
153
|
+
const view = views[i];
|
|
154
|
+
if (!view)
|
|
155
|
+
continue;
|
|
156
|
+
if (view.type == "wrapper") {
|
|
157
|
+
const found = needsTrailingHack(view.children);
|
|
158
|
+
if (found !== undefined)
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
else if (view.type == "widget") {
|
|
162
|
+
if (view.widget.type.inFlow)
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
else if (view.type == "node") {
|
|
166
|
+
const { shape, children } = view;
|
|
167
|
+
if (!(shape instanceof Elt)) {
|
|
168
|
+
if (shape.type.inFlow)
|
|
169
|
+
return false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!children)
|
|
173
|
+
return shape.tagName.toLowerCase() == "br";
|
|
174
|
+
// Structure around a plot's content is skipped, so the plot decides only
|
|
175
|
+
// when the hole is a direct child (its content, an inline plot's, ends in
|
|
176
|
+
// a buffer); otherwise the scan goes on before it.
|
|
177
|
+
if (shape.children.includes(0)) {
|
|
178
|
+
const found = needsTrailingHack(children);
|
|
179
|
+
if (found !== undefined)
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
};
|
|
189
|
+
// Whether views laid out with `head` and `tail` views of a flat layout kept
|
|
190
|
+
// are flat.
|
|
191
|
+
const isFlat = (plot, views, head, tail) => {
|
|
192
|
+
if (plot.isTextblock || plot.isInline || views.length != plot.content.length)
|
|
193
|
+
return false;
|
|
194
|
+
for (let i = head; i < views.length - tail; i++)
|
|
195
|
+
if (views[i]?.type != "node")
|
|
196
|
+
return false;
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
// The keys of a plot's React point widgets, by position. Each keeps the key of
|
|
200
|
+
// an old one its position maps to, as Wordgard keeps an old widget there
|
|
201
|
+
// (TilePointer.matchingWidget): an equal one on the same side, else one of the
|
|
202
|
+
// same type, which React re-renders in place.
|
|
203
|
+
const pointKeys = (context, old, oldStart) => {
|
|
204
|
+
const byPos = new Map();
|
|
205
|
+
if (old && oldStart !== null && context.changes) {
|
|
206
|
+
const map = pointMapper(context.changes);
|
|
207
|
+
const collect = (views) => {
|
|
208
|
+
for (const view of views) {
|
|
209
|
+
if (view.type == "wrapper")
|
|
210
|
+
collect(view.children);
|
|
211
|
+
else if (view.type == "widget" && view.offset !== undefined)
|
|
212
|
+
for (const pos of map(oldStart + view.offset)) {
|
|
213
|
+
const found = byPos.get(pos);
|
|
214
|
+
if (found)
|
|
215
|
+
found.push(view);
|
|
216
|
+
else
|
|
217
|
+
byPos.set(pos, [view]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
collect(old.views);
|
|
222
|
+
}
|
|
223
|
+
const used = new Set();
|
|
224
|
+
return (pos, widget, side) => {
|
|
225
|
+
const candidates = (byPos.get(pos) ?? []).filter((view) => Math.sign(view.side) == Math.sign(side) && view.key && !used.has(view.key));
|
|
226
|
+
const found = candidates.find((view) => view.widget.eq(widget)) ??
|
|
227
|
+
candidates.find((view) => view.widget.type == widget.type);
|
|
228
|
+
const key = found?.key ?? createNodeKey();
|
|
229
|
+
used.add(key);
|
|
230
|
+
return key;
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
const sources = (wrappers) => wrappers.map((src) => wrapperView(renderWrapper(src), spanningOf(src), src instanceof Mark ? src : undefined));
|
|
234
|
+
// A plot's content, from position `start`, for plots whose shape has a hole.
|
|
235
|
+
export const iterDeco = (context, plot, start, parent = { start: 0, oldStart: 0 }) => {
|
|
236
|
+
const end = start + plot.contentLength;
|
|
237
|
+
const oldStartOf = (layout) => layout && parent.oldStart !== null ? parent.oldStart + layout.offset : null;
|
|
238
|
+
// Kept only from the same place: a plot object can appear more than once.
|
|
239
|
+
const fromLast = (layout) => layout?.pass == context.pass ? undefined : layout;
|
|
240
|
+
const previous = fromLast(context.layouts.get(plot));
|
|
241
|
+
const previousStart = oldStartOf(previous);
|
|
242
|
+
if (previous &&
|
|
243
|
+
context.changes &&
|
|
244
|
+
previousStart !== null &&
|
|
245
|
+
samePlace(context.changes, previousStart, start) &&
|
|
246
|
+
!context.composing.has(plot) &&
|
|
247
|
+
isKept(context.changes, start, end)) {
|
|
248
|
+
previous.offset = start - parent.start;
|
|
249
|
+
previous.pass = context.pass;
|
|
250
|
+
return previous.views;
|
|
251
|
+
}
|
|
252
|
+
// The layout this plot replaces, and where it started.
|
|
253
|
+
const oldPlot = context.replaced.get(plot) ?? plot;
|
|
254
|
+
const old = fromLast(context.layouts.get(oldPlot));
|
|
255
|
+
const oldStart = oldStartOf(old);
|
|
256
|
+
const nodeKeys = context.store.keys.get(plot);
|
|
257
|
+
if (!nodeKeys)
|
|
258
|
+
throw new Error(`No keys for ${plot.name}`);
|
|
259
|
+
// A flat plot's unchanged ends keep their views; the walk covers the rest.
|
|
260
|
+
let head = 0;
|
|
261
|
+
let tail = 0;
|
|
262
|
+
// A plot object can appear more than once: only its layout from this place.
|
|
263
|
+
if (old?.flat &&
|
|
264
|
+
oldStart !== null &&
|
|
265
|
+
context.changes &&
|
|
266
|
+
samePlace(context.changes, oldStart, start) &&
|
|
267
|
+
!context.composing.has(plot))
|
|
268
|
+
({ head, tail } = unchangedEnds(context.changes, oldPlot, oldStart, plot));
|
|
269
|
+
const content = plot.content;
|
|
270
|
+
const middle = content.length - tail;
|
|
271
|
+
let from = start;
|
|
272
|
+
for (let i = 0; i < head; i++)
|
|
273
|
+
from += content[i]?.length ?? 0;
|
|
274
|
+
let to = end;
|
|
275
|
+
for (let i = middle; i < content.length; i++)
|
|
276
|
+
to -= content[i]?.length ?? 0;
|
|
277
|
+
const views = old && head ? old.views.slice(0, head) : [];
|
|
278
|
+
const plots = [];
|
|
279
|
+
// Where the walk is, for point widgets.
|
|
280
|
+
let at = start;
|
|
281
|
+
let keyAt = null;
|
|
282
|
+
const indexAt = new Map();
|
|
283
|
+
for (let index = head, offset = from; index < middle; index++) {
|
|
284
|
+
indexAt.set(offset, index);
|
|
285
|
+
offset += content[index]?.length ?? 0;
|
|
286
|
+
}
|
|
287
|
+
const wrapped = (wrappers) => {
|
|
288
|
+
let target = views;
|
|
289
|
+
for (const view of wrappers) {
|
|
290
|
+
const wrapper = openWrapper(target, view);
|
|
291
|
+
if (view.key)
|
|
292
|
+
wrapper.key = view.key;
|
|
293
|
+
target = wrapper.children;
|
|
294
|
+
}
|
|
295
|
+
return target;
|
|
296
|
+
};
|
|
297
|
+
const nodeView = (node, shape, pos) => {
|
|
298
|
+
const key = nodeKeys[indexAt.get(pos) ?? -1];
|
|
299
|
+
if (key === undefined)
|
|
300
|
+
throw new Error(`No key for ${node.name}`);
|
|
301
|
+
return { type: "node", node, key, shape, children: null };
|
|
302
|
+
};
|
|
303
|
+
const walker = {
|
|
304
|
+
enter: (node, shape, wrappers, pos) => {
|
|
305
|
+
const view = nodeView(node, shape, pos);
|
|
306
|
+
wrapped(sources(wrappers)).push(view);
|
|
307
|
+
plots.push({ view, plot: node, pos });
|
|
308
|
+
at = pos + node.length;
|
|
309
|
+
return false;
|
|
310
|
+
},
|
|
311
|
+
leave: () => { },
|
|
312
|
+
node: (node, shape, wrappers, partial, pos) => {
|
|
313
|
+
if (partial !== undefined)
|
|
314
|
+
throw new Error(`react-wordgard can't render part of ${node.name}`);
|
|
315
|
+
at = pos + node.length;
|
|
316
|
+
// Wordgard 0.6.2 can leave empty text leaves; React draws nothing for them
|
|
317
|
+
// (docs/wordgard-upstream-bugs.md).
|
|
318
|
+
if (node.is(Leaf.Text) && !node.param)
|
|
319
|
+
return;
|
|
320
|
+
let target = wrapped(sources(wrappers));
|
|
321
|
+
if (!node.is(Leaf.Text)) {
|
|
322
|
+
target.push(nodeView(node, shape, pos));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// A text node's attributes and wrappers become spanning wrappers.
|
|
326
|
+
while (shape instanceof Elt) {
|
|
327
|
+
if (isReactWrapper(shape))
|
|
328
|
+
throw new Error("A React wrapper can't wrap text");
|
|
329
|
+
const elt = Elt.create(shape.tagName, shape.attrs, [0]);
|
|
330
|
+
target = openWrapper(target, { elt, spanning: true }).children;
|
|
331
|
+
const [inner] = shape.children;
|
|
332
|
+
if (inner === undefined || inner === 0 || typeof inner == "string")
|
|
333
|
+
throw new Error("Unexpected text shape");
|
|
334
|
+
shape = inner;
|
|
335
|
+
}
|
|
336
|
+
addText(target, node.param);
|
|
337
|
+
},
|
|
338
|
+
nodePart: (node) => {
|
|
339
|
+
throw new Error(`react-wordgard can't render part of ${node.name}`);
|
|
340
|
+
},
|
|
341
|
+
widget: (widget, side, point) => {
|
|
342
|
+
if (widget == Widget.img)
|
|
343
|
+
views.push({ type: "buffer", side: side < 0 ? -1 : 1 });
|
|
344
|
+
else if (point && isReactWidget(widget)) {
|
|
345
|
+
keyAt ??= pointKeys(context, old, oldStart);
|
|
346
|
+
views.push({
|
|
347
|
+
type: "widget",
|
|
348
|
+
widget,
|
|
349
|
+
side,
|
|
350
|
+
key: keyAt(at, widget, side),
|
|
351
|
+
offset: at - start,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
views.push({ type: "widget", widget, side });
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
const { composition } = context;
|
|
359
|
+
if (composition?.plot == plot) {
|
|
360
|
+
const from = start + composition.layout.from;
|
|
361
|
+
context.deco.walk(start, true, from, walker);
|
|
362
|
+
const target = wrapped(composition.layout.wrappers);
|
|
363
|
+
if (composition.layout.cursor)
|
|
364
|
+
target.push({ type: "buffer", side: -1 });
|
|
365
|
+
else
|
|
366
|
+
target.push({
|
|
367
|
+
type: "composition",
|
|
368
|
+
key: composition.rendered?.key ?? "composition",
|
|
369
|
+
rendered: composition.rendered?.text ?? null,
|
|
370
|
+
});
|
|
371
|
+
at = start + composition.layout.to;
|
|
372
|
+
context.deco.walk(start + composition.layout.to, false, end, walker);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
context.deco.walk(from, true, to, walker);
|
|
376
|
+
}
|
|
377
|
+
for (const { view, plot, pos } of plots)
|
|
378
|
+
view.children = iterDeco(context, plot, pos + 1, { start, oldStart });
|
|
379
|
+
if (old && tail) {
|
|
380
|
+
views.push(...old.views.slice(old.views.length - tail));
|
|
381
|
+
// The layouts inside moved with their plots.
|
|
382
|
+
for (let i = middle, pos = to; i < content.length; i++) {
|
|
383
|
+
const child = content[i];
|
|
384
|
+
if (!child)
|
|
385
|
+
continue;
|
|
386
|
+
const layout = child.isPlot ? context.layouts.get(child) : undefined;
|
|
387
|
+
if (layout) {
|
|
388
|
+
layout.offset = pos + 1 - start;
|
|
389
|
+
layout.pass = context.pass;
|
|
390
|
+
}
|
|
391
|
+
pos += child.length;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if ((plot.isTextblock || plot.isInline) && needsTrailingHack(views) !== false)
|
|
395
|
+
views.push({ type: "trailingHack" });
|
|
396
|
+
// From this place only: two copies of a plot mustn't share view objects.
|
|
397
|
+
const fromHere = old && oldStart !== null && context.changes && samePlace(context.changes, oldStart, start);
|
|
398
|
+
const laid = old && fromHere ? keepSame(context.store, old.views, views, head, tail) : views;
|
|
399
|
+
context.layouts.set(plot, {
|
|
400
|
+
offset: start - parent.start,
|
|
401
|
+
pass: context.pass,
|
|
402
|
+
views: laid,
|
|
403
|
+
flat: isFlat(plot, laid, head, tail),
|
|
404
|
+
});
|
|
405
|
+
return laid;
|
|
406
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Wordgard's attributes are a flat [name, value, ...] array.
|
|
2
|
+
export const attrsToRecord = (attrs) => {
|
|
3
|
+
const record = {};
|
|
4
|
+
for (let i = 0; i + 1 < attrs.length; i += 2) {
|
|
5
|
+
const name = attrs[i];
|
|
6
|
+
const value = attrs[i + 1];
|
|
7
|
+
if (name !== undefined && value !== undefined)
|
|
8
|
+
record[name] = value;
|
|
9
|
+
}
|
|
10
|
+
return record;
|
|
11
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CSSProperties, HTMLProps } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Given a record of HTML attributes, returns tho
|
|
4
|
+
* equivalent React props.
|
|
5
|
+
*/
|
|
6
|
+
export declare const htmlAttrsToReactProps: (attrs: Record<string, string>) => HTMLProps<HTMLElement>;
|
|
7
|
+
export declare const cssToStyle: (css: string | undefined) => CSSProperties;
|
|
8
|
+
export declare const componentProps: (attrs: Record<string, string>) => HTMLProps<HTMLElement>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Adapted from react-prosemirror, for Wordgard: `draggable`, `hidden`,
|
|
2
|
+
// `colspan` and `rowspan` render as Wordgard's own setAttribute does (shape
|
|
3
|
+
// attributes, such as an image's `draggable="false"`, go through here).
|
|
4
|
+
// mergeReactProps is left out: nothing uses it.
|
|
5
|
+
let patched = false;
|
|
6
|
+
const patchConsoleError = () => {
|
|
7
|
+
if (patched)
|
|
8
|
+
return;
|
|
9
|
+
const consoleError = console.error;
|
|
10
|
+
console.error = (...data) => {
|
|
11
|
+
const [message, prop, correction] = data;
|
|
12
|
+
if (typeof message === "string" &&
|
|
13
|
+
message.includes("Invalid DOM property `%s`. Did you mean `%s`?") &&
|
|
14
|
+
prop === "STYLE" &&
|
|
15
|
+
correction === "style") {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
consoleError(...data);
|
|
19
|
+
};
|
|
20
|
+
patched = true;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Given a record of HTML attributes, returns tho
|
|
24
|
+
* equivalent React props.
|
|
25
|
+
*/
|
|
26
|
+
export const htmlAttrsToReactProps = (attrs) => {
|
|
27
|
+
patchConsoleError();
|
|
28
|
+
const props = {};
|
|
29
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
30
|
+
switch (attrName.toLowerCase()) {
|
|
31
|
+
case "class": {
|
|
32
|
+
props.className = attrValue;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case "style": {
|
|
36
|
+
// HACK: React expects the `style` prop to be an
|
|
37
|
+
// object mapping from CSS property name to value.
|
|
38
|
+
// However, it will pass un-recognized props through
|
|
39
|
+
// to the underlying DOM element, and HTML attributes
|
|
40
|
+
// are case insensitive. So we use `STYLE` instead,
|
|
41
|
+
// which React doesn't intercept, but the DOM treats
|
|
42
|
+
// as `style`
|
|
43
|
+
props.STYLE = attrValue;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "autocapitalize": {
|
|
47
|
+
props.autoCapitalize = attrValue;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case "contenteditable": {
|
|
51
|
+
if (attrValue === "" || attrValue === "true") {
|
|
52
|
+
props.contentEditable = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (attrValue === "false") {
|
|
56
|
+
props.contentEditable = false;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (attrValue === "plaintext-only") {
|
|
60
|
+
props.contentEditable = "plaintext-only";
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
props.contentEditable = null;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "draggable": {
|
|
67
|
+
props.draggable = attrValue !== "false";
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "enterkeyhint": {
|
|
71
|
+
props.enterKeyHint = attrValue;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "for": {
|
|
75
|
+
props.htmlFor = attrValue;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "hidden": {
|
|
79
|
+
props.hidden = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "inputmode": {
|
|
83
|
+
props.inputMode = attrValue;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "itemprop": {
|
|
87
|
+
props.itemProp = attrValue;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "spellcheck": {
|
|
91
|
+
if (attrValue === "" || attrValue === "true") {
|
|
92
|
+
props.spellCheck = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (attrValue === "false") {
|
|
96
|
+
props.spellCheck = false;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
props.spellCheck = null;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "colspan":
|
|
103
|
+
case "rowspan": {
|
|
104
|
+
const numValue = parseInt(attrValue, 10);
|
|
105
|
+
if (!Number.isNaN(numValue)) {
|
|
106
|
+
props[attrName.toLowerCase() == "colspan" ? "colSpan" : "rowSpan"] = numValue;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "tabindex": {
|
|
111
|
+
const numValue = parseInt(attrValue, 10);
|
|
112
|
+
if (!Number.isNaN(numValue)) {
|
|
113
|
+
props.tabIndex = numValue;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "autocomplete": {
|
|
118
|
+
props.autoComplete = attrValue;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "autofocus": {
|
|
122
|
+
props.autoFocus = attrValue != null;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "formaction": {
|
|
126
|
+
props.formAction = attrValue;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "formenctype": {
|
|
130
|
+
props.formEnctype = attrValue;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case "formmethod": {
|
|
134
|
+
props.formMethod = attrValue;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "formnovalidate": {
|
|
138
|
+
props.formNoValidate = attrValue;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "formtarget": {
|
|
142
|
+
props.formTarget = attrValue;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "maxlength": {
|
|
146
|
+
const numValue = parseInt(attrValue, 10);
|
|
147
|
+
if (!Number.isNaN(numValue)) {
|
|
148
|
+
props.maxLength = attrValue;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "minlength": {
|
|
153
|
+
const numValue = parseInt(attrValue, 10);
|
|
154
|
+
if (!Number.isNaN(numValue)) {
|
|
155
|
+
props.minLength = attrValue;
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "max": {
|
|
160
|
+
const numValue = parseInt(attrValue, 10);
|
|
161
|
+
if (!Number.isNaN(numValue)) {
|
|
162
|
+
props.max = attrValue;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "min": {
|
|
167
|
+
const numValue = parseInt(attrValue, 10);
|
|
168
|
+
if (!Number.isNaN(numValue)) {
|
|
169
|
+
props.min = attrValue;
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "multiple": {
|
|
174
|
+
props.multiple = attrValue != null;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "readonly": {
|
|
178
|
+
props.readOnly = attrValue != null;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "required": {
|
|
182
|
+
props.required = attrValue != null;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "size": {
|
|
186
|
+
const numValue = parseInt(attrValue, 10);
|
|
187
|
+
if (!Number.isNaN(numValue)) {
|
|
188
|
+
props.size = attrValue;
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "step": {
|
|
193
|
+
if (attrValue === "any") {
|
|
194
|
+
props.step = attrValue;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const numValue = parseInt(attrValue, 10);
|
|
198
|
+
if (!Number.isNaN(numValue) && numValue > 0) {
|
|
199
|
+
props.step = attrValue;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "disabled": {
|
|
204
|
+
props.disabled = attrValue != null;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "rows": {
|
|
208
|
+
const numValue = parseInt(attrValue, 10);
|
|
209
|
+
if (!Number.isNaN(numValue)) {
|
|
210
|
+
props.rows = attrValue;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
default: {
|
|
215
|
+
props[attrName] = attrValue;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return props;
|
|
221
|
+
};
|
|
222
|
+
// A style attribute as React's style object, for merging with a component's:
|
|
223
|
+
// `background-color` as `backgroundColor`, custom properties as they are.
|
|
224
|
+
export const cssToStyle = (css) => {
|
|
225
|
+
const style = {};
|
|
226
|
+
for (const declaration of css?.split(";") ?? []) {
|
|
227
|
+
const colon = declaration.indexOf(":");
|
|
228
|
+
if (colon < 0)
|
|
229
|
+
continue;
|
|
230
|
+
const name = declaration.slice(0, colon).trim();
|
|
231
|
+
const value = declaration.slice(colon + 1).trim();
|
|
232
|
+
if (!name || !value)
|
|
233
|
+
continue;
|
|
234
|
+
const key = name.startsWith("--")
|
|
235
|
+
? name
|
|
236
|
+
: name.replace(/^-ms-/, "ms-").replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
237
|
+
style[key] = value;
|
|
238
|
+
}
|
|
239
|
+
return style;
|
|
240
|
+
};
|
|
241
|
+
// Not in react-prosemirror: the props a React decoration's component gets for
|
|
242
|
+
// a root it draws by hand, Wordgard's style as an object it can merge with its
|
|
243
|
+
// own (`STYLE`, a string, would overwrite its `style` when Wordgard's changes).
|
|
244
|
+
export const componentProps = (attrs) => {
|
|
245
|
+
const { style, ...rest } = attrs;
|
|
246
|
+
const props = htmlAttrsToReactProps(rest);
|
|
247
|
+
return style ? { ...props, style: cssToStyle(style) } : props;
|
|
248
|
+
};
|