@quillmark/svelte 0.18.0 → 0.20.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/NOTICE +4 -2
- package/dist/core/codec/blocks.d.ts +11 -2
- package/dist/core/codec/blocks.js +45 -14
- package/dist/core/codec/breaks.d.ts +3 -2
- package/dist/core/codec/breaks.js +3 -2
- package/dist/core/codec/decode.d.ts +7 -3
- package/dist/core/codec/decode.js +34 -8
- package/dist/core/codec/field.d.ts +11 -5
- package/dist/core/codec/field.js +11 -8
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/core/codec/islands.d.ts +3 -1
- package/dist/core/codec/islands.js +8 -5
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +49 -0
- package/dist/core/codec/prose.css +109 -1
- package/dist/core/codec/schema.d.ts +8 -2
- package/dist/core/codec/schema.js +16 -5
- package/dist/core/codec/slash.d.ts +9 -6
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-paste.d.ts +7 -0
- package/dist/core/codec/table-paste.js +132 -0
- package/dist/core/codec/table-view.d.ts +12 -0
- package/dist/core/codec/table-view.js +401 -57
- package/dist/core/codec/table.d.ts +38 -16
- package/dist/core/codec/table.js +106 -35
- package/dist/core/theme.css +1 -1
- package/dist/visual/ArrayField.svelte +17 -6
- package/dist/visual/ArrayField.svelte.d.ts +1 -1
- package/dist/visual/Field.svelte +9 -3
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +62 -10
- package/dist/visual/FormatPopover.svelte +27 -31
- package/dist/visual/FormatPopover.svelte.d.ts +2 -2
- package/dist/visual/MatrixAdd.svelte +13 -132
- package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
- package/dist/visual/MatrixField.svelte +5 -63
- package/dist/visual/MatrixField.svelte.d.ts +0 -3
- package/dist/visual/ObjectField.svelte +8 -4
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseField.svelte +2 -1
- package/dist/visual/ProseValue.svelte +113 -40
- package/dist/visual/ProseValue.svelte.d.ts +8 -4
- package/dist/visual/VisualEditorInner.svelte +22 -22
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
- package/dist/visual/checklists.d.ts +0 -79
- package/dist/visual/checklists.js +0 -164
- package/dist/visual/guidance.d.ts +15 -0
- package/dist/visual/guidance.js +37 -0
- package/dist/visual/leaves.d.ts +27 -1
- package/dist/visual/leaves.js +46 -0
- package/dist/visual/strings.d.ts +3 -8
- package/dist/visual/strings.js +0 -3
- package/dist/visual/structure.js +1 -1
- package/dist/visual/tips.js +1 -1
- package/package.json +2 -2
package/NOTICE
CHANGED
|
@@ -11,8 +11,10 @@ chevron-right, arrow-right, chevron-down, chevron-up, bold, italic, underline,
|
|
|
11
11
|
strikethrough, code, link, hash, info, plus, x, trash-2, minus, check, calendar
|
|
12
12
|
and calendar-check.
|
|
13
13
|
|
|
14
|
-
`core/codec/table-view.ts` holds
|
|
15
|
-
|
|
14
|
+
`core/codec/table-view.ts` holds grip-horizontal, grip-vertical, align-left,
|
|
15
|
+
align-center, align-right, align-start-horizontal, align-center-horizontal,
|
|
16
|
+
align-end-horizontal and panel-top, derived from an earlier release of the same
|
|
17
|
+
project.
|
|
16
18
|
|
|
17
19
|
ISC License
|
|
18
20
|
|
|
@@ -18,9 +18,18 @@ export declare function insertDivider(): Command;
|
|
|
18
18
|
* item. */
|
|
19
19
|
export declare function wrapInQuote(): Command;
|
|
20
20
|
/** `- ` / `1. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
|
|
21
|
-
* above where `7. ` opens one.
|
|
22
|
-
* above it. */
|
|
21
|
+
* above where `7. ` opens one. */
|
|
23
22
|
export declare function wrapInList(ordered: boolean, attrs?: Attrs | null, joinBefore?: (node: PMNode) => boolean): Command;
|
|
23
|
+
/**
|
|
24
|
+
* A list pick, at whatever caret the writer has. `atHead` is the shorthand's guard,
|
|
25
|
+
* where a prefix firing mid-sentence would eat a dash; a pick is a gesture at the caret.
|
|
26
|
+
*
|
|
27
|
+
* At the end of a non-empty block it opens a fresh empty item below it, Enter then `- `
|
|
28
|
+
* as one gesture. Anywhere else it wraps the whole block, the smaller surprise: a writer
|
|
29
|
+
* who meant to split presses Enter first. It passes no `joinBefore`, so it continues
|
|
30
|
+
* whatever list is above.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pickList(ordered: boolean): Command;
|
|
24
33
|
/**
|
|
25
34
|
* Run `command` in place of the text at `[from, to)`: one transaction carrying the
|
|
26
35
|
* delete and the command's own steps. One rather than two because two would be two
|
|
@@ -137,28 +137,59 @@ export function wrapInQuote() {
|
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
/** `- ` / `1. `. `joinBefore` is the shorthand's ordinal test: `2. ` continues the list
|
|
140
|
-
* above where `7. ` opens one.
|
|
141
|
-
* above it. */
|
|
140
|
+
* above where `7. ` opens one. */
|
|
142
141
|
export function wrapInList(ordered, attrs = null, joinBefore) {
|
|
142
|
+
return (state, dispatch) => atHead(state) && wrapBlock(state, dispatch, ordered, attrs, joinBefore);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A list pick, at whatever caret the writer has. `atHead` is the shorthand's guard,
|
|
146
|
+
* where a prefix firing mid-sentence would eat a dash; a pick is a gesture at the caret.
|
|
147
|
+
*
|
|
148
|
+
* At the end of a non-empty block it opens a fresh empty item below it, Enter then `- `
|
|
149
|
+
* as one gesture. Anywhere else it wraps the whole block, the smaller surprise: a writer
|
|
150
|
+
* who meant to split presses Enter first. It passes no `joinBefore`, so it continues
|
|
151
|
+
* whatever list is above.
|
|
152
|
+
*/
|
|
153
|
+
export function pickList(ordered) {
|
|
143
154
|
return (state, dispatch) => {
|
|
144
|
-
const {
|
|
155
|
+
const { $from, empty } = state.selection;
|
|
156
|
+
if (!empty || !$from.parent.isTextblock)
|
|
157
|
+
return false;
|
|
158
|
+
const size = $from.parent.content.size;
|
|
159
|
+
if (!size || $from.parentOffset < size)
|
|
160
|
+
return wrapBlock(state, dispatch, ordered, null);
|
|
161
|
+
const { paragraph } = state.schema.nodes;
|
|
145
162
|
const list = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
|
|
146
|
-
|
|
147
|
-
if (!list || !paragraph || !atHead(state) || openingAnItem($from, item))
|
|
163
|
+
if (!list || !paragraph)
|
|
148
164
|
return false;
|
|
149
|
-
const
|
|
150
|
-
const tr = state.tr;
|
|
151
|
-
|
|
152
|
-
// mints `list_item > heading`, and a wrap keeping the heading is a second door onto
|
|
153
|
-
// that shape. Positions survive the retype — same content, same size.
|
|
154
|
-
if ($from.parent.type !== paragraph)
|
|
155
|
-
tr.setBlockType(pos, pos, paragraph);
|
|
156
|
-
if (!wrapAt(tr, pos, list, attrs, joinBefore))
|
|
165
|
+
const at = $from.after();
|
|
166
|
+
const tr = state.tr.insert(at, paragraph.create());
|
|
167
|
+
if (!wrapAt(tr, at + 1, list, null))
|
|
157
168
|
return false;
|
|
158
|
-
dispatch?.(tr);
|
|
169
|
+
dispatch?.(tr.setSelection(Selection.near(tr.doc.resolve(at + 1))));
|
|
159
170
|
return true;
|
|
160
171
|
};
|
|
161
172
|
}
|
|
173
|
+
/** Wrap the caret's whole block in a list, wherever in the block the caret sits: the
|
|
174
|
+
* wrap opens at the block's head, so it joins the list above as the shorthand's does. */
|
|
175
|
+
function wrapBlock(state, dispatch, ordered, attrs, joinBefore) {
|
|
176
|
+
const { paragraph, list_item: item } = state.schema.nodes;
|
|
177
|
+
const list = state.schema.nodes[ordered ? 'ordered_list' : 'bullet_list'];
|
|
178
|
+
const { $from } = state.selection;
|
|
179
|
+
if (!list || !paragraph || openingAnItem($from, item))
|
|
180
|
+
return false;
|
|
181
|
+
const pos = $from.start();
|
|
182
|
+
const tr = state.tr;
|
|
183
|
+
// A paragraph item wherever this fires: `# ` inside the item is the gesture that
|
|
184
|
+
// mints `list_item > heading`, and a wrap keeping the heading is a second door onto
|
|
185
|
+
// that shape. Positions survive the retype — same content, same size.
|
|
186
|
+
if ($from.parent.type !== paragraph)
|
|
187
|
+
tr.setBlockType(pos, pos, paragraph);
|
|
188
|
+
if (!wrapAt(tr, pos, list, attrs, joinBefore))
|
|
189
|
+
return false;
|
|
190
|
+
dispatch?.(tr);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
162
193
|
/**
|
|
163
194
|
* Run `command` in place of the text at `[from, to)`: one transaction carrying the
|
|
164
195
|
* delete and the command's own steps. One rather than two because two would be two
|
|
@@ -10,8 +10,9 @@ import { Plugin, type Command } from 'prosemirror-state';
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function linebreakPlugin(schema: Schema): Plugin;
|
|
12
12
|
/**
|
|
13
|
-
* The line-break link of the body's key chains
|
|
14
|
-
*
|
|
13
|
+
* The line-break link of the body's key chains, and a table cell's Shift-Enter
|
|
14
|
+
* (`table-view.ts`): `{}` for the inline schemas, which declare no break node. A
|
|
15
|
+
* `richtext(inline)` field is one line by construction
|
|
15
16
|
* (`Content::is_inline`), so there the key is swallowed beside Enter (`field.ts`)
|
|
16
17
|
* rather than answered here.
|
|
17
18
|
*/
|
|
@@ -81,8 +81,9 @@ function insertBreak(schema) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* The line-break link of the body's key chains
|
|
85
|
-
*
|
|
84
|
+
* The line-break link of the body's key chains, and a table cell's Shift-Enter
|
|
85
|
+
* (`table-view.ts`): `{}` for the inline schemas, which declare no break node. A
|
|
86
|
+
* `richtext(inline)` field is one line by construction
|
|
86
87
|
* (`Content::is_inline`), so there the key is swallowed beside Enter (`field.ts`)
|
|
87
88
|
* rather than answered here.
|
|
88
89
|
*/
|
|
@@ -27,10 +27,14 @@ export declare function fitsInline(rt: Content): boolean;
|
|
|
27
27
|
* next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
|
|
28
28
|
* with them set aside. */
|
|
29
29
|
export declare function fitsPlain(rt: Content): boolean;
|
|
30
|
-
/** Whether
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
31
|
+
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
32
|
+
* store; on the block schema anything but an element or a task item ({@link editable}),
|
|
33
|
+
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
34
|
+
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
35
|
+
* reads. */
|
|
33
36
|
export declare function fitsLeaf(rt: Content, leaf: {
|
|
34
37
|
inline: boolean;
|
|
35
38
|
plaintext: boolean;
|
|
39
|
+
islands: boolean;
|
|
36
40
|
}): boolean;
|
|
@@ -94,13 +94,23 @@ export function fitsInline(rt) {
|
|
|
94
94
|
export function fitsPlain(rt) {
|
|
95
95
|
return core().isPlain({ ...rt, marks: [] });
|
|
96
96
|
}
|
|
97
|
-
/** Whether
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
/** Whether a leaf so declared edits `rt` losing nothing: {@link fitsInline} narrowed
|
|
98
|
+
* and {@link fitsPlain} plain, past which the decode drops what the first commit would
|
|
99
|
+
* store; on the block schema anything but an element or a task item ({@link editable}),
|
|
100
|
+
* and an island where the leaf mounts no island view (`islands`), which draws as its
|
|
101
|
+
* placeholder tag, an atom a keystroke deletes unseen. What a leaf held over the rest
|
|
102
|
+
* reads. */
|
|
100
103
|
export function fitsLeaf(rt, leaf) {
|
|
101
104
|
if (leaf.inline)
|
|
102
105
|
return fitsInline(rt);
|
|
103
|
-
|
|
106
|
+
if (leaf.plaintext)
|
|
107
|
+
return fitsPlain(rt);
|
|
108
|
+
return (leaf.islands || rt.islands.length === 0) && rt.lines.every(editable);
|
|
109
|
+
}
|
|
110
|
+
/** Whether every container on `line` is one the block schema edits: an element, and a
|
|
111
|
+
* task item's `checked`, decode to no node, so the first commit would drop them. */
|
|
112
|
+
function editable(line) {
|
|
113
|
+
return line.containers.every((c) => c.container === 'quote' || (c.container === 'list_item' && c.attrs.checked === undefined));
|
|
104
114
|
}
|
|
105
115
|
/** Inline decode: one paragraph, containers and islands stripped. */
|
|
106
116
|
function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
|
|
@@ -182,6 +192,21 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
182
192
|
i = j;
|
|
183
193
|
continue;
|
|
184
194
|
}
|
|
195
|
+
if (here.container === 'element') {
|
|
196
|
+
// No node holds an element, and a leaf holding one is held (`fitsLeaf`), so
|
|
197
|
+
// what it wraps draws in place, read-only.
|
|
198
|
+
const key = containerKey(here);
|
|
199
|
+
let j = i + 1;
|
|
200
|
+
while (j < leaves.length) {
|
|
201
|
+
const c = atDepth(leaves[j], depth);
|
|
202
|
+
if (!c || containerKey(c) !== key)
|
|
203
|
+
break;
|
|
204
|
+
j++;
|
|
205
|
+
}
|
|
206
|
+
out.push(...groupBlocks(schema, leaves.slice(i, j), depth + 1, marks, cursor));
|
|
207
|
+
i = j;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
185
210
|
// The quote, and the whole of what is left: `satisfies` is the exhaustiveness
|
|
186
211
|
// check, so a container added upstream is a compile error here rather than a
|
|
187
212
|
// blockquote it is not. A wrapper over the run of leaves carrying the identical
|
|
@@ -216,11 +241,12 @@ function ordinalAt(leaf, depth) {
|
|
|
216
241
|
function instanceOf(c) {
|
|
217
242
|
return c.instance ?? 0;
|
|
218
243
|
}
|
|
219
|
-
/** Identity of a container for run gathering: its name
|
|
220
|
-
* as `markKey` joins a mark's. `list_item` carries its own run
|
|
221
|
-
* what
|
|
244
|
+
/** Identity of a container for run gathering: its name, its `instance` and an element's
|
|
245
|
+
* attributes, NUL-joined as `markKey` joins a mark's. `list_item` carries its own run
|
|
246
|
+
* rule, so the quote and the element are what key here. */
|
|
222
247
|
function containerKey(c) {
|
|
223
|
-
|
|
248
|
+
const attrs = c.container === 'element' ? JSON.stringify(c.attrs) : '';
|
|
249
|
+
return `${c.container}\u0000${instanceOf(c)}\u0000${attrs}`;
|
|
224
250
|
}
|
|
225
251
|
/** A single leaf block node from its segments. Exhaustive over the closed line
|
|
226
252
|
* vocabulary, so a kind added upstream is a compile error rather than a silent
|
|
@@ -4,7 +4,7 @@ import { EditorView } from 'prosemirror-view';
|
|
|
4
4
|
import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
|
|
5
5
|
import type { EditorErrorHandler } from '../errors.js';
|
|
6
6
|
import { type TableChromeStrings } from './table-view.js';
|
|
7
|
-
import { type SlashState } from './slash.js';
|
|
7
|
+
import { type SlashState, type SlashStrings } from './slash.js';
|
|
8
8
|
/** Options for {@link createField}. */
|
|
9
9
|
export interface CreateFieldOpts {
|
|
10
10
|
doc: Document;
|
|
@@ -26,7 +26,8 @@ export interface CreateFieldOpts {
|
|
|
26
26
|
* Fired when the leaf holds or releases, and at mount when it mounts held. A leaf
|
|
27
27
|
* declaring `inline`, or a `plaintext` one, is held over a value its schema cannot
|
|
28
28
|
* hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
|
|
29
|
-
* and drop containers and islands for the first commit to store.
|
|
29
|
+
* and drop containers and islands for the first commit to store. Any leaf is held
|
|
30
|
+
* over an element or a task item, which no node holds. A held leaf draws
|
|
30
31
|
* its content on the block schema, read-only, and commits nothing. Judged of the value
|
|
31
32
|
* each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
|
|
32
33
|
* re-evaluates it.
|
|
@@ -49,6 +50,9 @@ export interface CreateFieldOpts {
|
|
|
49
50
|
* it holds no island and no block to convert.
|
|
50
51
|
*/
|
|
51
52
|
onSlash?(state: SlashState | undefined): void;
|
|
53
|
+
/** The slash surface's wording, read per decoration pass. Absent leaves the
|
|
54
|
+
* package's English; read only beside {@link onSlash}, the hint being the menu's. */
|
|
55
|
+
slashStrings?: () => SlashStrings;
|
|
52
56
|
/** Accessible name → `aria-label` on the `contenteditable`. For a leaf nothing
|
|
53
57
|
* else names: an array element (the field label plus its 1-based index), the
|
|
54
58
|
* card body (no visible label at all). A leaf with a field label takes
|
|
@@ -190,9 +194,9 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
190
194
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
191
195
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
192
196
|
* the rest of the stack leaves, then the markdown-shorthand input rules, the field
|
|
193
|
-
* keymap over the base keymap, and last the
|
|
194
|
-
* shapes: the gap cursor, {@link pastAtomPlugin},
|
|
195
|
-
* (`
|
|
197
|
+
* keymap over the base keymap, and last the ones that answer to a block leaf's own
|
|
198
|
+
* shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass (`islands.ts`),
|
|
199
|
+
* the table paste (`table-paste.ts`) and the markdown paste (`markdown-paste.ts`).
|
|
196
200
|
*
|
|
197
201
|
* Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
|
|
198
202
|
* schema the shorthand rules build nothing (each is guarded on its mark type) and the
|
|
@@ -204,6 +208,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
204
208
|
/** The slash menu's report channel; absent mounts no menu and leaves
|
|
205
209
|
* Enter/Escape/the arrows to the links below (`keymap.ts`). */
|
|
206
210
|
slash?: (state: SlashState | undefined) => void;
|
|
211
|
+
/** The empty line's hint, read live; drawn only beside `slash`. */
|
|
212
|
+
slashHint?: () => string | undefined;
|
|
207
213
|
noInputRules?: boolean;
|
|
208
214
|
/** Read live, not captured: the ghost can move after mount
|
|
209
215
|
* ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this
|
package/dist/core/codec/field.js
CHANGED
|
@@ -31,8 +31,10 @@ import { inputRulesPlugin } from './inputrules.js';
|
|
|
31
31
|
import { linebreakPlugin } from './breaks.js';
|
|
32
32
|
import { bodyKeymap } from './keymap.js';
|
|
33
33
|
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
|
+
import { markdownPastePlugin } from './markdown-paste.js';
|
|
35
|
+
import { tablePastePlugin } from './table-paste.js';
|
|
34
36
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
35
|
-
import { focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
37
|
+
import { DEFAULT_SLASH_STRINGS, focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
36
38
|
const anchorKey = new PluginKey('quill-anchors');
|
|
37
39
|
/** Plugin-held anchor positions (PM coords), mapped through every edit's `StepMap`
|
|
38
40
|
* and mutated by an {@link AnchorEdit} meta (an insert/remove at a selection).
|
|
@@ -160,7 +162,7 @@ export function createField(opts) {
|
|
|
160
162
|
// Judged of the value being mounted rather than read off a diagnostic: a routed set
|
|
161
163
|
// can predate the value it arrives with, and `validation::not_plain` stands in for
|
|
162
164
|
// `not_inline` on a `plaintext(inline)` field.
|
|
163
|
-
const holds = (rt) => !fitsLeaf(rt, { inline, plaintext });
|
|
165
|
+
const holds = (rt) => !fitsLeaf(rt, { inline, plaintext, islands: true });
|
|
164
166
|
let held = holds(reconciler.last);
|
|
165
167
|
const named = proseAttributes(opts) ?? {};
|
|
166
168
|
const defaultNamed = { ...named, 'data-default': '' };
|
|
@@ -263,6 +265,7 @@ export function createField(opts) {
|
|
|
263
265
|
plugins: proseLeafPlugins(schema, {
|
|
264
266
|
inline,
|
|
265
267
|
slash,
|
|
268
|
+
slashHint: () => (opts.slashStrings?.() ?? DEFAULT_SLASH_STRINGS).slashHint,
|
|
266
269
|
noInputRules: opts.noInputRules,
|
|
267
270
|
// Always installed, so a leaf that mounts without a ghost can still be
|
|
268
271
|
// given one later; the plugin draws nothing while the text is empty.
|
|
@@ -502,9 +505,9 @@ export function createField(opts) {
|
|
|
502
505
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
503
506
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
504
507
|
* the rest of the stack leaves, then the markdown-shorthand input rules, the field
|
|
505
|
-
* keymap over the base keymap, and last the
|
|
506
|
-
* shapes: the gap cursor, {@link pastAtomPlugin},
|
|
507
|
-
* (`
|
|
508
|
+
* keymap over the base keymap, and last the ones that answer to a block leaf's own
|
|
509
|
+
* shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass (`islands.ts`),
|
|
510
|
+
* the table paste (`table-paste.ts`) and the markdown paste (`markdown-paste.ts`).
|
|
508
511
|
*
|
|
509
512
|
* Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
|
|
510
513
|
* schema the shorthand rules build nothing (each is guarded on its mark type) and the
|
|
@@ -514,15 +517,15 @@ export function createField(opts) {
|
|
|
514
517
|
export function proseLeafPlugins(schema, opts) {
|
|
515
518
|
const list = [history(), ...(opts.afterHistory ?? []), linebreakPlugin(schema)];
|
|
516
519
|
if (opts.slash)
|
|
517
|
-
list.push(slashPlugin(opts.slash));
|
|
520
|
+
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
518
521
|
if (!opts.noInputRules)
|
|
519
522
|
list.push(inputRulesPlugin(schema));
|
|
520
523
|
list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
|
|
521
524
|
list.push(keymap(baseKeymap));
|
|
522
|
-
//
|
|
525
|
+
// Each answers to something only the block schema holds: any other leaf is
|
|
523
526
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
524
527
|
if (isBlockSchema(schema))
|
|
525
|
-
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin());
|
|
528
|
+
list.push(gapCursor(), pastAtomPlugin(), islandPastePlugin(), tablePastePlugin(), markdownPastePlugin());
|
|
526
529
|
if (schema === plainSchema)
|
|
527
530
|
list.push(plainClipboardPlugin());
|
|
528
531
|
if (opts.placeholder)
|
|
@@ -13,7 +13,7 @@ export { pmToContent, contentEdit, lower } from './encode.js';
|
|
|
13
13
|
export type { ContentEdit } from './encode.js';
|
|
14
14
|
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
15
15
|
export type { LineIndex } from './positions.js';
|
|
16
|
-
export { createReconciler, contentEqual } from './reconcile.js';
|
|
16
|
+
export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
|
|
17
17
|
export type { Reconciler } from './reconcile.js';
|
|
18
18
|
export { inputRulesPlugin } from './inputrules.js';
|
|
19
19
|
export { bodyKeymap } from './keymap.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -36,7 +36,7 @@ export { decode, fitsInline, fitsLeaf, fitsPlain, renderContent, usvLength } fro
|
|
|
36
36
|
export { pmToContent, contentEdit, lower } from './encode.js';
|
|
37
37
|
export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
|
|
38
38
|
// Reconciliation gate.
|
|
39
|
-
export { createReconciler, contentEqual } from './reconcile.js';
|
|
39
|
+
export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
|
|
40
40
|
// Input rules (`createField` mounts them unless `noInputRules`/`plaintext`).
|
|
41
41
|
export { inputRulesPlugin } from './inputrules.js';
|
|
42
42
|
// The body leaf's structural keys: `createField` binds the composed chains
|
|
@@ -47,7 +47,9 @@ export declare function islandMinter(doc: PMNode): () => string;
|
|
|
47
47
|
* command placing exactly one island wants. */
|
|
48
48
|
export declare function mintIslandId(doc: PMNode): string;
|
|
49
49
|
/**
|
|
50
|
-
* The paste's island pass: an id the field already holds is re-minted on the way in
|
|
50
|
+
* The paste's island pass: an id the field already holds is re-minted on the way in, and
|
|
51
|
+
* an island arriving with none, a table read off foreign markup (`table-paste.ts`), takes
|
|
52
|
+
* its first.
|
|
51
53
|
*
|
|
52
54
|
* An id is an identity in the content and unique across the field, so two islands
|
|
53
55
|
* wearing one is a projection the store has no shape for. A copy rather than a cut is
|
|
@@ -170,9 +170,10 @@ export function mintIslandId(doc) {
|
|
|
170
170
|
function isIsland(node) {
|
|
171
171
|
return node.type.name === 'island_block' || node.type.name === 'island_inline';
|
|
172
172
|
}
|
|
173
|
-
/** `fragment` with every island whose id is already `taken`
|
|
174
|
-
* none was: an untouched paste stays the slice it arrived as. Every
|
|
175
|
-
* the set, so a slice carrying one id twice comes apart in the same
|
|
173
|
+
/** `fragment` with every island whose id is empty or already `taken` minted afresh, or
|
|
174
|
+
* `null` where none was: an untouched paste stays the slice it arrived as. Every
|
|
175
|
+
* surviving id joins the set, so a slice carrying one id twice comes apart in the same
|
|
176
|
+
* pass. */
|
|
176
177
|
function remintIslands(fragment, taken, mint) {
|
|
177
178
|
const out = [];
|
|
178
179
|
let changed = false;
|
|
@@ -184,7 +185,7 @@ function remintIslands(fragment, taken, mint) {
|
|
|
184
185
|
return;
|
|
185
186
|
}
|
|
186
187
|
let id = node.attrs.id;
|
|
187
|
-
if (taken.has(id)) {
|
|
188
|
+
if (!id || taken.has(id)) {
|
|
188
189
|
do
|
|
189
190
|
id = mint();
|
|
190
191
|
while (taken.has(id));
|
|
@@ -196,7 +197,9 @@ function remintIslands(fragment, taken, mint) {
|
|
|
196
197
|
return changed ? Fragment.fromArray(out) : null;
|
|
197
198
|
}
|
|
198
199
|
/**
|
|
199
|
-
* The paste's island pass: an id the field already holds is re-minted on the way in
|
|
200
|
+
* The paste's island pass: an id the field already holds is re-minted on the way in, and
|
|
201
|
+
* an island arriving with none, a table read off foreign markup (`table-paste.ts`), takes
|
|
202
|
+
* its first.
|
|
200
203
|
*
|
|
201
204
|
* An id is an identity in the content and unique across the field, so two islands
|
|
202
205
|
* wearing one is a projection the store has no shape for. A copy rather than a cut is
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
/**
|
|
3
|
+
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
4
|
+
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
5
|
+
* structure, so HTML wins. A paste as plain text keeps the text. An island the parse
|
|
6
|
+
* builds arrives under the id `importMarkdown` minted, which the paste's island pass
|
|
7
|
+
* re-mints where the field holds it (`islands.ts`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function markdownPastePlugin(): Plugin;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Markdown on the clipboard as text lands parsed in a block leaf (CODEC §"Markdown at the
|
|
2
|
+
// edges"). A line opening a block is the shape a draft written in Notepad or copied out of
|
|
3
|
+
// an LLM chat carries, and text holding one runs through `importMarkdown` and `decode`, so
|
|
4
|
+
// the paste stores what the source editor stores of the same text. Text holding none stays
|
|
5
|
+
// literal: a sentence with a dash in it, or a `**` with no block around it, keeps every
|
|
6
|
+
// character.
|
|
7
|
+
import { Slice } from 'prosemirror-model';
|
|
8
|
+
import { Plugin } from 'prosemirror-state';
|
|
9
|
+
import { core } from '../lifecycle.js';
|
|
10
|
+
import { decode } from './decode.js';
|
|
11
|
+
import { blockSchema } from './schema.js';
|
|
12
|
+
/** A line opening a block markdown spells and prose does not: a bullet or an ordered item at
|
|
13
|
+
* any indent, and within CommonMark's three spaces an ATX heading, a quote, a fence, or a
|
|
14
|
+
* pipe table's delimiter row. */
|
|
15
|
+
const BLOCK_HEAD = new RegExp([
|
|
16
|
+
String.raw `^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]`,
|
|
17
|
+
String.raw `^ {0,3}(?:#{1,6}|>)(?:[ \t]|$)`,
|
|
18
|
+
'^ {0,3}(?:```|~~~)',
|
|
19
|
+
String.raw `^ {0,3}(?=[^\n]*\|)\|?(?:[ \t]*:?-+:?[ \t]*\|)*[ \t]*:?-+:?[ \t]*\|?[ \t]*$`
|
|
20
|
+
].join('|'), 'm');
|
|
21
|
+
/**
|
|
22
|
+
* A block leaf's markdown paste, consulted after the table paste: ProseMirror hands text
|
|
23
|
+
* over only where the clipboard holds no HTML, whose DOM parse reads the source's own
|
|
24
|
+
* structure, so HTML wins. A paste as plain text keeps the text. An island the parse
|
|
25
|
+
* builds arrives under the id `importMarkdown` minted, which the paste's island pass
|
|
26
|
+
* re-mints where the field holds it (`islands.ts`).
|
|
27
|
+
*/
|
|
28
|
+
export function markdownPastePlugin() {
|
|
29
|
+
return new Plugin({
|
|
30
|
+
props: {
|
|
31
|
+
// A falsy return declines, handing the text to ProseMirror's own split into
|
|
32
|
+
// paragraphs; the prop's type has no spelling for it.
|
|
33
|
+
clipboardTextParser: ((text, _context, plain) => {
|
|
34
|
+
if (plain || !BLOCK_HEAD.test(text))
|
|
35
|
+
return null;
|
|
36
|
+
const { importMarkdown } = core();
|
|
37
|
+
let rt;
|
|
38
|
+
try {
|
|
39
|
+
rt = importMarkdown(text).content;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Nesting past the store's depth is refused; the literal text holds it.
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return new Slice(decode(rt, blockSchema).content, 0, 0);
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -223,6 +223,22 @@ li.ProseMirror-selectednode::after {
|
|
|
223
223
|
text-overflow: ellipsis;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/* The slash menu's hint (`slash.ts`, `slashHint`), drawn as the ghost is, on the one
|
|
227
|
+
empty line the caret holds. Only while the leaf has the focus: the decoration follows
|
|
228
|
+
the selection, which an unfocused leaf keeps. Upright, being chrome about the key
|
|
229
|
+
rather than an invitation in the document's voice, and never over a ghost, which is
|
|
230
|
+
the leaf's own word for the same empty line. */
|
|
231
|
+
.ProseMirror-focused .qm-slash-hint:not(.qm-prose-placeholder)[data-slash-hint]::after {
|
|
232
|
+
content: attr(data-slash-hint);
|
|
233
|
+
display: block;
|
|
234
|
+
margin-top: -1lh;
|
|
235
|
+
white-space: nowrap;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
text-overflow: ellipsis;
|
|
238
|
+
color: var(--_qm-ink-label);
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
226
242
|
/* ── The table island ────────────────────────────────────────────────────────
|
|
227
243
|
|
|
228
244
|
The one construct in this file with chrome in it. The rest of the stylesheet
|
|
@@ -371,7 +387,7 @@ li.ProseMirror-selectednode::after {
|
|
|
371
387
|
What differs is which edge each hangs off and what it draws. The grips take the
|
|
372
388
|
leading edges, where a line begins; the add bars take the trailing ones, past where
|
|
373
389
|
the axis ends. Position is the whole of what says which is which: the grip's marks are
|
|
374
|
-
the only glyph
|
|
390
|
+
the only glyph in the band, and they say draggable rather than which control this
|
|
375
391
|
is. */
|
|
376
392
|
:is(.qm-table-grip, .qm-table-add) {
|
|
377
393
|
position: absolute;
|
|
@@ -503,6 +519,98 @@ li.ProseMirror-selectednode::after {
|
|
|
503
519
|
outline-offset: var(--_qm-ring-offset);
|
|
504
520
|
}
|
|
505
521
|
|
|
522
|
+
/* A weighted table spans the leaf, laid out fixed, so the stored weights are the
|
|
523
|
+
proportions its columns draw, as the page's fractional columns span the text block
|
|
524
|
+
(`table-view.ts` §`weigh`). An auto-fit table keeps its content's width. */
|
|
525
|
+
.qm-table-frame[data-weighted] {
|
|
526
|
+
width: 100%;
|
|
527
|
+
}
|
|
528
|
+
.qm-table-frame[data-weighted] .qm-table {
|
|
529
|
+
width: 100%;
|
|
530
|
+
table-layout: fixed;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* A column border's handle: in the band, astride the border after its header cell, the
|
|
534
|
+
band's height. It draws a rule along the border while the pointer is on it or while
|
|
535
|
+
it is being dragged, and nothing at rest. */
|
|
536
|
+
.qm-table-resize {
|
|
537
|
+
position: absolute;
|
|
538
|
+
z-index: 1;
|
|
539
|
+
bottom: 100%;
|
|
540
|
+
right: calc(var(--_qm-space-3) / -2);
|
|
541
|
+
display: flex;
|
|
542
|
+
justify-content: center;
|
|
543
|
+
width: var(--_qm-space-3);
|
|
544
|
+
height: var(--_qm-tap-min);
|
|
545
|
+
cursor: col-resize;
|
|
546
|
+
/* The pointer owns this gesture, not the page's scroll: a border drags. */
|
|
547
|
+
touch-action: none;
|
|
548
|
+
}
|
|
549
|
+
.qm-table-resize::before {
|
|
550
|
+
content: '';
|
|
551
|
+
width: var(--_qm-ring-width);
|
|
552
|
+
border-radius: var(--_qm-radius-pill);
|
|
553
|
+
background: var(--_qm-accent);
|
|
554
|
+
opacity: 0;
|
|
555
|
+
transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
|
|
556
|
+
}
|
|
557
|
+
.qm-table-resize:is(:hover, [data-active])::before {
|
|
558
|
+
opacity: 1;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* The held column's alignment cluster, hung off its header cell and floating over the
|
|
562
|
+
first row, the band's top edge being the grips'. Up only while its column is held and
|
|
563
|
+
absent otherwise (`table-view.ts` §`alignCluster`), so the reveal is `hidden` rather
|
|
564
|
+
than the band's opacity. It floats over content it did not choose, so the edge and
|
|
565
|
+
`--_qm-lift` separate it and the raised rung joins them in dark (ARCHITECTURE
|
|
566
|
+
§Styling). The pressed toggle takes the selected grip's tint and accent: it names the
|
|
567
|
+
held column's alignment the way the grip names the held line. */
|
|
568
|
+
.qm-table-align {
|
|
569
|
+
position: absolute;
|
|
570
|
+
z-index: 2;
|
|
571
|
+
top: 100%;
|
|
572
|
+
inset-inline: 0;
|
|
573
|
+
width: max-content;
|
|
574
|
+
margin-inline: auto;
|
|
575
|
+
display: flex;
|
|
576
|
+
border: var(--_qm-border-width) solid var(--_qm-border);
|
|
577
|
+
border-radius: var(--_qm-radius-inner);
|
|
578
|
+
background: var(--_qm-surface-raised);
|
|
579
|
+
box-shadow: var(--_qm-lift);
|
|
580
|
+
}
|
|
581
|
+
.qm-table-align[hidden] {
|
|
582
|
+
display: none;
|
|
583
|
+
}
|
|
584
|
+
/* The header toggle hangs under the first row's last cell, flush with the grid's
|
|
585
|
+
trailing edge, clear of whatever a held row raises over its first. */
|
|
586
|
+
.qm-table-align[data-header] {
|
|
587
|
+
margin-inline: auto 0;
|
|
588
|
+
}
|
|
589
|
+
.qm-table-align-option {
|
|
590
|
+
display: flex;
|
|
591
|
+
align-items: center;
|
|
592
|
+
justify-content: center;
|
|
593
|
+
width: var(--_qm-tap-min);
|
|
594
|
+
height: var(--_qm-tap-min);
|
|
595
|
+
padding: 0;
|
|
596
|
+
border: none;
|
|
597
|
+
border-radius: var(--_qm-radius-inner);
|
|
598
|
+
background: none;
|
|
599
|
+
color: var(--_qm-ink);
|
|
600
|
+
cursor: pointer;
|
|
601
|
+
}
|
|
602
|
+
.qm-table-align-option:hover {
|
|
603
|
+
background: var(--_qm-surface-hover);
|
|
604
|
+
}
|
|
605
|
+
.qm-table-align-option[aria-pressed='true'] {
|
|
606
|
+
background: var(--_qm-accent-tint);
|
|
607
|
+
color: var(--_qm-accent);
|
|
608
|
+
}
|
|
609
|
+
.qm-table-align-option svg {
|
|
610
|
+
width: var(--_qm-glyph-control);
|
|
611
|
+
height: var(--_qm-glyph-control);
|
|
612
|
+
}
|
|
613
|
+
|
|
506
614
|
/* A drag in flight. The lifted line reads as picked up by tone, never a shadow:
|
|
507
615
|
elevation is a surface rung here, which says the same thing at both poles. The drop
|
|
508
616
|
rule is the accent, the ink the surface keeps for a state rather than for text. */
|
|
@@ -3,8 +3,7 @@ import type { NodeType } from 'prosemirror-model';
|
|
|
3
3
|
/** The full field schema: every block kind, container, mark, and island. */
|
|
4
4
|
export declare const blockSchema: Schema<string, string>;
|
|
5
5
|
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
6
|
-
* containers, no islands, and the full mark set (a `richtext(inline)` field
|
|
7
|
-
* a table cell, which is the same content unit). */
|
|
6
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field). */
|
|
8
7
|
export declare const inlineSchema: Schema<string, string>;
|
|
9
8
|
/**
|
|
10
9
|
* The inline schema with no mark types at all: a `plaintext(inline)` field, whose
|
|
@@ -16,6 +15,13 @@ export declare const inlineSchema: Schema<string, string>;
|
|
|
16
15
|
* carrying (CODEC §Inline mode).
|
|
17
16
|
*/
|
|
18
17
|
export declare const plaintextSchema: Schema<string, never>;
|
|
18
|
+
/**
|
|
19
|
+
* A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a
|
|
20
|
+
* `\n` in its `text`. Still one paragraph, so Enter has no block to split; a break is
|
|
21
|
+
* Shift-Enter's (`breaks.ts`). Declaring the break routes `decode` through the plain
|
|
22
|
+
* path, which folds a cell's `continues` lines into that one paragraph.
|
|
23
|
+
*/
|
|
24
|
+
export declare const cellSchema: Schema<"hard_break", string>;
|
|
19
25
|
/**
|
|
20
26
|
* A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
|
|
21
27
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|