@quillmark/svelte 0.0.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +58 -0
  3. package/README.md +241 -0
  4. package/THEMING.md +153 -0
  5. package/dist/core/address.d.ts +56 -0
  6. package/dist/core/address.js +95 -0
  7. package/dist/core/bloom.d.ts +37 -0
  8. package/dist/core/bloom.js +116 -0
  9. package/dist/core/codec/anchor.d.ts +19 -0
  10. package/dist/core/codec/anchor.js +36 -0
  11. package/dist/core/codec/code.d.ts +12 -0
  12. package/dist/core/codec/code.js +118 -0
  13. package/dist/core/codec/decode.d.ts +24 -0
  14. package/dist/core/codec/decode.js +270 -0
  15. package/dist/core/codec/encode.d.ts +71 -0
  16. package/dist/core/codec/encode.js +480 -0
  17. package/dist/core/codec/field.d.ts +171 -0
  18. package/dist/core/codec/field.js +493 -0
  19. package/dist/core/codec/index.d.ts +22 -0
  20. package/dist/core/codec/index.js +43 -0
  21. package/dist/core/codec/inputrules.d.ts +7 -0
  22. package/dist/core/codec/inputrules.js +168 -0
  23. package/dist/core/codec/islands.d.ts +36 -0
  24. package/dist/core/codec/islands.js +82 -0
  25. package/dist/core/codec/keymap.d.ts +13 -0
  26. package/dist/core/codec/keymap.js +29 -0
  27. package/dist/core/codec/lists.d.ts +22 -0
  28. package/dist/core/codec/lists.js +104 -0
  29. package/dist/core/codec/marks.d.ts +37 -0
  30. package/dist/core/codec/marks.js +74 -0
  31. package/dist/core/codec/positions.d.ts +17 -0
  32. package/dist/core/codec/positions.js +108 -0
  33. package/dist/core/codec/prose.css +458 -0
  34. package/dist/core/codec/reconcile.d.ts +26 -0
  35. package/dist/core/codec/reconcile.js +64 -0
  36. package/dist/core/codec/schema.d.ts +13 -0
  37. package/dist/core/codec/schema.js +155 -0
  38. package/dist/core/codec/slash.d.ts +79 -0
  39. package/dist/core/codec/slash.js +0 -0
  40. package/dist/core/codec/table-view.d.ts +50 -0
  41. package/dist/core/codec/table-view.js +928 -0
  42. package/dist/core/codec/table.d.ts +85 -0
  43. package/dist/core/codec/table.js +224 -0
  44. package/dist/core/errors.d.ts +73 -0
  45. package/dist/core/errors.js +24 -0
  46. package/dist/core/index.d.ts +5 -0
  47. package/dist/core/index.js +19 -0
  48. package/dist/core/lifecycle.d.ts +7 -0
  49. package/dist/core/lifecycle.js +21 -0
  50. package/dist/core/rebind.svelte.d.ts +18 -0
  51. package/dist/core/rebind.svelte.js +44 -0
  52. package/dist/core/teardown.d.ts +20 -0
  53. package/dist/core/teardown.js +37 -0
  54. package/dist/core/theme.css +276 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +9 -0
  57. package/dist/preset/index.css +10 -0
  58. package/dist/preset/recipes.css +188 -0
  59. package/dist/preset/scale.css +97 -0
  60. package/dist/preview/Preview.svelte +118 -0
  61. package/dist/preview/Preview.svelte.d.ts +39 -0
  62. package/dist/preview/bridge.d.ts +13 -0
  63. package/dist/preview/bridge.js +88 -0
  64. package/dist/preview/controller.d.ts +40 -0
  65. package/dist/preview/controller.js +141 -0
  66. package/dist/preview/geometry.d.ts +37 -0
  67. package/dist/preview/geometry.js +45 -0
  68. package/dist/preview/index.d.ts +5 -0
  69. package/dist/preview/index.js +14 -0
  70. package/dist/preview/overlay.d.ts +10 -0
  71. package/dist/preview/overlay.js +82 -0
  72. package/dist/preview/paint.d.ts +17 -0
  73. package/dist/preview/paint.js +262 -0
  74. package/dist/preview/strings.d.ts +14 -0
  75. package/dist/preview/strings.js +22 -0
  76. package/dist/visual/ArrayField.svelte +362 -0
  77. package/dist/visual/ArrayField.svelte.d.ts +24 -0
  78. package/dist/visual/BooleanField.svelte +88 -0
  79. package/dist/visual/BooleanField.svelte.d.ts +20 -0
  80. package/dist/visual/Card.svelte +849 -0
  81. package/dist/visual/Card.svelte.d.ts +43 -0
  82. package/dist/visual/CardControls.svelte +92 -0
  83. package/dist/visual/CardControls.svelte.d.ts +11 -0
  84. package/dist/visual/DateField.svelte +203 -0
  85. package/dist/visual/DateField.svelte.d.ts +28 -0
  86. package/dist/visual/DiagnosticList.svelte +50 -0
  87. package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
  88. package/dist/visual/EnumField.svelte +169 -0
  89. package/dist/visual/EnumField.svelte.d.ts +25 -0
  90. package/dist/visual/Field.svelte +270 -0
  91. package/dist/visual/Field.svelte.d.ts +37 -0
  92. package/dist/visual/FieldHint.svelte +185 -0
  93. package/dist/visual/FieldHint.svelte.d.ts +13 -0
  94. package/dist/visual/FieldLabel.svelte +141 -0
  95. package/dist/visual/FieldLabel.svelte.d.ts +19 -0
  96. package/dist/visual/FormatPopover.svelte +396 -0
  97. package/dist/visual/FormatPopover.svelte.d.ts +9 -0
  98. package/dist/visual/NumberField.svelte +72 -0
  99. package/dist/visual/NumberField.svelte.d.ts +19 -0
  100. package/dist/visual/ObjectField.svelte +131 -0
  101. package/dist/visual/ObjectField.svelte.d.ts +17 -0
  102. package/dist/visual/ProseArrayElement.svelte +98 -0
  103. package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
  104. package/dist/visual/ProseField.svelte +213 -0
  105. package/dist/visual/ProseField.svelte.d.ts +50 -0
  106. package/dist/visual/SlashMenu.svelte +126 -0
  107. package/dist/visual/SlashMenu.svelte.d.ts +14 -0
  108. package/dist/visual/TextField.svelte +63 -0
  109. package/dist/visual/TextField.svelte.d.ts +23 -0
  110. package/dist/visual/TipsCard.svelte +191 -0
  111. package/dist/visual/TipsCard.svelte.d.ts +13 -0
  112. package/dist/visual/VisualEditor.svelte +96 -0
  113. package/dist/visual/VisualEditor.svelte.d.ts +14 -0
  114. package/dist/visual/VisualEditorInner.svelte +1032 -0
  115. package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
  116. package/dist/visual/controls.css +272 -0
  117. package/dist/visual/diagnostics.d.ts +52 -0
  118. package/dist/visual/diagnostics.js +83 -0
  119. package/dist/visual/domid.d.ts +21 -0
  120. package/dist/visual/domid.js +36 -0
  121. package/dist/visual/ext.d.ts +19 -0
  122. package/dist/visual/ext.js +29 -0
  123. package/dist/visual/icons/Icon.svelte +39 -0
  124. package/dist/visual/icons/Icon.svelte.d.ts +12 -0
  125. package/dist/visual/icons/nodes.d.ts +103 -0
  126. package/dist/visual/icons/nodes.js +55 -0
  127. package/dist/visual/index.d.ts +9 -0
  128. package/dist/visual/index.js +24 -0
  129. package/dist/visual/motion.d.ts +18 -0
  130. package/dist/visual/motion.js +53 -0
  131. package/dist/visual/props.d.ts +77 -0
  132. package/dist/visual/props.js +1 -0
  133. package/dist/visual/signals.d.ts +59 -0
  134. package/dist/visual/signals.js +1 -0
  135. package/dist/visual/strings.d.ts +123 -0
  136. package/dist/visual/strings.js +80 -0
  137. package/dist/visual/structure.d.ts +224 -0
  138. package/dist/visual/structure.js +270 -0
  139. package/dist/visual/synced.svelte.d.ts +15 -0
  140. package/dist/visual/synced.svelte.js +34 -0
  141. package/dist/visual/tips.d.ts +24 -0
  142. package/dist/visual/tips.js +50 -0
  143. package/package.json +102 -0
@@ -0,0 +1,493 @@
1
+ // The prose leaf: `createField` (VISUAL_EDITOR §Surface). One content field, one
2
+ // PM `EditorState`/`EditorView`, wired to the WASM edit surface. It reads the
3
+ // leaf's `Content`, decodes to a PM state, mounts a view + plugin stack (history,
4
+ // keymap, input rules, the anchor-position plugin), and `dispatchTransaction`:
5
+ // (a) apply optimistically to the view,
6
+ // (b) lower the tr to a `ChangeBundle` (or `overwrite` for a field not yet at
7
+ // content rest, which has nothing to splice),
8
+ // (c) commit via `doc.applyChange(addr, bundle)`,
9
+ // (d) fire `onChange` when the transaction COMMITTED, then `onCaretMove` with
10
+ // the new USV caret, which a bare selection move fires on its own.
11
+ // On an `applyChange` throw the optimistic PM state stays and the failure reports
12
+ // through `onError`: never a crash. Caret continuity across own-edits is the PM
13
+ // `StepMap`; an EXTERNAL content change re-hydrates through `applyExternal`, gated
14
+ // by `reconcile`.
15
+ import { baseKeymap, toggleMark } from 'prosemirror-commands';
16
+ import { gapCursor } from 'prosemirror-gapcursor';
17
+ import { history, redo, undo } from 'prosemirror-history';
18
+ import { keymap } from 'prosemirror-keymap';
19
+ import { EditorState, NodeSelection, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
20
+ import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
21
+ import { reportError, errorMessage } from '../errors.js';
22
+ import { decode } from './decode.js';
23
+ import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
24
+ import { lower, pmToContent, contentEdit } from './encode.js';
25
+ import { anchorsFromContent } from './marks.js';
26
+ import { createReconciler } from './reconcile.js';
27
+ import { inputRulesPlugin } from './inputrules.js';
28
+ import { bodyKeymap } from './keymap.js';
29
+ import { blockSchema, inlineSchema } from './schema.js';
30
+ import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
31
+ import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashItems, slashPlugin } from './slash.js';
32
+ const anchorKey = new PluginKey('quill-anchors');
33
+ /** Plugin-held anchor positions (PM coords), mapped through every edit's `StepMap`
34
+ * and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection). */
35
+ function anchorPlugin(seed) {
36
+ return new Plugin({
37
+ key: anchorKey,
38
+ state: {
39
+ init: () => seed,
40
+ apply: (tr, anchors) => {
41
+ let next = tr.docChanged
42
+ ? anchors.map((a) => ({ id: a.id, pos: tr.mapping.map(a.pos) }))
43
+ : anchors;
44
+ const edit = tr.getMeta(anchorKey);
45
+ if (edit?.op === 'add')
46
+ next = [...next, { id: edit.id, pos: edit.pos }];
47
+ else if (edit?.op === 'remove')
48
+ next = next.filter((a) => a.id !== edit.id);
49
+ return next;
50
+ }
51
+ }
52
+ });
53
+ }
54
+ /** Read the leaf's `Content` for `addr`: `reader.getContent` (DOCUMENT_MODEL),
55
+ * the schema-plane read that decodes through the codec the field's declared type
56
+ * names. A field value, or the body `Content` when `addr.field` is absent, without
57
+ * materializing the whole card. Reads are total over the field axis, so an absent
58
+ * field, a default-only richtext field (e.g. `tag_line`, no stored value until
59
+ * first edited), reads `undefined`; decode an empty content rather than crash, and
60
+ * the first edit overwrites it. Only an out-of-range `addr.card` throws, unreachable
61
+ * here: a removed card unmounts its keyed leaf before a stale index is read.
62
+ *
63
+ * Bound rather than verbatim, because the storage form is not the leaf's business
64
+ * and the verbatim read cannot answer it alone. A field the editor committed rests
65
+ * as `Content`; a field a markdown parse left rests as the authored STRING, and
66
+ * what that string MEANS is the declared type's to say: `richtext` is markdown,
67
+ * `plaintext` is literal text, and reading one as the other silently eats every
68
+ * `*` the author typed. */
69
+ function readLeaf(reader, addr) {
70
+ return reader.getContent(addr) ?? emptyContent();
71
+ }
72
+ /** The canonical empty `Content`: one empty `para` line. The zero value a prose
73
+ * leaf (or an array's prose element) seeds from. */
74
+ export function emptyContent() {
75
+ return { text: '', lines: [{ containers: [], kind: 'para' }], marks: [], islands: [] };
76
+ }
77
+ /** Whether ops may commit here: the stored value is a `Content` object, so
78
+ * `applyChange` splices exactly the content the leaf read and PM is showing.
79
+ *
80
+ * The two other rest forms both take `overwrite` instead. An UNSET field (`undefined`;
81
+ * a default-only richtext field before its first edit) has nothing to splice.
82
+ * An AUTHORED STRING is the trap: `applyChange` reads it as markdown whatever the
83
+ * declared type is, so on a `plaintext` field its pre-image is not the content the
84
+ * leaf read and a delta computed against that content lands at the wrong offsets.
85
+ * Installing over it costs nothing either way, since content-only marks do not
86
+ * survive markdown and a string therefore carries no anchors to pay. A body always
87
+ * rests as `Content`. */
88
+ function opsCommittable(doc, addr) {
89
+ const stored = doc.getStored(addr);
90
+ return stored !== null && typeof stored === 'object';
91
+ }
92
+ /** The naming attributes for the `contenteditable`. `aria-labelledby` supersedes
93
+ * `aria-label`: a leaf carrying both is the ambiguity where implementations
94
+ * disagree about which wins, and `undefined` is returned whole when a leaf takes
95
+ * neither, since ProseMirror reads the absence, not an empty object. */
96
+ function proseAttributes(opts) {
97
+ const attrs = {};
98
+ if (opts.labelledBy)
99
+ attrs['aria-labelledby'] = opts.labelledBy;
100
+ else if (opts.label)
101
+ attrs['aria-label'] = opts.label;
102
+ if (opts.describedBy)
103
+ attrs['aria-describedby'] = opts.describedBy;
104
+ return Object.keys(attrs).length ? attrs : undefined;
105
+ }
106
+ export function createField(opts) {
107
+ const { doc, addr, container } = opts;
108
+ const inline = !!opts.inline || !!opts.plaintext;
109
+ const plaintext = !!opts.plaintext;
110
+ const schema = inline ? inlineSchema : blockSchema;
111
+ // Bound once and held: the reader is a `{quill, doc}` pair that reads live, so
112
+ // every read below sees the commit before it.
113
+ const reader = opts.quill.reader(doc);
114
+ // `known` is the codec's view of the stored content: kept in sync after every
115
+ // own-edit so `reconcile` can tell an external change from the field's own.
116
+ const reconciler = createReconciler(readLeaf(reader, addr));
117
+ let index; // rebuilt on every structural change
118
+ let view;
119
+ // The ghost's live cell: the placeholder plugin reads it per pass, so moving it
120
+ // is an assignment plus a re-render rather than a rebuilt plugin stack.
121
+ let placeholderText = opts.placeholder;
122
+ // The views nested INSIDE this leaf: one per table cell (`table-view.ts`). The
123
+ // leaf holds the set because chrome asks the leaf, not the island, which view
124
+ // holds the caret: the format popover raises over a cell exactly as over the
125
+ // body, and only the leaf knows both.
126
+ const nested = new Set();
127
+ // The menu's offers, derived from the wording per read: the chrome names an item
128
+ // and the codec runs it, so a locale swap changes the labels and not the picks.
129
+ const menuItems = () => slashItems(opts.slashStrings?.() ?? DEFAULT_SLASH_STRINGS);
130
+ const slash = !inline && opts.onSlash ? { items: menuItems, onState: opts.onSlash } : undefined;
131
+ const state = buildState(reconciler.last);
132
+ index = buildLineIndex(state.doc);
133
+ view = new EditorView(container, {
134
+ state,
135
+ attributes: proseAttributes(opts),
136
+ // Islands are block-schema only, so an inline leaf mounts no node view at all.
137
+ nodeViews: inline
138
+ ? undefined
139
+ : {
140
+ island_block: tableNodeView({
141
+ strings: opts.tableStrings ?? (() => DEFAULT_TABLE_STRINGS),
142
+ register: (cellView) => {
143
+ nested.add(cellView);
144
+ return () => nested.delete(cellView);
145
+ },
146
+ // A focus event does not bubble, so the leaf's own `focus` handler
147
+ // never fires for a cell: without this the active address would not
148
+ // follow a caret clicked straight into a table.
149
+ onCellFocus: () => opts.onFocus?.(addr)
150
+ })
151
+ },
152
+ dispatchTransaction: (tr) => {
153
+ const oldRt = reconciler.last;
154
+ const next = view.state.apply(tr);
155
+ view.updateState(next); // (a) optimistic
156
+ index = buildLineIndex(next.doc);
157
+ // Commit a content edit OR an anchor mutation. An anchor insert/remove is
158
+ // zero-width, so `docChanged` is false: the `anchorKey` meta is what
159
+ // routes it through the same commit path (the diff emits the anchor op).
160
+ if (tr.docChanged || tr.getMeta(anchorKey)) {
161
+ // (d) the change signal, only for what actually landed.
162
+ if (commitEdit(oldRt, next.doc))
163
+ opts.onChange?.(addr);
164
+ }
165
+ // The caret, for both structural and selection-only changes: a host
166
+ // following the caret wants an arrow key, and a host recompiling wants
167
+ // `onChange`. Reported after the commit, so the edit has landed by the
168
+ // time the caret names where it is.
169
+ opts.onCaretMove?.(addr, pmToUsv(index, next.selection.head));
170
+ },
171
+ handleDOMEvents: {
172
+ focus: () => {
173
+ opts.onFocus?.(addr);
174
+ return false;
175
+ }
176
+ }
177
+ });
178
+ function buildState(rt) {
179
+ const pmDoc = decode(rt, schema, { plaintext });
180
+ const anchors = plaintext ? [] : anchorsFromContent(rt);
181
+ // Seed anchor plugin positions in PM coords via a fresh index over pmDoc.
182
+ const seedIndex = buildLineIndex(pmDoc);
183
+ const seededAnchors = anchors.map((a) => ({
184
+ id: a.id,
185
+ pos: usvToPM(seedIndex, a.pos)
186
+ }));
187
+ return EditorState.create({
188
+ doc: pmDoc,
189
+ plugins: proseLeafPlugins(schema, {
190
+ inline,
191
+ plaintext,
192
+ slash,
193
+ noInputRules: opts.noInputRules,
194
+ // Always installed, so a leaf that mounts without a ghost can still be
195
+ // given one later; the plugin draws nothing while the text is empty.
196
+ placeholder: () => placeholderText,
197
+ afterHistory: [anchorPlugin(seededAnchors)]
198
+ })
199
+ });
200
+ }
201
+ // (b)+(c): lower the edit to ops and commit, or `overwrite` a field not yet at
202
+ // content rest. Keep the optimistic PM on throw.
203
+ // Returns whether anything landed: the caller's change signal, which must not
204
+ // be a property of which branch the commit took.
205
+ function commitEdit(oldRt, newDoc) {
206
+ // The projection and its text splice, each computed ONCE per keystroke: the gate
207
+ // below, both overwrite fallbacks, and `lower` all read this one `edit`.
208
+ const edit = contentEdit(oldRt, pmToContent(newDoc));
209
+ try {
210
+ // A field not yet at content rest is the only edit that overwrites by choice:
211
+ // `applyChange` throws on an absent declared field (verified) and mis-reads
212
+ // an authored string (`opsCommittable`), and neither has anchors to lose.
213
+ // Every structural edit lowers, island creation included: the island channel
214
+ // places a slot the `delta` may not carry.
215
+ if (!opsCommittable(doc, addr)) {
216
+ doc.overwrite(addr, edit.newRt); // brings the field to content rest
217
+ }
218
+ else {
219
+ // Pre-edit anchors are the stored content's anchors (USV); post-edit
220
+ // anchors are the plugin's positions (mapped through the tr) as USV.
221
+ const oldAnchors = plaintext ? [] : anchorsFromContent(oldRt);
222
+ const newAnchors = plaintext ? [] : readAnchorsUsv(newDoc);
223
+ doc.applyChange(addr, lower(edit, { oldAnchors, newAnchors }));
224
+ }
225
+ reconciler.commit(readLeaf(reader, addr));
226
+ return true;
227
+ }
228
+ catch (e) {
229
+ // Bound the damage: an op path the gates missed leaves the store STALE
230
+ // while PM keeps the edit; and because the reconciler then re-diffs
231
+ // from that stale content, every later edit re-throws and the field
232
+ // silently stops persisting. Install the full projection instead
233
+ // (correct store, pays this field's anchors).
234
+ reportError(opts.onError, {
235
+ code: 'commit-fallback',
236
+ severity: 'error',
237
+ message: `applyChange refused; overwrote the whole field: ${errorMessage(e)}`,
238
+ cause: e
239
+ });
240
+ try {
241
+ doc.overwrite(addr, edit.newRt);
242
+ reconciler.commit(readLeaf(reader, addr));
243
+ return true;
244
+ }
245
+ catch (e2) {
246
+ // Optimistic PM stays; the store is stale for this field from here.
247
+ reportError(opts.onError, {
248
+ code: 'commit-lost',
249
+ severity: 'error',
250
+ message: `overwrite fallback failed; the stored value is stale while the editor keeps the edit: ${errorMessage(e2)}`,
251
+ cause: e2
252
+ });
253
+ return false;
254
+ }
255
+ }
256
+ }
257
+ /** The anchors the plugin currently holds (PM coords); `[]` before the first apply. */
258
+ const heldAnchors = () => anchorKey.getState(view.state) ?? [];
259
+ /** Anchor positions in the CURRENT (new) doc, as USV. */
260
+ function readAnchorsUsv(newDoc) {
261
+ return heldAnchors().map((a) => ({ id: a.id, pos: pmToUsv(index, a.pos) }));
262
+ }
263
+ const controller = {
264
+ el: container,
265
+ setCaret(pos) {
266
+ const pm = usvToPM(index, pos);
267
+ // `Selection.near`, not `TextSelection.create`: the mapped position can
268
+ // be non-inline (before an island/rule block, or doc-level in an empty
269
+ // nested textblock), where a raw TextSelection is invalid.
270
+ const sel = Selection.near(view.state.doc.resolve(pm));
271
+ view.dispatch(view.state.tr.setSelection(sel));
272
+ view.focus();
273
+ },
274
+ applyExternal() {
275
+ const current = readLeaf(reader, addr);
276
+ if (!reconciler.shouldRehydrate(current))
277
+ return; // own edit / no change
278
+ const caretUsv = pmToUsv(index, view.state.selection.head);
279
+ const fresh = buildState(current);
280
+ view.updateState(fresh);
281
+ index = buildLineIndex(fresh.doc);
282
+ // Best-effort caret continuity across an external change: keep the USV.
283
+ const pm = usvToPM(index, caretUsv);
284
+ view.dispatch(view.state.tr.setSelection(Selection.near(fresh.doc.resolve(pm))));
285
+ reconciler.commit(current);
286
+ },
287
+ setPlaceholder(text) {
288
+ if (text === placeholderText)
289
+ return;
290
+ placeholderText = text;
291
+ // `setProps` re-runs the decoration pass against the unchanged state: no
292
+ // transaction, so nothing commits and no caret is reported.
293
+ view.setProps({});
294
+ },
295
+ focus() {
296
+ view.focus();
297
+ },
298
+ getContent() {
299
+ return readLeaf(reader, addr);
300
+ },
301
+ insertAnchor(id, pos) {
302
+ if (plaintext)
303
+ return; // a plaintext field carries no marks (§Inline mode)
304
+ if (heldAnchors().some((a) => a.id === id))
305
+ return; // ids are unique + invariant (0.97 policy)
306
+ // An anchor commits through `applyChange`, so the field is brought to content
307
+ // rest first; else the commit's overwrite branch (value semantics) would drop
308
+ // the just-added anchor.
309
+ if (!opsCommittable(doc, addr))
310
+ doc.overwrite(addr, reconciler.last);
311
+ const pm = usvToPM(index, pos);
312
+ view.dispatch(view.state.tr.setMeta(anchorKey, { op: 'add', id, pos: pm }));
313
+ },
314
+ removeAnchor(id) {
315
+ if (plaintext)
316
+ return;
317
+ if (!heldAnchors().some((a) => a.id === id))
318
+ return; // absent: nothing to commit
319
+ view.dispatch(view.state.tr.setMeta(anchorKey, { op: 'remove', id }));
320
+ },
321
+ anchorsInRange(from, to) {
322
+ return heldAnchors()
323
+ .filter((a) => {
324
+ const u = pmToUsv(index, a.pos);
325
+ return u >= from && u <= to;
326
+ })
327
+ .map((a) => a.id);
328
+ },
329
+ slashFocus(id) {
330
+ if (slash)
331
+ focusSlashItem(view, id, slash.items());
332
+ },
333
+ slashPick(id) {
334
+ if (slash)
335
+ runSlashItem(view, id);
336
+ },
337
+ selectionRange() {
338
+ const { from, to } = view.state.selection;
339
+ return {
340
+ from: pmToUsv(index, from),
341
+ to: pmToUsv(index, to)
342
+ };
343
+ },
344
+ destroy() {
345
+ view.destroy();
346
+ }
347
+ };
348
+ // The underlying views, exposed as available (undocumented) handles: the
349
+ // VisualEditor composes views, and tests drive edits through them.
350
+ const handles = controller;
351
+ handles.view = view;
352
+ handles.nestedViews = () => [...nested];
353
+ handles.focusedView = () => {
354
+ for (const cellView of nested)
355
+ if (cellView.hasFocus())
356
+ return cellView;
357
+ return view;
358
+ };
359
+ return controller;
360
+ }
361
+ /**
362
+ * The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
363
+ * {@link createField} and the array-element inline editor (`ProseArrayElement`),
364
+ * so the two never fork the keymap/plugin ordering. History first, then any
365
+ * leaf-specific plugins (`afterHistory`: the addressed leaf passes its
366
+ * anchor-position plugin; a by-value array element passes none), the
367
+ * markdown-shorthand input rules, then the field keymap over the base keymap, and
368
+ * last the two that answer to a caret or a selection BESIDE a block: the gap cursor
369
+ * and {@link pastAtomPlugin}.
370
+ *
371
+ * A `plaintext` field carries no marks (decode strips them, the keymap suppresses
372
+ * Mod-b/i/u), so it also skips the input rules: `inlineSchema` still declares the
373
+ * mark types, so a `**bold**` rule would apply a strong mark AND eat the literal
374
+ * delimiters. The inline schema has no block nodes, so those rules are the ONLY
375
+ * ones it would add: skip all.
376
+ */
377
+ export function proseLeafPlugins(schema, opts) {
378
+ const list = [history(), ...(opts.afterHistory ?? [])];
379
+ if (opts.slash)
380
+ list.push(slashPlugin(opts.slash));
381
+ if (!opts.noInputRules && !opts.plaintext)
382
+ list.push(inputRulesPlugin(schema));
383
+ list.push(keymap(editorKeymap(schema, opts.inline, opts.plaintext, opts.slash?.items)));
384
+ list.push(keymap(baseKeymap));
385
+ // Both answer to a selection over an ATOM, which only a block leaf holds: an
386
+ // inline leaf is one textblock with no island and no gap to put a cursor in.
387
+ if (!opts.inline)
388
+ list.push(gapCursor(), pastAtomPlugin());
389
+ if (opts.placeholder)
390
+ list.push(placeholderPlugin(opts.placeholder));
391
+ return list;
392
+ }
393
+ /**
394
+ * A printable key over a selected atom writes PAST it. A selection is the subject
395
+ * of the next command, never a thing armed for
396
+ * replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
397
+ * it. A BLOCK island takes a new paragraph after it, an INLINE one the caret after
398
+ * the image in the same line: one rule, both node types.
399
+ *
400
+ * PM routes text input over a non-`TextSelection` through `handleTextInput` before
401
+ * falling back to replacing the selection, so this one prop is the whole of it.
402
+ * Paste keeps replacing: a paste is deliberate, and it is the gesture that swaps one
403
+ * island for another.
404
+ */
405
+ function pastAtomPlugin() {
406
+ return new Plugin({
407
+ props: {
408
+ handleTextInput(view, _from, _to, text) {
409
+ const { selection, schema } = view.state;
410
+ if (!text || !(selection instanceof NodeSelection) || !selection.node.isAtom)
411
+ return false;
412
+ const tr = view.state.tr;
413
+ const at = selection.to; // just past the node, in both coordinate senses
414
+ if (selection.node.isInline) {
415
+ tr.setSelection(TextSelection.create(tr.doc, at)).insertText(text);
416
+ }
417
+ else {
418
+ const para = schema.nodes.paragraph.create(null, schema.text(text));
419
+ tr.insert(at, para);
420
+ tr.setSelection(TextSelection.create(tr.doc, at + para.nodeSize - 1));
421
+ }
422
+ view.dispatch(tr.scrollIntoView());
423
+ return true;
424
+ }
425
+ }
426
+ });
427
+ }
428
+ /**
429
+ * The empty-leaf ghost placeholder. A node decoration stamps the sole empty
430
+ * textblock with a class + the ghost text,
431
+ * which CSS renders via `::before { content: attr(data-placeholder) }`: so the
432
+ * text never enters the document, the caret path, or a `pmToContent` export. It
433
+ * vanishes the instant the leaf holds any content (the emptiness test fails).
434
+ *
435
+ * `read` is called per decoration pass rather than closed over, so moving the
436
+ * ghost is a re-render and never a document edit.
437
+ */
438
+ function placeholderPlugin(read) {
439
+ return new Plugin({
440
+ props: {
441
+ decorations(state) {
442
+ const text = read();
443
+ if (!text)
444
+ return null;
445
+ const { doc } = state;
446
+ const first = doc.firstChild;
447
+ const empty = doc.childCount === 1 && !!first && first.isTextblock && first.content.size === 0;
448
+ if (!empty)
449
+ return null;
450
+ return DecorationSet.create(doc, [
451
+ Decoration.node(0, first.nodeSize, {
452
+ class: 'qm-prose-placeholder',
453
+ 'data-placeholder': text
454
+ })
455
+ ]);
456
+ }
457
+ }
458
+ });
459
+ }
460
+ /**
461
+ * The field's keymap: history, mark toggles, and the body's structural keys; Enter
462
+ * suppressed inline.
463
+ *
464
+ * Tab forks on the leaf's ROLE, not on the caret's position. An inline/plaintext
465
+ * leaf is a form field: Tab stays unbound, so field navigation is open for a shell
466
+ * keymap (VISUAL_EDITOR §Settled and open). A block-schema body is a document: Tab
467
+ * is structural, a chain each nested surface prepends to (`keymap.ts`). One key
468
+ * never means two things within one surface.
469
+ */
470
+ function editorKeymap(schema, inline, plaintext, slashItems) {
471
+ const map = {
472
+ 'Mod-z': undo,
473
+ 'Mod-y': redo,
474
+ 'Shift-Mod-z': redo
475
+ };
476
+ if (!plaintext) {
477
+ if (schema.marks.strong)
478
+ map['Mod-b'] = toggleMark(schema.marks.strong);
479
+ if (schema.marks.em)
480
+ map['Mod-i'] = toggleMark(schema.marks.em);
481
+ if (schema.marks.underline)
482
+ map['Mod-u'] = toggleMark(schema.marks.underline);
483
+ }
484
+ if (inline) {
485
+ // One textblock only: swallow Enter so no second block is attempted.
486
+ map['Enter'] = () => true;
487
+ }
488
+ else {
489
+ // Each binding falls through to `baseKeymap` when no link claims the key.
490
+ Object.assign(map, bodyKeymap(schema, slashItems));
491
+ }
492
+ return map;
493
+ }
@@ -0,0 +1,22 @@
1
+ import 'prosemirror-view/style/prosemirror.css';
2
+ import 'prosemirror-gapcursor/style/gapcursor.css';
3
+ import './prose.css';
4
+ export { rangeAnchor } from './anchor.js';
5
+ export type { RangeAnchor } from './anchor.js';
6
+ export { createField, emptyContent, proseLeafPlugins } from './field.js';
7
+ export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
8
+ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
9
+ export type { TableChromeStrings } from './table-view.js';
10
+ export { DEFAULT_SLASH_STRINGS } from './slash.js';
11
+ export type { SlashState, SlashStrings } from './slash.js';
12
+ export { blockSchema, inlineSchema } from './schema.js';
13
+ export { decode, renderContent, usvLength } from './decode.js';
14
+ export { pmToContent, contentEdit, lower } from './encode.js';
15
+ export type { ContentEdit } from './encode.js';
16
+ export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
17
+ export type { LineIndex } from './positions.js';
18
+ export { createReconciler, contentEqual } from './reconcile.js';
19
+ export type { Reconciler } from './reconcile.js';
20
+ export { inputRulesPlugin } from './inputrules.js';
21
+ export { bodyKeymap } from './keymap.js';
22
+ export { listKeymap } from './lists.js';
@@ -0,0 +1,43 @@
1
+ // The codec barrel: the bidirectional bridge between one content field (`Content`)
2
+ // and one ProseMirror document (CODEC). Decode (content → PM), lower (PM tr → a
3
+ // `ChangeBundle` for `applyChange`), the USV↔PM position map, and the `createField`
4
+ // prose leaf. Consumed by the VisualEditor and the codec test suite.
5
+ // ProseMirror's structural base styles and the gap cursor's, then the retint of the
6
+ // hues they mint. Imported HERE because `createField` is what mounts a view: a
7
+ // consumer reaching the codec directly gets them with it, and the order is what makes
8
+ // the retint win.
9
+ import 'prosemirror-view/style/prosemirror.css';
10
+ import 'prosemirror-gapcursor/style/gapcursor.css';
11
+ import './prose.css';
12
+ // What a floating surface anchors to. The visual tier's own surfaces mint one over a
13
+ // selection (`visual/FormatPopover.svelte`), so it is reached off-barrel.
14
+ export { rangeAnchor } from './anchor.js';
15
+ // The prose leaf.
16
+ export { createField, emptyContent, proseLeafPlugins } from './field.js';
17
+ // The table island's chrome vocabulary: the visual tier's `strings` set extends it,
18
+ // so the island's wording is overridden beside every other key.
19
+ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
20
+ // The slash menu: the codec owns the model and the picks, the chrome the words and
21
+ // the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
22
+ export { DEFAULT_SLASH_STRINGS } from './slash.js';
23
+ // Schemas (the decode/encode target; the VisualEditor mounts them).
24
+ export { blockSchema, inlineSchema } from './schema.js';
25
+ // Decode / encode / positions (tests + VisualEditor).
26
+ //
27
+ // THE BARREL CARRIES WHAT HAS AN OFF-BARREL CALLER, and nothing else. The mark
28
+ // algebra, the island bridge, the schema predicate, the `diff*` helpers,
29
+ // `scanDoc`, `codePoints`: every one of them is reached by RELATIVE import
30
+ // within `codec/`, so none of them is here. An export nothing imports is surface
31
+ // that still has to stay honest; a symbol earns its line when a caller outside
32
+ // this folder wants it.
33
+ export { decode, renderContent, usvLength } from './decode.js';
34
+ export { pmToContent, contentEdit, lower } from './encode.js';
35
+ export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
36
+ // Reconciliation gate.
37
+ export { createReconciler, contentEqual } from './reconcile.js';
38
+ // Input rules (`createField` mounts them unless `noInputRules`/`plaintext`).
39
+ export { inputRulesPlugin } from './inputrules.js';
40
+ // The body leaf's structural keys: `createField` binds the composed chains
41
+ // (`bodyKeymap`); the suite drives both it and the list link directly.
42
+ export { bodyKeymap } from './keymap.js';
43
+ export { listKeymap } from './lists.js';
@@ -0,0 +1,7 @@
1
+ import { InputRule } from 'prosemirror-inputrules';
2
+ import type { Schema } from 'prosemirror-model';
3
+ import type { Plugin } from 'prosemirror-state';
4
+ /** The full markdown-shorthand rule set for a block schema. */
5
+ export declare function markdownInputRules(schema: Schema): InputRule[];
6
+ /** The input-rules plugin the field mounts by default (opt-out via createField). */
7
+ export declare function inputRulesPlugin(schema: Schema): Plugin;