@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,397 @@
1
+ ---
2
+ name: react-wordgard
3
+ description: Build rich text editors with Wordgard and react-wordgard in a React app. Use when creating or changing a Wordgard editor drawn by React - hosting it, drawing nodes, marks, wrappers or widgets as React components, reading the editor's state in components, positioning menus and popovers, handling the editor's events, or migrating react-prosemirror node views.
4
+ ---
5
+
6
+ # Building editors with Wordgard and react-wordgard
7
+
8
+ react-wordgard is a React view for [Wordgard](https://wordgard.net/docs/), a rich text editor.
9
+ Wordgard runs the editor (document, state, input, selection, IME composition, decorations,
10
+ commands); react-wordgard has React draw its document, inside Wordgard's own update, in the app's
11
+ React tree, so the app's contexts reach what it draws. **The API is Wordgard's**: a customisation
12
+ is a Wordgard extension or decoration, and react-wordgard adds functions that let a React
13
+ component be the decoration.
14
+
15
+ Wordgard's own API: its docs (https://wordgard.net/docs/) and its typings in
16
+ `node_modules/wordgard/dist/*.d.ts` (`wordgard/editor`, `wordgard/state`, `wordgard/doc`,
17
+ `wordgard/schema`, `wordgard/types`, `wordgard/command`). react-wordgard's: its README and
18
+ `node_modules/@stesura/react-wordgard/dist/index.d.ts`. Read them rather than guessing a signature.
19
+
20
+ **New to Wordgard, or porting ProseMirror code?** Read `references/wordgard.md`, next to this file:
21
+ Wordgard's model (plots, leaves, tags, marks on tags), changes, configuration, commands and the
22
+ editor, each against its ProseMirror counterpart, with the traps.
23
+
24
+ ## Setup
25
+
26
+ ```sh
27
+ npm install @stesura/react-wordgard wordgard react react-dom
28
+ ```
29
+
30
+ - Peers: `wordgard` `~0.6.2` (react-wordgard uses its internals: only its patches), React 18 or 19.
31
+ The app must have **one copy** of `wordgard`, or Wordgard's `instanceof` checks fail.
32
+ - TypeScript: Wordgard's typings need `skipLibCheck: true`.
33
+ - Name the editor for screen readers with `Wordgard.contentAttributes.of({ "aria-label": "…" })`
34
+ in the config (`Wordgard.label` names the wrong element in 0.6.2).
35
+
36
+ ## Hosting the editor
37
+
38
+ ```tsx
39
+ import { WordgardDoc, WordgardEditor } from "@stesura/react-wordgard";
40
+ import { basicSchema } from "wordgard/schema";
41
+
42
+ export const Editor = () => (
43
+ <WordgardEditor doc="<p>Hello</p>" config={[basicSchema()]}>
44
+ <WordgardDoc className="editor" />
45
+ </WordgardEditor>
46
+ );
47
+ ```
48
+
49
+ - `WordgardEditor` takes a `Wordgard.Spec` (`doc` as HTML, `config` as extensions, or a `state`),
50
+ **read once**: the editor is uncontrolled and owns its state. Change the document through
51
+ `wg.dispatch(…)`, the configuration through Wordgard's compartments or
52
+ `GardState.appendConfig`, never by re-rendering with new props.
53
+ - `WordgardDoc` puts the editor in the page; one per editor. Components inside `WordgardEditor`
54
+ (toolbars, menus, sidebars, next to `WordgardDoc`) can use the hooks: below.
55
+ - Don't create editors with `Wordgard.create` for a React app: react-wordgard's editor is made by
56
+ `WordgardEditor`. In StrictMode (development) it creates the editor twice and drops one, which is
57
+ never put in the page.
58
+
59
+ ## Reading and changing the editor from components
60
+
61
+ ```tsx
62
+ import { Command, toggleStrong } from "wordgard/command";
63
+ import { useEditorEventCallback, useEditorStateSelector } from "@stesura/react-wordgard";
64
+
65
+ const Toolbar = () => {
66
+ const empty = useEditorStateSelector((state) => state.selection.empty);
67
+ const bold = useEditorEventCallback((wg) => Command.dispatch(wg, toggleStrong));
68
+ return (
69
+ <button
70
+ onMouseDown={(e) => {
71
+ e.preventDefault();
72
+ bold();
73
+ }}
74
+ >
75
+ Bold
76
+ </button>
77
+ );
78
+ };
79
+ ```
80
+
81
+ - `useWordgard()`: the editor (`wg`).
82
+ - `useEditorState()`, `useEditorStateSelector(selector, isEqual?)`: the state as of the editor's
83
+ last update (the one the DOM shows). Prefer the selector: the component re-renders only when its
84
+ result changes (`Object.is`, or `isEqual`).
85
+ - `useEditorEventCallback((wg, ...args) => …)`: a stable callback getting the editor, for event
86
+ handlers. Commands run through `Command.dispatch(wg, command, param?)`; plain changes through
87
+ `wg.dispatch({ changes: { from, to, insert }, selection })`.
88
+ - `useEditorEventListener(type, (wg, event) => handled)`: the editor's DOM events of `type`, before
89
+ Wordgard's handlers and keymaps; return `true` to handle the event (Wordgard then calls
90
+ `preventDefault`). Types: `keydown`, `keyup`, `keypress`, `beforeinput`, `input`, composition
91
+ events, `mousedown`, `mouseup`, `mouseover`, `mouseout`, `click`, `dblclick`, `contextmenu`,
92
+ `focus`, `blur`, `copy`, `cut`, `paste`, drag-and-drop events. Key events during an IME
93
+ composition don't come. Handling `input`, `beforeinput` or a composition event skips Wordgard's
94
+ own handling of it: leave typing to Wordgard.
95
+
96
+ ### Effects and layout
97
+
98
+ Wordgard drives the update and React draws inside it, so there are two effect hooks:
99
+
100
+ | Hook | Runs | May dispatch | What it sets shows | For |
101
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------ | ------------------ | ---------------------------------------------------- |
102
+ | `useEditorEffect((wg) => cleanup?, deps)` | after the editor's update | yes | a frame later | focusing, follow-up dispatches, menus |
103
+ | `useEditorLayoutEffect({ read: (wg) => value, write: (value, wg) => … })` | inside every update: `read` in Wordgard's read phase, `write` in its write phase | **no** (Wordgard throws) | on the same paint | what moves with the text: margin markers, pagination |
104
+
105
+ - **Never read the editor's layout (`coordsAtPos`, `posAtCoords`) in a plain React `useEffect` or
106
+ `useLayoutEffect`**: it runs inside Wordgard's update, before the view of the new DOM exists, and
107
+ react-wordgard refuses the update. Use one of the two hooks above.
108
+ - `useEditorLayoutEffect`'s `read` runs on every update: return `undefined` when nothing changed,
109
+ which skips the write and, when no read asks for one, Wordgard's second measure. Reads run in
110
+ registration order; an engine with ordered parts (pagination) registers one effect and drives
111
+ them.
112
+ - Menus and popovers: position them with your popover library (Floating UI, Radix, Base UI) from a
113
+ virtual anchor built from `wg.coordsAtPos(pos)` in `useEditorEffect`; show or hide them from
114
+ `useEditorStateSelector`. Wordgard's own tooltips and panels also work, as plain DOM.
115
+
116
+ ## Drawing parts of the document with React
117
+
118
+ Prefer Wordgard's declarative shapes (`Elt` shapes, `Decoration.Tag.attribute`, mark shapes) for
119
+ anything that needs no code: React draws them all already. Use a React component where a part needs
120
+ state, interaction or the app's contexts. Each factory below is **called at module level**, with a
121
+ component defined there, and returns a Wordgard value you pass in `config`. `createShape`,
122
+ `createWrapper` and `createMarkShape` return the same value for the same component, so composing
123
+ the config again (per editor) is safe; a component created inside a render is new each time. The
124
+ components need no memoizing: React draws a node again only when it changed, or from the
125
+ component's own state, contexts and hooks.
126
+
127
+ | Need | Factory | Passed to |
128
+ | ------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------- |
129
+ | A node, with content or an atom (callout, image, mention) | `createShape(component)` | `Decoration.Tag.shape(Type, shape)`; `shape.of(element, { atom? })` for `Point.shape` |
130
+ | An element around a node or part of its shape | `createWrapper(component, { element })` | `Decoration.Tag.wrapper(Type, wrapper, { target? })`, `Decoration.Point.wrapper` |
131
+ | A mark's element (link, comment highlight) | `createMarkShape(component, { element })` | `Decoration.Mark.shape(MarkType, shape)` |
132
+ | A widget at a position or beside a node (placeholder, badge) | `createWidgetType({ component, … })` | `Decoration.Point.widget(MyType.of(value), { side })`, `Decoration.Tag.widget` |
133
+
134
+ ```tsx
135
+ import { Decoration } from "wordgard/editor";
136
+ import { basicSchema, blockquote, image } from "wordgard/schema";
137
+ import { Blockquote, Image, Link } from "wordgard/types";
138
+ import { DecorationRoot, createMarkShape, createShape } from "@stesura/react-wordgard";
139
+
140
+ const CalloutShape = createShape(function Callout({ nodeProps: { contentDOMRef }, children }) {
141
+ return (
142
+ <DecorationRoot>
143
+ <span aria-hidden="true">!</span>
144
+ <div ref={contentDOMRef}>{children}</div>
145
+ </DecorationRoot>
146
+ );
147
+ });
148
+
149
+ const LinkMark = createMarkShape(
150
+ function LinkMark({ markProps: { mark, contentDOMRef }, children }) {
151
+ return (
152
+ <DecorationRoot href={String(mark.value)}>
153
+ <span ref={contentDOMRef}>{children}</span>
154
+ </DecorationRoot>
155
+ );
156
+ },
157
+ { element: "a" }
158
+ );
159
+
160
+ // An atom: no content, everything it draws is chrome.
161
+ const PictureShape = createShape(function Picture() {
162
+ return <DecorationRoot />;
163
+ });
164
+
165
+ export const config = [
166
+ basicSchema(),
167
+ blockquote(),
168
+ image(),
169
+ Decoration.Tag.shape(Blockquote, CalloutShape),
170
+ Decoration.Mark.shape(Link, LinkMark),
171
+ Decoration.Tag.shape(Image, PictureShape),
172
+ ];
173
+ ```
174
+
175
+ An image's `src` is its type's own attribute and its `alt` the `ImageAlt` mark's: `DecorationRoot`
176
+ puts both on the `<img>`. Check the parameter types of the node and mark types you use in
177
+ `wordgard/types`.
178
+
179
+ ### The component's contract
180
+
181
+ - **The root is the element the decoration says, and stays that element.** For a shape, that's
182
+ the node type's own outer element, per node (`h1` to `h6` for a heading); for `.of`,
183
+ `createWrapper` and `createMarkShape`, their `element` (`div` for wrappers, `span` for marks, by
184
+ default). Wordgard applies decorations to the shape before React draws (`target` selectors
185
+ match its tag), so the tag can't come from the component. Re-render freely, but don't swap the
186
+ root for another element.
187
+ - **Draw it with `<DecorationRoot>`**, once, at the component's root. It renders that tag with
188
+ Wordgard's attributes (the node type's own, the selection classes `wg-selected-node` and
189
+ `wg-selected-cell`, tag, point and mark attributes, `contenteditable` on an atom) and the root's
190
+ ref. Its own props win, except `className`, joined with Wordgard's, and `style`, merged (its
191
+ keys winning); its `ref` gets the root too. It works on React 18 and 19 without `forwardRef`.
192
+ Widgets use it too (see "React widgets").
193
+ - **Or draw the root by hand**, for more control over the decoration's attributes (drop or change
194
+ one, type the root as its element): the element the decoration says, `{...props}` (Wordgard's
195
+ attributes) and `ref` (`forwardRef` on React 18), never both ways at once. Join your classes with
196
+ `className` (Wordgard's, the selection's among them), and spread your style over `style`
197
+ (Wordgard's, as an object):
198
+
199
+ ```tsx
200
+ const ImageShape = createShape(
201
+ forwardRef<HTMLImageElement, ShapeComponentProps<HTMLImageElement>>(function ImageShape(
202
+ { nodeProps, children, className, style, ...props },
203
+ ref
204
+ ) {
205
+ return (
206
+ <img
207
+ {...props}
208
+ ref={ref}
209
+ className={["picture", className].join(" ").trim()}
210
+ style={{ ...style, outline: "1px solid" }}
211
+ />
212
+ );
213
+ })
214
+ );
215
+ ```
216
+
217
+ - **Only the outer element comes from the node type.** The elements inside a structure shape (a
218
+ figure's `img`) are the component's to draw, from `nodeProps.node`; attributes aimed at them
219
+ arrive on the root (see "A shape, a wrapper, or both").
220
+ - **Content goes where `contentDOMRef` points**, else in the root. `children` is the content (a
221
+ wrapper's `children` is what it wraps; no `contentDOMRef` there).
222
+ - **An atom** (a leaf, or a plot whose shape has no hole) has no content: `children` is empty,
223
+ and a `contentDOMRef` throws. `.of(element, { atom: true })` has no hole: a leaf needs it (for
224
+ `Decoration.Point.shape` before a leaf), and it draws a plot as an atom.
225
+ - Everything else the component draws is **chrome**: made uneditable, never read as document
226
+ content, free to change from the component's state, a context or a store.
227
+ - In a textblock (a paragraph-like node) or a mark, the content element holds only the content.
228
+ Chrome beside a textblock's text (line numbers) is a React widget at its `"start"` or `"end"`:
229
+ `Decoration.Tag.widget(Paragraph, "start", LineNumber.of(null))` (see "React widgets").
230
+ - In a node holding blocks, chrome may sit before or after the content in its element, not
231
+ between the content's nodes.
232
+ - Moving `{children}` under another element remounts the content (a selection or IME
233
+ composition in it ends).
234
+ - Node parameters: `nodeProps.node.tag.param` for a node with content, `node.param` for a leaf;
235
+ `markProps.mark` is the document's mark (`mark.value`).
236
+ - `createWrapper` can't wrap a node a native widget draws (draw it with a React shape or widget), nor text.
237
+ - **Don't write attributes on document elements by hand.** On a React decoration's root they
238
+ survive only until React sets that attribute (a `classList.add` is lost when the selection
239
+ changes `className`); on elements Wordgard draws they're undone at the next update. Put the
240
+ state in the component (`data-resizing={resizing}`), or in a decoration from a state field.
241
+
242
+ ### React widgets
243
+
244
+ ```tsx
245
+ import { Decoration } from "wordgard/editor";
246
+ import { Paragraph } from "wordgard/types";
247
+ import {
248
+ DecorationRoot,
249
+ type WidgetComponentProps,
250
+ createWidgetType,
251
+ } from "@stesura/react-wordgard";
252
+
253
+ // The root is a DecorationRoot, a <span> unless the spec gives an `element`.
254
+ const Badge = createWidgetType({
255
+ component: function Badge({ value }: WidgetComponentProps<string>) {
256
+ return (
257
+ <DecorationRoot className="badge" aria-hidden="true">
258
+ {value}
259
+ </DecorationRoot>
260
+ );
261
+ },
262
+ });
263
+
264
+ export const badges = [
265
+ Decoration.Tag.widget(Paragraph, "start", Badge.of("¶")),
266
+ Decoration.Point.source.of(() =>
267
+ Decoration.Point.set([[Decoration.Point.widget(Badge.of("!")), 1]])
268
+ ),
269
+ ];
270
+ ```
271
+
272
+ - `createWidgetType({ component, element?, eq?, editable?, inFlow?, propagateEvent?, connect?,
273
+ disconnect? })` returns a real `Widget.Type`: its widgets are `Type.of(value)`. The component
274
+ gets `value`.
275
+ - **Draw its root with `<DecorationRoot>`**: `element` (`span` by default), the ref, and the
276
+ node's attributes for a widget drawing a node. Or by hand: any element, with the `ref` the
277
+ component gets (`forwardRef` on React 18), which it **must** put on its root.
278
+ - Widgets are uneditable unless their spec sets `editable`, as in Wordgard. A React widget
279
+ re-renders freely while it keeps its root element.
280
+ - A widget drawing a node (`Decoration.Tag.shape(Type, (tag) => W.of(tag.param))`):
281
+ `DecorationRoot` puts the node's attributes on its root; a root drawn by hand gets them as
282
+ props, to spread. A React shape is the usual way to draw a node.
283
+ - A widget's component may read a context a React shape around it provides (line numbers beside a
284
+ paragraph's text, from the paragraph's shape).
285
+
286
+ ### A shape, a wrapper, or both
287
+
288
+ - The node type's shape decides where attributes go (`target`, `preferTarget`: `ImageAlt` and
289
+ `ImageSize` land on a figure's `img`). Don't replace a shape just to add chrome.
290
+ - React chrome around a node, or an element in its shape, is a **wrapper**:
291
+ `Decoration.Tag.wrapper(Figure, createWrapper(Frame), { target: "img" })`. The `img` keeps its
292
+ `src`, `alt` and width; `Frame` draws a resize handle beside `{children}`.
293
+ - A shape and wrappers on the same type apply to all its nodes. A table: a React shape drawing
294
+ `table > tbody > {children}`, plus a wrapper for a scroll container around it.
295
+ - **One `Decoration.Tag.shape` per node type** draws it: where several features draw the same type
296
+ (react-prosemirror apps merging node views), fold their components into one in the app's code
297
+ and register that, or give each feature a wrapper or a tag widget of its own.
298
+ - Porting a react-prosemirror node view that draws chrome around the node's own element: keep the
299
+ type's shape (or a React shape for the element), and move the chrome into a wrapper.
300
+
301
+ ### Changing a node without remounting it
302
+
303
+ A component keeps its state while its node changes in place or moves within its parent; it
304
+ remounts, with everything inside, when the node is replaced whole, changes type or moves to another
305
+ parent.
306
+
307
+ - **Data an action changes in place** (size, alignment, a callout's kind, a table's widths): a mark
308
+ on the tag, `Mark.Type.define(name, { target: NodeType, … })`, read with
309
+ `node.tag.mark(Type)`, changed with `wg.dispatch({ changes: { from: pos, add: Type.of(v) } })`
310
+ or `{ from: pos, remove: Type.of(v) }`. It merges in collaboration and inverts in history.
311
+ - **What identifies the node** (an image's `src`, a heading's level): its parameter, changed by
312
+ retagging the opening token, `{ from: pos, to: pos + 1, insert: [Heading.of(2)] }`.
313
+ - **Never** replace the whole node (`{ from, to, insert: [node] }`) to change its data: that
314
+ remounts the component.
315
+ - ProseMirror's `setNodeAttribute` / `setNodeMarkup` port to one of the first two.
316
+ - **Moving a node**: delete it and insert the same node object elsewhere in the same parent, in one
317
+ transaction (`changes: [{ from, to: from + node.length }, { from: target, insert: [node] }]`).
318
+ A copy (a new object, or one inserted while the original stays) mounts anew. React moves the
319
+ DOM, so an `<iframe>` in a moved node reloads. Wordgard's drag-and-drop rebuilds what it drops
320
+ from HTML: those moves remount.
321
+
322
+ ### Hooks inside React decorations
323
+
324
+ - `useNodePos()`: a function returning the position of what the decoration draws (call it in
325
+ handlers and effects, not during render).
326
+ - `useIsNodeSelected()`, `useSelectNode(select, deselect?)`: the node selection.
327
+ - `useStopEvent((wg, event) => stop)`: asked for events in the decoration's chrome (inputs,
328
+ buttons in a title bar), on its own elements (its root and the elements around its content
329
+ element: an atom's root, a frame's padding; not a root that is itself the content element) and
330
+ in React widgets. Never for events in the content, unlike react-prosemirror. For an event it
331
+ stops, Wordgard's handlers and keymaps are skipped and the browser's default goes ahead.
332
+ - `useIgnoreMutation((wg, record) => ignore)`: Wordgard doesn't read the DOM mutations it claims
333
+ (typing aside: Wordgard takes it from `beforeinput`).
334
+ - `useMergedDOMRefs(...refs)`: one ref setting several (the forwarded `ref` and `contentDOMRef`
335
+ on one element, a measuring ref).
336
+
337
+ ## Accessibility
338
+
339
+ - The editable element has `role="textbox"` and `aria-multiline`; give it a name (Setup).
340
+ - Chrome is read inline with the document's text: `aria-hidden` on decorative parts, a name on
341
+ every control, controls focusable and wrapped in `useStopEvent` so Wordgard leaves their keys.
342
+
343
+ ## Migrating react-prosemirror node views
344
+
345
+ | react-prosemirror | react-wordgard |
346
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
347
+ | `<ProseMirror state dispatchTransaction>` | `<WordgardEditor doc config>`, uncontrolled; changes via `wg.dispatch` |
348
+ | `<ProseMirrorDoc />` | `<WordgardDoc />` |
349
+ | `nodeViews` | `Decoration.Tag.shape(Type, createShape(component))` in `config`, atoms included |
350
+ | `markViews` | `createMarkShape` with `Decoration.Mark.shape` |
351
+ | `NodeViewComponentProps` | `ShapeComponentProps` (same `nodeProps`, `contentDOMRef`, `children`, `ref`) |
352
+ | `node.attrs` | `node.tag.param` for identity, `node.tag.mark(Type)` for data changed in place |
353
+ | `view` in callbacks | `wg` |
354
+ | `ProseMirror-selectednode`, `selectedCell` | `wg-selected-node`, `wg-selected-cell` |
355
+ | `useNodePos()` returning a number | `useNodePos()` returning a function |
356
+ | `useEditorEffect` for same-paint layout | `useEditorLayoutEffect` |
357
+ | `useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useEditorEventListener` | the same names, the editor first in callbacks |
358
+ | `useIsNodeSelected`, `useMergedDOMRefs` | the same |
359
+ | `innerDecorations`, `useIsComposingIn`, `useIsEditorStatic` | none |
360
+
361
+ ProseMirror's model maps to Wordgard's in its migration guide:
362
+ https://wordgard.net/docs/prosemirror/
363
+
364
+ ## When something fails
365
+
366
+ - "…must draw a DecorationRoot, or forward its ref to its root element": the component's root is
367
+ neither a `DecorationRoot` nor given `ref` (on React 18, a hand-written root needs `forwardRef`).
368
+ - "…drew <x> where its shape says <y>": draw the element the shape says (the node type's, or
369
+ `element`), or pass the drawn one to the shape's `of`.
370
+ - "React's DOM has extra nodes" or "…chrome is between its content's nodes": chrome sits in a
371
+ content element where it can't (see the contract).
372
+ - "A React widget's component must forward its ref to its root element": a widget drawing its
373
+ root by hand needs `forwardRef` (React 18) and `ref` on its root; or use `DecorationRoot`.
374
+ - "DecorationRoot is the root of a React shape, wrapper, mark or widget: use it in their
375
+ component": a `DecorationRoot` outside those components (in a toolbar, say).
376
+ - "A React wrapper's component must draw its children": a wrapper drew nothing where it wraps.
377
+ - "A React shape drawing an atom has no content: drop its contentDOMRef": the node type draws an
378
+ atom (a leaf, a plot without a hole), so its React shape has no content element.
379
+ - "A React shape can't wrap: use createWrapper", "A React wrapper isn't a shape: use createShape":
380
+ a factory's value went to the other decoration.
381
+ - "…is called outside a React shape, wrapper, mark or widget": a decoration hook (`useNodePos`,
382
+ `useStopEvent`, `useIgnoreMutation`, `useSelectNode`, `useIsNodeSelected`) used elsewhere.
383
+ "Used outside a Wordgard editor": an editor hook outside `WordgardEditor`.
384
+ - "This React shape or wrapper isn't drawn" (or widget, or mark): `useNodePos()`'s function called
385
+ during render or after unmount; call it in handlers and effects.
386
+ - "Leaf nodes shapes shouldn't have a content hole": a React shape's `.of(element)` given to a
387
+ leaf; use `.of(element, { atom: true })`.
388
+ - "A React wrapper can't wrap a native widget: draw … with a React widget", "A React wrapper
389
+ can't wrap text": a wrapper around a node a native widget draws, or around text.
390
+ - "This editor is already rendered by another WordgardDoc": one `WordgardDoc` per editor.
391
+ - "react-wordgard can't render part of …", "…can't render the shape of … yet": a shape or
392
+ decoration react-wordgard doesn't draw yet; the message names the node type.
393
+ - "ReactWordgard can't update while React renders or commits": layout was read from a plain React
394
+ effect; use `useEditorEffect` or `useEditorLayoutEffect`.
395
+ - "Cannot dispatch new updates during the editor flush phase": a dispatch from
396
+ `useEditorLayoutEffect` or a render; dispatch from an event handler or `useEditorEffect`.
397
+ - Two copies of `wordgard` (`npm ls wordgard`) break `instanceof` checks in odd places.
@@ -0,0 +1,189 @@
1
+ # Wordgard, for ProseMirror developers
2
+
3
+ Read this when you're new to Wordgard or porting ProseMirror code (react-prosemirror apps
4
+ included). It covers the model and the traps; for exact signatures, read the typings in
5
+ `node_modules/wordgard/dist/*.d.ts`, whose doc comments are Wordgard's reference manual. Wordgard's
6
+ own guides: https://wordgard.net/docs/guide/ and, for ProseMirror users,
7
+ https://wordgard.net/docs/prosemirror/. Written for Wordgard 0.6.
8
+
9
+ ## Modules
10
+
11
+ One package, `wordgard`, in submodules:
12
+
13
+ | Wordgard | Replaces |
14
+ | ------------------ | ----------------------------------------------------------------------------------- |
15
+ | `wordgard/doc` | `prosemirror-model`, `prosemirror-transform` |
16
+ | `wordgard/state` | `prosemirror-state` |
17
+ | `wordgard/editor` | `prosemirror-view`, `prosemirror-keymap`, `prosemirror-menu`, input rules |
18
+ | `wordgard/command` | `prosemirror-commands` |
19
+ | `wordgard/history` | `prosemirror-history` |
20
+ | `wordgard/collab` | `prosemirror-collab` |
21
+ | `wordgard/types` | `prosemirror-schema-basic`, `prosemirror-schema-list`: the node and mark types |
22
+ | `wordgard/schema` | extensions adding those types with their commands, keys and menus (`basicSchema()`) |
23
+
24
+ ## The document
25
+
26
+ - **A node is a plot or a leaf.** A `Plot` has content (`plot.content`, an array); a `Leaf` has
27
+ none. Text is a leaf (`Leaf.Text`, its parameter the string), and so is a line break.
28
+ - **A tag is a node's type, parameter and marks.** A leaf is its own tag; a plot has one
29
+ (`plot.tag`). Read a parameter as `plot.tag.param` or `leaf.param`, a mark as
30
+ `node.tag.mark(Type)` (the value, or `undefined`).
31
+ - **One parameter, no `attrs`.** What identifies a node (a heading's level, an image's `src`) is
32
+ its parameter. Anything else (alignment, size, a callout's kind) is a mark on its tag: a mark
33
+ type with a `target`, added or removed without touching the node.
34
+ - **Marks live on tags**, a plot's as a leaf's, and a mark type says which nodes it may go on
35
+ (`target`), not the parent which content it allows.
36
+ - **No content expressions.** A plot type says what it holds (`blockContent: Node.Group.Content`,
37
+ or `inlineContent: true`); finer rules go above the document (corrections, `wordgard/state`).
38
+ - **Positions count as in ProseMirror**: a character or leaf is 1, a plot 2 plus its content.
39
+ `node.length` is ProseMirror's `nodeSize`; the document's excludes its own tokens.
40
+ - **Walking it:** `doc.iterate(from, to, (node, pos, parent, index) => …)` for `descendants` and
41
+ `nodesBetween` (return `false` to skip a node's content), `doc.nodeAt(pos)`, `doc.plotAt(pos)`,
42
+ `doc.resolve(pos)` (a `Pos`, ProseMirror's `ResolvedPos`), `plot.textContent({ blockSeparator })`.
43
+ - **Types are objects, not names.** Import them (`Heading`, `Paragraph` from `wordgard/types`) and
44
+ compare with `node.type == Heading` or `node.is(Heading)`; names are for JSON and debugging.
45
+
46
+ ### Defining types
47
+
48
+ ```ts
49
+ import { Leaf, Mark, Node, Plot } from "wordgard/doc";
50
+ import { GardState } from "wordgard/state";
51
+
52
+ // A plot without a parameter: `define` gives its tag.
53
+ export const Callout = Plot.define("Callout", {
54
+ blockContent: Node.Group.Content,
55
+ group: Node.Group.Content,
56
+ shape: { element: "aside", selector: "aside.callout", attributes: { class: "callout" } },
57
+ });
58
+
59
+ // Data an editing action changes is a mark on the tag, not a parameter.
60
+ export const CalloutKind = Mark.Type.define<string>("CalloutKind", {
61
+ target: Callout,
62
+ validate: "string",
63
+ shape: { attribute: "data-kind", value: 0 },
64
+ });
65
+
66
+ // A leaf with a parameter: `Type.define`, and nodes are `Mention.of(param)`.
67
+ export const Mention = Leaf.Type.define<string>("Mention", {
68
+ inline: true,
69
+ validate: "string",
70
+ shape: {
71
+ element: "span",
72
+ selector: "span[data-mention]",
73
+ attributes: (id) => ({ "data-mention": id }),
74
+ readElement: (elt) => elt.getAttribute("data-mention") ?? "",
75
+ },
76
+ });
77
+
78
+ // The schema is assembled from the configuration.
79
+ export const schemaPart = GardState.schemaElement.of([Callout, CalloutKind, Mention]);
80
+ ```
81
+
82
+ - `Plot.define` / `Leaf.define` / `Mark.define` for types without a parameter;
83
+ `Plot.Type.define<P>` / `Leaf.Type.define<P>` / `Mark.Type.define<P>` with one (`Heading.of(2)`).
84
+ - `shape` is both `toDOM` and `parseDOM`: an element (with `selector` and `readElement` for
85
+ parsing) or, for a mark, an attribute merged onto its node's element. A `structure` shape draws
86
+ inner elements; `0` marks where the content goes.
87
+
88
+ ## Changes
89
+
90
+ ProseMirror's steps are gone. A transaction is a spec, every position in it counts in the
91
+ document before it, and its changes are one `ChangeSet`, which also maps positions.
92
+
93
+ ```ts
94
+ import type { Node } from "wordgard/doc";
95
+ import type { Wordgard } from "wordgard/editor";
96
+ import { Heading, Strong } from "wordgard/types";
97
+
98
+ export const examples = (wg: Wordgard, pos: number, node: Node, target: number) => {
99
+ // Insert, mark a range, delete: all in the original document's positions.
100
+ wg.dispatch({
101
+ changes: [
102
+ { from: 1, insert: [node] },
103
+ { from: 8, to: 10, add: Strong },
104
+ { from: 20, to: 30 },
105
+ ],
106
+ selection: { anchor: 2 },
107
+ });
108
+ // A parameter change: replace the node's opening token by one of the same type.
109
+ wg.dispatch({ changes: { from: pos, to: pos + 1, insert: [Heading.of(2)] } });
110
+ // A move: delete the node and insert the same object elsewhere.
111
+ wg.dispatch({
112
+ changes: [
113
+ { from: pos, to: pos + node.length },
114
+ { from: target, insert: [node] },
115
+ ],
116
+ });
117
+ };
118
+ ```
119
+
120
+ | ProseMirror | Wordgard |
121
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
122
+ | `tr.insert`, `tr.replaceWith`, `tr.delete` | `changes: { from, to?, insert? }`; `insert` is an array of tokens |
123
+ | `tr.addMark`, `tr.removeMark` | `{ from, to, add: Mark }`, `{ from, to, remove: Mark }` |
124
+ | `tr.setNodeMarkup`, `tr.setNodeAttribute` | a retag (`{ from: pos, to: pos + 1, insert: [Type.of(param)] }`) or a mark on the tag (`{ from: pos, add: Type.of(value) }`) |
125
+ | `tr.mapping.map(pos)` | `tr.changes.mapPos(pos, assoc?)` |
126
+ | `tr.setMeta` | `Transaction.Annotation` (about the transaction), `Transaction.Effect` (a change to state, mapped through changes) |
127
+ | `tr.steps`, `tr.docChanged` | `tr.changes`, `tr.docChanged` |
128
+ | `state.apply(tr)` | `tr.state` (`state.update(spec)` builds the transaction) |
129
+ | `Slice` with open depths | `Slice`, a flat run of tokens: nodes, `Plot.Tag` (an opening token), `Plot.End` |
130
+
131
+ ## Configuration
132
+
133
+ - **Extensions, not plugins.** The configuration is a tree of extensions (CodeMirror 6's system):
134
+ facets (`GardState.Facet`), state fields, precedence (`GardState.prec.highest(…)`), and
135
+ compartments (`new GardState.Compartment()`, `.of(…)`, `.reconfigure(…)` in an effect) or
136
+ `GardState.appendConfig` to change it later.
137
+ - **Plugin state** is a field: `GardState.Field.define({ create, update: (value, tr) => …, provide })`.
138
+ - **Plugin views** are `Wordgard.Plugin.define((wg) => ({ update(update) {}, docUpdate(wg) {} }))`.
139
+ - **Props** are facets: `Wordgard.domEventHandler(type, (event, wg) => handled)`,
140
+ `Wordgard.contentAttributes.of({…})`, `Wordgard.updateListener.of((update) => …)`, decorations.
141
+ - **Keymaps:** `KeyBinding.of({ key: "Mod-b", run: command })`, each binding an extension.
142
+ - **Commands** are `(wg, param) => boolean | Transaction.Spec`, run with
143
+ `Command.dispatch(wg, command, param?)`. `Command.Pure` takes `{ state }` and returns a spec or
144
+ `false`; `Command.bind(command, param)` fixes the parameter for menus and keys;
145
+ `Command.handler(command, other)` lets an extension change what a command does.
146
+ - **History:** `history()` from `wordgard/history`, with the `undo` and `redo` commands.
147
+ - **HTML and JSON:** `parse(schema, element)` and `serialize(doc).toHTML()` from `wordgard/doc`;
148
+ `node.toJSON()` and `state.schema.nodeFromJSON(json)`. A spec's `doc` may be an HTML string.
149
+
150
+ ## Selection
151
+
152
+ - `state.selection` (`GardSelection`) holds raw positions (`anchor`, `head`, `from`, `to`,
153
+ `empty`); `state.sel` the resolved ones (`sel.head.parent.node`).
154
+ - Set it in a spec, `selection: { anchor, head? }`, or with `GardSelection.node(pos, node)` for a
155
+ node selection.
156
+
157
+ ## The editor
158
+
159
+ - **`Wordgard`** is the view. It updates the DOM asynchronously, on the next frame (its _flush_):
160
+ read layout with `coordsAtPos` and the like, which flush first, or after `wg.flush()`. A
161
+ dispatch during the flush throws.
162
+ - **It draws its own cursor** and handles selection keys itself; bind commands to them.
163
+ - **Decorations** come as point decorations (`Decoration.Point`: widgets, attributes, a node's
164
+ shape or wrapper, at a position), range decorations (`Decoration.Range`: wrappers and attributes
165
+ over a range) and tag decorations (`Decoration.Tag`: for every node of a type, in place of node
166
+ views). Point and range sources are functions of the state called on every update: keep large
167
+ sets in a state field and map them (`set.map(tr.changes)`). `decoratePlots` decorates each plot
168
+ of a type from its own content.
169
+ - **No node views.** A node's DOM is its shape; interactive parts are widgets in it, beside its
170
+ content. With react-wordgard, a React component can be the shape: see `SKILL.md`.
171
+
172
+ ## Traps
173
+
174
+ - **Don't replace a node to change its data.** `{ from, to, insert: [newNode] }` makes a new node:
175
+ views remount, and history and collaboration see a replacement. Retag or change a mark.
176
+ - **Don't carry ProseMirror's `attrs` over as one object parameter.** Split identity (the
177
+ parameter) from data that changes (marks with a `target`).
178
+ - **Positions in one spec all count in the original document**: don't adjust the later ones for
179
+ the earlier changes, as with ProseMirror's step-by-step mapping.
180
+ - **Refer to types by object.** `node.type.name == "heading"` breaks when the name differs and
181
+ can't tell two types with one name apart.
182
+ - **Don't read layout right after `dispatch`** without letting the editor flush; and never
183
+ dispatch from inside an update (a plugin's `update`, a field's `update`).
184
+ - **Point and range sources run on every update**: computing a big set there costs every
185
+ keystroke.
186
+ - **Drag-and-drop inside the editor rebuilds what it moves from HTML** (Wordgard 0.6): moved nodes
187
+ are new objects, and whatever their HTML doesn't carry is lost.
188
+ - **`Wordgard.label` names the outer element in 0.6.2**, not the textbox: use
189
+ `Wordgard.contentAttributes.of({ "aria-label": … })`.