@quillmark/svelte 0.5.0 → 0.6.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 (56) hide show
  1. package/README.md +8 -2
  2. package/THEMING.md +22 -27
  3. package/dist/core/address.d.ts +17 -3
  4. package/dist/core/address.js +20 -3
  5. package/dist/core/codec/blocks.d.ts +37 -0
  6. package/dist/core/codec/blocks.js +163 -0
  7. package/dist/core/codec/field.js +3 -5
  8. package/dist/core/codec/index.d.ts +1 -1
  9. package/dist/core/codec/index.js +3 -2
  10. package/dist/core/codec/inputrules.js +24 -144
  11. package/dist/core/codec/prose.css +14 -25
  12. package/dist/core/codec/schema.d.ts +8 -0
  13. package/dist/core/codec/schema.js +33 -1
  14. package/dist/core/codec/slash.d.ts +18 -16
  15. package/dist/core/codec/slash.js +0 -0
  16. package/dist/core/codec/table-view.js +127 -157
  17. package/dist/core/codec/table.d.ts +0 -10
  18. package/dist/core/codec/table.js +0 -15
  19. package/dist/core/theme.css +350 -347
  20. package/dist/preset/recipes.css +10 -21
  21. package/dist/preset/scale.css +6 -7
  22. package/dist/preview/Preview.svelte +4 -2
  23. package/dist/preview/geometry.d.ts +10 -9
  24. package/dist/preview/geometry.js +11 -13
  25. package/dist/visual/ArrayField.svelte +242 -63
  26. package/dist/visual/ArrayField.svelte.d.ts +5 -1
  27. package/dist/visual/Card.svelte +21 -2
  28. package/dist/visual/DiagnosticList.svelte +6 -11
  29. package/dist/visual/EnumField.svelte +33 -1
  30. package/dist/visual/Field.svelte +29 -5
  31. package/dist/visual/FieldLabel.svelte +2 -2
  32. package/dist/visual/FormatPopover.svelte +13 -1
  33. package/dist/visual/ObjectField.svelte +157 -35
  34. package/dist/visual/ObjectField.svelte.d.ts +13 -4
  35. package/dist/visual/ProseArrayElement.svelte +22 -2
  36. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
  37. package/dist/visual/TipsCard.svelte +33 -13
  38. package/dist/visual/VariantField.svelte +98 -0
  39. package/dist/visual/VariantField.svelte.d.ts +21 -0
  40. package/dist/visual/VisualEditorInner.svelte +18 -15
  41. package/dist/visual/controls.css +282 -283
  42. package/dist/visual/diagnostics.d.ts +15 -24
  43. package/dist/visual/diagnostics.js +14 -30
  44. package/dist/visual/domid.d.ts +13 -0
  45. package/dist/visual/domid.js +16 -0
  46. package/dist/visual/leaves.d.ts +9 -7
  47. package/dist/visual/links.d.ts +5 -2
  48. package/dist/visual/links.js +9 -3
  49. package/dist/visual/props.d.ts +1 -1
  50. package/dist/visual/strings.d.ts +21 -6
  51. package/dist/visual/strings.js +3 -0
  52. package/dist/visual/structure.d.ts +32 -7
  53. package/dist/visual/structure.js +49 -3
  54. package/package.json +2 -2
  55. package/dist/core/codec/clipboard.d.ts +0 -59
  56. package/dist/core/codec/clipboard.js +0 -202
package/README.md CHANGED
@@ -60,6 +60,12 @@ const session = await new Engine().open(quill, doc);
60
60
 
61
61
  Opening a document that names a **different** quill is the same sequence, in order: resolve the new ref, `engine.open(quill, next)`, swap the props, then free the replaced handles that were yours to free. `<VisualEditor>` re-keys itself on the new `doc` (see below); `<Preview>` swaps by remount (`{#key session}`).
62
62
 
63
+ ## What a document is trusted to be
64
+
65
+ **A `Document` may be one its reader did not write** — an import, a shared file, a row from a multi-tenant store — and two properties hold over one regardless. Markdown becomes typed nodes and then DOM, never a markup string: this package has no `{@html}`, no `innerHTML` and no `eval`, so a document's text cannot become tags. And a link renders as one only for `http`, `https`, `mailto`, `tel`, `ftp`, or a value naming no scheme at all; every other scheme draws as an inert span, on the editable surface and in the tips card that paints `$ext.editor.tips` outside any `contenteditable`. That gate is the render's rather than the model's, so a refused href stays on the mark and round-trips: opening a document never edits it.
66
+
67
+ What stays yours is the text edge and the ref. The markdown is the host's at both ends, and `doc.quillRef` is the **document's** own — a stored file names the quill that opens it, so the ref → `Quill` mapping above is what bounds which templates it can reach.
68
+
63
69
  ## Preview
64
70
 
65
71
  `createPreview` supplies the layer the session omits: viewport, DOM, DPR, click mapping. It is a pure view: it never calls `session.apply`; you drive the edit and hand it the resulting `ChangeSet`.
@@ -152,7 +158,7 @@ The package ships English and a seam to replace it. `strings` is keyed and **par
152
158
  addCard: '+ Ajouter une carte'
153
159
  }}
154
160
  formatDiagnostic={(d) =>
155
- d.code === 'validation::must_fill' ? `${d.path} est obligatoire` : undefined}
161
+ d.code === 'validation::type_mismatch' ? `${d.path} : type incorrect` : undefined}
156
162
  />
157
163
  ```
158
164
 
@@ -184,7 +190,7 @@ function recompileNow() {
184
190
  }
185
191
  ```
186
192
 
187
- `session.warnings` is a **getter on a handle Svelte does not track**: pull it per apply, or the editor shows the previous compile's diagnostics.
193
+ `session.warnings` is a **getter on a handle Svelte does not track**: pull it per apply, or the editor shows the previous compile's errors.
188
194
 
189
195
  This shell layer is deliberately yours: the debounce value, what applies at once, and which surfaces refresh are host policy, so the package ships no scheduler over them. Three obligations ride with that: an edit of your own (an import, an undo, a direct `doc` write) recompiles by the same calls, since nothing polls the document; a compile owes the diagnostics re-read above, since nothing polls the session; and teardown clears the timer **before** freeing the handles, so a pending recompile never touches a freed session.
190
196
 
package/THEMING.md CHANGED
@@ -4,11 +4,11 @@
4
4
 
5
5
  There are three depths to that. An app mounting into an unstyled page stops at the first; an app with a plate of its own starts at the second:
6
6
 
7
- | | You write | You get |
8
- | ----------------- | --------------------- | ------------------------------------------------------- |
9
- | **Drop it in** | nothing | a mounted surface that looks right in a bare `<div>` |
10
- | **Make it yours** | ten custom properties | the whole surface rescaled and recolored to your brand |
11
- | **Match ours** | one CSS import | the page around the surface, drawn the way we draw ours |
7
+ | | You write | You get |
8
+ | ----------------- | ---------------------- | ------------------------------------------------------- |
9
+ | **Drop it in** | nothing | a mounted surface that looks right in a bare `<div>` |
10
+ | **Make it yours** | nine custom properties | the whole surface rescaled and recolored to your brand |
11
+ | **Match ours** | one CSS import | the page around the surface, drawn the way we draw ours |
12
12
 
13
13
  ## Drop it in
14
14
 
@@ -24,11 +24,11 @@ Nothing to import and nothing to set. The package pulls its own stylesheet, and
24
24
 
25
25
  **Placing and sizing**: each surface carries a stable root class (`.qm-editor`, `.qm-preview`), so you can place it from your own stylesheet without a wrapper. Each also takes a `class` prop that merges onto the same element, which is the better handle when you have one to give.
26
26
 
27
- **Taking the column back**: your CSS is unlayered and ours is not (below), so `padding: 0` or a `background` of your own on `.qm-editor` wins outright. Nothing is locked.
27
+ **Taking the column back**: our selectors compute at specificity zero (below), so `padding: 0` or a `background` of your own on `.qm-editor` wins outright. Nothing is locked.
28
28
 
29
29
  ## Make it yours
30
30
 
31
- The whole contract is **ten CSS custom properties**. They are dials, not a palette: each derives a closed private scale that every component reads, so one override rescales or recolors the whole surface. Set them on any ancestor of a mounted surface (the app, or one pane):
31
+ The whole contract is **nine CSS custom properties**. They are dials, not a palette: each derives a closed private scale that every component reads, so one override rescales or recolors the whole surface. Set them on any ancestor of a mounted surface (the app, or one pane):
32
32
 
33
33
  ```css
34
34
  .my-editor {
@@ -43,7 +43,7 @@ The whole contract is **ten CSS custom properties**. They are dials, not a palet
43
43
 
44
44
  `--qm-bg` and `--qm-fg` are the two a themed host sets before any brand decision. Their defaults are a self-consistent neutral for a bare mounting site, not a guess at your page.
45
45
 
46
- `--qm-bg` is the **card**, not the column behind it: the plane a document is written on, which is also the sheet the preview paints. Everything else is a step off it, and the column the cards sit in is a step down, so a mounted editor draws its own ground and the cards read as islands on it. Give it the tone you want a card to be — white, in most light themes — and let the column fall out. If you want the column to meet your page instead, set a `background` of your own on `.qm-editor`; your CSS is unlayered and wins.
46
+ `--qm-bg` is the **card**, not the column behind it: the plane a document is written on, which is also the sheet the preview paints. Everything else is a step off it, and the column the cards sit in is a step down, so a mounted editor draws its own ground and the cards read as islands on it. Give it the tone you want a card to be — white, in most light themes — and let the column fall out. If you want the column to meet your page instead, set a `background` of your own on `.qm-editor`; any rule of yours wins.
47
47
 
48
48
  The poles take a colour, not `transparent` or `inherit`: the raised surfaces, the borders and the muted inks are each a `color-mix` against `--qm-bg`, so a transparent pole makes every one of them translucent instead of letting your page through. Hand over the value your page is painted with and the derivation steps off it.
49
49
 
@@ -55,7 +55,6 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
55
55
  | `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
56
56
  | `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
57
57
  | `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics, the required marker, the delete glyph. |
58
- | `--qm-warning` | `#926510` / `#fdc776` | Warning diagnostics. |
59
58
  | `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
60
59
  | `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
61
60
  | `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
@@ -66,15 +65,12 @@ Give a length dial a length: `--qm-space: 4` is a valid custom property and an i
66
65
 
67
66
  ### Your CSS beats ours
68
67
 
69
- The surfaces' own rules live in the `qm` cascade layer, so **unlayered CSS of yours beats them outright**, at any specificity and without `!important`. Nothing to declare; this is the case most apps are in.
68
+ The rules behind every class the surfaces promise you — `.qm-editor`, `.qm-preview`, `.qm-pane` — are wrapped in `:where()` and sit in no cascade layer, as are the derivation and the shared control recipes under them. So **each computes at specificity zero**, and two things follow:
70
69
 
71
- If your app uses cascade layers of its own, layer order decides it, and layer order is first-declaration order across the document — which makes it the bundler's, not yours. Name our layer first in one statement of your own, anywhere in your CSS, and it stops mattering:
70
+ - **Any rule of yours wins**, at any specificity and without `!important`. A bare `.qm-editor { … }` does it; so does a bare `input`.
71
+ - **No layer of yours can reach them**, because unlayered rules outrank every layer. A reset in a layer — Tailwind v4's preflight in `@layer base` is the common one — cannot strip the controls however your bundler orders the sheets. There is no layer statement to write and no sheet order to get right.
72
72
 
73
- ```css
74
- @layer qm, app;
75
- ```
76
-
77
- Declared before our sheet it fixes the order outright; declared after, `qm` is already first and `app` appends behind it. Either way your layer lands after ours and wins. `qm` is the contract; the sub-layers under it (`qm.scale`, `qm.chrome`) are ours to re-cut.
73
+ One thing still outranks us: an **unlayered** blanket reset of your own (`* { padding: 0 }` in no layer) ties at specificity zero and wins on source order. Put it in a layer, where Tailwind, Bootstrap and Open Props already put theirs, and it stops reaching us.
78
74
 
79
75
  ### The surface follows your colour scheme
80
76
 
@@ -106,9 +102,9 @@ Setting the poles yourself wins over the default in both schemes, so a fixed pal
106
102
  }
107
103
  ```
108
104
 
109
- That cuts both ways for the status hues: `--qm-danger` and `--qm-warning` each default to a pair calibrated against the card at both poles, and one pinned value replaces both arms, so a red measured against white carries into the dark scheme unmeasured. Pin them under your own scheme rule (`.dark { --qm-danger: … }`) where the colour your app means by error is itself scheme-aware.
105
+ That cuts both ways for the status hue: `--qm-danger` defaults to a pair calibrated against the card at both poles, and one pinned value replaces both arms, so a red measured against white carries into the dark scheme unmeasured. Pin it under your own scheme rule (`.dark { --qm-danger: … }`) where the colour your app means by error is itself scheme-aware.
110
106
 
111
- A palette **pinned against** your own scheme still wants a second line, for a smaller reason than it used to be: the planes read your card's lightness, so the surface itself lands right either way, but the poles' own defaults, the status hues and native chrome all still read `color-scheme`. Pin it with them (`.my-editor { color-scheme: dark }`). Skip that line and what the browser paints — the selection highlight, the scrollbar, the spinner — stays on whichever scheme the host inherits, and a status hue you left to us is measured against the wrong card.
107
+ A palette **pinned against** your own scheme still wants a second line, for a smaller reason than it used to be: the planes read your card's lightness, so the surface itself lands right either way, but the poles' own defaults, the status hue and native chrome all still read `color-scheme`. Pin it with them (`.my-editor { color-scheme: dark }`). Skip that line and what the browser paints — the selection highlight, the scrollbar, the spinner — stays on whichever scheme the host inherits, and a status hue you left to us is measured against the wrong card.
112
108
 
113
109
  Pinning **one mount** rather than a page is the `style` prop, which lands on the same root: `<Preview style="color-scheme: dark" />`. There is no `scheme` prop, and deliberately — an inline declaration of ours would be the one thing your own CSS could not beat.
114
110
 
@@ -131,7 +127,7 @@ import '@quillmark/svelte/preset';
131
127
 
132
128
  This is the stylesheet our own apps import: the playground and studio draw with these rules and nothing else, so what you get is what we ship, not a reduction of it.
133
129
 
134
- It carries a `--qmh-*` scale for the page, derived from the same ten dials and calibrated against the surfaces' own steps, so a plate of yours and a card of ours agree about which way "raised" goes. Turning a dial moves both.
130
+ It carries a `--qmh-*` scale for the page, derived from the same nine dials and calibrated against the surfaces' own steps, so a plate of yours and a card of ours agree about which way "raised" goes. Turning a dial moves both.
135
131
 
136
132
  | Class | What it is |
137
133
  | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
@@ -139,7 +135,7 @@ It carries a `--qmh-*` scale for the page, derived from the same ten dials and c
139
135
  | `.qm-panel` | The plate your controls and readouts sit on: one rung off the page, a hairline, a corner. |
140
136
  | `.qm-label` | A run of chrome type: a section label, the name over a value. |
141
137
  | `.qm-readout` | A value read back out of the session, monospace and tabular. `<pre class="qm-readout">` is the block form. |
142
- | `.qm-status` | A phase line. `.qm-status-error` and `.qm-status-warn` are the two it fails to. |
138
+ | `.qm-status` | A phase line. `.qm-status-error` is the one it fails to. |
143
139
  | `.qm-control` | A boxed button or select, which is what a host's controls are and the package's are not. |
144
140
  | `.qm-measure` | A reading column at the width a passage wants. |
145
141
 
@@ -181,18 +177,17 @@ The narrow shape is one mount at a time, not two of them stacked: half a narrow
181
177
 
182
178
  The shape is shared; the look of a band is not. Your wordmark, your nav and the depth of your head are the most app-specific things on the screen, and a preset that drew them would hand every tool built on this the same face, so they read `--qmh-*` like everything else and you write the rules. `.qm-split`'s gap is a default rather than a fact about splitting: close it to `0` and carry a hairline on one pane if you want the two mounts to meet instead of stand apart.
183
179
 
184
- It is **unlayered**, so your own rules beat it by ordinary precedence, exactly as they beat the surfaces. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
180
+ It is **unlayered** and at ordinary specificity, so your own rules beat it by ordinary precedence. It is also opt-in and side-effect free until imported: nothing that styles your document arrives with a component.
185
181
 
186
- One rule in it earns its place regardless of whether you take the rest, and you want it if you draw a focus ring of your own:
182
+ One rule in it earns its place regardless of whether you take the rest, and you want it if you draw a focus ring of your own — cut the mounted surfaces out of the blanket:
187
183
 
188
184
  ```css
189
- [data-qm-root] :focus-visible {
190
- outline: revert-layer;
191
- outline-offset: revert-layer;
185
+ :focus-visible:not(:where([data-qm-root], [data-qm-root] *)) {
186
+ outline: 2px solid; /* your ring, on your page */
192
187
  }
193
188
  ```
194
189
 
195
- A blanket `:focus-visible` of yours is unlayered, so it beats `@layer qm.chrome`, including on the controls that already draw a ring. `revert-layer` hands those back to ours and leaves yours everywhere else.
190
+ A blanket `:focus-visible` of yours outranks ours, including on the controls that already draw one, so a ring written for the controls that draw none repaints the ones that do. The cut-out leaves yours everywhere else, and `:not(:where(…))` contributes no specificity, so your rule ranks exactly as it would without it.
196
191
 
197
192
  ## What is deliberately not public
198
193
 
@@ -202,4 +197,4 @@ The derived surface scale (surface / border / ink rungs, the blur radius, the po
202
197
 
203
198
  ## Requirements
204
199
 
205
- `@layer`, `color-mix()`, `light-dark()` and relative colour syntax (`oklab(from …)`): Baseline since 2022, 2023, 2024 and 2024 respectively — the last is what lets a plane step by the card's own lightness. `@property` is the one feature the surface does not require: where it is missing, an invalid length dial collapses the surface rather than falling back.
200
+ `:where()`, `color-mix()`, `light-dark()` and relative colour syntax (`oklab(from …)`): Baseline since 2021, 2023, 2024 and 2024 respectively — the last is what lets a plane step by the card's own lightness. `@property` is the one feature the surface does not require: where it is missing, an invalid length dial collapses the surface rather than falling back.
@@ -63,14 +63,28 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
63
63
  /**
64
64
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
65
65
  * `undefined` for a path that names no single commit address — a nested or
66
- * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
67
- * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
68
- * terminal both land on the field-less `{card: i}` the body leaf answers to.
66
+ * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath} and
67
+ * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
68
+ * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
69
+ * the body leaf answers to.
69
70
  *
70
71
  * Needs no `kinds`: the path carries the absolute index, and the kind in it is
71
72
  * decoration the `Addr` has no room for.
72
73
  */
73
74
  export declare function addrForFieldPath(path: DocPath): Addr | undefined;
75
+ /**
76
+ * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the
77
+ * path has one, else the root and its first field. `main.contact.email` and
78
+ * `main.keywords[0]` land on their field, any deeper nesting on the top-level field
79
+ * it hangs off. `undefined` where those two segments name nothing — a field-rooted
80
+ * (config-space) or malformed path.
81
+ *
82
+ * An `Addr` reaches a root and one field, so two segments is the whole search: a
83
+ * deeper prefix is unaddressable by arity alone. Addressability being `Addr`'s reach
84
+ * rather than the grammar's is what makes the truncation a consumer's to make
85
+ * (VISUAL_EDITOR §Diagnostics).
86
+ */
87
+ export declare function nearestAddrForFieldPath(path: DocPath): Addr | undefined;
74
88
  /**
75
89
  * An array element's address, split: `main.keywords[0]` lands here, and anything
76
90
  * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
@@ -37,9 +37,10 @@ export function cardPath(index, kinds) {
37
37
  /**
38
38
  * The inverse: a canonical `DocPath` back to the `Addr` the document verbs take, or
39
39
  * `undefined` for a path that names no single commit address — a nested or
40
- * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
41
- * takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
42
- * terminal both land on the field-less `{card: i}` the body leaf answers to.
40
+ * array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath} and
41
+ * {@link nearestAddrForFieldPath} take instead), a field-rooted one, or a malformed
42
+ * one. A bare card and a `.body` terminal both land on the field-less `{card: i}`
43
+ * the body leaf answers to.
43
44
  *
44
45
  * Needs no `kinds`: the path carries the absolute index, and the kind in it is
45
46
  * decoration the `Addr` has no room for.
@@ -48,6 +49,22 @@ export function addrForFieldPath(path) {
48
49
  const segs = segsOf(path);
49
50
  return segs && addrForSegs(segs);
50
51
  }
52
+ /**
53
+ * The nearest ancestor a commit address can name: {@link addrForFieldPath} where the
54
+ * path has one, else the root and its first field. `main.contact.email` and
55
+ * `main.keywords[0]` land on their field, any deeper nesting on the top-level field
56
+ * it hangs off. `undefined` where those two segments name nothing — a field-rooted
57
+ * (config-space) or malformed path.
58
+ *
59
+ * An `Addr` reaches a root and one field, so two segments is the whole search: a
60
+ * deeper prefix is unaddressable by arity alone. Addressability being `Addr`'s reach
61
+ * rather than the grammar's is what makes the truncation a consumer's to make
62
+ * (VISUAL_EDITOR §Diagnostics).
63
+ */
64
+ export function nearestAddrForFieldPath(path) {
65
+ const segs = segsOf(path);
66
+ return segs && addrForSegs(segs.slice(0, 2));
67
+ }
51
68
  /**
52
69
  * An array element's address, split: `main.keywords[0]` lands here, and anything
53
70
  * {@link addrForFieldPath} can name — or whose trailing segment is not an index —
@@ -0,0 +1,37 @@
1
+ import type { Attrs, Node as PMNode } from 'prosemirror-model';
2
+ import type { Command, EditorState, Transaction } from 'prosemirror-state';
3
+ /** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
4
+ * shape the content holds and `importMarkdown` produces from `- # title`. */
5
+ export declare function toHeading(level: number): Command;
6
+ /** A fence. The exit paragraph after it is the one a block island mints for the same
7
+ * reason, sharpened: a code block is the one block a gap cursor will not sit beside
8
+ * either, so a fence at the end of a body leaves the caret nowhere to go. The caret
9
+ * stays in the fence, not in the exit it minted. */
10
+ export declare function toCodeBlock(): Command;
11
+ /** `---`. A divider is a whole block, so this replaces its textblock rather than
12
+ * retyping one: `horizontal_rule` holds no content to retype into, and an empty block
13
+ * is the whole of what it may replace — anything else in one is a paragraph being
14
+ * edited, which a replace would eat. */
15
+ export declare function insertDivider(): Command;
16
+ /** `> `. No item guard: what this wraps an item's first block in is a container the
17
+ * content holds, where a list wrapping one mints an item whose only content is an
18
+ * item. */
19
+ export declare function wrapInQuote(): Command;
20
+ /** `- ` / `1. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
21
+ * above where `7. ` opens one. A pick passes none and continues whatever list is
22
+ * above it. */
23
+ export declare function wrapInList(ordered: boolean, attrs?: Attrs | null, joinBefore?: (node: PMNode) => boolean): Command;
24
+ /**
25
+ * Run `command` in place of the text at `[from, to)`: one transaction carrying the
26
+ * delete and the command's own steps. One rather than two because two would be two
27
+ * commits, two undo steps and two recompiles for one gesture; the command is built
28
+ * against the state the delete leaves, which is exactly what its steps were computed
29
+ * for.
30
+ *
31
+ * `null` is a command that declined, and nothing is consumed: the shorthand stays the
32
+ * text it was typed as.
33
+ */
34
+ export declare function consuming(state: EditorState, from: number, to: number, command: Command): {
35
+ tr: Transaction;
36
+ produced: Transaction;
37
+ } | null;
@@ -0,0 +1,163 @@
1
+ import { Selection } from 'prosemirror-state';
2
+ import { canJoin, findWrapping } from 'prosemirror-transform';
3
+ /** The head of a textblock: where a shorthand fires, and where a pick lands once its
4
+ * run is consumed. */
5
+ function atHead(state) {
6
+ const { $from, empty } = state.selection;
7
+ return empty && $from.parent.isTextblock && $from.parentOffset === 0;
8
+ }
9
+ /** Whether the caret's parent holds `type` in its block's place. `setBlockType` and
10
+ * `replaceWith` both mint the shape without asking. */
11
+ function fits($from, type) {
12
+ return $from.node(-1).canReplaceWith($from.index(-1), $from.indexAfter(-1), type);
13
+ }
14
+ /**
15
+ * The head of an item that already exists: where a list shorthand is the text an author
16
+ * typed rather than a shorthand at all.
17
+ *
18
+ * Firing there wraps the item's own paragraph in a fresh list, minting an item whose
19
+ * only content is another item — a level with nothing on it. Tab is the indent gesture
20
+ * and lands the shape a nesting is supposed to have, under the previous sibling. A
21
+ * later block of the item is not this case: wrapping a continuation paragraph is how a
22
+ * sub-list opens under text.
23
+ */
24
+ function openingAnItem($from, item) {
25
+ if (!item || $from.depth < 2)
26
+ return false;
27
+ return $from.node(-1).type === item && $from.index(-1) === 0;
28
+ }
29
+ function retype(name, attrs) {
30
+ return (state, dispatch) => {
31
+ const type = state.schema.nodes[name];
32
+ const { $from } = state.selection;
33
+ if (!type || !atHead(state) || !fits($from, type))
34
+ return false;
35
+ dispatch?.(state.tr.setBlockType($from.pos, $from.pos, type, attrs));
36
+ return true;
37
+ };
38
+ }
39
+ /** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a preceding
40
+ * sibling of the same type — the one boundary the wrap itself opens (`lists.ts`
41
+ * §cleanup: command-local, never a global pass). */
42
+ function wrapAt(tr, pos, type, attrs, joinBefore) {
43
+ const range = tr.doc.resolve(pos).blockRange();
44
+ const wrapping = range && findWrapping(range, type, attrs);
45
+ if (!range || !wrapping)
46
+ return false;
47
+ tr.wrap(range, wrapping);
48
+ const before = tr.doc.resolve(pos - 1).nodeBefore;
49
+ if (before &&
50
+ before.type === type &&
51
+ canJoin(tr.doc, pos - 1) &&
52
+ (!joinBefore || joinBefore(before))) {
53
+ tr.join(pos - 1);
54
+ }
55
+ return true;
56
+ }
57
+ /** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
58
+ * shape the content holds and `importMarkdown` produces from `- # title`. */
59
+ export function toHeading(level) {
60
+ return retype('heading', { level });
61
+ }
62
+ /** A fence. The exit paragraph after it is the one a block island mints for the same
63
+ * reason, sharpened: a code block is the one block a gap cursor will not sit beside
64
+ * either, so a fence at the end of a body leaves the caret nowhere to go. The caret
65
+ * stays in the fence, not in the exit it minted. */
66
+ export function toCodeBlock() {
67
+ return (state, dispatch) => {
68
+ const { code_block: code, paragraph } = state.schema.nodes;
69
+ const { $from } = state.selection;
70
+ if (!code || !paragraph || !atHead(state) || !fits($from, code))
71
+ return false;
72
+ const tr = state.tr.setBlockType($from.pos, $from.pos, code);
73
+ const at = tr.doc.resolve($from.pos).after();
74
+ if (!tr.doc.nodeAt(at))
75
+ tr.insert(at, paragraph.create());
76
+ dispatch?.(tr);
77
+ return true;
78
+ };
79
+ }
80
+ /** `---`. A divider is a whole block, so this replaces its textblock rather than
81
+ * retyping one: `horizontal_rule` holds no content to retype into, and an empty block
82
+ * is the whole of what it may replace — anything else in one is a paragraph being
83
+ * edited, which a replace would eat. */
84
+ export function insertDivider() {
85
+ return (state, dispatch) => {
86
+ const { horizontal_rule: rule, paragraph } = state.schema.nodes;
87
+ const { $from } = state.selection;
88
+ if (!rule || !paragraph || !atHead(state) || !fits($from, rule))
89
+ return false;
90
+ if ($from.parent.content.size !== 0)
91
+ return false;
92
+ const from = $from.before();
93
+ const tr = state.tr.replaceWith(from, $from.after(), rule.create());
94
+ const at = from + 1;
95
+ if (!tr.doc.nodeAt(at))
96
+ tr.insert(at, paragraph.create());
97
+ tr.setSelection(Selection.near(tr.doc.resolve(at), 1));
98
+ dispatch?.(tr);
99
+ return true;
100
+ };
101
+ }
102
+ /** `> `. No item guard: what this wraps an item's first block in is a container the
103
+ * content holds, where a list wrapping one mints an item whose only content is an
104
+ * item. */
105
+ export function wrapInQuote() {
106
+ return (state, dispatch) => {
107
+ const quote = state.schema.nodes.blockquote;
108
+ const { $from } = state.selection;
109
+ if (!quote || !atHead(state))
110
+ return false;
111
+ const tr = state.tr;
112
+ if (!wrapAt(tr, $from.pos, quote, null))
113
+ return false;
114
+ dispatch?.(tr);
115
+ return true;
116
+ };
117
+ }
118
+ /** `- ` / `1. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
119
+ * above where `7. ` opens one. A pick passes none and continues whatever list is
120
+ * above it. */
121
+ export function wrapInList(ordered, attrs = null, joinBefore) {
122
+ return (state, dispatch) => {
123
+ const { paragraph, list_item: item } = state.schema.nodes;
124
+ const list = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
125
+ const { $from } = state.selection;
126
+ if (!list || !paragraph || !atHead(state) || openingAnItem($from, item))
127
+ return false;
128
+ const pos = $from.pos;
129
+ const tr = state.tr;
130
+ // A paragraph item wherever this fires: `# ` inside the item is the gesture that
131
+ // mints `list_item > heading`, and a wrap keeping the heading is a second door onto
132
+ // that shape. Positions survive the retype — same content, same size.
133
+ if ($from.parent.type !== paragraph)
134
+ tr.setBlockType(pos, pos, paragraph);
135
+ if (!wrapAt(tr, pos, list, attrs, joinBefore))
136
+ return false;
137
+ dispatch?.(tr);
138
+ return true;
139
+ };
140
+ }
141
+ /**
142
+ * Run `command` in place of the text at `[from, to)`: one transaction carrying the
143
+ * delete and the command's own steps. One rather than two because two would be two
144
+ * commits, two undo steps and two recompiles for one gesture; the command is built
145
+ * against the state the delete leaves, which is exactly what its steps were computed
146
+ * for.
147
+ *
148
+ * `null` is a command that declined, and nothing is consumed: the shorthand stays the
149
+ * text it was typed as.
150
+ */
151
+ export function consuming(state, from, to, command) {
152
+ const tr = state.tr.delete(from, to);
153
+ let produced;
154
+ command(state.apply(tr), (t) => {
155
+ produced = t;
156
+ });
157
+ if (!produced)
158
+ return null;
159
+ for (const step of produced.steps)
160
+ tr.step(step);
161
+ tr.setSelection(Selection.fromJSON(tr.doc, produced.selection.toJSON()));
162
+ return { tr, produced };
163
+ }
@@ -19,7 +19,6 @@ import { keymap } from 'prosemirror-keymap';
19
19
  import { EditorState, NodeSelection, Plugin, PluginKey, Selection, TextSelection } from 'prosemirror-state';
20
20
  import { Decoration, DecorationSet, EditorView } from 'prosemirror-view';
21
21
  import { reportError, errorMessage } from '../errors.js';
22
- import { tableClipboardPlugin } from './clipboard.js';
23
22
  import { decode } from './decode.js';
24
23
  import { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
25
24
  import { lower, pmToContent, contentEdit } from './encode.js';
@@ -407,11 +406,10 @@ export function proseLeafPlugins(schema, opts) {
407
406
  list.push(inputRulesPlugin(schema));
408
407
  list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
409
408
  list.push(keymap(baseKeymap));
410
- // All three answer to something only a block leaf holds: an inline leaf is one
411
- // textblock with no island, no gap to put a cursor in, and nowhere for a pasted
412
- // table to land.
409
+ // Both answer to something only a block leaf holds: an inline leaf is one textblock
410
+ // with no island and no gap to put a cursor in.
413
411
  if (!opts.inline)
414
- list.push(gapCursor(), pastAtomPlugin(), tableClipboardPlugin(schema));
412
+ list.push(gapCursor(), pastAtomPlugin());
415
413
  if (opts.placeholder)
416
414
  list.push(placeholderPlugin(opts.placeholder));
417
415
  return list;
@@ -9,7 +9,7 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
9
9
  export type { TableChromeStrings } from './table-view.js';
10
10
  export { DEFAULT_SLASH_STRINGS } from './slash.js';
11
11
  export type { SlashState, SlashStrings } from './slash.js';
12
- export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
12
+ export { blockSchema, inlineSchema, plaintextSchema, hasMarks, rendersHref } from './schema.js';
13
13
  export { decode, renderContent, usvLength } from './decode.js';
14
14
  export { pmToContent, contentEdit, lower } from './encode.js';
15
15
  export type { ContentEdit } from './encode.js';
@@ -21,8 +21,9 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
21
21
  // the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
22
22
  export { DEFAULT_SLASH_STRINGS } from './slash.js';
23
23
  // Schemas (the decode/encode target; the VisualEditor mounts them). `hasMarks` is
24
- // what a mark surface asks before offering itself over a leaf.
25
- export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
24
+ // what a mark surface asks before offering itself over a leaf, and `rendersHref` the
25
+ // link gate the prompt re-asks on submit (`visual/links.ts`).
26
+ export { blockSchema, inlineSchema, plaintextSchema, hasMarks, rendersHref } from './schema.js';
26
27
  // Decode / encode / positions (tests + VisualEditor).
27
28
  //
28
29
  // The barrel carries what has an off-barrel caller, and nothing else. A symbol