@quillmark/svelte 0.19.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.
Files changed (55) hide show
  1. package/NOTICE +4 -2
  2. package/dist/core/codec/blocks.d.ts +11 -2
  3. package/dist/core/codec/blocks.js +45 -14
  4. package/dist/core/codec/breaks.d.ts +3 -2
  5. package/dist/core/codec/breaks.js +3 -2
  6. package/dist/core/codec/decode.d.ts +7 -3
  7. package/dist/core/codec/decode.js +34 -8
  8. package/dist/core/codec/field.d.ts +11 -5
  9. package/dist/core/codec/field.js +11 -8
  10. package/dist/core/codec/index.d.ts +1 -1
  11. package/dist/core/codec/index.js +1 -1
  12. package/dist/core/codec/islands.d.ts +3 -1
  13. package/dist/core/codec/islands.js +8 -5
  14. package/dist/core/codec/markdown-paste.d.ts +9 -0
  15. package/dist/core/codec/markdown-paste.js +49 -0
  16. package/dist/core/codec/prose.css +109 -1
  17. package/dist/core/codec/schema.d.ts +8 -2
  18. package/dist/core/codec/schema.js +16 -5
  19. package/dist/core/codec/slash.d.ts +9 -6
  20. package/dist/core/codec/slash.js +0 -0
  21. package/dist/core/codec/table-paste.d.ts +7 -0
  22. package/dist/core/codec/table-paste.js +132 -0
  23. package/dist/core/codec/table-view.d.ts +12 -0
  24. package/dist/core/codec/table-view.js +401 -57
  25. package/dist/core/codec/table.d.ts +38 -16
  26. package/dist/core/codec/table.js +106 -35
  27. package/dist/core/theme.css +1 -1
  28. package/dist/visual/ArrayField.svelte +4 -0
  29. package/dist/visual/Field.svelte +8 -2
  30. package/dist/visual/Field.svelte.d.ts +1 -1
  31. package/dist/visual/FieldHint.svelte +62 -10
  32. package/dist/visual/FormatPopover.svelte +27 -31
  33. package/dist/visual/FormatPopover.svelte.d.ts +2 -2
  34. package/dist/visual/MatrixAdd.svelte +13 -132
  35. package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
  36. package/dist/visual/MatrixField.svelte +5 -63
  37. package/dist/visual/MatrixField.svelte.d.ts +0 -3
  38. package/dist/visual/ObjectField.svelte +7 -3
  39. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  40. package/dist/visual/ProseField.svelte +2 -1
  41. package/dist/visual/ProseValue.svelte +113 -40
  42. package/dist/visual/ProseValue.svelte.d.ts +8 -4
  43. package/dist/visual/VisualEditorInner.svelte +22 -22
  44. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
  45. package/dist/visual/checklists.d.ts +0 -79
  46. package/dist/visual/checklists.js +0 -164
  47. package/dist/visual/guidance.d.ts +15 -0
  48. package/dist/visual/guidance.js +37 -0
  49. package/dist/visual/leaves.d.ts +27 -1
  50. package/dist/visual/leaves.js +46 -0
  51. package/dist/visual/strings.d.ts +3 -8
  52. package/dist/visual/strings.js +0 -3
  53. package/dist/visual/structure.js +1 -1
  54. package/dist/visual/tips.js +1 -1
  55. 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 a plus and four text-alignment glyphs derived
15
- from an earlier release of the same project.
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. A pick passes none and continues whatever list is
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. A pick passes none and continues whatever list is
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 { paragraph, list_item: item } = state.schema.nodes;
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
- const { $from } = state.selection;
147
- if (!list || !paragraph || !atHead(state) || openingAnItem($from, item))
163
+ if (!list || !paragraph)
148
164
  return false;
149
- const pos = $from.pos;
150
- const tr = state.tr;
151
- // A paragraph item wherever this fires: `# ` inside the item is the gesture that
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: `{}` for the inline schemas, which
14
- * declare no break node. A `richtext(inline)` field is one line by construction
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: `{}` for the inline schemas, which
85
- * declare no break node. A `richtext(inline)` field is one line by construction
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 `rt` decodes under the schema a leaf so declared mounts, losing nothing its
31
- * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
32
- * anything on the block schema. What a leaf held over the rest reads. */
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 `rt` decodes under the schema a leaf so declared mounts, losing nothing its
98
- * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
99
- * anything on the block schema. What a leaf held over the rest reads. */
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
- return !leaf.plaintext || fitsPlain(rt);
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 and its `instance`, NUL-joined
220
- * as `markKey` joins a mark's. `list_item` carries its own run rule, so the quote is
221
- * what keys here. */
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
- return `${c.container}\u0000${instanceOf(c)}`;
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. A held leaf draws
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 three that answer to a block leaf's own
194
- * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
195
- * (`islands.ts`).
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
@@ -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 three that answer to a block leaf's own
506
- * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
507
- * (`islands.ts`).
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
- // All three answer to something only the block schema holds: any other leaf is
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';
@@ -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` re-minted, or `null` where
174
- * none was: an untouched paste stays the slice it arrived as. Every surviving id joins
175
- * the set, so a slice carrying one id twice comes apart in the same pass. */
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 on the island, and they say draggable rather than which control this
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, and
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