@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,96 @@
1
+ // Adapted from react-prosemirror's ReactEditorView, to fit Wordgard: it
2
+ // extends Wordgard instead of EditorView and, where ReactEditorView replaces
3
+ // the docView ProseMirror's constructor builds, has the base constructor build
4
+ // a tile tree React renders.
5
+ import { Wordgard } from "wordgard/editor";
6
+ import { GardState } from "wordgard/state";
7
+ import { editorOf } from "./contexts/EditorContext.js";
8
+ import { editorEventHandlers, hasEditorEventHandlers } from "./contexts/editorEvents.js";
9
+ import { ReactDocTile, RefusedUpdate, observeShapes } from "./tile.js";
10
+ import { DocTile } from "./wordgardInternals.js";
11
+ // Asks Wordgard for a frame: a no-op read schedules a flush.
12
+ const retry = () => { };
13
+ export class ReactWordgard
14
+ // @ts-expect-error Wordgard's constructor is private in its typings.
15
+ extends Wordgard {
16
+ // An editor with a React root of its own.
17
+ static create = (spec) => new ReactWordgard(spec, false);
18
+ // An editor WordgardEditor hosts: its document renders through
19
+ // WordgardDoc's portal, in the app's React tree.
20
+ static host = (spec) => new ReactWordgard(spec, true);
21
+ // Wordgard's own tests build tile trees through `Wordgard.DocTile`.
22
+ static DocTile = ReactDocTile;
23
+ constructor(spec, hosted = false) {
24
+ // Wordgard's constructor draws the document with `DocTile.create`, before
25
+ // this one can run: React draws it there instead, so it's drawn once. Put
26
+ // back at once, for any editor React's render creates; other editors
27
+ // created first (by a plugin) draw as before.
28
+ const create = DocTile.create;
29
+ const self = { editor: null };
30
+ DocTile.create = (state, dom, wg) => {
31
+ if (!(wg instanceof ReactWordgard))
32
+ return create(state, dom, wg);
33
+ DocTile.create = create;
34
+ if (!hosted)
35
+ return ReactDocTile.create(state, dom, wg);
36
+ return ReactDocTile.host(state, dom, wg, () => {
37
+ const { editor } = self;
38
+ if (editor)
39
+ editor.docTile = editor.observer.ignore(() => ReactDocTile.mount(editor.contentDOM, editor, editor.docTile));
40
+ });
41
+ };
42
+ // Given a `parent`, it would also put the editor in the page, where
43
+ // connecting measures it: that's left until it's built.
44
+ // useEditorEventListener's handlers come first; Wordgard keeps one copy of
45
+ // an extension, so a state taken from another ReactWordgard is fine.
46
+ const events = spec.state
47
+ ? {
48
+ state: spec.state.update({ effects: GardState.appendConfig.of(editorEventHandlers) })
49
+ .state,
50
+ }
51
+ : { config: [editorEventHandlers, spec.config ?? []] };
52
+ try {
53
+ // @ts-expect-error The typings drop the private constructor's parameter.
54
+ super({ ...spec, ...events, parent: undefined });
55
+ }
56
+ finally {
57
+ DocTile.create = create;
58
+ }
59
+ self.editor = this;
60
+ // React shapes' own commits are sorted out of what Wordgard's DOM
61
+ // observer reads; replaced before the editor is connected and observes.
62
+ this.observer.observer = observeShapes(this, (records) => {
63
+ this.observer.queue.push(...records);
64
+ this.scheduleFlush();
65
+ });
66
+ spec.parent?.appendChild(this.dom);
67
+ }
68
+ // Wordgard's flush runs from an animation frame and from the methods that
69
+ // need an up-to-date DOM; useEditorEffect's effects run after it. A refused
70
+ // update leaves Wordgard's transactions pending with no frame requested for
71
+ // them, so one is.
72
+ flush() {
73
+ const editor = editorOf(this);
74
+ // React shapes that committed since their records were delivered get
75
+ // their tiles first (taking the records rebuilds them): Wordgard reads
76
+ // them before the build.
77
+ this.observer.queue.push(...this.observer.observer.takeRecords());
78
+ editor.flushing = true;
79
+ try {
80
+ super.flush();
81
+ }
82
+ catch (error) {
83
+ if (error instanceof RefusedUpdate)
84
+ this.scheduleDOMRead(retry);
85
+ throw error;
86
+ }
87
+ finally {
88
+ editor.flushing = false;
89
+ }
90
+ editor.runEffects();
91
+ // A reconfiguration (GardState.reconfigure) dropped useEditorEventListener's
92
+ // handlers: they come back with a transaction of their own.
93
+ if (!hasEditorEventHandlers(this.state))
94
+ this.dispatch({ effects: GardState.appendConfig.of(editorEventHandlers) });
95
+ }
96
+ }
@@ -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
+ };
@@ -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,38 @@
1
+ // Named after react-prosemirror's ChildNodeViews, rewritten for Wordgard: draws
2
+ // what iterDeco lays out: nodes, text, mark wrappers, Wordgard's hack nodes and
3
+ // React widgets. Native widgets aren't React's: ReactDocTile puts their DOM in
4
+ // place after the commit.
5
+ import React, { memo } from "react";
6
+ import { cellOf } from "../contexts/RenderStoreContext.js";
7
+ import { isReactWidget } from "../decorations/ReactWidgetType.js";
8
+ import { shapeOf } from "../decorations/iterDeco.js";
9
+ import { childViewKeys } from "../reactKeys.js";
10
+ import { SeparatorHackView } from "./SeparatorHackView.js";
11
+ import { TextNodeView } from "./TextNodeView.js";
12
+ import { TrailingHackView } from "./TrailingHackView.js";
13
+ import { WidgetView } from "./WidgetView.js";
14
+ import { MarkView } from "./marks/MarkView.js";
15
+ import { DefaultNodeView } from "./nodes/DefaultNodeView.js";
16
+ export const renderChildViews = (views) => childViewKeys(views).map(({ view, key }) => {
17
+ switch (view.type) {
18
+ case "node":
19
+ return React.createElement(DefaultNodeView, { key: key, cell: cellOf(view) });
20
+ case "text":
21
+ return React.createElement(TextNodeView, { key: key, text: view.text });
22
+ case "wrapper":
23
+ return React.createElement(MarkView, { key: key, elt: view.elt, views: view.children, mark: view.mark });
24
+ case "buffer":
25
+ return React.createElement(SeparatorHackView, { key: key });
26
+ case "composition":
27
+ // React keeps what it last committed in its own node, so it never
28
+ // writes to it; the browser's own node isn't React's to draw.
29
+ return view.rendered === null ? null : React.createElement(TextNodeView, { key: key, text: view.rendered });
30
+ case "trailingHack":
31
+ return React.createElement(TrailingHackView, { key: key });
32
+ case "widget":
33
+ return isReactWidget(view.widget) ? React.createElement(WidgetView, { key: key, widget: view.widget }) : null;
34
+ }
35
+ });
36
+ // Skipped when a plot's content keeps its shape (iterDeco's shapeOf): the
37
+ // nodes that changed in it get their views from their cells.
38
+ export const ChildNodeViews = memo(({ views }) => React.createElement(React.Fragment, null, renderChildViews(views)), (a, b) => shapeOf(a.views) == shapeOf(b.views));
@@ -0,0 +1,7 @@
1
+ import { type AllHTMLAttributes, type ReactNode } from "react";
2
+ export type DecorationRootProps = AllHTMLAttributes<HTMLElement> & {
3
+ children?: ReactNode;
4
+ };
5
+ export declare const DecorationRoot: import("react").ForwardRefExoticComponent<AllHTMLAttributes<HTMLElement> & {
6
+ children?: ReactNode;
7
+ } & import("react").RefAttributes<Element>>;
@@ -0,0 +1,28 @@
1
+ // A React shape's, wrapper's or mark's root, drawn for its component: the tag
2
+ // its Elt says, Wordgard's attributes and the root's ref, so the component
3
+ // can't get them wrong. The component's own props win, but `className` joins
4
+ // Wordgard's and `style` merges, its keys winning. Not in react-prosemirror.
5
+ import { createElement, forwardRef, useContext, useMemo, } from "react";
6
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
7
+ import { cssToStyle, htmlAttrsToReactProps } from "../helpers/props.js";
8
+ import { mergeRefs, toCallbackRef } from "../helpers/refs.js";
9
+ export const DecorationRoot = forwardRef(({ className, style, ...props }, ref) => {
10
+ const root = useContext(DecorationRootContext);
11
+ const rootRef = root?.ref;
12
+ const merged = useMemo(() => mergeRefs(rootRef, toCallbackRef(ref)), [rootRef, ref]);
13
+ if (!root)
14
+ throw new Error("DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their component");
15
+ // Wordgard's style is a string, which React can't merge with an object.
16
+ const { style: css, ...attrs } = root.attrs;
17
+ const own = htmlAttrsToReactProps(style ? attrs : root.attrs);
18
+ const classes = [own.className, className].filter(Boolean).join(" ");
19
+ return createElement(root.tagName, {
20
+ ...own,
21
+ ...props,
22
+ className: classes || undefined,
23
+ ...(style && { style: { ...cssToStyle(css), ...style } }),
24
+ ref: merged,
25
+ suppressContentEditableWarning: true,
26
+ });
27
+ });
28
+ 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,16 @@
1
+ // Not in react-prosemirror: catches an error thrown while React renders the
2
+ // document, for ReactDocTile to throw it from the update. React 18 rethrows it
3
+ // from flushSync, React 19 reports it as uncaught and commits nothing, and the
4
+ // tiles can't be built over what didn't commit. A class: React has no hook for
5
+ // this.
6
+ import { Component } from "react";
7
+ export class ErrorBoundary extends Component {
8
+ state = { failed: false };
9
+ static getDerivedStateFromError = () => ({ failed: true });
10
+ componentDidCatch(error) {
11
+ this.props.onError(error);
12
+ }
13
+ render() {
14
+ return this.state.failed ? null : this.props.children;
15
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const SeparatorHackView: () => React.JSX.Element;
@@ -0,0 +1,5 @@
1
+ // Named after react-prosemirror's SeparatorHackView, rewritten for Wordgard: the
2
+ // `<img class="wg-buffer">` Wordgard puts at the start and end of an inline plot
3
+ // (`Widget.img`), which gives the cursor somewhere to go.
4
+ import React from "react";
5
+ 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,26 @@
1
+ // Named after react-prosemirror's ReactNodeView, for Wordgard: draws a React
2
+ // shape's component, with the node's content as its children. ReactDocTile
3
+ // builds the tiles over what it commits; the component's own commits are
4
+ // sorted out of Wordgard's DOM observer (`sortRecords`).
5
+ import React, { useMemo, useState } from "react";
6
+ import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
7
+ import { atomRef, createShapeHandle, drawShape, nodePos, shapeAttributes, } from "../decorations/ReactShapeType.js";
8
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
9
+ import { attrsToRecord, componentProps } from "../helpers/index.js";
10
+ import { mergeRefs } from "../helpers/refs.js";
11
+ export const ShapeView = ({ elt, node, content, selected, outerRef }) => {
12
+ const [{ handle, ref, contentDOMRef }] = useState(createShapeHandle);
13
+ const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
14
+ const atom = !elt.hasContent;
15
+ const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers), atomRef(handle, atom)), [ref, outerRef, handlers, handle, atom]);
16
+ const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
17
+ const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
18
+ const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
19
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
20
+ React.createElement(DecorationRootContext.Provider, { value: root }, drawShape(elt, {
21
+ attrs: componentProps(attrs),
22
+ nodeProps: { node, getPos: handlers.getPos, contentDOMRef },
23
+ children: content,
24
+ ref: rootRef,
25
+ }))));
26
+ };
@@ -0,0 +1,5 @@
1
+ type Props = {
2
+ text: string;
3
+ };
4
+ export declare const TextNodeView: import("react").MemoExoticComponent<({ text }: Props) => string>;
5
+ export {};
@@ -0,0 +1,5 @@
1
+ // Named after react-prosemirror's TextNodeView, rewritten for Wordgard: a text
2
+ // leaf renders as a bare text node, its tile is built after the commit
3
+ // (ReactDocTile). Skipped while its text is the same.
4
+ import { memo } from "react";
5
+ export const TextNodeView = memo(({ text }) => text);
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const TrailingHackView: () => React.JSX.Element;
@@ -0,0 +1,5 @@
1
+ // Named after react-prosemirror's TrailingHackView, rewritten for Wordgard: the
2
+ // `<br>` Wordgard's own renderer adds to a textblock that needs one
3
+ // (`Widget.br`).
4
+ import React from "react";
5
+ export const TrailingHackView = () => React.createElement("br", null);
@@ -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,35 @@
1
+ // Named after react-prosemirror's WidgetView, rewritten for Wordgard: draws a
2
+ // React widget's component, whose root element becomes the widget's DOM.
3
+ // ReactDocTile builds its tile after the commit, so there's no view
4
+ // description to register here. Skipped while the widget is `eq`, so the
5
+ // component keeps its DOM, as Wordgard keeps an equal widget's.
6
+ import React, { memo, useMemo, useRef, useState } from "react";
7
+ import { DecorationContext, createDecorationHandlers, decorationRoot, } from "../contexts/DecorationContext.js";
8
+ import { DecorationRootContext } from "../contexts/DecorationRootContext.js";
9
+ import { drawWidget, widgetElement, widgetRoot } from "../decorations/ReactWidgetType.js";
10
+ import { attrsToRecord, componentProps } from "../helpers/index.js";
11
+ import { mergeRefs } from "../helpers/refs.js";
12
+ export const WidgetView = memo(({ widget, attrs = none, selected = false, outerRef }) => {
13
+ const root = useRef(null);
14
+ const [handlers] = useState(() => createDecorationHandlers(() => {
15
+ // A removed element keeps its last tile.
16
+ const tile = root.current?.isConnected && root.current.wgTile;
17
+ if (!tile)
18
+ throw new Error("This React widget isn't drawn");
19
+ return tile.posBefore;
20
+ }));
21
+ const ref = useMemo(() => mergeRefs((dom) => {
22
+ root.current = dom;
23
+ }, widgetRoot, decorationRoot(handlers), outerRef), [root, handlers, outerRef]);
24
+ const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
25
+ const record = useMemo(() => attrsToRecord(attrs), [attrs]);
26
+ const tagName = widgetElement(widget);
27
+ const decorationRootValue = useMemo(() => ({ tagName, attrs: record, ref }), [tagName, record, ref]);
28
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
29
+ React.createElement(DecorationRootContext.Provider, { value: decorationRootValue }, drawWidget(widget, ref, componentProps(record)))));
30
+ }, (a, b) => a.widget.eq(b.widget) &&
31
+ a.outerRef == b.outerRef &&
32
+ a.selected == b.selected &&
33
+ sameAttrs(a.attrs ?? none, b.attrs ?? none));
34
+ const none = [];
35
+ const sameAttrs = (a, b) => a.length == b.length && a.every((value, i) => value == b[i]);
@@ -0,0 +1,4 @@
1
+ import React, { type HTMLAttributes } from "react";
2
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, "children">;
3
+ export declare const WordgardDoc: (props: Props) => React.JSX.Element;
4
+ export {};
@@ -0,0 +1,34 @@
1
+ // Named after react-prosemirror's ProseMirrorDoc, rewritten for Wordgard:
2
+ // puts the editor's element in the page, inside a `div` taking the props, and
3
+ // renders its document through a portal into Wordgard's content element, so
4
+ // the document shares the app's React tree and contexts. The portal commits
5
+ // first and builds the tiles; attaching the element then connects the editor,
6
+ // as Wordgard's own editor is built before it's connected.
7
+ import React, { useCallback, useInsertionEffect, useLayoutEffect, useSyncExternalStore, } from "react";
8
+ import { createPortal } from "react-dom";
9
+ import { editorOf, useEditorContext } from "../contexts/EditorContext.js";
10
+ import { docPortal } from "../tile.js";
11
+ const DocPortal = ({ wg }) => {
12
+ const portal = docPortal(wg.contentDOM);
13
+ const element = useSyncExternalStore(portal.subscribe, portal.get);
14
+ // Insertion effects all run before any layout effect, so a second portal
15
+ // committed alongside this one has claimed the editor by the time `mount`
16
+ // builds the tiles. StrictMode doesn't run them twice.
17
+ useInsertionEffect(() => portal.claim(), [portal]);
18
+ useLayoutEffect(() => portal.committed(element));
19
+ useLayoutEffect(() => portal.mount(), [portal]);
20
+ return createPortal(element, wg.contentDOM);
21
+ };
22
+ export const WordgardDoc = (props) => {
23
+ const { wg } = useEditorContext();
24
+ const attach = useCallback((dom) => {
25
+ if (!dom)
26
+ return;
27
+ dom.appendChild(wg.dom);
28
+ // Effects waiting for the editor to be in the page.
29
+ editorOf(wg).requestEffects();
30
+ }, [wg]);
31
+ return (React.createElement(React.Fragment, null,
32
+ React.createElement(DocPortal, { wg: wg }),
33
+ React.createElement("div", { ...props, ref: attach })));
34
+ };
@@ -0,0 +1,7 @@
1
+ import React, { type ReactNode } from "react";
2
+ import type { Wordgard } from "wordgard/editor";
3
+ type Props = Omit<Wordgard.Spec, "parent"> & {
4
+ children?: ReactNode;
5
+ };
6
+ export declare const WordgardEditor: ({ children, ...spec }: Props) => React.JSX.Element;
7
+ export {};
@@ -0,0 +1,13 @@
1
+ // Named after react-prosemirror's ProseMirror component, rewritten for
2
+ // Wordgard: creates the editor and provides it to what it holds, WordgardDoc
3
+ // and the app's own components (menus, hooks). The spec is read once, as
4
+ // react-prosemirror's `defaultState`: Wordgard's editor owns its state, so
5
+ // changes go through `dispatch`, and configuration through Wordgard's
6
+ // compartments or `GardState.appendConfig`.
7
+ import React, { useState } from "react";
8
+ import { ReactWordgard } from "../ReactWordgard.js";
9
+ import { EditorContext, editorOf } from "../contexts/EditorContext.js";
10
+ export const WordgardEditor = ({ children, ...spec }) => {
11
+ const [editor] = useState(() => editorOf(ReactWordgard.host(spec)));
12
+ return React.createElement(EditorContext.Provider, { value: editor }, children);
13
+ };
@@ -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
+ wrapped: (ref: (dom: Element | null) => void) => ReactNode;
8
+ selected: boolean;
9
+ outerRef?: ((dom: Element | null) => void) | undefined;
10
+ };
11
+ export declare const WrapperView: ({ elt, node, wrapped, selected, outerRef }: Props) => React.JSX.Element;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ // Draws a React wrapper's component, with what it wraps (the node's shape, or
2
+ // the element its `target` picks) as its children. Like 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 { createWrapperHandle, nodePos, shapeAttributes } from "../decorations/ReactShapeType.js";
8
+ import { drawWrapper } from "../decorations/ReactWrapperType.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 WrapperView = ({ elt, node, wrapped, selected, outerRef }) => {
13
+ const [{ handle, ref, wrappedRef }] = useState(createWrapperHandle);
14
+ const [handlers] = useState(() => createDecorationHandlers(nodePos(handle)));
15
+ const rootRef = useMemo(() => mergeRefs(ref, outerRef, decorationRoot(handlers)), [ref, outerRef, handlers]);
16
+ const decoration = useMemo(() => ({ handlers, selected }), [handlers, selected]);
17
+ const attrs = useMemo(() => attrsToRecord(shapeAttributes(elt)), [elt]);
18
+ const root = useMemo(() => ({ tagName: elt.tagName, attrs, ref: rootRef }), [elt.tagName, attrs, rootRef]);
19
+ return (React.createElement(DecorationContext.Provider, { value: decoration },
20
+ React.createElement(DecorationRootContext.Provider, { value: root }, drawWrapper(elt, {
21
+ attrs: componentProps(attrs),
22
+ nodeProps: { node, getPos: handlers.getPos },
23
+ children: wrapped(wrappedRef),
24
+ ref: rootRef,
25
+ }))));
26
+ };
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { Mark } from "wordgard/doc";
3
+ import { type ChildView } from "../../decorations/iterDeco.js";
4
+ import type { DecoElt } from "../../wordgardInternals.js";
5
+ type Props = {
6
+ elt: DecoElt;
7
+ views: readonly ChildView[];
8
+ mark?: Mark | undefined;
9
+ };
10
+ export declare const MarkView: React.MemoExoticComponent<({ elt, views, mark }: Props) => React.JSX.Element>;
11
+ export {};