@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +450 -2
  4. package/THIRD_PARTY_LICENSES.txt +93 -0
  5. package/dist/ReactWordgard.d.ts +13 -0
  6. package/dist/ReactWordgard.js +96 -0
  7. package/dist/changes.d.ts +14 -0
  8. package/dist/changes.js +163 -0
  9. package/dist/components/ChildNodeViews.d.ts +8 -0
  10. package/dist/components/ChildNodeViews.js +38 -0
  11. package/dist/components/DecorationRoot.d.ts +7 -0
  12. package/dist/components/DecorationRoot.js +28 -0
  13. package/dist/components/ErrorBoundary.d.ts +18 -0
  14. package/dist/components/ErrorBoundary.js +16 -0
  15. package/dist/components/SeparatorHackView.d.ts +2 -0
  16. package/dist/components/SeparatorHackView.js +5 -0
  17. package/dist/components/ShapeView.d.ts +12 -0
  18. package/dist/components/ShapeView.js +26 -0
  19. package/dist/components/TextNodeView.d.ts +5 -0
  20. package/dist/components/TextNodeView.js +5 -0
  21. package/dist/components/TrailingHackView.d.ts +2 -0
  22. package/dist/components/TrailingHackView.js +5 -0
  23. package/dist/components/WidgetView.d.ts +10 -0
  24. package/dist/components/WidgetView.js +35 -0
  25. package/dist/components/WordgardDoc.d.ts +4 -0
  26. package/dist/components/WordgardDoc.js +34 -0
  27. package/dist/components/WordgardEditor.d.ts +7 -0
  28. package/dist/components/WordgardEditor.js +13 -0
  29. package/dist/components/WrapperView.d.ts +12 -0
  30. package/dist/components/WrapperView.js +26 -0
  31. package/dist/components/marks/MarkView.d.ts +11 -0
  32. package/dist/components/marks/MarkView.js +13 -0
  33. package/dist/components/marks/ReactMarkView.d.ts +10 -0
  34. package/dist/components/marks/ReactMarkView.js +32 -0
  35. package/dist/components/nodes/DefaultNodeView.d.ts +16 -0
  36. package/dist/components/nodes/DefaultNodeView.js +116 -0
  37. package/dist/components/nodes/DocNodeView.d.ts +7 -0
  38. package/dist/components/nodes/DocNodeView.js +6 -0
  39. package/dist/composition.d.ts +23 -0
  40. package/dist/composition.js +97 -0
  41. package/dist/contexts/DecorationContext.d.ts +15 -0
  42. package/dist/contexts/DecorationContext.js +38 -0
  43. package/dist/contexts/DecorationRootContext.d.ts +6 -0
  44. package/dist/contexts/DecorationRootContext.js +6 -0
  45. package/dist/contexts/EditorContext.d.ts +26 -0
  46. package/dist/contexts/EditorContext.js +87 -0
  47. package/dist/contexts/RenderStoreContext.d.ts +15 -0
  48. package/dist/contexts/RenderStoreContext.js +43 -0
  49. package/dist/contexts/editorEvents.d.ts +12 -0
  50. package/dist/contexts/editorEvents.js +95 -0
  51. package/dist/decorations/DecoIterator.d.ts +43 -0
  52. package/dist/decorations/DecoIterator.js +462 -0
  53. package/dist/decorations/ReactMarkType.d.ts +30 -0
  54. package/dist/decorations/ReactMarkType.js +48 -0
  55. package/dist/decorations/ReactShapeType.d.ts +49 -0
  56. package/dist/decorations/ReactShapeType.js +204 -0
  57. package/dist/decorations/ReactWidgetType.d.ts +19 -0
  58. package/dist/decorations/ReactWidgetType.js +69 -0
  59. package/dist/decorations/ReactWrapperType.d.ts +22 -0
  60. package/dist/decorations/ReactWrapperType.js +44 -0
  61. package/dist/decorations/iterDeco.d.ts +82 -0
  62. package/dist/decorations/iterDeco.js +409 -0
  63. package/dist/helpers/attributes.d.ts +2 -0
  64. package/dist/helpers/attributes.js +11 -0
  65. package/dist/helpers/index.d.ts +2 -0
  66. package/dist/helpers/index.js +2 -0
  67. package/dist/helpers/props.d.ts +8 -0
  68. package/dist/helpers/props.js +248 -0
  69. package/dist/helpers/refs.d.ts +6 -0
  70. package/dist/helpers/refs.js +20 -0
  71. package/dist/hooks/useEditorEffect.d.ts +3 -0
  72. package/dist/hooks/useEditorEffect.js +24 -0
  73. package/dist/hooks/useEditorEventCallback.d.ts +2 -0
  74. package/dist/hooks/useEditorEventCallback.js +13 -0
  75. package/dist/hooks/useEditorEventListener.d.ts +3 -0
  76. package/dist/hooks/useEditorEventListener.js +26 -0
  77. package/dist/hooks/useEditorLayoutEffect.d.ts +6 -0
  78. package/dist/hooks/useEditorLayoutEffect.js +38 -0
  79. package/dist/hooks/useEditorState.d.ts +1 -0
  80. package/dist/hooks/useEditorState.js +10 -0
  81. package/dist/hooks/useEditorStateSelector.d.ts +2 -0
  82. package/dist/hooks/useEditorStateSelector.js +6 -0
  83. package/dist/hooks/useIgnoreMutation.d.ts +2 -0
  84. package/dist/hooks/useIgnoreMutation.js +18 -0
  85. package/dist/hooks/useIsNodeSelected.d.ts +1 -0
  86. package/dist/hooks/useIsNodeSelected.js +6 -0
  87. package/dist/hooks/useMergedDOMRefs.d.ts +2 -0
  88. package/dist/hooks/useMergedDOMRefs.js +17 -0
  89. package/dist/hooks/useNodePos.d.ts +1 -0
  90. package/dist/hooks/useNodePos.js +8 -0
  91. package/dist/hooks/useSelectNode.d.ts +2 -0
  92. package/dist/hooks/useSelectNode.js +24 -0
  93. package/dist/hooks/useStopEvent.d.ts +2 -0
  94. package/dist/hooks/useStopEvent.js +20 -0
  95. package/dist/hooks/useWordgard.d.ts +1 -0
  96. package/dist/hooks/useWordgard.js +4 -0
  97. package/dist/index.d.ts +20 -0
  98. package/dist/index.js +20 -0
  99. package/dist/reactKeys.d.ts +39 -0
  100. package/dist/reactKeys.js +188 -0
  101. package/dist/tile.d.ts +32 -0
  102. package/dist/tile.js +1324 -0
  103. package/dist/viewRoots.d.ts +2 -0
  104. package/dist/viewRoots.js +8 -0
  105. package/dist/wordgardInternals.d.ts +211 -0
  106. package/dist/wordgardInternals.js +85 -0
  107. package/package.json +119 -4
  108. package/skills/react-wordgard/SKILL.md +397 -0
  109. package/skills/react-wordgard/references/wordgard.md +189 -0
@@ -0,0 +1,13 @@
1
+ // Named after react-prosemirror's MarkView, rewritten for Wordgard: draws a
2
+ // wrapper, either an element-shaped mark or the span holding a text run's
3
+ // attribute-shaped marks, or a React mark's component. It draws what it holds
4
+ // from its views, so it can be skipped while they keep their shape (iterDeco
5
+ // keeps a wrapper's unchanged views; a node that changed gets its view from
6
+ // its cell).
7
+ import React, { createElement, memo } from "react";
8
+ import { shapeOf } from "../../decorations/iterDeco.js";
9
+ import { attrsToRecord, htmlAttrsToReactProps } from "../../helpers/index.js";
10
+ import { renderChildViews } from "../ChildNodeViews.js";
11
+ import { viewRoot } from "../../viewRoots.js";
12
+ import { ReactMarkView } from "./ReactMarkView.js";
13
+ export const MarkView = memo(({ elt, views, mark }) => mark ? (React.createElement(ReactMarkView, { elt: elt, mark: mark }, renderChildViews(views))) : (createElement(elt.tagName, { ...htmlAttrsToReactProps(attrsToRecord(elt.attrs)), ref: viewRoot }, renderChildViews(views))), (a, b) => shapeOf(a.views) == shapeOf(b.views) && a.elt.eq(b.elt) && a.mark == b.mark);
@@ -0,0 +1,10 @@
1
+ import React, { type ReactNode } from "react";
2
+ import type { Mark } from "wordgard/doc";
3
+ import type { DecoElt } from "../../wordgardInternals.js";
4
+ type Props = {
5
+ elt: DecoElt;
6
+ mark: Mark;
7
+ children: ReactNode;
8
+ };
9
+ export declare const ReactMarkView: ({ elt, mark, children }: Props) => React.JSX.Element;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ // Named after react-prosemirror's ReactMarkView, rewritten for Wordgard: draws
2
+ // a React mark's component around the marked content. As ShapeView, it has no
3
+ // effects: ReactDocTile builds the tiles over what it commits, and the
4
+ // component's own commits are sorted out of Wordgard's DOM observer.
5
+ import React, { useMemo, useState } from "react";
6
+ import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../../contexts/DecorationContext.js";
7
+ import { drawMark } from "../../decorations/ReactMarkType.js";
8
+ import { createShapeHandle, shapeAttributes } from "../../decorations/ReactShapeType.js";
9
+ import { DecorationRootContext } from "../../contexts/DecorationRootContext.js";
10
+ import { attrsToRecord, componentProps } from "../../helpers/index.js";
11
+ import { mergeRefs } from "../../helpers/refs.js";
12
+ export const ReactMarkView = ({ elt, mark, children }) => {
13
+ const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
14
+ const [handlers] = useState(() => createDecorationHandlers(() => {
15
+ const tile = handle.root?.isConnected ? handle.root.wgTile : undefined;
16
+ if (!tile)
17
+ throw new Error("This React mark isn't drawn");
18
+ return tile.posBefore;
19
+ }));
20
+ const rootRef = useMemo(() => mergeRefs(ref, decorationRoot(handlers)), [ref, handlers]);
21
+ // A mark is no node: never selected.
22
+ const decoration = useMemo(() => ({ handlers, selected: false }), [handlers]);
23
+ const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
24
+ const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
25
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
26
+ React.createElement(DecorationRootContext.Provider, { value: root }, drawMark(elt, {
27
+ attrs: componentProps(attrs),
28
+ markProps: { mark, getPos: handlers.getPos, contentDOMRef },
29
+ children,
30
+ ref: rootRef,
31
+ }))));
32
+ };
@@ -0,0 +1,16 @@
1
+ import React, { Component } from "react";
2
+ import type { NodeCell } from "../../contexts/RenderStoreContext.js";
3
+ type Props = {
4
+ cell: NodeCell;
5
+ };
6
+ export declare class DefaultNodeView extends Component<Props> {
7
+ private drawn;
8
+ private readonly redraw;
9
+ shouldComponentUpdate(next: Props): boolean;
10
+ componentDidMount(): void;
11
+ componentDidUpdate(prev: Props): void;
12
+ componentWillUnmount(): void;
13
+ private listen;
14
+ render(): React.JSX.Element | null;
15
+ }
16
+ export {};
@@ -0,0 +1,116 @@
1
+ // Named after react-prosemirror's DefaultNodeView, rewritten for Wordgard: a
2
+ // node renders as the shape Wordgard's decorations give it (its
3
+ // buildNodeShape), with the plot's content in the hole. In Wordgard every node
4
+ // type has a shape, so there's no missing `toDOM` to check for. Widgets, as a
5
+ // node's shape or inside it, are drawn by their component when they're React
6
+ // widgets, else draw nothing: ReactDocTile puts their DOM in place. React
7
+ // shapes and wrappers in the shape are drawn by ShapeView and WrapperView.
8
+ import React, { Component, createElement } from "react";
9
+ import { Attributes, Elt } from "wordgard/doc";
10
+ import { isReactShape, shapeAttributes } from "../../decorations/ReactShapeType.js";
11
+ import { attributedWidget, isReactWidget } from "../../decorations/ReactWidgetType.js";
12
+ import { isReactWrapper } from "../../decorations/ReactWrapperType.js";
13
+ import { uneditable } from "../../decorations/iterDeco.js";
14
+ import { attrsToRecord, htmlAttrsToReactProps } from "../../helpers/index.js";
15
+ import { ChildNodeViews } from "../ChildNodeViews.js";
16
+ import { ShapeView } from "../ShapeView.js";
17
+ import { WidgetView } from "../WidgetView.js";
18
+ import { WrapperView } from "../WrapperView.js";
19
+ import { viewRoot } from "../../viewRoots.js";
20
+ const renderShape = (draw, shape, inEditable, key, ref) => {
21
+ const { node, content, selected } = draw;
22
+ const widget = attributedWidget(shape);
23
+ if (widget)
24
+ return (React.createElement(WidgetView, { key: key, widget: widget, attrs: shapeAttributes(shape), selected: selected, outerRef: ref }));
25
+ const { shape: elt, inEditable: inner } = uneditable(shape, inEditable);
26
+ // Wordgard fills a wrapper's hole with what it wraps, which a React shape
27
+ // would drop.
28
+ if (isReactShape(elt) && (elt.children.length > 1 || elt.children.some((child) => child !== 0)))
29
+ throw new Error("A React shape can't wrap: use createWrapper");
30
+ if (isReactShape(elt))
31
+ return (React.createElement(ShapeView, { key: key, elt: elt, node: node, content: content, selected: selected, outerRef: ref }));
32
+ if (isReactWrapper(elt))
33
+ return (React.createElement(WrapperView, { key: key, elt: elt, node: node, wrapped: (wrappedRef) => renderWrapped(draw, elt, inner, wrappedRef), selected: selected, outerRef: ref }));
34
+ const props = {
35
+ key,
36
+ ref,
37
+ suppressContentEditableWarning: true,
38
+ ...htmlAttrsToReactProps(attrsToRecord(elt.attrs)),
39
+ };
40
+ return createElement(elt.tagName, props, ...elt.children.map((child, i) => {
41
+ if (child === 0)
42
+ return content;
43
+ if (typeof child == "string")
44
+ return child;
45
+ if (child instanceof Elt)
46
+ return renderShape(draw, child, inner, i);
47
+ return isReactWidget(child) ? (React.createElement(WidgetView, { key: i, widget: child, selected: selected })) : null;
48
+ }));
49
+ };
50
+ // What a React wrapper wraps: its hole holds one element or widget, whose root
51
+ // the wrapper's tiles are built down to. A native widget's DOM is placed after
52
+ // the commit, where React drew nothing: there'd be no telling where the
53
+ // component put its children.
54
+ const renderWrapped = (draw, wrapper, inEditable, ref) => {
55
+ const [child, ...rest] = wrapper.children;
56
+ if (child === 0)
57
+ throw new Error("A React wrapper isn't a shape: use createShape");
58
+ if (rest.length || child === undefined || typeof child == "string")
59
+ throw new Error("A React wrapper's hole holds one element or widget");
60
+ if (child instanceof Elt)
61
+ return renderShape(draw, child, inEditable, undefined, ref);
62
+ if (!isReactWidget(child))
63
+ throw new Error(`A React wrapper can't wrap a native widget: draw ${draw.node.name} with a React widget`);
64
+ return React.createElement(WidgetView, { widget: child, selected: draw.selected, outerRef: ref });
65
+ };
66
+ // Wordgard draws a node selection as a class on the node's shape: on its outer
67
+ // element (a widget-drawn node gets one around it), or inside a wrapper a point
68
+ // decoration added after it.
69
+ const isSelected = (shape) => (Attributes.get(shape.attrs, "class") ?? "").split(" ").includes("wg-selected-node") ||
70
+ shape.children.some((child) => child instanceof Elt && isSelected(child));
71
+ const draw = ({ node, shape, children }) => {
72
+ if (!(shape instanceof Elt))
73
+ return isReactWidget(shape) ? React.createElement(WidgetView, { widget: shape }) : null;
74
+ // Keyed, so React keeps the content where a shape moves its hole: the tile
75
+ // build counts on an element React kept keeping its content.
76
+ const content = node.isPlot && children ? React.createElement(ChildNodeViews, { key: "content", views: children }) : null;
77
+ return renderShape({ node, content, selected: isSelected(shape) }, shape, true, undefined, viewRoot);
78
+ };
79
+ // Renders when its cell gets a new view, not when its list renders again:
80
+ // Wordgard's documents share unchanged nodes between states, and their layouts
81
+ // are kept outside redrawn ranges. A class, so a node's listener costs no hook
82
+ // state and an update no effect (useSyncExternalStore made Bold all over 5,000
83
+ // paragraphs about 15% slower, and held more memory).
84
+ export class DefaultNodeView extends Component {
85
+ // What it drew last, to catch a view its cell got before it listened.
86
+ drawn = null;
87
+ redraw = () => this.forceUpdate();
88
+ shouldComponentUpdate(next) {
89
+ return next.cell != this.props.cell;
90
+ }
91
+ componentDidMount() {
92
+ this.listen();
93
+ }
94
+ componentDidUpdate(prev) {
95
+ if (prev.cell == this.props.cell)
96
+ return;
97
+ if (prev.cell.listener == this.redraw)
98
+ prev.cell.listener = null;
99
+ this.listen();
100
+ }
101
+ componentWillUnmount() {
102
+ const { cell } = this.props;
103
+ if (cell.listener == this.redraw)
104
+ cell.listener = null;
105
+ }
106
+ listen() {
107
+ const { cell } = this.props;
108
+ cell.listener = this.redraw;
109
+ if (cell.view != this.drawn)
110
+ this.forceUpdate();
111
+ }
112
+ render() {
113
+ this.drawn = this.props.cell.view;
114
+ return draw(this.drawn);
115
+ }
116
+ }
@@ -0,0 +1,7 @@
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 DocNodeView: ({ views }: Props) => React.JSX.Element;
7
+ export {};
@@ -0,0 +1,6 @@
1
+ // Named after react-prosemirror's DocNodeView, rewritten for Wordgard: the
2
+ // document's content renders straight into Wordgard's content element, which
3
+ // is React's container.
4
+ import React from "react";
5
+ import { ChildNodeViews } from "../ChildNodeViews.js";
6
+ export const DocNodeView = ({ views }) => React.createElement(ChildNodeViews, { views: views });
@@ -0,0 +1,23 @@
1
+ import { Elt, type Mark, type Plot } from "wordgard/doc";
2
+ import { type CompositionLayout } from "./decorations/iterDeco.js";
3
+ import { type CompositionInfo } from "./wordgardInternals.js";
4
+ export type RenderedText = {
5
+ readonly key: string;
6
+ readonly text: string;
7
+ };
8
+ export type Rendered = {
9
+ readonly texts: WeakMap<Text, RenderedText>;
10
+ readonly wrappers: WeakMap<Element, string>;
11
+ };
12
+ export type CompositionView = {
13
+ readonly plot: Plot;
14
+ readonly path: ReadonlySet<Plot>;
15
+ readonly layout: CompositionLayout;
16
+ readonly target: Text | null;
17
+ readonly node: ChildNode | null;
18
+ readonly text: string;
19
+ readonly wrapCursor: Mark.Set | null;
20
+ readonly rendered: RenderedText | null;
21
+ };
22
+ export declare const compositionView: (doc: Plot, info: CompositionInfo, changes: readonly number[], rendered: Rendered) => CompositionView | null;
23
+ export declare const browserElt: (dom: Element) => Elt<never>;
@@ -0,0 +1,97 @@
1
+ // How the React view keeps a composition alive through Wordgard's flush, as
2
+ // Wordgard's ContentUpdate.composition does (src/editor/tile.ts in its repo):
3
+ // the browser's text node stays as it is, in a tile of its own. React must not
4
+ // write to it: when React drew it, React keeps rendering the value it last
5
+ // committed there, in the wrappers it committed around it; when the browser
6
+ // made it (or an element around it), React draws nothing in its place.
7
+ import { Attributes, Elt } from "wordgard/doc";
8
+ import { cursorWrappers, } from "./decorations/iterDeco.js";
9
+ import { markOfTile, markTiles } from "./decorations/ReactMarkType.js";
10
+ import { TileFlag, getTileClasses } from "./wordgardInternals.js";
11
+ // The innermost plot holding [from, to), and the plots down to it.
12
+ const plotAround = (doc, from, to) => {
13
+ const path = new Set();
14
+ let plot = doc;
15
+ let start = 0;
16
+ for (;;) {
17
+ let pos = start;
18
+ let next = null;
19
+ for (const child of plot.content) {
20
+ if (child.isPlot && pos + 1 <= from && to <= pos + child.length - 1) {
21
+ next = { plot: child, start: pos + 1 };
22
+ break;
23
+ }
24
+ pos += child.length;
25
+ }
26
+ if (!next)
27
+ return { plot, start, path };
28
+ path.add(next.plot);
29
+ ({ plot, start } = next);
30
+ }
31
+ };
32
+ // An element React put there: one of its wrappers, a React mark's element, or
33
+ // what holds a plot's content.
34
+ const isReacts = (dom, rendered) => (dom instanceof Element && rendered.wrappers.has(dom)) ||
35
+ !!dom.wgTile?.isDoc ||
36
+ !!(dom.wgTile && (dom.wgTile.flags & TileFlag.PlotContent || markTiles.has(dom.wgTile)));
37
+ // The outermost node around `target` that React didn't put there.
38
+ const browserMade = (target, rendered) => {
39
+ let node = target;
40
+ while (node.parentNode instanceof Element && !isReacts(node.parentNode, rendered))
41
+ node = node.parentNode;
42
+ return node;
43
+ };
44
+ // The wrappers `node` sits in, outermost first, as the tiles over them say
45
+ // (ContentUpdate.composition walks up to the node), with React's keys. A React
46
+ // mark's elements are one wrapper, its root's.
47
+ const domWrappers = (node, rendered) => {
48
+ const wrappers = [];
49
+ for (let dom = node.parentNode; dom instanceof Element; dom = dom.parentNode) {
50
+ const tile = dom.wgTile;
51
+ if (!(tile instanceof getTileClasses().EltTile) || !(tile.flags & TileFlag.Wrapper))
52
+ break;
53
+ const mark = markOfTile.get(tile);
54
+ if (markTiles.has(tile) && !mark)
55
+ continue;
56
+ const key = rendered.wrappers.get(dom);
57
+ wrappers.unshift({ elt: tile.elt, spanning: tile.isSpanning, key, ...(mark && { mark }) });
58
+ }
59
+ return wrappers;
60
+ };
61
+ // Wordgard's separateChange fails, and the flush drops the composition, when a
62
+ // replaced range reaches across the composition's edges.
63
+ const separates = (changes, fromB, toB) => {
64
+ for (let i = 0, posB = 0; i < changes.length; i += 2) {
65
+ const len = changes[i] ?? 0;
66
+ const ins = changes[i + 1] ?? 0;
67
+ const endB = posB + (ins < 0 ? len : ins);
68
+ if (ins >= 0 && fromB < endB && toB > posB && (posB < fromB || endB > toB))
69
+ return false;
70
+ posB = endB;
71
+ }
72
+ return true;
73
+ };
74
+ export const compositionView = (doc, info, changes, rendered) => {
75
+ if (!separates(changes, info.fromB, info.toB))
76
+ return null;
77
+ const { plot, start, path } = plotAround(doc, info.fromB, info.toB);
78
+ const node = info.target && browserMade(info.target, rendered);
79
+ return {
80
+ plot,
81
+ path,
82
+ layout: {
83
+ from: info.fromB - start,
84
+ to: info.toB - start,
85
+ wrappers: node ? domWrappers(node, rendered) : cursorWrappers(info.wrapCursor ?? []),
86
+ cursor: !info.target,
87
+ },
88
+ target: info.target,
89
+ node,
90
+ text: info.text,
91
+ wrapCursor: info.wrapCursor ?? null,
92
+ rendered: (info.target && node == info.target && rendered.texts.get(info.target)) || null,
93
+ };
94
+ };
95
+ // The tile Wordgard's ContentUpdate.composition makes for an element the
96
+ // browser put around the composed text: its tag and attributes, a content hole.
97
+ export const browserElt = (dom) => Elt.create(dom.nodeName.toLowerCase(), Attributes.read(Object.fromEntries(dom.getAttributeNames().map((name) => [name, dom.getAttribute(name)]))), [0]);
@@ -0,0 +1,15 @@
1
+ export type DecorationHandlers = {
2
+ readonly getPos: () => number;
3
+ readonly stopEvent: Set<(event: Event) => boolean>;
4
+ readonly ignoreMutation: Set<(record: MutationRecord) => boolean>;
5
+ };
6
+ export type DecorationContextValue = {
7
+ readonly handlers: DecorationHandlers;
8
+ readonly selected: boolean;
9
+ };
10
+ export declare const DecorationContext: import("react").Context<DecorationContextValue | null>;
11
+ export declare const useDecorationContext: (hook: string) => DecorationContextValue;
12
+ export declare const createDecorationHandlers: (getPos: () => number) => DecorationHandlers;
13
+ export declare const decorationRoot: (handlers: DecorationHandlers) => (dom: Element | null) => void;
14
+ export declare const stopsEvent: (event: Event) => boolean;
15
+ export declare const ignoresMutation: (record: MutationRecord) => boolean;
@@ -0,0 +1,38 @@
1
+ // The React decoration (shape, wrapper, mark or widget) a component is drawn
2
+ // in, for the hooks react-prosemirror's node views use: its position, whether
3
+ // its node is selected, and the handlers its components register. It takes
4
+ // the place of react-prosemirror's GetPosContext, SelectNodeContext,
5
+ // StopEventContext and IgnoreMutationContext.
6
+ import { createContext, useContext } from "react";
7
+ export const DecorationContext = createContext(null);
8
+ export const useDecorationContext = (hook) => {
9
+ const context = useContext(DecorationContext);
10
+ if (!context)
11
+ throw new Error(`${hook} is called outside a React shape, wrapper, mark or widget`);
12
+ return context;
13
+ };
14
+ export const createDecorationHandlers = (getPos) => ({
15
+ getPos,
16
+ stopEvent: new Set(),
17
+ ignoreMutation: new Set(),
18
+ });
19
+ // The handlers of the decorations whose root elements they are.
20
+ const roots = new WeakMap();
21
+ export const decorationRoot = (handlers) => (dom) => {
22
+ if (dom)
23
+ roots.set(dom, handlers);
24
+ };
25
+ // Whether a decoration around `node`, innermost first, says yes. Only in
26
+ // `node`'s own editor: one nested in a decoration isn't its.
27
+ const someAround = (node, says) => {
28
+ for (let cur = node; cur && !cur.wgTile?.isDoc; cur = cur.parentNode) {
29
+ const handlers = cur instanceof Element ? roots.get(cur) : undefined;
30
+ if (handlers && says(handlers))
31
+ return true;
32
+ }
33
+ return false;
34
+ };
35
+ // Whether a decoration around the event's target stops it (useStopEvent).
36
+ export const stopsEvent = (event) => someAround(event.target instanceof Node ? event.target : null, ({ stopEvent }) => [...stopEvent].some((stop) => stop(event)));
37
+ // Whether a decoration around the record's target claims it (useIgnoreMutation).
38
+ export const ignoresMutation = (record) => someAround(record.target, ({ ignoreMutation }) => [...ignoreMutation].some((ignore) => ignore(record)));
@@ -0,0 +1,6 @@
1
+ export type DecorationRootValue = {
2
+ tagName: string;
3
+ attrs: Record<string, string>;
4
+ ref: (dom: Element | null) => void;
5
+ };
6
+ export declare const DecorationRootContext: import("react").Context<DecorationRootValue | null>;
@@ -0,0 +1,6 @@
1
+ // What DecorationRoot draws: the root a React shape's, wrapper's or mark's Elt
2
+ // says, with Wordgard's attributes and the root's ref. Not in
3
+ // react-prosemirror, whose node views draw their root themselves: Wordgard
4
+ // fixes the root's tag before React draws.
5
+ import { createContext } from "react";
6
+ export const DecorationRootContext = createContext(null);
@@ -0,0 +1,26 @@
1
+ import type { Wordgard } from "wordgard/editor";
2
+ import type { GardState } from "wordgard/state";
3
+ export type EditorStore = {
4
+ readonly getState: () => GardState;
5
+ readonly subscribe: (listener: () => void) => () => void;
6
+ };
7
+ export type Measure = () => (() => void) | null;
8
+ export type EditorContextValue = {
9
+ readonly wg: Wordgard;
10
+ readonly store: EditorStore;
11
+ readonly afterFlush: (effect: () => void) => void;
12
+ readonly cancel: (effect: () => void) => void;
13
+ readonly measures: Set<Measure>;
14
+ readonly requestMeasure: () => void;
15
+ };
16
+ type Editor = EditorContextValue & {
17
+ readonly publish: (state: GardState) => void;
18
+ readonly readMeasures: (wg: Wordgard) => void;
19
+ readonly runEffects: () => void;
20
+ readonly requestEffects: () => void;
21
+ flushing: boolean;
22
+ };
23
+ export declare const editorOf: (wg: Wordgard) => Editor;
24
+ export declare const EditorContext: import("react").Context<EditorContextValue | null>;
25
+ export declare const useEditorContext: () => EditorContextValue;
26
+ export {};
@@ -0,0 +1,87 @@
1
+ // Named after react-prosemirror's EditorContext, rewritten for Wordgard: the
2
+ // editor, the state as of its last flush (react-prosemirror's state store),
3
+ // and what the hooks register to run around its flush. One value per editor,
4
+ // which never changes, so memoised components aren't scanned for it.
5
+ import { createContext, useContext } from "react";
6
+ import { flushSync } from "react-dom";
7
+ const editors = new WeakMap();
8
+ export const editorOf = (wg) => {
9
+ const found = editors.get(wg);
10
+ if (found)
11
+ return found;
12
+ let state = wg.state;
13
+ const listeners = new Set();
14
+ const effects = new Set();
15
+ const measures = new Set();
16
+ let requested = false;
17
+ // After a flush the DOM shows `state`, even when a dispatch since then
18
+ // (an update listener's) has put `wg.state` ahead.
19
+ const runEffects = () => {
20
+ if (!wg.connected)
21
+ return;
22
+ const pending = [...effects];
23
+ effects.clear();
24
+ for (const effect of pending)
25
+ effect();
26
+ };
27
+ // Without a flush under way or pending, the DOM is up to date.
28
+ const requestEffects = () => {
29
+ if (requested)
30
+ return;
31
+ requested = true;
32
+ queueMicrotask(() => {
33
+ requested = false;
34
+ if (!editor.flushing && wg.state == state)
35
+ runEffects();
36
+ });
37
+ };
38
+ const readMeasures = (wg) => {
39
+ const writes = [...measures].flatMap((measure) => measure() ?? []);
40
+ // A scheduled write makes Wordgard measure again.
41
+ if (!writes.length)
42
+ return;
43
+ // React state set by a write commits now, on the same paint.
44
+ wg.scheduleDOMWrite(() => flushSync(() => {
45
+ for (const write of writes)
46
+ write();
47
+ }));
48
+ };
49
+ const editor = {
50
+ wg,
51
+ store: {
52
+ getState: () => state,
53
+ subscribe: (listener) => {
54
+ listeners.add(listener);
55
+ return () => listeners.delete(listener);
56
+ },
57
+ },
58
+ afterFlush: (effect) => {
59
+ effects.add(effect);
60
+ if (!editor.flushing)
61
+ requestEffects();
62
+ },
63
+ cancel: (effect) => effects.delete(effect),
64
+ measures,
65
+ requestMeasure: () => wg.scheduleDOMRead(readMeasures),
66
+ publish: (next) => {
67
+ if (next == state)
68
+ return;
69
+ state = next;
70
+ for (const listener of listeners)
71
+ listener();
72
+ },
73
+ readMeasures,
74
+ runEffects,
75
+ requestEffects,
76
+ flushing: false,
77
+ };
78
+ editors.set(wg, editor);
79
+ return editor;
80
+ };
81
+ export const EditorContext = createContext(null);
82
+ export const useEditorContext = () => {
83
+ const context = useContext(EditorContext);
84
+ if (!context)
85
+ throw new Error("Used outside a Wordgard editor");
86
+ return context;
87
+ };
@@ -0,0 +1,15 @@
1
+ import type { NodeView } from "../decorations/iterDeco.js";
2
+ import type { ReactKeys } from "../reactKeys.js";
3
+ export type NodeCell = {
4
+ view: NodeView;
5
+ listener: (() => void) | null;
6
+ };
7
+ export type RenderStore = {
8
+ readonly keys: ReactKeys;
9
+ readonly pending: Map<NodeCell, NodeView>;
10
+ };
11
+ export declare const cellOf: (view: NodeView) => NodeCell;
12
+ export declare const moveCell: (store: RenderStore, old: NodeView, view: NodeView) => void;
13
+ export declare const publishCells: (store: RenderStore) => () => void;
14
+ export declare const RenderStoreContext: import("react").Context<RenderStore | null>;
15
+ export declare const useRenderStore: () => RenderStore;
@@ -0,0 +1,43 @@
1
+ // Named after our react-prosemirror fork's RenderStoreContext, rewritten for
2
+ // Wordgard: what the view computes for rendering, held in one object per
3
+ // content element and provided once, so its context value never changes and
4
+ // memoised node views are never scanned for it. ReactDocTile writes it inside
5
+ // Wordgard's flush, before React renders.
6
+ import { createContext, useContext } from "react";
7
+ const cells = new WeakMap();
8
+ export const cellOf = (view) => {
9
+ let cell = cells.get(view);
10
+ if (!cell)
11
+ cells.set(view, (cell = { view, listener: null }));
12
+ return cell;
13
+ };
14
+ // `view` takes the place of `old`, its node's view in the last layout.
15
+ export const moveCell = (store, old, view) => {
16
+ const cell = cellOf(old);
17
+ cells.set(view, cell);
18
+ if (cell.view == view)
19
+ store.pending.delete(cell);
20
+ else
21
+ store.pending.set(cell, view);
22
+ };
23
+ // Gives the cells their views, inside the commit's flushSync. Returns what
24
+ // takes them back, for an update React refused.
25
+ export const publishCells = (store) => {
26
+ const before = [];
27
+ for (const [cell, view] of store.pending) {
28
+ before.push([cell, cell.view]);
29
+ cell.view = view;
30
+ cell.listener?.();
31
+ }
32
+ return () => {
33
+ for (const [cell, view] of before)
34
+ cell.view = view;
35
+ };
36
+ };
37
+ export const RenderStoreContext = createContext(null);
38
+ export const useRenderStore = () => {
39
+ const store = useContext(RenderStoreContext);
40
+ if (!store)
41
+ throw new Error("Rendered outside a ReactDocTile");
42
+ return store;
43
+ };
@@ -0,0 +1,12 @@
1
+ import { Wordgard } from "wordgard/editor";
2
+ import { GardState } from "wordgard/state";
3
+ export declare const editorEvents: readonly ["keydown", "keyup", "keypress", "beforeinput", "input", "mousedown", "mouseup", "mouseover", "mouseout", "click", "dblclick", "contextmenu", "focus", "blur", "copy", "cut", "paste", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "drop", "compositionstart", "compositionupdate", "compositionend"];
4
+ export type EditorEvent = (typeof editorEvents)[number];
5
+ type Listener<Type extends EditorEvent> = (event: HTMLElementEventMap[Type]) => boolean | void;
6
+ type Listeners = {
7
+ [Type in EditorEvent]: Listener<Type>[];
8
+ };
9
+ export declare const listenersOf: <Type extends EditorEvent>(wg: Wordgard, type: Type) => Listeners[Type];
10
+ export declare const hasEditorEventHandlers: (state: GardState) => boolean;
11
+ export declare const editorEventHandlers: GardState.Extension;
12
+ export {};