@quillmark/svelte 0.3.2 → 0.4.1
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 +1 -1
- package/THEMING.md +10 -6
- package/dist/core/address.d.ts +5 -11
- package/dist/core/address.js +7 -18
- package/dist/core/codec/decode.d.ts +2 -7
- package/dist/core/codec/decode.js +9 -5
- package/dist/core/codec/field.d.ts +6 -7
- package/dist/core/codec/field.js +21 -21
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +17 -19
- package/dist/core/codec/prose.css +8 -7
- package/dist/core/codec/schema.d.ts +15 -6
- package/dist/core/codec/schema.js +30 -17
- package/dist/core/theme.css +52 -40
- package/dist/preset/recipes.css +3 -3
- package/dist/preset/scale.css +21 -19
- package/dist/preview/geometry.d.ts +4 -3
- package/dist/preview/geometry.js +7 -4
- package/dist/visual/ArrayField.svelte +11 -6
- package/dist/visual/Card.svelte +5 -1
- package/dist/visual/DateField.svelte +1 -1
- package/dist/visual/EnumField.svelte +4 -4
- package/dist/visual/Field.svelte +2 -2
- package/dist/visual/FieldHint.svelte +16 -2
- package/dist/visual/FormatPopover.svelte +12 -1
- package/dist/visual/ObjectField.svelte +3 -3
- package/dist/visual/ProseArrayElement.svelte +5 -6
- package/dist/visual/ProseArrayElement.svelte.d.ts +0 -1
- package/dist/visual/ProseField.svelte +5 -3
- package/dist/visual/VisualEditorInner.svelte +5 -6
- package/dist/visual/controls.css +11 -7
- package/dist/visual/structure.d.ts +0 -2
- package/dist/visual/structure.js +1 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -200,7 +200,7 @@ onPick: (at) => visualEditor.setCaret(at);
|
|
|
200
200
|
onCaretMove: preview.focusPosition;
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.
|
|
203
|
+
A pick carries a caret where the compile tracks the content under the point, and the field alone where it tracks only the placement — a scalar the plate prints without tracking. `setCaret` takes both: an absent `pos` reveals and focuses the field, which is the whole of what a click on plate-placed ink can mean. The address may name an array element (`main.keywords[0]`), which lands on that row.
|
|
204
204
|
|
|
205
205
|
A consumer holding an `Addr` of its own maps it with `fieldPathForAddr` (from `@quillmark/svelte/core`). The playground app's split-pane route is the full reference shell: one session, both bridge directions, the preview following edits, and diagnostics routed inline.
|
|
206
206
|
|
package/THEMING.md
CHANGED
|
@@ -54,8 +54,8 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
|
|
|
54
54
|
| `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
|
|
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
|
-
| `--qm-danger` | `#c5221f`
|
|
58
|
-
| `--qm-warning` | `#b25000`
|
|
57
|
+
| `--qm-danger` | `#c5221f` / `#ff9c94` | Error diagnostics, the required marker, the delete glyph. |
|
|
58
|
+
| `--qm-warning` | `#b25000` / `#e0b44c` | Warning diagnostics. |
|
|
59
59
|
| `--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
60
|
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
|
|
61
61
|
| `--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. |
|
|
@@ -80,7 +80,7 @@ Declared before our sheet it fixes the order outright; declared after, `qm` is a
|
|
|
80
80
|
|
|
81
81
|
Ink steps `fg → bg` and the borders and hover fills step `bg → fg`, mixed in **oklab**, so inverting the two poles inverts all of them together.
|
|
82
82
|
|
|
83
|
-
**Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole.
|
|
83
|
+
**Planes are the exception**, and deliberately: the raised plane is the brighter one in both schemes, which is what makes a card read as a card rather than as a differently-tinted page. So the column under the cards sinks from `--qm-bg` toward shadow and a floating surface rises from it toward light, and neither reads the ink pole. How far each steps is a function of the card's own lightness — a dark card has to spend more before the plane above it reads as lifted — so a palette pinned at neither pole takes a proportionate step without being told which scheme it is in.
|
|
84
84
|
|
|
85
85
|
Which way the poles default is **your `color-scheme`**, not the operating system's. Declare the scheme your app is in and the surface lands on it, along with native chrome, which reads the same property:
|
|
86
86
|
|
|
@@ -106,7 +106,11 @@ Setting the poles yourself wins over the default in both schemes, so a fixed pal
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
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.
|
|
110
|
+
|
|
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.
|
|
112
|
+
|
|
113
|
+
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.
|
|
110
114
|
|
|
111
115
|
No JS runs and no media query: the derivation resolves through the cascade at paint time, so there is no `dark` prop and no mode toggle. The consumer's palette decides.
|
|
112
116
|
|
|
@@ -121,7 +125,7 @@ import '@quillmark/svelte/preset';
|
|
|
121
125
|
```html
|
|
122
126
|
<body class="qm-page">
|
|
123
127
|
<p class="qm-label">quill</p>
|
|
124
|
-
<pre class="qm-readout">
|
|
128
|
+
<pre class="qm-readout">specimen@1.0.0</pre>
|
|
125
129
|
</body>
|
|
126
130
|
```
|
|
127
131
|
|
|
@@ -198,4 +202,4 @@ The derived surface scale (surface / border / ink rungs, the blur radius, the po
|
|
|
198
202
|
|
|
199
203
|
## Requirements
|
|
200
204
|
|
|
201
|
-
`@layer`, `color-mix()
|
|
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.
|
package/dist/core/address.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ 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.
|
|
66
|
+
* array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
|
|
67
67
|
* takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
|
|
68
68
|
* terminal both land on the field-less `{card: i}` the body leaf answers to.
|
|
69
69
|
*
|
|
@@ -72,16 +72,10 @@ export declare function cardPath(index: number, kinds: readonly string[]): DocPa
|
|
|
72
72
|
*/
|
|
73
73
|
export declare function addrForFieldPath(path: DocPath): Addr | undefined;
|
|
74
74
|
/**
|
|
75
|
-
* An array element's address, split
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* Both spellings, because the boundary mints one and formats the other: `regions()`
|
|
80
|
-
* and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
|
|
81
|
-
* literally named `"0"`, while `formatDocPath` spells the index segment
|
|
82
|
-
* `main.references[0]`. A trailing all-digits field name is therefore an index that
|
|
83
|
-
* lost its brackets — but only under a field the schema declares an array, the
|
|
84
|
-
* caller's guard to apply: this module holds the grammar and no schema.
|
|
75
|
+
* An array element's address, split: `main.keywords[0]` lands here, and anything
|
|
76
|
+
* {@link addrForFieldPath} can name — or whose trailing segment is not an index —
|
|
77
|
+
* does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
|
|
78
|
+
* segment bracketed, so there is one spelling to read and none to bridge.
|
|
85
79
|
*/
|
|
86
80
|
export declare function elementAddrForFieldPath(path: DocPath): ElementAddr | undefined;
|
|
87
81
|
/** An array element's address: the array's `Addr`, and which element of it. */
|
package/dist/core/address.js
CHANGED
|
@@ -37,7 +37,7 @@ 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.
|
|
40
|
+
* array-element path (`main.keywords[0]`, which {@link elementAddrForFieldPath}
|
|
41
41
|
* takes instead), a field-rooted one, or a malformed one. A bare card and a `.body`
|
|
42
42
|
* terminal both land on the field-less `{card: i}` the body leaf answers to.
|
|
43
43
|
*
|
|
@@ -49,32 +49,21 @@ export function addrForFieldPath(path) {
|
|
|
49
49
|
return segs && addrForSegs(segs);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* An array element's address, split
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Both spellings, because the boundary mints one and formats the other: `regions()`
|
|
57
|
-
* and `positionAt` emit `main.references.0`, which `parseDocPath` reads as a field
|
|
58
|
-
* literally named `"0"`, while `formatDocPath` spells the index segment
|
|
59
|
-
* `main.references[0]`. A trailing all-digits field name is therefore an index that
|
|
60
|
-
* lost its brackets — but only under a field the schema declares an array, the
|
|
61
|
-
* caller's guard to apply: this module holds the grammar and no schema.
|
|
52
|
+
* An array element's address, split: `main.keywords[0]` lands here, and anything
|
|
53
|
+
* {@link addrForFieldPath} can name — or whose trailing segment is not an index —
|
|
54
|
+
* does not. `regions()`, `positionAt` and `formatDocPath` all spell the index
|
|
55
|
+
* segment bracketed, so there is one spelling to read and none to bridge.
|
|
62
56
|
*/
|
|
63
57
|
export function elementAddrForFieldPath(path) {
|
|
64
58
|
const segs = segsOf(path);
|
|
65
59
|
if (!segs)
|
|
66
60
|
return undefined;
|
|
67
61
|
const last = segs[segs.length - 1];
|
|
68
|
-
|
|
69
|
-
? last.index
|
|
70
|
-
: last?.seg === 'field' && /^\d+$/.test(last.name)
|
|
71
|
-
? Number(last.name)
|
|
72
|
-
: undefined;
|
|
73
|
-
if (index == null)
|
|
62
|
+
if (last?.seg !== 'index')
|
|
74
63
|
return undefined;
|
|
75
64
|
const field = addrForSegs(segs.slice(0, -1));
|
|
76
65
|
// A body has no elements: the parent must name a field.
|
|
77
|
-
return field?.field != null ? { field, index } : undefined;
|
|
66
|
+
return field?.field != null ? { field, index: last.index } : undefined;
|
|
78
67
|
}
|
|
79
68
|
/** `parseDocPath`, with a malformed path as `undefined` rather than a throw. The gate
|
|
80
69
|
* is read outside the try: an uninitialized core is not a malformed path, and would
|
|
@@ -4,10 +4,6 @@ import type { Content } from '@quillmark/wasm';
|
|
|
4
4
|
export declare function codePoints(s: string): string[];
|
|
5
5
|
/** USV length of `s` (code points, not UTF-16 units). */
|
|
6
6
|
export declare function usvLength(s: string): number;
|
|
7
|
-
interface DecodeOpts {
|
|
8
|
-
/** Strip all marks (a `plaintext` field). */
|
|
9
|
-
plaintext?: boolean;
|
|
10
|
-
}
|
|
11
7
|
/**
|
|
12
8
|
* A `Content` as read-only DOM: decode under `schema`, then the nodes' own `toDOM`.
|
|
13
9
|
* The rendering half of the codec's job with no editing attached (no PM view, no
|
|
@@ -18,7 +14,6 @@ interface DecodeOpts {
|
|
|
18
14
|
* `DOMSerializer.fromSchema` memoizes on the schema, so repeat calls build no
|
|
19
15
|
* serializer.
|
|
20
16
|
*/
|
|
21
|
-
export declare function renderContent(rt: Content, schema: Schema
|
|
17
|
+
export declare function renderContent(rt: Content, schema: Schema): Node;
|
|
22
18
|
/** Decode a `Content` to a PM document under `schema`. */
|
|
23
|
-
export declare function decode(rt: Content, schema: Schema
|
|
24
|
-
export {};
|
|
19
|
+
export declare function decode(rt: Content, schema: Schema): PMNode;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
11
|
import { ISLAND_SLOT } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
|
-
import { isInlineSchema } from './schema.js';
|
|
13
|
+
import { hasMarks, isInlineSchema } from './schema.js';
|
|
14
14
|
import { isAnchorMark, isCodeLine, isHeadingLine, isListItemContainer } from '@quillmark/wasm';
|
|
15
15
|
/** Code points of `s` (USV units): the iteration granularity the content speaks. */
|
|
16
16
|
export function codePoints(s) {
|
|
@@ -33,11 +33,11 @@ export function usvLength(s) {
|
|
|
33
33
|
* `DOMSerializer.fromSchema` memoizes on the schema, so repeat calls build no
|
|
34
34
|
* serializer.
|
|
35
35
|
*/
|
|
36
|
-
export function renderContent(rt, schema
|
|
37
|
-
return DOMSerializer.fromSchema(schema).serializeFragment(decode(rt, schema
|
|
36
|
+
export function renderContent(rt, schema) {
|
|
37
|
+
return DOMSerializer.fromSchema(schema).serializeFragment(decode(rt, schema).content);
|
|
38
38
|
}
|
|
39
39
|
/** Decode a `Content` to a PM document under `schema`. */
|
|
40
|
-
export function decode(rt, schema
|
|
40
|
+
export function decode(rt, schema) {
|
|
41
41
|
const lineTexts = rt.text.split('\n');
|
|
42
42
|
// Per-line USV start: line i begins after all earlier lines and their `\n`s.
|
|
43
43
|
const starts = [];
|
|
@@ -46,7 +46,11 @@ export function decode(rt, schema, opts = {}) {
|
|
|
46
46
|
starts.push(acc);
|
|
47
47
|
acc += usvLength(lineTexts[i]) + 1; // +1 for the `\n` boundary
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// A mark-free schema (`plaintextSchema`) has nothing to apply them with, and a
|
|
50
|
+
// plaintext field that acquired one anyway — an older build's popover, a hand-
|
|
51
|
+
// written document — is exactly the content that must open and then heal on the
|
|
52
|
+
// next commit, not throw here.
|
|
53
|
+
const marks = hasMarks(schema) ? rt.marks.filter((m) => !isAnchorMark(m)) : [];
|
|
50
54
|
const cursor = { i: 0, rt };
|
|
51
55
|
if (isInlineSchema(schema)) {
|
|
52
56
|
return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
|
|
@@ -16,7 +16,8 @@ export interface CreateFieldOpts {
|
|
|
16
16
|
container: HTMLElement;
|
|
17
17
|
/** Constrained single-textblock schema (a `richtext(inline)` field). */
|
|
18
18
|
inline?: boolean;
|
|
19
|
-
/**
|
|
19
|
+
/** The mark-free inline schema (a `plaintext` field): literal text, no formatting
|
|
20
|
+
* and no anchors. Implies `inline`. */
|
|
20
21
|
plaintext?: boolean;
|
|
21
22
|
/** Suppress the markdown-shorthand input rules. */
|
|
22
23
|
noInputRules?: boolean;
|
|
@@ -145,15 +146,13 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
145
146
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
146
147
|
* and {@link pastAtomPlugin}.
|
|
147
148
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* ones it would add: skip all.
|
|
149
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over
|
|
150
|
+
* `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
|
|
151
|
+
* type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
|
|
152
|
+
* typed without a second rule saying so.
|
|
153
153
|
*/
|
|
154
154
|
export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
155
155
|
inline: boolean;
|
|
156
|
-
plaintext: boolean;
|
|
157
156
|
/** The slash menu's report channel; absent mounts no menu and leaves
|
|
158
157
|
* Enter/Escape/the arrows to the links below (`keymap.ts`). */
|
|
159
158
|
slash?: (state: SlashState | undefined) => void;
|
package/dist/core/codec/field.js
CHANGED
|
@@ -27,7 +27,7 @@ import { anchorsFromContent } from './marks.js';
|
|
|
27
27
|
import { createReconciler } from './reconcile.js';
|
|
28
28
|
import { inputRulesPlugin } from './inputrules.js';
|
|
29
29
|
import { bodyKeymap } from './keymap.js';
|
|
30
|
-
import { blockSchema, inlineSchema } from './schema.js';
|
|
30
|
+
import { blockSchema, inlineSchema, plaintextSchema } from './schema.js';
|
|
31
31
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
32
32
|
import { focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
33
33
|
const anchorKey = new PluginKey('quill-anchors');
|
|
@@ -108,7 +108,10 @@ export function createField(opts) {
|
|
|
108
108
|
const { doc, addr, container } = opts;
|
|
109
109
|
const inline = !!opts.inline || !!opts.plaintext;
|
|
110
110
|
const plaintext = !!opts.plaintext;
|
|
111
|
-
|
|
111
|
+
// The declared type picks the schema, and the schema is the whole of what
|
|
112
|
+
// `plaintext` suppresses: no mark types to toggle, to paste in, or to mint a
|
|
113
|
+
// shorthand with (`schema.ts`).
|
|
114
|
+
const schema = plaintext ? plaintextSchema : inline ? inlineSchema : blockSchema;
|
|
112
115
|
// Bound once and held: the reader is a `{quill, doc}` pair that reads live, so
|
|
113
116
|
// every read below sees the commit before it.
|
|
114
117
|
const reader = opts.quill.reader(doc);
|
|
@@ -176,7 +179,7 @@ export function createField(opts) {
|
|
|
176
179
|
}
|
|
177
180
|
});
|
|
178
181
|
function buildState(rt) {
|
|
179
|
-
const pmDoc = decode(rt, schema
|
|
182
|
+
const pmDoc = decode(rt, schema);
|
|
180
183
|
const anchors = plaintext ? [] : anchorsFromContent(rt);
|
|
181
184
|
// Seed anchor plugin positions in PM coords via a fresh index over pmDoc.
|
|
182
185
|
const seedIndex = buildLineIndex(pmDoc);
|
|
@@ -188,7 +191,6 @@ export function createField(opts) {
|
|
|
188
191
|
doc: pmDoc,
|
|
189
192
|
plugins: proseLeafPlugins(schema, {
|
|
190
193
|
inline,
|
|
191
|
-
plaintext,
|
|
192
194
|
slash,
|
|
193
195
|
noInputRules: opts.noInputRules,
|
|
194
196
|
// Always installed, so a leaf that mounts without a ghost can still be
|
|
@@ -368,19 +370,18 @@ export function createField(opts) {
|
|
|
368
370
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
369
371
|
* and {@link pastAtomPlugin}.
|
|
370
372
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* ones it would add: skip all.
|
|
373
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over
|
|
374
|
+
* `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
|
|
375
|
+
* type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
|
|
376
|
+
* typed without a second rule saying so.
|
|
376
377
|
*/
|
|
377
378
|
export function proseLeafPlugins(schema, opts) {
|
|
378
379
|
const list = [history(), ...(opts.afterHistory ?? [])];
|
|
379
380
|
if (opts.slash)
|
|
380
381
|
list.push(slashPlugin(opts.slash));
|
|
381
|
-
if (!opts.noInputRules
|
|
382
|
+
if (!opts.noInputRules)
|
|
382
383
|
list.push(inputRulesPlugin(schema));
|
|
383
|
-
list.push(keymap(editorKeymap(schema, opts.inline,
|
|
384
|
+
list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
|
|
384
385
|
list.push(keymap(baseKeymap));
|
|
385
386
|
// All three answer to something only a block leaf holds: an inline leaf is one
|
|
386
387
|
// textblock with no island, no gap to put a cursor in, and nowhere for a pasted
|
|
@@ -460,7 +461,8 @@ function placeholderPlugin(read) {
|
|
|
460
461
|
}
|
|
461
462
|
/**
|
|
462
463
|
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
463
|
-
* suppressed inline.
|
|
464
|
+
* suppressed inline. The toggles are bound per mark type the schema declares, so a
|
|
465
|
+
* plaintext leaf leaves Mod-b/i/u to the browser.
|
|
464
466
|
*
|
|
465
467
|
* Tab forks on the leaf's role, not on the caret's position. An inline/plaintext
|
|
466
468
|
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
@@ -468,20 +470,18 @@ function placeholderPlugin(read) {
|
|
|
468
470
|
* is structural, a chain each nested surface prepends to (`keymap.ts`). One key
|
|
469
471
|
* never means two things within one surface.
|
|
470
472
|
*/
|
|
471
|
-
function editorKeymap(schema, inline,
|
|
473
|
+
function editorKeymap(schema, inline, slash) {
|
|
472
474
|
const map = {
|
|
473
475
|
'Mod-z': undo,
|
|
474
476
|
'Mod-y': redo,
|
|
475
477
|
'Shift-Mod-z': redo
|
|
476
478
|
};
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
484
|
-
}
|
|
479
|
+
if (schema.marks.strong)
|
|
480
|
+
map['Mod-b'] = toggleMark(schema.marks.strong);
|
|
481
|
+
if (schema.marks.em)
|
|
482
|
+
map['Mod-i'] = toggleMark(schema.marks.em);
|
|
483
|
+
if (schema.marks.underline)
|
|
484
|
+
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
485
485
|
if (inline) {
|
|
486
486
|
// One textblock only: swallow Enter so no second block is attempted.
|
|
487
487
|
map['Enter'] = () => true;
|
|
@@ -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 } from './schema.js';
|
|
12
|
+
export { blockSchema, inlineSchema, plaintextSchema, hasMarks } 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
|
@@ -20,8 +20,9 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
|
20
20
|
// The slash menu: the codec owns the model and the picks, the chrome the words and
|
|
21
21
|
// the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
|
|
22
22
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
23
|
-
// Schemas (the decode/encode target; the VisualEditor mounts them).
|
|
24
|
-
|
|
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';
|
|
25
26
|
// Decode / encode / positions (tests + VisualEditor).
|
|
26
27
|
//
|
|
27
28
|
// The barrel carries what has an off-barrel caller, and nothing else. A symbol
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// block rule with no guard of its own reuses `wrappingInputRule`, and the rest mirror
|
|
6
6
|
// an upstream body around one. These produce transactions the codec already lowers:
|
|
7
7
|
// they add no new lowering surface.
|
|
8
|
+
//
|
|
9
|
+
// A rule declines only where what it would mint is unrepresentable, or is a shape
|
|
10
|
+
// another gesture owns — never on how a quill renders it. What a quill typesets is not
|
|
11
|
+
// knowable here (a quill this build has not seen is the open-sets case, CODEC §"Open
|
|
12
|
+
// sets"), and a construct that arrives through `importMarkdown` must stay authorable or
|
|
13
|
+
// a document opens carrying a shape the editor refuses to make. So one guard survives:
|
|
14
|
+
// `- ` / `1. ` at the head of an existing item, where Tab owns the nesting.
|
|
8
15
|
import { InputRule, inputRules, wrappingInputRule } from 'prosemirror-inputrules';
|
|
9
16
|
import { canJoin, findWrapping } from 'prosemirror-transform';
|
|
10
17
|
import { Selection } from 'prosemirror-state';
|
|
@@ -43,12 +50,10 @@ function markInputRule(regexp, markType, delimLen) {
|
|
|
43
50
|
* `wrappingInputRule` (whose body this mirrors), plus one normalization: a
|
|
44
51
|
* `heading` being wrapped becomes a `paragraph` first.
|
|
45
52
|
*
|
|
46
|
-
* `list_item` is `block+`, so `list_item > heading` is *representable
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* nothing. This is the wrap-side route into that shape; the `# ` rule guards the
|
|
51
|
-
* other, in {@link markdownInputRules}.
|
|
53
|
+
* `list_item` is `block+`, so `list_item > heading` is *representable*, and `# `
|
|
54
|
+
* inside the item is the gesture that mints it ({@link markdownInputRules}). A wrap
|
|
55
|
+
* keeping the heading would be a second door onto that shape, so `- ` / `1. ` mint a
|
|
56
|
+
* paragraph item wherever they fire.
|
|
52
57
|
*
|
|
53
58
|
* `item` is the second guard, and it is about the gesture rather than the shape: the
|
|
54
59
|
* rule declines at the start of an item's own first block ({@link openingAnItem}).
|
|
@@ -115,16 +120,13 @@ export function markdownInputRules(schema) {
|
|
|
115
120
|
// Block shorthands (only where the schema has the node; the inline schema
|
|
116
121
|
// omits them, so this stays a no-op there).
|
|
117
122
|
if (schema.nodes.heading) {
|
|
118
|
-
// `textblockTypeInputRule`'s body
|
|
119
|
-
//
|
|
120
|
-
//
|
|
123
|
+
// `textblockTypeInputRule`'s body. It fires inside a list item like anywhere else:
|
|
124
|
+
// `list_item > heading` is a shape the content holds and `importMarkdown` produces
|
|
125
|
+
// from `- # title`, so a rule that declined there would refuse to author what a
|
|
126
|
+
// document can arrive carrying.
|
|
121
127
|
const heading = schema.nodes.heading;
|
|
122
128
|
rules.push(new InputRule(/^(#{1,6})\s$/, (state, match, start, end) => {
|
|
123
129
|
const $start = state.doc.resolve(start);
|
|
124
|
-
for (let d = $start.depth; d > 0; d--) {
|
|
125
|
-
if (item && $start.node(d).type === item)
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
130
|
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), heading)) {
|
|
129
131
|
return null;
|
|
130
132
|
}
|
|
@@ -166,12 +168,6 @@ export function markdownInputRules(schema) {
|
|
|
166
168
|
if ($start.parentOffset !== 0 || $start.parent.content.size !== end - start) {
|
|
167
169
|
return null;
|
|
168
170
|
}
|
|
169
|
-
// The `# ` rule's guard, for its reason: `list_item` is `block+`, so a
|
|
170
|
-
// divider inside an item is representable and renders to nothing.
|
|
171
|
-
for (let d = $start.depth; d > 0; d--) {
|
|
172
|
-
if (item && $start.node(d).type === item)
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
171
|
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), rule)) {
|
|
176
172
|
return null;
|
|
177
173
|
}
|
|
@@ -195,6 +191,8 @@ export function markdownInputRules(schema) {
|
|
|
195
191
|
if (schema.nodes.ordered_list && schema.nodes.paragraph) {
|
|
196
192
|
rules.push(listWrappingRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, schema.nodes.paragraph, item, (match) => ({ start: +match[1] }), (match, node) => node.childCount + node.attrs.start === +match[1]));
|
|
197
193
|
}
|
|
194
|
+
// No item guard: a quote wrapping an item's own first block is a container the
|
|
195
|
+
// content holds, where a list wrapping one is an item whose only content is an item.
|
|
198
196
|
if (schema.nodes.blockquote) {
|
|
199
197
|
rules.push(wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote));
|
|
200
198
|
}
|
|
@@ -84,16 +84,17 @@ li.ProseMirror-selectednode::after {
|
|
|
84
84
|
emits, minus the four the UA already renders correctly (`strong`, `em`, `u`, `s`)
|
|
85
85
|
and the blockquote above.
|
|
86
86
|
|
|
87
|
-
Each rule answers to the
|
|
87
|
+
Each rule answers to the leaf's own job rather than to a browser default or to a
|
|
88
88
|
web editor's chrome, because the leaf sits beside a live preview of the same
|
|
89
|
-
content.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
at
|
|
89
|
+
content. A leaf shows *that* a span is code and a line is a heading: never a
|
|
90
|
+
prediction of the size paper will set it at, which the package cannot know for a
|
|
91
|
+
quill it has not seen. The reference quill is the case in point — it sets `raw` in
|
|
92
|
+
the face it bundles, scales a heading by its level and colours it, and gives a link
|
|
93
|
+
no decoration at all — and mirroring any of that here would be a guess about the
|
|
94
|
+
next quill.
|
|
94
95
|
|
|
95
96
|
Scoping is by tag, and the schema does the rest: `h1`–`h6`, `hr` and the lists
|
|
96
|
-
cannot exist under the inline schema, so no rule for them can land on
|
|
97
|
+
cannot exist under the inline schema, so no rule for them can land on an inline
|
|
97
98
|
field however that field is mounted. The two marks are the exception: `code` and
|
|
98
99
|
`link` are in both schemas, and a `plaintext` leaf strips them at decode while
|
|
99
100
|
mounting a schema that still carries them, so a paste can put either in one. The
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { Schema } from 'prosemirror-model';
|
|
2
2
|
/** The full field schema: every block kind, container, mark, and island. */
|
|
3
3
|
export declare const blockSchema: Schema<string, string>;
|
|
4
|
+
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
5
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field, and
|
|
6
|
+
* a table cell, which is the same content unit). */
|
|
7
|
+
export declare const inlineSchema: Schema<string, string>;
|
|
4
8
|
/**
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The inline schema with no mark types at all: a `plaintext` field, whose value is
|
|
10
|
+
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
11
|
+
*
|
|
12
|
+
* Declaring none is what makes that structural rather than a rule each path
|
|
13
|
+
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
14
|
+
* a paste parses its marks away, and `decode` drops any a stray content arrives
|
|
15
|
+
* carrying (CODEC §Inline mode).
|
|
10
16
|
*/
|
|
11
|
-
export declare const
|
|
17
|
+
export declare const plaintextSchema: Schema<string, never>;
|
|
18
|
+
/** Whether `schema` can carry formatting at all: false for {@link plaintextSchema}
|
|
19
|
+
* alone. What a mark command asks before it offers itself. */
|
|
20
|
+
export declare function hasMarks(schema: Schema): boolean;
|
|
12
21
|
/** True for the constrained inline schema (no block containers); decode branches on it. */
|
|
13
22
|
export declare function isInlineSchema(schema: Schema): boolean;
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// Each of the three open sets gets an inert carrier so an unrecognized value
|
|
5
5
|
// survives a round-trip: the `unknown` mark, the paragraph's `unknown` attribute,
|
|
6
6
|
// and the `unknown_container` node. `blockSchema` is the full field; `inlineSchema`
|
|
7
|
-
// is the constrained single-textblock form for `richtext(inline)`
|
|
8
|
-
//
|
|
9
|
-
// position machinery, narrower shape. Anchors are
|
|
7
|
+
// is the constrained single-textblock form for `richtext(inline)` (one paragraph,
|
|
8
|
+
// no block split, no containers, no islands) and `plaintextSchema` is that one
|
|
9
|
+
// without marks: same decode/lower/position machinery, narrower shape. Anchors are
|
|
10
|
+
// not marks here (decorations).
|
|
10
11
|
import { Schema } from 'prosemirror-model';
|
|
11
12
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
12
|
-
// ── Marks (
|
|
13
|
+
// ── Marks (the block and inline schemas share them; plaintext declares none) ─
|
|
13
14
|
const marks = {
|
|
14
15
|
// Order matters: it fixes mark-set sort order and parse precedence. `link`
|
|
15
16
|
// last so it wraps outermost; `unknown` after it, non-exclusive so several
|
|
@@ -134,21 +135,33 @@ export const blockSchema = new Schema({
|
|
|
134
135
|
nodes: { ...blockNodes, ...inlineLeafNodes },
|
|
135
136
|
marks
|
|
136
137
|
});
|
|
138
|
+
// ── The constrained inline nodes (both inline schemas) ──────────────────────
|
|
139
|
+
// `doc: "paragraph"` (exactly one child) is what makes an Enter a no-op at the
|
|
140
|
+
// model level: there is no second block for a split to land.
|
|
141
|
+
const inlineNodes = {
|
|
142
|
+
doc: { content: 'paragraph' },
|
|
143
|
+
paragraph: { content: 'inline*', toDOM: () => ['p', 0] },
|
|
144
|
+
text: { group: 'inline' }
|
|
145
|
+
};
|
|
146
|
+
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
147
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field, and
|
|
148
|
+
* a table cell, which is the same content unit). */
|
|
149
|
+
export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
137
150
|
/**
|
|
138
|
-
* The
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
151
|
+
* The inline schema with no mark types at all: a `plaintext` field, whose value is
|
|
152
|
+
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
153
|
+
*
|
|
154
|
+
* Declaring none is what makes that structural rather than a rule each path
|
|
155
|
+
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
156
|
+
* a paste parses its marks away, and `decode` drops any a stray content arrives
|
|
157
|
+
* carrying (CODEC §Inline mode).
|
|
143
158
|
*/
|
|
144
|
-
export const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
marks
|
|
151
|
-
});
|
|
159
|
+
export const plaintextSchema = new Schema({ nodes: inlineNodes, marks: {} });
|
|
160
|
+
/** Whether `schema` can carry formatting at all: false for {@link plaintextSchema}
|
|
161
|
+
* alone. What a mark command asks before it offers itself. */
|
|
162
|
+
export function hasMarks(schema) {
|
|
163
|
+
return Object.keys(schema.marks).length > 0;
|
|
164
|
+
}
|
|
152
165
|
/** True for the constrained inline schema (no block containers); decode branches on it. */
|
|
153
166
|
export function isInlineSchema(schema) {
|
|
154
167
|
return !schema.nodes.blockquote;
|