@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,6 @@
|
|
|
1
|
+
import { type ForwardedRef, type RefCallback } from "react";
|
|
2
|
+
type DOMRef = (dom: Element | null) => void;
|
|
3
|
+
export declare const mergeRefs: (...refs: (DOMRef | undefined)[]) => DOMRef;
|
|
4
|
+
export declare const toCallbackRef: <T>(ref: ForwardedRef<T>) => RefCallback<T> | undefined;
|
|
5
|
+
export declare const takesRef: (component: unknown) => boolean;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { version } from "react";
|
|
2
|
+
// A React decoration's root ref, with what else must know its root: the
|
|
3
|
+
// React wrapper around it, its hooks' handlers.
|
|
4
|
+
export const mergeRefs = (...refs) => (dom) => {
|
|
5
|
+
for (const ref of refs)
|
|
6
|
+
ref?.(dom);
|
|
7
|
+
};
|
|
8
|
+
// A forwarded ref as a callback, for mergeRefs.
|
|
9
|
+
export const toCallbackRef = (ref) => typeof ref == "function"
|
|
10
|
+
? ref
|
|
11
|
+
: ref
|
|
12
|
+
? (value) => {
|
|
13
|
+
ref.current = value;
|
|
14
|
+
}
|
|
15
|
+
: undefined;
|
|
16
|
+
// Whether React gives `component` a ref: React 19 passes it as a prop, React
|
|
17
|
+
// 18 only to a forwardRef (an object) and warns for a function component,
|
|
18
|
+
// which needs none with DecorationRoot.
|
|
19
|
+
const refAsProp = Number(version.split(".")[0]) >= 19;
|
|
20
|
+
export const takesRef = (component) => refAsProp || typeof component != "function";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Named after react-prosemirror's useEditorEffect, rewritten for Wordgard: an
|
|
2
|
+
// effect that runs once the editor's DOM is up to date, after the flush under
|
|
3
|
+
// way or pending, else in a microtask, never inside React's commit, so it may
|
|
4
|
+
// read the editor's layout and dispatch. A plain React effect may run inside
|
|
5
|
+
// the flush, before the tiles over the new DOM exist.
|
|
6
|
+
import { useLayoutEffect } from "react";
|
|
7
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
8
|
+
export const useEditorEffect = (effect, dependencies) => {
|
|
9
|
+
const { wg, afterFlush, cancel } = useEditorContext();
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
let cleanup;
|
|
12
|
+
const run = () => {
|
|
13
|
+
cleanup = effect(wg);
|
|
14
|
+
};
|
|
15
|
+
afterFlush(run);
|
|
16
|
+
return () => {
|
|
17
|
+
cancel(run);
|
|
18
|
+
if (typeof cleanup == "function")
|
|
19
|
+
cleanup();
|
|
20
|
+
};
|
|
21
|
+
// The caller's dependencies, as with React's own effects.
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, dependencies);
|
|
24
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Named after react-prosemirror's useEditorEventCallback, rewritten for
|
|
2
|
+
// Wordgard: a stable function calling the latest `callback` with the editor,
|
|
3
|
+
// for event handlers.
|
|
4
|
+
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
5
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
6
|
+
export const useEditorEventCallback = (callback) => {
|
|
7
|
+
const { wg } = useEditorContext();
|
|
8
|
+
const latest = useRef(callback);
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
latest.current = callback;
|
|
11
|
+
});
|
|
12
|
+
return useCallback((...args) => latest.current(wg, ...args), [wg]);
|
|
13
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Wordgard } from "wordgard/editor";
|
|
2
|
+
import { type EditorEvent } from "../contexts/editorEvents.js";
|
|
3
|
+
export declare const useEditorEventListener: <Type extends EditorEvent>(type: Type, handler: (wg: Wordgard, event: HTMLElementEventMap[Type]) => boolean | void) => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Named after react-prosemirror's useEditorEventListener, rewritten for
|
|
2
|
+
// Wordgard: `handler` gets the editor's DOM events of `type` before Wordgard's
|
|
3
|
+
// own handlers and keymaps, as react-prosemirror's `handleDOMEvents` came
|
|
4
|
+
// before plugins: ReactWordgard installs a Wordgard.domEventHandler for each
|
|
5
|
+
// type, at the highest precedence. Returning true says it handled the event:
|
|
6
|
+
// Wordgard then calls `preventDefault` and goes no further. The latest
|
|
7
|
+
// registered runs first, as in react-prosemirror. Unlike there, key events
|
|
8
|
+
// during a composition don't come (Wordgard ignores them first), and handling
|
|
9
|
+
// `input`, `beforeinput` or a composition event skips Wordgard's own tracking
|
|
10
|
+
// of it. Among `prec.highest` handlers, these come first in an editor made
|
|
11
|
+
// from `config`, after the state's own in one made from a `state`.
|
|
12
|
+
import { useLayoutEffect } from "react";
|
|
13
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
14
|
+
import { listenersOf } from "../contexts/editorEvents.js";
|
|
15
|
+
import { useEditorEventCallback } from "./useEditorEventCallback.js";
|
|
16
|
+
export const useEditorEventListener = (type, handler) => {
|
|
17
|
+
const { wg } = useEditorContext();
|
|
18
|
+
const listener = useEditorEventCallback(handler);
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
const listeners = listenersOf(wg, type);
|
|
21
|
+
listeners.unshift(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
listeners.splice(listeners.indexOf(listener), 1);
|
|
24
|
+
};
|
|
25
|
+
}, [wg, type, listener]);
|
|
26
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The other half of react-prosemirror's useEditorEffect, which ran inside
|
|
2
|
+
// React's commit: what it set showed on the same paint. Here React commits
|
|
3
|
+
// inside Wordgard's flush, so useEditorEffect runs after it, and this hook
|
|
4
|
+
// runs in it, as React's useLayoutEffect to useEffect. It measures the
|
|
5
|
+
// editor's layout and writes from it on the same paint as the document update:
|
|
6
|
+
// `read` runs in every flush's read phase, `write` with its result in the
|
|
7
|
+
// write phase after (Wordgard's scheduleDOMRead and scheduleDOMWrite), React
|
|
8
|
+
// state it sets committing there too. A write that changes the size of the
|
|
9
|
+
// editor's content makes Wordgard update and measure again within the flush,
|
|
10
|
+
// so pagination can settle there. A `read` returning undefined skips its write;
|
|
11
|
+
// with no write to do, none is scheduled, which spares Wordgard measuring
|
|
12
|
+
// again. Reads run in registration order: an engine whose parts must run in
|
|
13
|
+
// another order registers one effect and drives them. Neither may dispatch
|
|
14
|
+
// (Wordgard throws during
|
|
15
|
+
// its flush): dispatch from useEditorEffect or an event handler. Writes into
|
|
16
|
+
// the document's own DOM are foreign edits to Wordgard: write inside widgets,
|
|
17
|
+
// outside the editor, or through decorations.
|
|
18
|
+
import { useLayoutEffect, useRef } from "react";
|
|
19
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
20
|
+
export const useEditorLayoutEffect = (measure) => {
|
|
21
|
+
const { wg, measures, requestMeasure } = useEditorContext();
|
|
22
|
+
const latest = useRef(measure);
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
latest.current = measure;
|
|
25
|
+
});
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
const entry = () => {
|
|
28
|
+
const { read, write } = latest.current;
|
|
29
|
+
const value = read(wg);
|
|
30
|
+
return value === undefined ? null : () => write(value, wg);
|
|
31
|
+
};
|
|
32
|
+
measures.add(entry);
|
|
33
|
+
requestMeasure();
|
|
34
|
+
return () => {
|
|
35
|
+
measures.delete(entry);
|
|
36
|
+
};
|
|
37
|
+
}, [wg, measures, requestMeasure]);
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useEditorState: () => import("wordgard/state").GardState;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Named after react-prosemirror's useEditorState, rewritten for Wordgard: the
|
|
2
|
+
// state the editor's DOM shows, as of its last flush. Re-renders on every
|
|
3
|
+
// state change, in the same commit as the document; `wg.state` is ahead of it
|
|
4
|
+
// between a dispatch and the flush.
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
7
|
+
export const useEditorState = () => {
|
|
8
|
+
const { store } = useEditorContext();
|
|
9
|
+
return useSyncExternalStore(store.subscribe, store.getState);
|
|
10
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
|
|
2
|
+
import { useEditorContext } from "../contexts/EditorContext.js";
|
|
3
|
+
export const useEditorStateSelector = (selector, isEqual) => {
|
|
4
|
+
const { store } = useEditorContext();
|
|
5
|
+
return useSyncExternalStoreWithSelector(store.subscribe, store.getState, store.getState, selector, isEqual);
|
|
6
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Named after react-prosemirror's useIgnoreMutation, rewritten for Wordgard:
|
|
2
|
+
// when `ignoreMutation` returns true for a DOM mutation inside the React
|
|
3
|
+
// decoration (its content included), Wordgard doesn't read it. Mutations in
|
|
4
|
+
// the decoration's chrome never reach Wordgard anyway (`sortRecords`). Typing
|
|
5
|
+
// isn't stopped: Wordgard takes it from `beforeinput`, not the mutation.
|
|
6
|
+
import { useLayoutEffect } from "react";
|
|
7
|
+
import { useDecorationContext } from "../contexts/DecorationContext.js";
|
|
8
|
+
import { useEditorEventCallback } from "./useEditorEventCallback.js";
|
|
9
|
+
export const useIgnoreMutation = (ignoreMutation) => {
|
|
10
|
+
const { handlers } = useDecorationContext("useIgnoreMutation");
|
|
11
|
+
const ignore = useEditorEventCallback(ignoreMutation);
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
handlers.ignoreMutation.add(ignore);
|
|
14
|
+
return () => {
|
|
15
|
+
handlers.ignoreMutation.delete(ignore);
|
|
16
|
+
};
|
|
17
|
+
}, [handlers, ignore]);
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useIsNodeSelected: () => boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Named after react-prosemirror's useIsNodeSelected, rewritten for Wordgard:
|
|
2
|
+
// whether the node the React decoration a component is drawn in draws is the
|
|
3
|
+
// node selection, read from the class Wordgard draws it with
|
|
4
|
+
// (`wg-selected-node`), so the component renders with it, without an effect.
|
|
5
|
+
import { useDecorationContext } from "../contexts/DecorationContext.js";
|
|
6
|
+
export const useIsNodeSelected = () => useDecorationContext("useIsNodeSelected").selected;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Adapted from react-prosemirror's useMergedDOMRefs (src/refs.ts there),
|
|
2
|
+
// without its typecast: one ref callback setting each of `refs`, as a React
|
|
3
|
+
// decoration's component gives its root both the forwarded `ref` and
|
|
4
|
+
// `contentDOMRef`, or a measuring ref.
|
|
5
|
+
import { useCallback } from "react";
|
|
6
|
+
export const useMergedDOMRefs = (...refs) => useCallback((value) => {
|
|
7
|
+
for (const ref of refs) {
|
|
8
|
+
if (typeof ref == "function")
|
|
9
|
+
ref(value);
|
|
10
|
+
else if (ref)
|
|
11
|
+
Object.assign(ref, { current: value });
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
// The refs themselves, as react-prosemirror's: a new callback when one
|
|
15
|
+
// changes, so React gives it the element.
|
|
16
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
|
|
17
|
+
refs);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useNodePos: () => () => number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Named after react-prosemirror's useNodePos, rewritten for Wordgard: the
|
|
2
|
+
// position of what the React decoration a component is drawn in draws (a
|
|
3
|
+
// shape's, wrapper's or widget's node, or where a mark or point widget
|
|
4
|
+
// starts), read from its tile. Returns a stable function, for event handlers
|
|
5
|
+
// and effects, rather than the position: during render the tile may not be
|
|
6
|
+
// built yet.
|
|
7
|
+
import { useDecorationContext } from "../contexts/DecorationContext.js";
|
|
8
|
+
export const useNodePos = () => useDecorationContext("useNodePos").handlers.getPos;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Named after react-prosemirror's useSelectNode, rewritten for Wordgard:
|
|
2
|
+
// `selectNode` runs once the node the React decoration draws becomes the node
|
|
3
|
+
// selection, `deselectNode` once it stops being it, after the flush that draws
|
|
4
|
+
// the change, as ProseMirror's NodeView.selectNode and deselectNode.
|
|
5
|
+
import { useRef } from "react";
|
|
6
|
+
import { useEditorEffect } from "./useEditorEffect.js";
|
|
7
|
+
import { useEditorEventCallback } from "./useEditorEventCallback.js";
|
|
8
|
+
import { useIsNodeSelected } from "./useIsNodeSelected.js";
|
|
9
|
+
const noop = () => { };
|
|
10
|
+
export const useSelectNode = (selectNode, deselectNode = noop) => {
|
|
11
|
+
const selected = useIsNodeSelected();
|
|
12
|
+
const select = useEditorEventCallback(selectNode);
|
|
13
|
+
const deselect = useEditorEventCallback(deselectNode);
|
|
14
|
+
const was = useRef(false);
|
|
15
|
+
useEditorEffect(() => {
|
|
16
|
+
if (selected == was.current)
|
|
17
|
+
return;
|
|
18
|
+
was.current = selected;
|
|
19
|
+
if (selected)
|
|
20
|
+
select();
|
|
21
|
+
else
|
|
22
|
+
deselect();
|
|
23
|
+
}, [selected, select, deselect]);
|
|
24
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Named after react-prosemirror's useStopEvent, rewritten for Wordgard: when
|
|
2
|
+
// `stopEvent` returns true, Wordgard doesn't handle the event, nor do its
|
|
3
|
+
// handlers and keymaps, and the browser's default goes ahead. It's consulted
|
|
4
|
+
// through widget types' `propagateEvent`, for events in a React decoration's
|
|
5
|
+
// chrome or in a React widget, its own or a nested one's, and through its own
|
|
6
|
+
// elements' tiles (tile.ts `ownTile`), for events on them; not for those in
|
|
7
|
+
// its content.
|
|
8
|
+
import { useLayoutEffect } from "react";
|
|
9
|
+
import { useDecorationContext } from "../contexts/DecorationContext.js";
|
|
10
|
+
import { useEditorEventCallback } from "./useEditorEventCallback.js";
|
|
11
|
+
export const useStopEvent = (stopEvent) => {
|
|
12
|
+
const { handlers } = useDecorationContext("useStopEvent");
|
|
13
|
+
const stop = useEditorEventCallback(stopEvent);
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
handlers.stopEvent.add(stop);
|
|
16
|
+
return () => {
|
|
17
|
+
handlers.stopEvent.delete(stop);
|
|
18
|
+
};
|
|
19
|
+
}, [handlers, stop]);
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useWordgard: () => import("wordgard/editor").Wordgard;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { DecorationRoot, type DecorationRootProps } from "./components/DecorationRoot.js";
|
|
2
|
+
export { WordgardDoc } from "./components/WordgardDoc.js";
|
|
3
|
+
export { WordgardEditor } from "./components/WordgardEditor.js";
|
|
4
|
+
export { type MarkComponentProps, type MarkShapeOptions, createMarkShape, } from "./decorations/ReactMarkType.js";
|
|
5
|
+
export { type ReactShape, type ShapeComponentProps, createShape, } from "./decorations/ReactShapeType.js";
|
|
6
|
+
export { type WidgetTypeSpec, type WidgetComponentProps, createWidgetType, } from "./decorations/ReactWidgetType.js";
|
|
7
|
+
export { type WrapperOptions, type WrapperComponentProps, createWrapper, } from "./decorations/ReactWrapperType.js";
|
|
8
|
+
export { useEditorEffect } from "./hooks/useEditorEffect.js";
|
|
9
|
+
export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
|
|
10
|
+
export { useEditorEventListener } from "./hooks/useEditorEventListener.js";
|
|
11
|
+
export { type EditorLayoutEffect, useEditorLayoutEffect } from "./hooks/useEditorLayoutEffect.js";
|
|
12
|
+
export { useEditorState } from "./hooks/useEditorState.js";
|
|
13
|
+
export { useEditorStateSelector } from "./hooks/useEditorStateSelector.js";
|
|
14
|
+
export { useIgnoreMutation } from "./hooks/useIgnoreMutation.js";
|
|
15
|
+
export { useIsNodeSelected } from "./hooks/useIsNodeSelected.js";
|
|
16
|
+
export { useMergedDOMRefs } from "./hooks/useMergedDOMRefs.js";
|
|
17
|
+
export { useNodePos } from "./hooks/useNodePos.js";
|
|
18
|
+
export { useSelectNode } from "./hooks/useSelectNode.js";
|
|
19
|
+
export { useStopEvent } from "./hooks/useStopEvent.js";
|
|
20
|
+
export { useWordgard } from "./hooks/useWordgard.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { DecorationRoot } from "./components/DecorationRoot.js";
|
|
2
|
+
export { WordgardDoc } from "./components/WordgardDoc.js";
|
|
3
|
+
export { WordgardEditor } from "./components/WordgardEditor.js";
|
|
4
|
+
export { createMarkShape, } from "./decorations/ReactMarkType.js";
|
|
5
|
+
export { createShape, } from "./decorations/ReactShapeType.js";
|
|
6
|
+
export { createWidgetType, } from "./decorations/ReactWidgetType.js";
|
|
7
|
+
export { createWrapper, } from "./decorations/ReactWrapperType.js";
|
|
8
|
+
export { useEditorEffect } from "./hooks/useEditorEffect.js";
|
|
9
|
+
export { useEditorEventCallback } from "./hooks/useEditorEventCallback.js";
|
|
10
|
+
export { useEditorEventListener } from "./hooks/useEditorEventListener.js";
|
|
11
|
+
export { useEditorLayoutEffect } from "./hooks/useEditorLayoutEffect.js";
|
|
12
|
+
export { useEditorState } from "./hooks/useEditorState.js";
|
|
13
|
+
export { useEditorStateSelector } from "./hooks/useEditorStateSelector.js";
|
|
14
|
+
export { useIgnoreMutation } from "./hooks/useIgnoreMutation.js";
|
|
15
|
+
export { useIsNodeSelected } from "./hooks/useIsNodeSelected.js";
|
|
16
|
+
export { useMergedDOMRefs } from "./hooks/useMergedDOMRefs.js";
|
|
17
|
+
export { useNodePos } from "./hooks/useNodePos.js";
|
|
18
|
+
export { useSelectNode } from "./hooks/useSelectNode.js";
|
|
19
|
+
export { useStopEvent } from "./hooks/useStopEvent.js";
|
|
20
|
+
export { useWordgard } from "./hooks/useWordgard.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Plot } from "wordgard/doc";
|
|
2
|
+
import type { Widget } from "wordgard/editor";
|
|
3
|
+
import type { ChildView } from "./decorations/iterDeco.js";
|
|
4
|
+
export type ReactKeys = WeakMap<Plot, readonly string[]>;
|
|
5
|
+
export declare const createNodeKey: () => string;
|
|
6
|
+
export declare const addFreshKeys: (keys: ReactKeys, plot: Plot) => void;
|
|
7
|
+
export declare const mapKeys: (keys: ReactKeys, oldDoc: Plot, newDoc: Plot, changes: readonly number[]) => WeakMap<Plot, Plot>;
|
|
8
|
+
export declare const childViewKeys: (views: readonly ChildView[]) => ({
|
|
9
|
+
view: {
|
|
10
|
+
type: "widget";
|
|
11
|
+
widget: Widget;
|
|
12
|
+
side: number;
|
|
13
|
+
key?: string;
|
|
14
|
+
offset?: number;
|
|
15
|
+
};
|
|
16
|
+
key: string;
|
|
17
|
+
} | {
|
|
18
|
+
view: import("./decorations/iterDeco.js").NodeView | {
|
|
19
|
+
type: "text";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "wrapper";
|
|
23
|
+
elt: import("./wordgardInternals.js").DecoElt;
|
|
24
|
+
spanning: boolean;
|
|
25
|
+
children: ChildView[];
|
|
26
|
+
key?: string;
|
|
27
|
+
mark?: import("wordgard/doc").Mark;
|
|
28
|
+
} | {
|
|
29
|
+
type: "buffer";
|
|
30
|
+
side: -1 | 1;
|
|
31
|
+
} | {
|
|
32
|
+
type: "composition";
|
|
33
|
+
key: string;
|
|
34
|
+
rendered: string | null;
|
|
35
|
+
} | {
|
|
36
|
+
type: "trailingHack";
|
|
37
|
+
};
|
|
38
|
+
key: string;
|
|
39
|
+
})[];
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { mapNodePos, unchangedEnds } from "./changes.js";
|
|
2
|
+
export const createNodeKey = () => Math.floor(Math.random() * 0xffffffffffff).toString(16);
|
|
3
|
+
// Gives keys to `plot` and everything in it that has none yet.
|
|
4
|
+
export const addFreshKeys = (keys, plot) => {
|
|
5
|
+
if (keys.has(plot))
|
|
6
|
+
return;
|
|
7
|
+
keys.set(plot, plot.content.map((child) => (child.isText ? "" : createNodeKey())));
|
|
8
|
+
for (const child of plot.content)
|
|
9
|
+
if (child.isPlot)
|
|
10
|
+
addFreshKeys(keys, child);
|
|
11
|
+
};
|
|
12
|
+
// Carries keys from `oldDoc` to `newDoc` through `changes` (the sections
|
|
13
|
+
// DocTile.update receives), as react-prosemirror maps them through a
|
|
14
|
+
// transaction's steps: a node whose start survives keeps its key, as does a
|
|
15
|
+
// node moved within its parent (the same object); others get fresh ones. Only
|
|
16
|
+
// plots that changed identity are visited. Returns, for each
|
|
17
|
+
// of those, the plot it replaces.
|
|
18
|
+
export const mapKeys = (keys, oldDoc, newDoc, changes) => {
|
|
19
|
+
const replaced = new WeakMap();
|
|
20
|
+
const mapChildren = (oldPlot, oldStart, newPlot, newStart) => {
|
|
21
|
+
if (keys.has(newPlot))
|
|
22
|
+
return;
|
|
23
|
+
replaced.set(newPlot, oldPlot);
|
|
24
|
+
const oldKeys = keys.get(oldPlot) ?? [];
|
|
25
|
+
// The children at both ends are the same nodes at the same places: they
|
|
26
|
+
// keep their keys, and only the middle is mapped.
|
|
27
|
+
const { head, tail } = unchangedEnds(changes, oldPlot, oldStart, newPlot);
|
|
28
|
+
const before = oldPlot.content;
|
|
29
|
+
const after = newPlot.content;
|
|
30
|
+
const oldTail = before.length - tail;
|
|
31
|
+
const newTail = after.length - tail;
|
|
32
|
+
// Only the middle is walked node by node: typing in one of 5,000 blocks
|
|
33
|
+
// mustn't allocate per block.
|
|
34
|
+
let headSize = 0;
|
|
35
|
+
for (let i = 0; i < head; i++)
|
|
36
|
+
headSize += before[i]?.length ?? 0;
|
|
37
|
+
const found = new Map();
|
|
38
|
+
const byNode = new Map();
|
|
39
|
+
const retagged = new Map();
|
|
40
|
+
for (let i = head, pos = oldStart + headSize; i < oldTail; i++) {
|
|
41
|
+
const node = before[i];
|
|
42
|
+
if (!node)
|
|
43
|
+
continue;
|
|
44
|
+
const key = oldKeys[i];
|
|
45
|
+
if (key && !node.isText) {
|
|
46
|
+
const old = { key, node, start: pos };
|
|
47
|
+
const mapped = mapNodePos(changes, pos, node.isLeaf);
|
|
48
|
+
if (mapped && !mapped.retagged)
|
|
49
|
+
found.set(mapped.pos, old);
|
|
50
|
+
else {
|
|
51
|
+
byNode.set(node, [...(byNode.get(node) ?? []), old]);
|
|
52
|
+
if (mapped)
|
|
53
|
+
retagged.set(mapped.pos, old);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
pos += node.length;
|
|
57
|
+
}
|
|
58
|
+
// Each old key goes once: to its node where it's still there (moved),
|
|
59
|
+
// else to what a retag put in its place, if of its type.
|
|
60
|
+
const claimed = new Set();
|
|
61
|
+
const moved = [];
|
|
62
|
+
for (let i = head; i < newTail; i++) {
|
|
63
|
+
const child = after[i];
|
|
64
|
+
const old = child && !child.isText ? byNode.get(child)?.find((old) => !claimed.has(old)) : undefined;
|
|
65
|
+
if (old)
|
|
66
|
+
claimed.add(old);
|
|
67
|
+
moved.push(old);
|
|
68
|
+
}
|
|
69
|
+
const same = (child, key) => {
|
|
70
|
+
if (key !== undefined)
|
|
71
|
+
return key;
|
|
72
|
+
if (child.isPlot)
|
|
73
|
+
addFreshKeys(keys, child);
|
|
74
|
+
return child.isText ? "" : createNodeKey();
|
|
75
|
+
};
|
|
76
|
+
const newKeys = [];
|
|
77
|
+
for (let i = 0; i < head; i++) {
|
|
78
|
+
const child = after[i];
|
|
79
|
+
if (child)
|
|
80
|
+
newKeys.push(same(child, oldKeys[i]));
|
|
81
|
+
}
|
|
82
|
+
for (let i = head, pos = newStart + headSize; i < newTail; i++) {
|
|
83
|
+
const child = after[i];
|
|
84
|
+
if (!child)
|
|
85
|
+
continue;
|
|
86
|
+
const start = pos;
|
|
87
|
+
pos += child.length;
|
|
88
|
+
if (child.isText) {
|
|
89
|
+
newKeys.push("");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
let kept = found.get(start) ?? moved[i - head];
|
|
93
|
+
const retag = kept ? undefined : retagged.get(start);
|
|
94
|
+
if (retag && !claimed.has(retag) && retag.node.type == child.type) {
|
|
95
|
+
claimed.add(retag);
|
|
96
|
+
kept = retag;
|
|
97
|
+
}
|
|
98
|
+
// A moved plot is the same object: its content keeps its keys.
|
|
99
|
+
if (child.isPlot && kept?.node.isPlot && kept.node != child)
|
|
100
|
+
mapChildren(kept.node, kept.start + 1, child, start + 1);
|
|
101
|
+
else if (child.isPlot)
|
|
102
|
+
addFreshKeys(keys, child);
|
|
103
|
+
newKeys.push(kept ? kept.key : createNodeKey());
|
|
104
|
+
}
|
|
105
|
+
for (let i = newTail; i < after.length; i++) {
|
|
106
|
+
const child = after[i];
|
|
107
|
+
if (child)
|
|
108
|
+
newKeys.push(same(child, oldKeys[i - newTail + oldTail]));
|
|
109
|
+
}
|
|
110
|
+
keys.set(newPlot, newKeys);
|
|
111
|
+
};
|
|
112
|
+
mapChildren(oldDoc, 0, newDoc, 0);
|
|
113
|
+
return replaced;
|
|
114
|
+
};
|
|
115
|
+
const typeIds = new WeakMap();
|
|
116
|
+
let lastTypeId = 0;
|
|
117
|
+
const typeId = (type) => {
|
|
118
|
+
let id = typeIds.get(type);
|
|
119
|
+
if (id === undefined)
|
|
120
|
+
typeIds.set(type, (id = ++lastTypeId));
|
|
121
|
+
return id;
|
|
122
|
+
};
|
|
123
|
+
// The keys of a list of sibling views (ChildNodeViews renders them, ReactDocTile
|
|
124
|
+
// records them), paired with them. A node's key comes from ReactKeys, through
|
|
125
|
+
// its view (iterDeco); other
|
|
126
|
+
// views are keyed after their previous sibling, so a text run keeps its text
|
|
127
|
+
// node while its start changes. Widgets don't count as that sibling: one
|
|
128
|
+
// coming or going leaves the next text node alone. React point widgets come
|
|
129
|
+
// with keys (iterDeco); other widgets are keyed after the sibling on their
|
|
130
|
+
// side (the next one for a positive side, as a `before` widget sticks to its
|
|
131
|
+
// node) and their type, so a React tag widget keeps its instance while its
|
|
132
|
+
// node does.
|
|
133
|
+
// A composition, and the wrappers around it, keep the keys of the nodes React
|
|
134
|
+
// committed there, and nothing else in their list may take those.
|
|
135
|
+
export const childViewKeys = (views) => {
|
|
136
|
+
const fixed = (view) => view.type == "composition" || view.type == "wrapper" ? view.key : undefined;
|
|
137
|
+
const taken = new Set();
|
|
138
|
+
for (const view of views) {
|
|
139
|
+
const key = fixed(view);
|
|
140
|
+
if (key !== undefined)
|
|
141
|
+
taken.add(key);
|
|
142
|
+
}
|
|
143
|
+
let previous = "";
|
|
144
|
+
let widgets = false;
|
|
145
|
+
// Widgets first get the key before them, and their own key below.
|
|
146
|
+
const keyed = views.map((view) => {
|
|
147
|
+
if (view.type == "widget") {
|
|
148
|
+
if (view.key !== undefined)
|
|
149
|
+
return { view, key: view.key };
|
|
150
|
+
widgets = true;
|
|
151
|
+
return { view, key: previous };
|
|
152
|
+
}
|
|
153
|
+
let key = fixed(view);
|
|
154
|
+
if (key === undefined) {
|
|
155
|
+
key =
|
|
156
|
+
view.type == "node"
|
|
157
|
+
? view.key
|
|
158
|
+
: `${previous}>${view.type == "buffer" ? `buffer${view.side}` : view.type}`;
|
|
159
|
+
while (taken.has(key))
|
|
160
|
+
key += "+";
|
|
161
|
+
}
|
|
162
|
+
previous = key;
|
|
163
|
+
return { view, key };
|
|
164
|
+
});
|
|
165
|
+
if (!widgets)
|
|
166
|
+
return keyed;
|
|
167
|
+
let next = "";
|
|
168
|
+
const used = new Set();
|
|
169
|
+
for (let i = keyed.length - 1; i >= 0; i--) {
|
|
170
|
+
const entry = keyed[i];
|
|
171
|
+
if (!entry)
|
|
172
|
+
continue;
|
|
173
|
+
const { view } = entry;
|
|
174
|
+
if (view.type != "widget") {
|
|
175
|
+
next = entry.key;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (view.key !== undefined)
|
|
179
|
+
continue;
|
|
180
|
+
const id = typeId(view.widget.type);
|
|
181
|
+
let key = view.side > 0 ? `${next}<w${id}` : `${entry.key}>w${id}`;
|
|
182
|
+
while (used.has(key))
|
|
183
|
+
key += "+";
|
|
184
|
+
used.add(key);
|
|
185
|
+
entry.key = key;
|
|
186
|
+
}
|
|
187
|
+
return keyed;
|
|
188
|
+
};
|
package/dist/tile.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import { type Wordgard } from "wordgard/editor";
|
|
3
|
+
import type { GardState } from "wordgard/state";
|
|
4
|
+
import { type NodeView } from "./decorations/iterDeco.js";
|
|
5
|
+
import { type CompositionInfo, type DecoSet, DocTile, type Tile } from "./wordgardInternals.js";
|
|
6
|
+
export type DocPortal = {
|
|
7
|
+
readonly get: () => ReactElement;
|
|
8
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
9
|
+
readonly committed: (element: ReactElement) => void;
|
|
10
|
+
readonly claim: () => () => void;
|
|
11
|
+
readonly mount: () => void;
|
|
12
|
+
};
|
|
13
|
+
export declare const nodeViewOf: (tile: Tile) => NodeView | undefined;
|
|
14
|
+
export declare class RefusedUpdate extends Error {
|
|
15
|
+
}
|
|
16
|
+
export declare const docPortal: (dom: Element) => DocPortal;
|
|
17
|
+
export declare class ReactDocTile extends DocTile {
|
|
18
|
+
widgets: boolean;
|
|
19
|
+
static create: (state: GardState, dom: Element, wg: Wordgard) => ReactDocTile;
|
|
20
|
+
static host: (state: GardState, dom: Element, wg: Wordgard, mount: () => void) => ReactDocTile;
|
|
21
|
+
static mount: (dom: Element, wg: Wordgard, old: DocTile) => ReactDocTile;
|
|
22
|
+
private static render;
|
|
23
|
+
private static build;
|
|
24
|
+
updateRanges(state: GardState, decoSet: DecoSet, changes: readonly number[], wg: Wordgard, info?: CompositionInfo | null): ReactDocTile;
|
|
25
|
+
}
|
|
26
|
+
export declare const observeShapes: (wg: Wordgard, queue: (records: MutationRecord[]) => void) => {
|
|
27
|
+
takeRecords(): MutationRecord[];
|
|
28
|
+
discard(): void;
|
|
29
|
+
disconnect(): void;
|
|
30
|
+
observe(target: Node, options?: MutationObserverInit): void;
|
|
31
|
+
};
|
|
32
|
+
export declare const rebuildShapes: (wg: Wordgard) => void;
|