@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,95 @@
|
|
|
1
|
+
// The DOM events useEditorEventListener can listen to, and the handlers
|
|
2
|
+
// ReactWordgard installs for them. Wordgard has a handler per event type, so
|
|
3
|
+
// the types are fixed when the editor is created. Adding a handler makes
|
|
4
|
+
// Wordgard's listener for the type non-passive: the events that block
|
|
5
|
+
// scrolling (touch, wheel) aren't among them, nor the most frequent ones
|
|
6
|
+
// (mousemove, pointer events), each of which would then walk the tiles.
|
|
7
|
+
import { Wordgard } from "wordgard/editor";
|
|
8
|
+
import { GardState } from "wordgard/state";
|
|
9
|
+
export const editorEvents = [
|
|
10
|
+
"keydown",
|
|
11
|
+
"keyup",
|
|
12
|
+
"keypress",
|
|
13
|
+
"beforeinput",
|
|
14
|
+
"input",
|
|
15
|
+
"mousedown",
|
|
16
|
+
"mouseup",
|
|
17
|
+
"mouseover",
|
|
18
|
+
"mouseout",
|
|
19
|
+
"click",
|
|
20
|
+
"dblclick",
|
|
21
|
+
"contextmenu",
|
|
22
|
+
"focus",
|
|
23
|
+
"blur",
|
|
24
|
+
"copy",
|
|
25
|
+
"cut",
|
|
26
|
+
"paste",
|
|
27
|
+
"dragstart",
|
|
28
|
+
"dragend",
|
|
29
|
+
"dragenter",
|
|
30
|
+
"dragover",
|
|
31
|
+
"dragleave",
|
|
32
|
+
"drop",
|
|
33
|
+
"compositionstart",
|
|
34
|
+
"compositionupdate",
|
|
35
|
+
"compositionend",
|
|
36
|
+
];
|
|
37
|
+
// Every type's list made at once: the type checks none is missing, and they're
|
|
38
|
+
// then only read.
|
|
39
|
+
const createListeners = () => ({
|
|
40
|
+
keydown: [],
|
|
41
|
+
keyup: [],
|
|
42
|
+
keypress: [],
|
|
43
|
+
beforeinput: [],
|
|
44
|
+
input: [],
|
|
45
|
+
mousedown: [],
|
|
46
|
+
mouseup: [],
|
|
47
|
+
mouseover: [],
|
|
48
|
+
mouseout: [],
|
|
49
|
+
click: [],
|
|
50
|
+
dblclick: [],
|
|
51
|
+
contextmenu: [],
|
|
52
|
+
focus: [],
|
|
53
|
+
blur: [],
|
|
54
|
+
copy: [],
|
|
55
|
+
cut: [],
|
|
56
|
+
paste: [],
|
|
57
|
+
dragstart: [],
|
|
58
|
+
dragend: [],
|
|
59
|
+
dragenter: [],
|
|
60
|
+
dragover: [],
|
|
61
|
+
dragleave: [],
|
|
62
|
+
drop: [],
|
|
63
|
+
compositionstart: [],
|
|
64
|
+
compositionupdate: [],
|
|
65
|
+
compositionend: [],
|
|
66
|
+
});
|
|
67
|
+
const registries = new WeakMap();
|
|
68
|
+
export const listenersOf = (wg, type) => {
|
|
69
|
+
let registry = registries.get(wg);
|
|
70
|
+
if (!registry) {
|
|
71
|
+
registry = createListeners();
|
|
72
|
+
registries.set(wg, registry);
|
|
73
|
+
}
|
|
74
|
+
return registry[type];
|
|
75
|
+
};
|
|
76
|
+
const handler = (type) => Wordgard.domEventHandler(type, (event, wg) => {
|
|
77
|
+
// A copy: a listener may unregister one.
|
|
78
|
+
for (const listener of [...listenersOf(wg, type)])
|
|
79
|
+
if (listener(event) || event.defaultPrevented)
|
|
80
|
+
return true;
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
83
|
+
// Whether a state has the handlers: `GardState.reconfigure` drops them.
|
|
84
|
+
const installed = GardState.Facet.define({
|
|
85
|
+
combine: (values) => values.length > 0,
|
|
86
|
+
});
|
|
87
|
+
export const hasEditorEventHandlers = (state) => state.facet(installed);
|
|
88
|
+
// Wordgard redoes its handlers only when its plugins change: this one makes
|
|
89
|
+
// adding or dropping the handlers such a change.
|
|
90
|
+
const refresh = Wordgard.Plugin.define(() => ({}));
|
|
91
|
+
export const editorEventHandlers = GardState.prec.highest([
|
|
92
|
+
editorEvents.map(handler),
|
|
93
|
+
installed.of(true),
|
|
94
|
+
refresh,
|
|
95
|
+
]);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Mark, type Node, type Plot, type Pos } from "wordgard/doc";
|
|
2
|
+
import { Decoration, Widget } from "wordgard/editor";
|
|
3
|
+
import { type Set } from "wordgard/set";
|
|
4
|
+
import type { GardState } from "wordgard/state";
|
|
5
|
+
import { type DecoElt, type DecoSet, type TagAttribute, type TagWidget, type TagWrapper, type WrapperRangeDecoration } from "../wordgardInternals.js";
|
|
6
|
+
declare const enum WidgetPlace {
|
|
7
|
+
Before = 0,
|
|
8
|
+
After = 1,
|
|
9
|
+
Start = 2,
|
|
10
|
+
End = 3
|
|
11
|
+
}
|
|
12
|
+
export type WrapperSource = Mark | WrapperRangeDecoration;
|
|
13
|
+
export interface DecoWalker {
|
|
14
|
+
enter(node: Plot, shape: DecoElt, wrappers: readonly WrapperSource[], pos: number): boolean;
|
|
15
|
+
leave(): void;
|
|
16
|
+
node(node: Node, shape: Decoration.Shape, wrappers: readonly WrapperSource[], partial: number | undefined, pos: number): void;
|
|
17
|
+
nodePart(node: Node, length: number, done: boolean): void;
|
|
18
|
+
widget(widget: Widget, side: number, point: boolean): void;
|
|
19
|
+
}
|
|
20
|
+
export declare const getDecoSet: (state: GardState) => DecoSet;
|
|
21
|
+
export declare const renderMarkWrapper: (arg: Mark<unknown>) => DecoElt;
|
|
22
|
+
export declare const documentMark: (mark: Mark) => Mark<unknown>;
|
|
23
|
+
export declare const renderWrapper: (src: WrapperSource) => DecoElt;
|
|
24
|
+
export declare class DecoIterator {
|
|
25
|
+
readonly state: GardState;
|
|
26
|
+
readonly decoSet: DecoSet;
|
|
27
|
+
tagShapes: Map<Node.Type, (tag: Node.Tag) => Decoration.Shape>;
|
|
28
|
+
globalWidgets: readonly TagWidget[];
|
|
29
|
+
globalWrappers: readonly TagWrapper[];
|
|
30
|
+
globalAttrs: readonly TagAttribute[];
|
|
31
|
+
markShapes: Map<Mark.Type, Mark.Type>;
|
|
32
|
+
pos: Pos;
|
|
33
|
+
rangeCursor: Set.Cursor<Decoration.Range>;
|
|
34
|
+
pointSets: readonly Decoration.Point.Set[];
|
|
35
|
+
pointCursor: Set.Cursor<Decoration.Point>;
|
|
36
|
+
constructor(state: GardState, decoSet: DecoSet);
|
|
37
|
+
widgets(tag: Node.Tag, place: WidgetPlace, walker: DecoWalker): void;
|
|
38
|
+
nodeWrappers(tag: Node.Tag, active: readonly Decoration.Range[], atom: boolean): readonly WrapperSource[];
|
|
39
|
+
walk(from: number, inclusiveStart: boolean, to: number, walker: DecoWalker): void;
|
|
40
|
+
tagShape(tag: Node.Tag, active: readonly Decoration.Range[]): Decoration.Shape;
|
|
41
|
+
}
|
|
42
|
+
export declare const spanningOf: (src: WrapperSource) => boolean;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// Copied from Wordgard 0.6.2 (src/editor/decoration.ts in its repo: DecoIterator,
|
|
2
|
+
// SpanIterator, getDecoSet and the shape helpers), which doesn't export them.
|
|
3
|
+
// Changes, so React can lay out one plot at a time:
|
|
4
|
+
// - `walker.enter` says whether to descend. When it doesn't, the walk goes
|
|
5
|
+
// past the plot as a whole, emitting its `after` widgets once past.
|
|
6
|
+
// - The walker gets each node's position, and is told which widgets come from
|
|
7
|
+
// point decorations.
|
|
8
|
+
// - A walk can start before the previous one ended.
|
|
9
|
+
// - Decoration sets come from the DecoSet, not from calling sources again, and
|
|
10
|
+
// range sets are split into layers a cursor can seek into.
|
|
11
|
+
// - `nodeWrappers` without wrappers returns a shared empty array, as upstream.
|
|
12
|
+
// - It records the document's mark behind a mark of an overridden shape
|
|
13
|
+
// (`documentMark`), for React marks.
|
|
14
|
+
// - The element it wraps a React widget in to add attributes is marked
|
|
15
|
+
// (`attributeWidget`): the view draws the widget with them as props.
|
|
16
|
+
// The internal classes and facets come from src/wordgardInternals.ts.
|
|
17
|
+
import { Attributes, Elt, Leaf } from "wordgard/doc";
|
|
18
|
+
import { Decoration, Widget } from "wordgard/editor";
|
|
19
|
+
import { PointSet, RangeSet } from "wordgard/set";
|
|
20
|
+
import { getDecorationInternals, } from "../wordgardInternals.js";
|
|
21
|
+
import { attributeWidget } from "./ReactWidgetType.js";
|
|
22
|
+
var DecorationScope;
|
|
23
|
+
(function (DecorationScope) {
|
|
24
|
+
DecorationScope[DecorationScope["Atom"] = 1] = "Atom";
|
|
25
|
+
DecorationScope[DecorationScope["InlineAtom"] = 2] = "InlineAtom";
|
|
26
|
+
DecorationScope[DecorationScope["All"] = 4] = "All";
|
|
27
|
+
})(DecorationScope || (DecorationScope = {}));
|
|
28
|
+
var WidgetPlace;
|
|
29
|
+
(function (WidgetPlace) {
|
|
30
|
+
WidgetPlace[WidgetPlace["Before"] = 0] = "Before";
|
|
31
|
+
WidgetPlace[WidgetPlace["After"] = 1] = "After";
|
|
32
|
+
WidgetPlace[WidgetPlace["Start"] = 2] = "Start";
|
|
33
|
+
WidgetPlace[WidgetPlace["End"] = 3] = "End";
|
|
34
|
+
})(WidgetPlace || (WidgetPlace = {}));
|
|
35
|
+
var Side;
|
|
36
|
+
(function (Side) {
|
|
37
|
+
Side[Side["After"] = 1000000000] = "After";
|
|
38
|
+
})(Side || (Side = {}));
|
|
39
|
+
export const getDecoSet = (state) => {
|
|
40
|
+
const set = { points: new Map(), ranges: new Map() };
|
|
41
|
+
for (const src of state.facet(Decoration.Point.source))
|
|
42
|
+
set.points.set(src, src(state));
|
|
43
|
+
for (const src of state.facet(Decoration.Range.source))
|
|
44
|
+
set.ranges.set(src, src(state));
|
|
45
|
+
return set;
|
|
46
|
+
};
|
|
47
|
+
const memo = (f) => {
|
|
48
|
+
const map = new WeakMap();
|
|
49
|
+
return (arg) => {
|
|
50
|
+
let found = map.get(arg);
|
|
51
|
+
if (found === undefined)
|
|
52
|
+
map.set(arg, (found = f(arg)));
|
|
53
|
+
return found;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const addMarkAttributes = (overrides, shape, tag) => {
|
|
57
|
+
let attrs;
|
|
58
|
+
for (const mark of tag.marks) {
|
|
59
|
+
const type = overrides.get(mark.type) || mark.type;
|
|
60
|
+
if (type.attribute && (mark.type.spanning || !tag.isText)) {
|
|
61
|
+
const { get, target } = type.attribute;
|
|
62
|
+
const markAttrs = get(mark.value);
|
|
63
|
+
if (markAttrs.length) {
|
|
64
|
+
if (target && shape instanceof Elt)
|
|
65
|
+
shape = shape.addAttrs(markAttrs, target);
|
|
66
|
+
else
|
|
67
|
+
attrs = attrs ? Attributes.merge(attrs, markAttrs) : markAttrs;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return attrs ? addAttrs(shape, attrs, tag.type.isInline) : shape;
|
|
72
|
+
};
|
|
73
|
+
const addAttrs = (shape, attrs, inline) => shape instanceof Elt
|
|
74
|
+
? shape.addAttrs(attrs)
|
|
75
|
+
: attributeWidget(shape, attrs, inline ? "span" : "div");
|
|
76
|
+
const applyDeco = (shape, deco, tag) => {
|
|
77
|
+
const { AttributeDecoration, WrapperDecoration } = getDecorationInternals();
|
|
78
|
+
if (deco instanceof AttributeDecoration) {
|
|
79
|
+
return deco.selector && shape instanceof Elt
|
|
80
|
+
? shape.addAttrs(deco.attrs, deco.selector)
|
|
81
|
+
: addAttrs(shape, deco.attrs, tag.type.isInline);
|
|
82
|
+
}
|
|
83
|
+
else if (deco instanceof WrapperDecoration) {
|
|
84
|
+
return deco.selector && shape instanceof Elt
|
|
85
|
+
? shape.wrap(deco.elt, deco.selector)
|
|
86
|
+
: deco.elt.fill([shape]);
|
|
87
|
+
}
|
|
88
|
+
return shape;
|
|
89
|
+
};
|
|
90
|
+
const baseTagShape = memo((tag) => tag.is(Leaf.Text) ? Widget.editableText.of(tag.param) : tag.type.shape.create(tag.param));
|
|
91
|
+
class SpanIterator {
|
|
92
|
+
ranges;
|
|
93
|
+
points;
|
|
94
|
+
end;
|
|
95
|
+
active = [];
|
|
96
|
+
activeEnd = [];
|
|
97
|
+
from;
|
|
98
|
+
to;
|
|
99
|
+
point = null;
|
|
100
|
+
pointSource = null;
|
|
101
|
+
done = false;
|
|
102
|
+
constructor(ranges, points, start, end) {
|
|
103
|
+
this.ranges = ranges;
|
|
104
|
+
this.points = points;
|
|
105
|
+
this.end = end;
|
|
106
|
+
this.from = this.to = start;
|
|
107
|
+
}
|
|
108
|
+
next() {
|
|
109
|
+
if (this.done)
|
|
110
|
+
return this;
|
|
111
|
+
if (this.point)
|
|
112
|
+
this.point = null;
|
|
113
|
+
const { ranges, points, active, activeEnd } = this;
|
|
114
|
+
for (;;) {
|
|
115
|
+
const [startPos, startSide] = ranges.value
|
|
116
|
+
? [ranges.from, ranges.value.inclusiveStart ? -1 : 1]
|
|
117
|
+
: [1e9, 0];
|
|
118
|
+
let endPos = 1e9;
|
|
119
|
+
let endSide = 0;
|
|
120
|
+
let nextActive = -1;
|
|
121
|
+
for (let i = 0; i < active.length; i++) {
|
|
122
|
+
const pos = activeEnd[i] ?? 0;
|
|
123
|
+
const side = active[i]?.inclusiveEnd ? 1 : -1;
|
|
124
|
+
if ((pos - endPos || side - endSide) < 0) {
|
|
125
|
+
endPos = pos;
|
|
126
|
+
endSide = side;
|
|
127
|
+
nextActive = i;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const [pointPos, pointSide] = points.value ? [points.from, points.value.side] : [1e9, 1];
|
|
131
|
+
const nextPos = Math.min(startPos, endPos, pointPos);
|
|
132
|
+
if (this.to == this.end && nextPos > this.to) {
|
|
133
|
+
this.done = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
else if (nextPos > this.to) {
|
|
137
|
+
this.from = this.to;
|
|
138
|
+
this.to = Math.min(this.end, nextPos);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
else if (pointPos == nextPos &&
|
|
142
|
+
(startPos > pointPos || pointSide < 0) &&
|
|
143
|
+
(endPos > pointPos || pointSide < 0)) {
|
|
144
|
+
this.point = points.value;
|
|
145
|
+
this.pointSource = points.set;
|
|
146
|
+
this.from = this.to = points.from;
|
|
147
|
+
points.next();
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
else if ((startPos - endPos || startSide - endSide) < 0) {
|
|
151
|
+
if (ranges.value)
|
|
152
|
+
active.push(ranges.value);
|
|
153
|
+
activeEnd.push(ranges.to);
|
|
154
|
+
ranges.next();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
active.splice(nextActive, 1);
|
|
158
|
+
activeEnd.splice(nextActive, 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const tagScope = (tag, atom) => DecorationScope.All |
|
|
165
|
+
(atom ? DecorationScope.Atom | (tag.type.isInline ? DecorationScope.InlineAtom : 0) : 0);
|
|
166
|
+
export const renderMarkWrapper = memo((mark) => {
|
|
167
|
+
const shape = mark.type.element;
|
|
168
|
+
if (!shape)
|
|
169
|
+
throw new Error(`${mark.name} has no element shape`);
|
|
170
|
+
return Elt.create(shape.name, shape.attrs(mark.value), [0]);
|
|
171
|
+
});
|
|
172
|
+
// The document's mark a wrapper draws: `Decoration.Mark.shape` draws one of
|
|
173
|
+
// a type it clones.
|
|
174
|
+
const documentMarks = new WeakMap();
|
|
175
|
+
export const documentMark = (mark) => documentMarks.get(mark) ?? mark;
|
|
176
|
+
export const renderWrapper = (src) => src instanceof getDecorationInternals().WrapperRangeDecoration ? src.elt : renderMarkWrapper(src);
|
|
177
|
+
const compareSetPrec = (setA, setB, array) => {
|
|
178
|
+
if (setA != setB)
|
|
179
|
+
for (const set of array) {
|
|
180
|
+
if (set == setA)
|
|
181
|
+
return -1;
|
|
182
|
+
if (set == setB)
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
};
|
|
187
|
+
// Where a walk ending at `target` has to stop: past the whole plot `target`
|
|
188
|
+
// falls inside, among the content after `pos`.
|
|
189
|
+
const pastPlotAt = (pos, target) => {
|
|
190
|
+
const { content } = pos.parent.node;
|
|
191
|
+
let start = pos.pos - pos.inText;
|
|
192
|
+
for (let i = pos.index; i < content.length; i++) {
|
|
193
|
+
const child = content[i];
|
|
194
|
+
if (!child)
|
|
195
|
+
break;
|
|
196
|
+
const end = start + child.length;
|
|
197
|
+
if (target < end)
|
|
198
|
+
return child.isPlot && target > start ? end : target;
|
|
199
|
+
start = end;
|
|
200
|
+
}
|
|
201
|
+
return target;
|
|
202
|
+
};
|
|
203
|
+
// Wordgard bug (docs/wordgard-upstream-bugs.md): a range set's second layer can
|
|
204
|
+
// hold overlapping ranges, which its cursor can't seek into. Wordgard walks
|
|
205
|
+
// forward only; React's per-plot walks seek, so they get the set's ranges in
|
|
206
|
+
// layers that don't overlap.
|
|
207
|
+
const seekable = memo((set) => {
|
|
208
|
+
const layers = [];
|
|
209
|
+
for (const cursor = set.cursor(0); cursor.value; cursor.next()) {
|
|
210
|
+
const { value, from, to } = cursor;
|
|
211
|
+
let layer = layers.find((layer) => layer.to <= from);
|
|
212
|
+
if (!layer)
|
|
213
|
+
layers.push((layer = { to, ranges: [] }));
|
|
214
|
+
layer.ranges.push([value, from, to]);
|
|
215
|
+
layer.to = to;
|
|
216
|
+
}
|
|
217
|
+
return layers.length > 1 ? layers.map(({ ranges }) => Decoration.Range.set(ranges)) : [set];
|
|
218
|
+
});
|
|
219
|
+
const none = [];
|
|
220
|
+
export class DecoIterator {
|
|
221
|
+
state;
|
|
222
|
+
decoSet;
|
|
223
|
+
tagShapes;
|
|
224
|
+
globalWidgets;
|
|
225
|
+
globalWrappers;
|
|
226
|
+
globalAttrs;
|
|
227
|
+
markShapes;
|
|
228
|
+
pos;
|
|
229
|
+
rangeCursor;
|
|
230
|
+
pointSets;
|
|
231
|
+
pointCursor;
|
|
232
|
+
constructor(state, decoSet) {
|
|
233
|
+
this.state = state;
|
|
234
|
+
this.decoSet = decoSet;
|
|
235
|
+
const { tagShape, tagWidget, tagWrapper, tagAttribute, markShape } = getDecorationInternals();
|
|
236
|
+
this.tagShapes = state.facet(tagShape);
|
|
237
|
+
this.globalWidgets = state.facet(tagWidget);
|
|
238
|
+
this.globalWrappers = state.facet(tagWrapper);
|
|
239
|
+
this.globalAttrs = state.facet(tagAttribute);
|
|
240
|
+
this.markShapes = state.facet(markShape);
|
|
241
|
+
this.pos = state.doc.resolve(0);
|
|
242
|
+
this.rangeCursor = RangeSet.cursor([...decoSet.ranges.values()].flatMap(seekable));
|
|
243
|
+
this.pointSets = [...decoSet.points.values()];
|
|
244
|
+
this.pointCursor = PointSet.cursor(this.pointSets);
|
|
245
|
+
}
|
|
246
|
+
widgets(tag, place, walker) {
|
|
247
|
+
if (place == WidgetPlace.Start && tag.type.isInline)
|
|
248
|
+
walker.widget(Widget.img, -1, false);
|
|
249
|
+
for (const src of this.globalWidgets) {
|
|
250
|
+
if (src.place == place && tag.type == src.type) {
|
|
251
|
+
const widget = typeof src.widget == "function" ? src.widget(tag) : src.widget;
|
|
252
|
+
if (widget)
|
|
253
|
+
walker.widget(widget, place == WidgetPlace.Before || place == WidgetPlace.End ? 1 : -1, false);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (place == WidgetPlace.End && tag.type.isInline)
|
|
257
|
+
walker.widget(Widget.img, 1, false);
|
|
258
|
+
}
|
|
259
|
+
// Enumerate all wrapper elements for a given node. Spanning wrappers
|
|
260
|
+
// are always moved to the front of the result. Within the
|
|
261
|
+
// spanning/non-spanning wrappers, the ordering is determined by rank.
|
|
262
|
+
nodeWrappers(tag, active, atom) {
|
|
263
|
+
const { WrapperRangeDecoration } = getDecorationInternals();
|
|
264
|
+
const wrappers = [];
|
|
265
|
+
for (const mark of tag.marks) {
|
|
266
|
+
const shape = this.markShapes.get(mark.type) || mark.type;
|
|
267
|
+
if (shape.element) {
|
|
268
|
+
const drawn = shape == mark.type ? mark : shape.of(mark.value);
|
|
269
|
+
if (drawn != mark)
|
|
270
|
+
documentMarks.set(drawn, mark);
|
|
271
|
+
wrappers.push(drawn);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const val of active) {
|
|
275
|
+
if (val instanceof WrapperRangeDecoration &&
|
|
276
|
+
tagScope(tag, atom) & val.scope &&
|
|
277
|
+
(!val.query || this.state.schema.matchNode(tag.type, val.query)))
|
|
278
|
+
wrappers.push(val);
|
|
279
|
+
}
|
|
280
|
+
if (!wrappers.length)
|
|
281
|
+
return none;
|
|
282
|
+
if (wrappers.length > 1)
|
|
283
|
+
wrappers.sort((a, b) => (spanningOf(a) == spanningOf(b) ? 0 : spanningOf(a) ? -1 : 1) || rankOf(a) - rankOf(b));
|
|
284
|
+
return wrappers;
|
|
285
|
+
}
|
|
286
|
+
walk(from, inclusiveStart, to, walker) {
|
|
287
|
+
const { WidgetDecoration, ShapeDecoration } = getDecorationInternals();
|
|
288
|
+
this.rangeCursor.goto(from);
|
|
289
|
+
this.pointCursor.goto(from, inclusiveStart ? -1e9 : Side.After);
|
|
290
|
+
const iter = new SpanIterator(this.rangeCursor, this.pointCursor, from, to);
|
|
291
|
+
let pos = from >= this.pos.pos ? this.pos.advance(from - this.pos.pos) : this.state.doc.resolve(from);
|
|
292
|
+
let started = inclusiveStart;
|
|
293
|
+
let atomParent;
|
|
294
|
+
for (let p = pos.parent; p; p = p.parent)
|
|
295
|
+
if (this.state.isAtom(p.node.type))
|
|
296
|
+
atomParent = p;
|
|
297
|
+
// Track points that may apply to the node at the start of the next range
|
|
298
|
+
const pendingDeco = [];
|
|
299
|
+
let pendingPos = -1;
|
|
300
|
+
let pendingShape = null;
|
|
301
|
+
let pendingShapeSet = null;
|
|
302
|
+
// A plot the walker didn't enter: its `after` widgets come once past it.
|
|
303
|
+
let skipped = null;
|
|
304
|
+
const flushSkipped = () => {
|
|
305
|
+
if (!skipped)
|
|
306
|
+
return;
|
|
307
|
+
const tag = skipped;
|
|
308
|
+
skipped = null;
|
|
309
|
+
this.widgets(tag, WidgetPlace.After, walker);
|
|
310
|
+
};
|
|
311
|
+
const wrap = {
|
|
312
|
+
skip: (node, pos) => {
|
|
313
|
+
// Only done for leaf nodes.
|
|
314
|
+
flushSkipped();
|
|
315
|
+
if (started)
|
|
316
|
+
this.widgets(node.tag, WidgetPlace.Before, walker);
|
|
317
|
+
else
|
|
318
|
+
started = true;
|
|
319
|
+
const hasPending = pendingPos == pos && !node.isText;
|
|
320
|
+
let shape = hasPending && pendingShape ? pendingShape : this.tagShape(node.tag, iter.active);
|
|
321
|
+
if (hasPending)
|
|
322
|
+
for (const deco of pendingDeco)
|
|
323
|
+
shape = applyDeco(shape, deco, node.tag);
|
|
324
|
+
if (shape.hasContent)
|
|
325
|
+
throw new Error("Leaf nodes shapes shouldn't have a content hole");
|
|
326
|
+
walker.node(node, shape, this.nodeWrappers(node.tag, iter.active, true), undefined, pos);
|
|
327
|
+
this.widgets(node.tag, WidgetPlace.After, walker);
|
|
328
|
+
},
|
|
329
|
+
enterPlot: (node, pos) => {
|
|
330
|
+
flushSkipped();
|
|
331
|
+
if (started)
|
|
332
|
+
this.widgets(node.tag, WidgetPlace.Before, walker);
|
|
333
|
+
else
|
|
334
|
+
started = true;
|
|
335
|
+
let shape = pendingShape && pendingPos == pos ? pendingShape : this.tagShape(node.tag, iter.active);
|
|
336
|
+
if (pendingPos == pos)
|
|
337
|
+
for (const deco of pendingDeco)
|
|
338
|
+
shape = applyDeco(shape, deco, node.tag);
|
|
339
|
+
const wrappers = this.nodeWrappers(node.tag, iter.active, !shape.hasContent);
|
|
340
|
+
const atom = !shape.hasContent;
|
|
341
|
+
if (atom) {
|
|
342
|
+
walker.node(node, shape, wrappers, pos + node.length > to ? to - pos : undefined, pos);
|
|
343
|
+
}
|
|
344
|
+
else if (shape instanceof Elt && !walker.enter(node, shape, wrappers, pos)) {
|
|
345
|
+
skipped = node.tag;
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
this.widgets(node.tag, WidgetPlace.Start, walker);
|
|
349
|
+
return !atom;
|
|
350
|
+
},
|
|
351
|
+
leavePlot: (tag) => {
|
|
352
|
+
flushSkipped();
|
|
353
|
+
if (started)
|
|
354
|
+
this.widgets(tag, WidgetPlace.End, walker);
|
|
355
|
+
else
|
|
356
|
+
started = true;
|
|
357
|
+
walker.leave();
|
|
358
|
+
this.widgets(tag, WidgetPlace.After, walker);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
if (inclusiveStart) {
|
|
362
|
+
const before = pos.nodeBefore;
|
|
363
|
+
if (before)
|
|
364
|
+
this.widgets(before.tag, WidgetPlace.After, walker);
|
|
365
|
+
else
|
|
366
|
+
this.widgets(pos.parent.node.tag, WidgetPlace.Start, walker);
|
|
367
|
+
}
|
|
368
|
+
for (; !iter.next().done;) {
|
|
369
|
+
if (atomParent) {
|
|
370
|
+
const end = Math.min(to, atomParent.after);
|
|
371
|
+
const done = atomParent.after <= to;
|
|
372
|
+
walker.nodePart(atomParent.node, end - pos.pos, done);
|
|
373
|
+
pos = pos.advance(end - pos.pos);
|
|
374
|
+
if (done)
|
|
375
|
+
this.widgets(atomParent.node.tag, WidgetPlace.After, walker);
|
|
376
|
+
atomParent = undefined;
|
|
377
|
+
while (iter.point && iter.from < end)
|
|
378
|
+
iter.next();
|
|
379
|
+
}
|
|
380
|
+
else if (iter.point) {
|
|
381
|
+
// Inside a plot the walk went past.
|
|
382
|
+
if (iter.from < pos.pos)
|
|
383
|
+
continue;
|
|
384
|
+
flushSkipped();
|
|
385
|
+
const value = iter.point;
|
|
386
|
+
if (value instanceof WidgetDecoration) {
|
|
387
|
+
walker.widget(value.widget, value.side, true);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
if (pendingPos < pos.pos) {
|
|
391
|
+
pendingDeco.length = 0;
|
|
392
|
+
pendingShape = null;
|
|
393
|
+
pendingPos = pos.pos;
|
|
394
|
+
}
|
|
395
|
+
if (value instanceof ShapeDecoration &&
|
|
396
|
+
(!pendingShape ||
|
|
397
|
+
(pendingShapeSet &&
|
|
398
|
+
iter.pointSource &&
|
|
399
|
+
compareSetPrec(pendingShapeSet, iter.pointSource, this.pointSets)))) {
|
|
400
|
+
pendingShape = value.shape;
|
|
401
|
+
pendingShapeSet = iter.pointSource;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
pendingDeco.push(value);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
else if (iter.to > pos.pos) {
|
|
409
|
+
pos = pos.walk(pastPlotAt(pos, iter.to) - pos.pos, wrap);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (pos.pos < to)
|
|
413
|
+
pos = pos.walk(pastPlotAt(pos, to) - pos.pos, wrap);
|
|
414
|
+
flushSkipped();
|
|
415
|
+
if (atomParent) {
|
|
416
|
+
walker.nodePart(atomParent.node, 0, atomParent.after == to);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const after = pos.nodeAfter;
|
|
420
|
+
if (after)
|
|
421
|
+
this.widgets(after.tag, WidgetPlace.Before, walker);
|
|
422
|
+
else
|
|
423
|
+
this.widgets(pos.parent.node.tag, WidgetPlace.End, walker);
|
|
424
|
+
}
|
|
425
|
+
this.pos = pos;
|
|
426
|
+
}
|
|
427
|
+
tagShape(tag, active) {
|
|
428
|
+
const { AttributeRangeDecoration } = getDecorationInternals();
|
|
429
|
+
const override = this.tagShapes.get(tag.type);
|
|
430
|
+
let shape = override ? override(tag) : baseTagShape(tag);
|
|
431
|
+
if (tag.marks.length)
|
|
432
|
+
shape = addMarkAttributes(this.markShapes, shape, tag);
|
|
433
|
+
let add;
|
|
434
|
+
for (const src of this.globalAttrs)
|
|
435
|
+
if (tag.type == src.type) {
|
|
436
|
+
if (src.target && shape instanceof Elt)
|
|
437
|
+
shape = shape.addAttrs([src.attr, src.value(tag)], src.target);
|
|
438
|
+
else
|
|
439
|
+
Attributes.push((add ||= []), src.attr, src.value(tag));
|
|
440
|
+
}
|
|
441
|
+
const scope = tagScope(tag, !shape.hasContent);
|
|
442
|
+
for (const { type, elt, target } of this.globalWrappers)
|
|
443
|
+
if (tag.type == type) {
|
|
444
|
+
shape = target && shape instanceof Elt ? shape.wrap(elt, target) : elt.fill([shape]);
|
|
445
|
+
}
|
|
446
|
+
for (const deco of active) {
|
|
447
|
+
if (deco instanceof AttributeRangeDecoration &&
|
|
448
|
+
scope & deco.scope &&
|
|
449
|
+
(!deco.query || this.state.schema.matchNode(tag.type, deco.query)))
|
|
450
|
+
Attributes.push((add ||= []), deco.attribute, deco.value);
|
|
451
|
+
}
|
|
452
|
+
if (add) {
|
|
453
|
+
if (shape instanceof Elt)
|
|
454
|
+
shape = Elt.create(shape.tagName, Attributes.merge(shape.attrs, add), shape.children);
|
|
455
|
+
else
|
|
456
|
+
shape = attributeWidget(shape, add, tag.type.isBlock ? "div" : "span");
|
|
457
|
+
}
|
|
458
|
+
return shape;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const rankOf = (src) => src instanceof getDecorationInternals().WrapperRangeDecoration ? src.rank : src.type.rank;
|
|
462
|
+
export const spanningOf = (src) => src instanceof getDecorationInternals().WrapperRangeDecoration ? src.spanning : src.type.spanning;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type AllHTMLAttributes, type ComponentType, type HTMLProps, type ReactElement, type ReactNode, type RefAttributes, type RefCallback } from "react";
|
|
2
|
+
import type { Elt, Mark } from "wordgard/doc";
|
|
3
|
+
import type { Tile } from "../wordgardInternals.js";
|
|
4
|
+
export type MarkComponentProps<E extends Element = HTMLElement> = AllHTMLAttributes<HTMLElement> & {
|
|
5
|
+
markProps: {
|
|
6
|
+
mark: Mark;
|
|
7
|
+
getPos: () => number;
|
|
8
|
+
contentDOMRef: RefCallback<Element>;
|
|
9
|
+
};
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
} & RefAttributes<E>;
|
|
12
|
+
export type MarkShapeOptions = {
|
|
13
|
+
element?: string;
|
|
14
|
+
};
|
|
15
|
+
type DrawProps = {
|
|
16
|
+
attrs: HTMLProps<HTMLElement>;
|
|
17
|
+
markProps: MarkComponentProps["markProps"];
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
ref: RefCallback<Element>;
|
|
20
|
+
};
|
|
21
|
+
type MarkShape = {
|
|
22
|
+
element: string;
|
|
23
|
+
attributes: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
export declare const createMarkShape: <E extends Element = HTMLElement>(component: ComponentType<MarkComponentProps<E>>, { element }?: MarkShapeOptions) => MarkShape;
|
|
26
|
+
export declare const isReactMark: (elt: Elt<unknown>) => boolean;
|
|
27
|
+
export declare const drawMark: (elt: Elt<unknown>, props: DrawProps) => ReactElement<any, string | import("react").JSXElementConstructor<any>>;
|
|
28
|
+
export declare const markTiles: WeakSet<Tile>;
|
|
29
|
+
export declare const markOfTile: WeakMap<Tile, Mark>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Named after ReactWidgetType: a React mark is a mark's element shape (for
|
|
2
|
+
// `Decoration.Mark.shape`, or a mark type's own spec), the element plain
|
|
3
|
+
// Wordgard draws, registered as drawn by a component, which renders the
|
|
4
|
+
// marked content as `children`. Wordgard's mark shapes are declarative, so the
|
|
5
|
+
// registration is the reserved attribute React shapes use, which the element
|
|
6
|
+
// Wordgard makes from the shape keeps. The props follow react-prosemirror's
|
|
7
|
+
// MarkComponentProps.
|
|
8
|
+
import { createElement, } from "react";
|
|
9
|
+
import { takesRef } from "../helpers/refs.js";
|
|
10
|
+
import { idOf, reserved } from "./ReactShapeType.js";
|
|
11
|
+
// Keyed apart from React shapes' and wrappers' ids, which share the attribute.
|
|
12
|
+
const draws = new Map();
|
|
13
|
+
// By component, then root element, as React shapes are.
|
|
14
|
+
const marks = new WeakMap();
|
|
15
|
+
export const createMarkShape = (component, { element = "span" } = {}) => {
|
|
16
|
+
let registered = marks.get(component);
|
|
17
|
+
if (!registered) {
|
|
18
|
+
const id = `m${draws.size + 1}`;
|
|
19
|
+
draws.set(id, ({ attrs, markProps, children, ref }) => createElement(component, {
|
|
20
|
+
...attrs,
|
|
21
|
+
ref: takesRef(component) ? ref : undefined,
|
|
22
|
+
markProps,
|
|
23
|
+
children,
|
|
24
|
+
}));
|
|
25
|
+
registered = { id, shapes: new Map() };
|
|
26
|
+
marks.set(component, registered);
|
|
27
|
+
}
|
|
28
|
+
let shape = registered.shapes.get(element);
|
|
29
|
+
if (!shape) {
|
|
30
|
+
shape = { element, attributes: { [reserved]: registered.id } };
|
|
31
|
+
registered.shapes.set(element, shape);
|
|
32
|
+
}
|
|
33
|
+
return shape;
|
|
34
|
+
};
|
|
35
|
+
export const isReactMark = (elt) => {
|
|
36
|
+
const id = idOf(elt);
|
|
37
|
+
return id != null && draws.has(id);
|
|
38
|
+
};
|
|
39
|
+
export const drawMark = (elt, props) => {
|
|
40
|
+
const draw = draws.get(idOf(elt) ?? "");
|
|
41
|
+
if (!draw)
|
|
42
|
+
throw new Error("Not a React mark");
|
|
43
|
+
return draw(props);
|
|
44
|
+
};
|
|
45
|
+
// The tiles over React marks' elements, and the mark each root's is for: a
|
|
46
|
+
// composition inside one sits in one wrapper, not in each of its elements.
|
|
47
|
+
export const markTiles = new WeakSet();
|
|
48
|
+
export const markOfTile = new WeakMap();
|