@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.
- package/README.md +8 -2
- package/THEMING.md +22 -27
- package/dist/core/address.d.ts +17 -3
- package/dist/core/address.js +20 -3
- package/dist/core/codec/blocks.d.ts +37 -0
- package/dist/core/codec/blocks.js +163 -0
- package/dist/core/codec/field.js +3 -5
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +24 -144
- package/dist/core/codec/prose.css +14 -25
- package/dist/core/codec/schema.d.ts +8 -0
- package/dist/core/codec/schema.js +33 -1
- package/dist/core/codec/slash.d.ts +18 -16
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.js +127 -157
- package/dist/core/codec/table.d.ts +0 -10
- package/dist/core/codec/table.js +0 -15
- package/dist/core/theme.css +350 -347
- package/dist/preset/recipes.css +10 -21
- package/dist/preset/scale.css +6 -7
- package/dist/preview/Preview.svelte +4 -2
- package/dist/preview/geometry.d.ts +10 -9
- package/dist/preview/geometry.js +11 -13
- package/dist/visual/ArrayField.svelte +242 -63
- package/dist/visual/ArrayField.svelte.d.ts +5 -1
- package/dist/visual/Card.svelte +21 -2
- package/dist/visual/DiagnosticList.svelte +6 -11
- package/dist/visual/EnumField.svelte +33 -1
- package/dist/visual/Field.svelte +29 -5
- package/dist/visual/FieldLabel.svelte +2 -2
- package/dist/visual/FormatPopover.svelte +13 -1
- package/dist/visual/ObjectField.svelte +157 -35
- package/dist/visual/ObjectField.svelte.d.ts +13 -4
- package/dist/visual/ProseArrayElement.svelte +22 -2
- package/dist/visual/ProseArrayElement.svelte.d.ts +1 -0
- package/dist/visual/TipsCard.svelte +33 -13
- package/dist/visual/VariantField.svelte +98 -0
- package/dist/visual/VariantField.svelte.d.ts +21 -0
- package/dist/visual/VisualEditorInner.svelte +18 -15
- package/dist/visual/controls.css +282 -283
- package/dist/visual/diagnostics.d.ts +15 -24
- package/dist/visual/diagnostics.js +14 -30
- package/dist/visual/domid.d.ts +13 -0
- package/dist/visual/domid.js +16 -0
- package/dist/visual/leaves.d.ts +9 -7
- package/dist/visual/links.d.ts +5 -2
- package/dist/visual/links.js +9 -3
- package/dist/visual/props.d.ts +1 -1
- package/dist/visual/strings.d.ts +21 -6
- package/dist/visual/strings.js +3 -0
- package/dist/visual/structure.d.ts +32 -7
- package/dist/visual/structure.js +49 -3
- package/package.json +2 -2
- package/dist/core/codec/clipboard.d.ts +0 -59
- 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::
|
|
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
|
|
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
|
|
8
|
-
| ----------------- |
|
|
9
|
-
| **Drop it in** | nothing
|
|
10
|
-
| **Make it yours** |
|
|
11
|
-
| **Match ours** | one CSS import
|
|
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**:
|
|
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 **
|
|
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`;
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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]
|
|
190
|
-
outline:
|
|
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
|
|
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
|
-
|
|
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.
|
package/dist/core/address.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
68
|
-
* terminal both land on the field-less `{card: i}`
|
|
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 —
|
package/dist/core/address.js
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
* terminal both land on the field-less `{card: i}`
|
|
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
|
+
}
|
package/dist/core/codec/field.js
CHANGED
|
@@ -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
|
-
//
|
|
411
|
-
//
|
|
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()
|
|
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';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|