@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,93 @@
|
|
|
1
|
+
react-wordgard includes code from the projects below. Their copyright and
|
|
2
|
+
license notices are reproduced here, as their licenses require.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
================================================================================
|
|
6
|
+
react-prosemirror
|
|
7
|
+
https://github.com/handlewithcarecollective/react-prosemirror
|
|
8
|
+
================================================================================
|
|
9
|
+
|
|
10
|
+
react-wordgard is derived from react-prosemirror, maintained by Handle with
|
|
11
|
+
Care Collective and originally created at The New York Times Company. Files
|
|
12
|
+
that came from it keep their "Copyright (c) The New York Times Company"
|
|
13
|
+
headers.
|
|
14
|
+
|
|
15
|
+
react-prosemirror was released under the Apache License 2.0 until 2026-10-02
|
|
16
|
+
(commit 1827d3a) and under the MIT License since then. Both notices follow.
|
|
17
|
+
|
|
18
|
+
--- MIT License (since 2026-10-02) ---------------------------------------------
|
|
19
|
+
|
|
20
|
+
MIT License
|
|
21
|
+
|
|
22
|
+
Copyright (c) 2026 Handle with Care Collective
|
|
23
|
+
|
|
24
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
25
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
26
|
+
in the Software without restriction, including without limitation the rights
|
|
27
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
28
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
29
|
+
furnished to do so, subject to the following conditions:
|
|
30
|
+
|
|
31
|
+
The above copyright notice and this permission notice shall be included in all
|
|
32
|
+
copies or substantial portions of the Software.
|
|
33
|
+
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
SOFTWARE.
|
|
41
|
+
|
|
42
|
+
--- Apache License 2.0 (until 2026-10-02) --------------------------------------
|
|
43
|
+
|
|
44
|
+
Copyright (c) 2022 The New York Times Company
|
|
45
|
+
Copyright (c) 2025 Handle with Care Collective
|
|
46
|
+
|
|
47
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
48
|
+
this library except in compliance with the License. You may obtain a copy of the
|
|
49
|
+
License at
|
|
50
|
+
|
|
51
|
+
www.apache.org/licenses/LICENSE-2.0
|
|
52
|
+
|
|
53
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
54
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
55
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
56
|
+
specific language governing permissions and limitations under the License.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
================================================================================
|
|
60
|
+
Wordgard
|
|
61
|
+
https://wordgard.net
|
|
62
|
+
================================================================================
|
|
63
|
+
|
|
64
|
+
test/wordgard/ holds copies of Wordgard's browser tests and their helpers;
|
|
65
|
+
demo/main.tsx is adapted from Wordgard's demo (demo/demo.ts), and the
|
|
66
|
+
IME emulation in src/__tests__/tile.browser.test.ts from its `compose` test
|
|
67
|
+
helper (test/webtest-composition.ts). src/decorations/DecoIterator.ts copies
|
|
68
|
+
Wordgard's decoration walker (src/editor/decoration.ts), with changes noted in
|
|
69
|
+
the file. src/tile.ts copies `setUneditable` from Wordgard's tile tree
|
|
70
|
+
(src/editor/tile.ts) and follows its tile builder there.
|
|
71
|
+
|
|
72
|
+
MIT License
|
|
73
|
+
|
|
74
|
+
Copyright (C) 2026 by Marijn Haverbeke <marijn@haverbeke.berlin>.
|
|
75
|
+
|
|
76
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
77
|
+
a copy of this software and associated documentation files (the
|
|
78
|
+
"Software"), to deal in the Software without restriction, including
|
|
79
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
80
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
81
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
82
|
+
the following conditions:
|
|
83
|
+
|
|
84
|
+
The above copyright notice and this permission notice shall be
|
|
85
|
+
included in all copies or substantial portions of the Software.
|
|
86
|
+
|
|
87
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
88
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
89
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
90
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
91
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
92
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
93
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Wordgard } from "wordgard/editor";
|
|
2
|
+
import { ReactDocTile } from "./tile.js";
|
|
3
|
+
import { type DOMObserver, DocTile } from "./wordgardInternals.js";
|
|
4
|
+
export declare class ReactWordgard extends Wordgard {
|
|
5
|
+
docTile: DocTile;
|
|
6
|
+
observer: DOMObserver;
|
|
7
|
+
scheduleFlush: () => void;
|
|
8
|
+
static create: (spec: Wordgard.Spec) => ReactWordgard;
|
|
9
|
+
static host: (spec: Wordgard.Spec) => ReactWordgard;
|
|
10
|
+
static DocTile: typeof ReactDocTile;
|
|
11
|
+
constructor(spec: Wordgard.Spec, hosted?: boolean);
|
|
12
|
+
flush(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Adapted from react-prosemirror's ReactEditorView, to fit Wordgard: it extends Wordgard, and its
|
|
2
|
+
// base constructor builds a tile tree React renders, where ReactEditorView replaces the docView.
|
|
3
|
+
import { Wordgard } from "wordgard/editor";
|
|
4
|
+
import { GardState } from "wordgard/state";
|
|
5
|
+
import { editorOf } from "./contexts/EditorContext.js";
|
|
6
|
+
import { editorEventHandlers, hasEditorEventHandlers } from "./contexts/editorEvents.js";
|
|
7
|
+
import { ReactDocTile, RefusedUpdate, observeShapes } from "./tile.js";
|
|
8
|
+
import { DocTile } from "./wordgardInternals.js";
|
|
9
|
+
// Asks Wordgard for a frame: a no-op read schedules a flush.
|
|
10
|
+
const retry = () => { };
|
|
11
|
+
export class ReactWordgard
|
|
12
|
+
// @ts-expect-error Wordgard's constructor is private in its typings.
|
|
13
|
+
extends Wordgard {
|
|
14
|
+
// An editor with a React root of its own.
|
|
15
|
+
static create = (spec) => new ReactWordgard(spec, false);
|
|
16
|
+
// An editor WordgardEditor hosts: its document renders through
|
|
17
|
+
// WordgardDoc's portal, in the app's React tree.
|
|
18
|
+
static host = (spec) => new ReactWordgard(spec, true);
|
|
19
|
+
// Wordgard's own tests build tile trees through `Wordgard.DocTile`.
|
|
20
|
+
static DocTile = ReactDocTile;
|
|
21
|
+
constructor(spec, hosted = false) {
|
|
22
|
+
// Wordgard's constructor draws the document with `DocTile.create`, before
|
|
23
|
+
// this one can run: React draws it there instead, so it's drawn once. Put
|
|
24
|
+
// back at once, for any editor React's render creates; other editors
|
|
25
|
+
// created first (by a plugin) draw as before.
|
|
26
|
+
const create = DocTile.create;
|
|
27
|
+
const self = { editor: null };
|
|
28
|
+
DocTile.create = (state, dom, wg) => {
|
|
29
|
+
if (!(wg instanceof ReactWordgard))
|
|
30
|
+
return create(state, dom, wg);
|
|
31
|
+
DocTile.create = create;
|
|
32
|
+
if (!hosted)
|
|
33
|
+
return ReactDocTile.create(state, dom, wg);
|
|
34
|
+
return ReactDocTile.host(state, dom, wg, () => {
|
|
35
|
+
const { editor } = self;
|
|
36
|
+
if (editor)
|
|
37
|
+
editor.docTile = editor.observer.ignore(() => ReactDocTile.mount(editor.contentDOM, editor, editor.docTile));
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
// Given a `parent`, it would also put the editor in the page, where
|
|
41
|
+
// connecting measures it: that's left until it's built.
|
|
42
|
+
// useEditorEventListener's handlers come first; Wordgard keeps one copy of
|
|
43
|
+
// an extension, so a state taken from another ReactWordgard is fine.
|
|
44
|
+
const events = spec.state
|
|
45
|
+
? {
|
|
46
|
+
state: spec.state.update({ effects: GardState.appendConfig.of(editorEventHandlers) })
|
|
47
|
+
.state,
|
|
48
|
+
}
|
|
49
|
+
: { config: [editorEventHandlers, spec.config ?? []] };
|
|
50
|
+
try {
|
|
51
|
+
// @ts-expect-error The typings drop the private constructor's parameter.
|
|
52
|
+
super({ ...spec, ...events, parent: undefined });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
DocTile.create = create;
|
|
56
|
+
}
|
|
57
|
+
self.editor = this;
|
|
58
|
+
// React shapes' own commits are sorted out of what Wordgard's DOM
|
|
59
|
+
// observer reads; replaced before the editor is connected and observes.
|
|
60
|
+
this.observer.observer = observeShapes(this, (records) => {
|
|
61
|
+
this.observer.queue.push(...records);
|
|
62
|
+
this.scheduleFlush();
|
|
63
|
+
});
|
|
64
|
+
spec.parent?.appendChild(this.dom);
|
|
65
|
+
}
|
|
66
|
+
// Wordgard's flush runs from an animation frame and from the methods that
|
|
67
|
+
// need an up-to-date DOM; useEditorEffect's effects run after it. A refused
|
|
68
|
+
// update leaves Wordgard's transactions pending with no frame requested for
|
|
69
|
+
// them, so one is.
|
|
70
|
+
flush() {
|
|
71
|
+
const editor = editorOf(this);
|
|
72
|
+
// React shapes that committed since their records were delivered get
|
|
73
|
+
// their tiles first (taking the records rebuilds them): Wordgard reads
|
|
74
|
+
// them before the build.
|
|
75
|
+
this.observer.queue.push(...this.observer.observer.takeRecords());
|
|
76
|
+
editor.flushing = true;
|
|
77
|
+
try {
|
|
78
|
+
super.flush();
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof RefusedUpdate)
|
|
82
|
+
this.scheduleDOMRead(retry);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
editor.flushing = false;
|
|
87
|
+
}
|
|
88
|
+
editor.runEffects();
|
|
89
|
+
// A reconfiguration (GardState.reconfigure) dropped useEditorEventListener's
|
|
90
|
+
// handlers: they come back with a transaction of their own.
|
|
91
|
+
if (!hasEditorEventHandlers(this.state))
|
|
92
|
+
this.dispatch({ effects: GardState.appendConfig.of(editorEventHandlers) });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plot } from "wordgard/doc";
|
|
2
|
+
export declare const mapNodePos: (changes: readonly number[], pos: number, leaf?: boolean) => {
|
|
3
|
+
pos: number;
|
|
4
|
+
retagged: boolean;
|
|
5
|
+
} | null;
|
|
6
|
+
export declare const mapKept: (changes: readonly number[], pos: number) => number | null;
|
|
7
|
+
export declare const samePlace: (changes: readonly number[], oldStart: number, start: number) => boolean;
|
|
8
|
+
export declare const redrawn: (changes: readonly number[] | null, pos: number) => boolean;
|
|
9
|
+
export declare const isKept: (changes: readonly number[] | null, from: number, to: number) => boolean;
|
|
10
|
+
export declare const pointMapper: (changes: readonly number[]) => (pos: number) => number[];
|
|
11
|
+
export declare const unchangedEnds: (changes: readonly number[], oldPlot: Plot, oldStart: number, plot: Plot) => {
|
|
12
|
+
head: number;
|
|
13
|
+
tail: number;
|
|
14
|
+
};
|
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,36 @@
|
|
|
1
|
+
// Named after react-prosemirror's ChildNodeViews, rewritten for Wordgard: draws what iterDeco lays
|
|
2
|
+
// out. Native widgets aren't React's: ReactDocTile puts their DOM in place after the commit.
|
|
3
|
+
import React, { memo } from "react";
|
|
4
|
+
import { cellOf } from "../contexts/RenderStoreContext.js";
|
|
5
|
+
import { isReactWidget } from "../decorations/ReactWidgetType.js";
|
|
6
|
+
import { shapeOf } from "../decorations/iterDeco.js";
|
|
7
|
+
import { childViewKeys } from "../reactKeys.js";
|
|
8
|
+
import { SeparatorHackView } from "./SeparatorHackView.js";
|
|
9
|
+
import { TextNodeView } from "./TextNodeView.js";
|
|
10
|
+
import { TrailingHackView } from "./TrailingHackView.js";
|
|
11
|
+
import { WidgetView } from "./WidgetView.js";
|
|
12
|
+
import { MarkView } from "./marks/MarkView.js";
|
|
13
|
+
import { DefaultNodeView } from "./nodes/DefaultNodeView.js";
|
|
14
|
+
export const renderChildViews = (views) => childViewKeys(views).map(({ view, key }) => {
|
|
15
|
+
switch (view.type) {
|
|
16
|
+
case "node":
|
|
17
|
+
return React.createElement(DefaultNodeView, { key: key, cell: cellOf(view) });
|
|
18
|
+
case "text":
|
|
19
|
+
return React.createElement(TextNodeView, { key: key, text: view.text });
|
|
20
|
+
case "wrapper":
|
|
21
|
+
return React.createElement(MarkView, { key: key, elt: view.elt, views: view.children, mark: view.mark });
|
|
22
|
+
case "buffer":
|
|
23
|
+
return React.createElement(SeparatorHackView, { key: key });
|
|
24
|
+
case "composition":
|
|
25
|
+
// React keeps what it last committed in its own node, so it never
|
|
26
|
+
// writes to it; the browser's own node isn't React's to draw.
|
|
27
|
+
return view.rendered === null ? null : React.createElement(TextNodeView, { key: key, text: view.rendered });
|
|
28
|
+
case "trailingHack":
|
|
29
|
+
return React.createElement(TrailingHackView, { key: key });
|
|
30
|
+
case "widget":
|
|
31
|
+
return isReactWidget(view.widget) ? React.createElement(WidgetView, { key: key, widget: view.widget }) : null;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// Skipped when a plot's content keeps its shape (iterDeco's shapeOf): the
|
|
35
|
+
// nodes that changed in it get their views from their cells.
|
|
36
|
+
export const ChildNodeViews = memo(({ views }) => React.createElement(React.Fragment, null, renderChildViews(views)), (a, b) => shapeOf(a.views) == shapeOf(b.views));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AllHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
/** `DecorationRoot`'s props: its root's attributes, and its children. */
|
|
3
|
+
export type DecorationRootProps = AllHTMLAttributes<HTMLElement> & {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The root element of a React shape, wrapper, mark or widget: the tag the decoration says, with
|
|
8
|
+
* Wordgard's attributes merged with the props. See [React
|
|
9
|
+
* decorations](https://github.com/stesura/react-wordgard#react-decorations).
|
|
10
|
+
*/
|
|
11
|
+
export declare const DecorationRoot: import("react").ForwardRefExoticComponent<AllHTMLAttributes<HTMLElement> & {
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
} & import("react").RefAttributes<Element>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Not in react-prosemirror. The component's own props win, but `className` joins Wordgard's and
|
|
2
|
+
// `style` merges, its keys winning.
|
|
3
|
+
import { createElement, forwardRef, useContext, useMemo, } from "react";
|
|
4
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
5
|
+
import { cssToStyle, htmlAttrsToReactProps } from "../helpers/props.js";
|
|
6
|
+
import { mergeRefs, toCallbackRef } from "../helpers/refs.js";
|
|
7
|
+
/**
|
|
8
|
+
* The root element of a React shape, wrapper, mark or widget: the tag the decoration says, with
|
|
9
|
+
* Wordgard's attributes merged with the props. See [React
|
|
10
|
+
* decorations](https://github.com/stesura/react-wordgard#react-decorations).
|
|
11
|
+
*/
|
|
12
|
+
export const DecorationRoot = forwardRef(({ className, style, ...props }, ref) => {
|
|
13
|
+
const root = useContext(DecorationRootContext);
|
|
14
|
+
const rootRef = root?.ref;
|
|
15
|
+
const merged = useMemo(() => mergeRefs(rootRef, toCallbackRef(ref)), [rootRef, ref]);
|
|
16
|
+
if (!root)
|
|
17
|
+
throw new Error("DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their component");
|
|
18
|
+
// Wordgard's style is a string, which React can't merge with an object.
|
|
19
|
+
const { style: css, ...attrs } = root.attrs;
|
|
20
|
+
const own = htmlAttrsToReactProps(style ? attrs : root.attrs);
|
|
21
|
+
const classes = [own.className, className].filter(Boolean).join(" ");
|
|
22
|
+
return createElement(root.tagName, {
|
|
23
|
+
...own,
|
|
24
|
+
...props,
|
|
25
|
+
className: classes || undefined,
|
|
26
|
+
...(style && { style: { ...cssToStyle(css), ...style } }),
|
|
27
|
+
ref: merged,
|
|
28
|
+
suppressContentEditableWarning: true,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
DecorationRoot.displayName = "DecorationRoot";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
type Props = {
|
|
3
|
+
onError: (error: unknown) => void;
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export declare class ErrorBoundary extends Component<Props, {
|
|
7
|
+
failed: boolean;
|
|
8
|
+
}> {
|
|
9
|
+
state: {
|
|
10
|
+
failed: boolean;
|
|
11
|
+
};
|
|
12
|
+
static getDerivedStateFromError: () => {
|
|
13
|
+
failed: boolean;
|
|
14
|
+
};
|
|
15
|
+
componentDidCatch(error: unknown): void;
|
|
16
|
+
render(): ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Not in react-prosemirror: catches a render error for ReactDocTile to throw from the update. React
|
|
2
|
+
// 18 rethrows it from flushSync, React 19 reports it and commits nothing. A class: React has no
|
|
3
|
+
// hook for this.
|
|
4
|
+
import { Component } from "react";
|
|
5
|
+
export class ErrorBoundary extends Component {
|
|
6
|
+
state = { failed: false };
|
|
7
|
+
static getDerivedStateFromError = () => ({ failed: true });
|
|
8
|
+
componentDidCatch(error) {
|
|
9
|
+
this.props.onError(error);
|
|
10
|
+
}
|
|
11
|
+
render() {
|
|
12
|
+
return this.state.failed ? null : this.props.children;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Named after react-prosemirror's SeparatorHackView, rewritten for Wordgard: the
|
|
2
|
+
// `<img class="wg-buffer">` (`Widget.img`) at the ends of an inline plot.
|
|
3
|
+
import React from "react";
|
|
4
|
+
export const SeparatorHackView = () => React.createElement("img", { className: "wg-buffer" });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Node } from "wordgard/doc";
|
|
3
|
+
import type { DecoElt } from "../wordgardInternals.js";
|
|
4
|
+
type Props = {
|
|
5
|
+
elt: DecoElt;
|
|
6
|
+
node: Node;
|
|
7
|
+
content: ReactNode;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
outerRef?: ((dom: Element | null) => void) | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare const ShapeView: ({ elt, node, content, selected, outerRef }: Props) => React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Named after react-prosemirror's ReactNodeView, for Wordgard: draws a React shape's component. Its
|
|
2
|
+
// own commits are sorted out of Wordgard's DOM observer (`sortRecords`).
|
|
3
|
+
import React, { useMemo, useState } from "react";
|
|
4
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
5
|
+
import { atomRef, createShapeHandle, drawShape, nodePos, shapeAttributes, } from "../decorations/ReactShapeType.js";
|
|
6
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
7
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
8
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
9
|
+
export const ShapeView = ({ elt, node, content, selected, outerRef }) => {
|
|
10
|
+
const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
|
|
11
|
+
const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
|
|
12
|
+
const atom = !elt.hasContent;
|
|
13
|
+
const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers), atomRef(handle, atom)), [ref, outerRef, handlers, handle, atom]);
|
|
14
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
15
|
+
const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
|
|
16
|
+
const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
|
|
17
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
18
|
+
React.createElement(DecorationRootContext.Provider, { value: root }, drawShape(elt, {
|
|
19
|
+
attrs: componentProps(attrs),
|
|
20
|
+
nodeProps: { node, getPos: handlers.getPos, contentDOMRef },
|
|
21
|
+
children: content,
|
|
22
|
+
ref: rootRef,
|
|
23
|
+
}))));
|
|
24
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Widget } from "wordgard/editor";
|
|
3
|
+
type Props = {
|
|
4
|
+
widget: Widget;
|
|
5
|
+
attrs?: readonly string[];
|
|
6
|
+
selected?: boolean;
|
|
7
|
+
outerRef?: ((dom: Element | null) => void) | undefined;
|
|
8
|
+
};
|
|
9
|
+
export declare const WidgetView: React.MemoExoticComponent<({ widget, attrs, selected, outerRef }: Props) => React.JSX.Element>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Named after react-prosemirror's WidgetView, rewritten for Wordgard. Skipped while the widget is
|
|
2
|
+
// `eq`, so the component keeps its DOM, as Wordgard keeps an equal widget's.
|
|
3
|
+
import React, { memo, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
|
|
5
|
+
import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
|
|
6
|
+
import { drawWidget, widgetElement, widgetRoot } from "../decorations/ReactWidgetType.js";
|
|
7
|
+
import { attrsToRecord, componentProps } from "../helpers/index.js";
|
|
8
|
+
import { mergeRefs } from "../helpers/refs.js";
|
|
9
|
+
export const WidgetView = memo(({ widget, attrs = none, selected = false, outerRef }) => {
|
|
10
|
+
const root = useRef(null);
|
|
11
|
+
const [handlers] = useState(() => createDecorationHandlers(() => {
|
|
12
|
+
// A removed element keeps its last tile.
|
|
13
|
+
const tile = root.current?.isConnected && root.current.wgTile;
|
|
14
|
+
if (!tile)
|
|
15
|
+
throw new Error("This React widget isn't drawn");
|
|
16
|
+
return tile.posBefore;
|
|
17
|
+
}));
|
|
18
|
+
const ref = useMemo(() => mergeRefs((dom) => {
|
|
19
|
+
root.current = dom;
|
|
20
|
+
}, widgetRoot, decorationRoot(handlers), outerRef), [root, handlers, outerRef]);
|
|
21
|
+
const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
|
|
22
|
+
const record = useMemo(() => attrsToRecord(attrs), [attrs]);
|
|
23
|
+
const tagName = widgetElement(widget);
|
|
24
|
+
const decorationRootValue = useMemo(() => ({ tagName, attrs: record, ref }), [tagName, record, ref]);
|
|
25
|
+
return (React.createElement(DecorationContext.Provider, { value: decoration },
|
|
26
|
+
React.createElement(DecorationRootContext.Provider, { value: decorationRootValue }, drawWidget(widget, ref, componentProps(record)))));
|
|
27
|
+
}, (a, b) => a.widget.eq(b.widget) &&
|
|
28
|
+
a.outerRef == b.outerRef &&
|
|
29
|
+
a.selected == b.selected &&
|
|
30
|
+
sameAttrs(a.attrs ?? none, b.attrs ?? none));
|
|
31
|
+
const none = [];
|
|
32
|
+
const sameAttrs = (a, b) => a.length == b.length && a.every((value, i) => value == b[i]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { type HTMLAttributes } from "react";
|
|
2
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "children">;
|
|
3
|
+
/**
|
|
4
|
+
* Puts the editor in the page, inside a `div` taking the props, and renders the document through a
|
|
5
|
+
* portal, in your React tree. It goes under its `<WordgardEditor>`, inside any other components,
|
|
6
|
+
* with other children before or after it. One per editor: a second throws.
|
|
7
|
+
*/
|
|
8
|
+
export declare const WordgardDoc: (props: Props) => React.JSX.Element;
|
|
9
|
+
export {};
|