@quillmark/svelte 0.21.0 → 0.22.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 (67) hide show
  1. package/NOTICE +6 -7
  2. package/README.md +1 -1
  3. package/dist/core/address.d.ts +6 -1
  4. package/dist/core/address.js +12 -3
  5. package/dist/core/bloom.d.ts +9 -4
  6. package/dist/core/bloom.js +18 -49
  7. package/dist/core/codec/atoms.d.ts +1 -1
  8. package/dist/core/codec/atoms.js +1 -1
  9. package/dist/core/codec/blocks.js +4 -2
  10. package/dist/core/codec/code.d.ts +1 -1
  11. package/dist/core/codec/code.js +1 -1
  12. package/dist/core/codec/decode.d.ts +5 -5
  13. package/dist/core/codec/decode.js +18 -12
  14. package/dist/core/codec/encode.js +6 -1
  15. package/dist/core/codec/field.d.ts +15 -10
  16. package/dist/core/codec/field.js +46 -17
  17. package/dist/core/codec/index.d.ts +1 -1
  18. package/dist/core/codec/index.js +2 -2
  19. package/dist/core/codec/inputrules.js +1 -1
  20. package/dist/core/codec/lists.d.ts +3 -2
  21. package/dist/core/codec/lists.js +21 -5
  22. package/dist/core/codec/nesting.d.ts +20 -0
  23. package/dist/core/codec/nesting.js +62 -0
  24. package/dist/core/codec/prose.css +74 -20
  25. package/dist/core/codec/schema.d.ts +2 -2
  26. package/dist/core/codec/schema.js +16 -7
  27. package/dist/core/codec/table-copy.d.ts +17 -0
  28. package/dist/core/codec/table-copy.js +84 -0
  29. package/dist/core/codec/table-view.d.ts +1 -7
  30. package/dist/core/codec/table-view.js +70 -119
  31. package/dist/core/codec/table.d.ts +2 -9
  32. package/dist/core/codec/table.js +10 -19
  33. package/dist/core/codec/tasks.d.ts +19 -0
  34. package/dist/core/codec/tasks.js +140 -0
  35. package/dist/core/errors.d.ts +2 -2
  36. package/dist/visual/ArrayField.svelte +101 -63
  37. package/dist/visual/ArrayField.svelte.d.ts +0 -2
  38. package/dist/visual/Card.svelte +9 -3
  39. package/dist/visual/Card.svelte.d.ts +2 -2
  40. package/dist/visual/Field.svelte +20 -16
  41. package/dist/visual/Field.svelte.d.ts +4 -2
  42. package/dist/visual/FieldLabel.svelte +5 -33
  43. package/dist/visual/FieldLabel.svelte.d.ts +0 -2
  44. package/dist/visual/FormatPopover.svelte +54 -73
  45. package/dist/visual/MatrixField.svelte +18 -12
  46. package/dist/visual/NumberField.svelte +32 -3
  47. package/dist/visual/ObjectField.svelte +38 -20
  48. package/dist/visual/ObjectField.svelte.d.ts +6 -0
  49. package/dist/visual/ProseField.svelte +10 -13
  50. package/dist/visual/ProseField.svelte.d.ts +5 -2
  51. package/dist/visual/ProseValue.svelte +25 -19
  52. package/dist/visual/ProseValue.svelte.d.ts +4 -4
  53. package/dist/visual/TextField.svelte +3 -2
  54. package/dist/visual/TipsCard.svelte +5 -2
  55. package/dist/visual/VariantField.svelte +1 -1
  56. package/dist/visual/VariantField.svelte.d.ts +1 -1
  57. package/dist/visual/VisualEditorInner.svelte +13 -13
  58. package/dist/visual/controls.css +19 -1
  59. package/dist/visual/icons/nodes.d.ts +0 -21
  60. package/dist/visual/icons/nodes.js +1 -7
  61. package/dist/visual/leaves.d.ts +12 -9
  62. package/dist/visual/leaves.js +19 -0
  63. package/dist/visual/strings.d.ts +10 -5
  64. package/dist/visual/strings.js +16 -9
  65. package/dist/visual/structure.d.ts +21 -31
  66. package/dist/visual/structure.js +54 -51
  67. package/package.json +3 -3
@@ -13,7 +13,7 @@ import './prose.css';
13
13
  // selection (`visual/FormatPopover.svelte`), so it is reached off-barrel.
14
14
  export { rangeAnchor } from './anchor.js';
15
15
  // The prose leaf.
16
- export { createField, emptyContent, heldAttributes, proseAttributes, proseLeafPlugins } from './field.js';
16
+ export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
17
17
  // The table island's chrome vocabulary: the visual tier's `strings` set extends it,
18
18
  // so the island's wording is overridden beside every other key.
19
19
  export { DEFAULT_TABLE_STRINGS } from './table-view.js';
@@ -37,7 +37,7 @@ export { pmToContent, contentEdit, lower } from './encode.js';
37
37
  export { usvToPM, pmToUsv, buildLineIndex } from './positions.js';
38
38
  // Reconciliation gate.
39
39
  export { createReconciler, contentEqual, valueEqual } from './reconcile.js';
40
- // Input rules (`createField` mounts them unless `noInputRules`/`plaintext`).
40
+ // Input rules (`createField` mounts them unless `noInputRules`, or over a mark-free schema).
41
41
  export { inputRulesPlugin } from './inputrules.js';
42
42
  // The body leaf's structural keys: `createField` binds the composed chains
43
43
  // (`bodyKeymap`); the suite drives both it and the list link directly.
@@ -58,7 +58,7 @@ export function markdownInputRules(schema) {
58
58
  const rules = [];
59
59
  const m = schema.marks;
60
60
  // Inline marks. Non-greedy capture between matching delimiters. Built only where the
61
- // schema has the mark: the inline schema omits them, and a plaintext one declares none
61
+ // schema has the mark: the inline schema omits them, and a `text` one declares none
62
62
  // at all.
63
63
  if (m.strong)
64
64
  rules.push(markInputRule(/\*\*([^*]+)\*\*$/, m.strong, 2));
@@ -1,7 +1,7 @@
1
1
  import type { Schema } from 'prosemirror-model';
2
2
  import type { Command } from 'prosemirror-state';
3
3
  /**
4
- * The list bindings for a block-schema leaf: `{}` for the inline/plaintext
4
+ * The list bindings for a block-schema leaf: `{}` for the one-line and plain
5
5
  * schemas, which declare no list nodes.
6
6
  *
7
7
  * `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
@@ -13,7 +13,8 @@ import type { Command } from 'prosemirror-state';
13
13
  * shell structural keymap (VISUAL_EDITOR §Settled and open).
14
14
  *
15
15
  * `Enter` is a chain in precedence order: the escape-above gesture, then
16
- * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
16
+ * `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
17
+ * then `liftListItem`. The middle link carries two behaviors of
17
18
  * its own: it splits a non-empty item, and on an empty item it either splits the
18
19
  * wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
19
20
  * so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
@@ -1,6 +1,8 @@
1
- import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
1
+ import { liftListItem, sinkListItem } from 'prosemirror-schema-list';
2
2
  import { EditorState, Selection, TextSelection } from 'prosemirror-state';
3
3
  import { chainCommands, joinTextblockBackward, joinTextblockForward } from 'prosemirror-commands';
4
+ import { withinNesting } from './nesting.js';
5
+ import { splitTaskItem } from './tasks.js';
4
6
  /** The caret's enclosing `list_item` when it sits at the very start of that item's
5
7
  * first block: the position both structural keys branch on. `null` otherwise
6
8
  * (mid-text, a non-first block of a multi-paragraph item, a non-empty selection),
@@ -187,8 +189,21 @@ function backspaceBelow(itemType, press) {
187
189
  return press(EditorState.create({ doc: state.doc, selection: below }), dispatch && keepCaret);
188
190
  };
189
191
  }
192
+ /** Tab → `sinkListItem`, declined where the sink would nest past `MAX_NESTING_DEPTH`
193
+ * (`nesting.ts`), so Tab there leaves the body as it does on an item with nothing to
194
+ * sink under. */
195
+ function sinkWithin(itemType) {
196
+ const sink = sinkListItem(itemType);
197
+ return (state, dispatch) => {
198
+ let sunk;
199
+ if (!sink(state, (tr) => (sunk = tr)) || !sunk || !withinNesting(sunk.doc))
200
+ return false;
201
+ dispatch?.(sunk);
202
+ return true;
203
+ };
204
+ }
190
205
  /**
191
- * The list bindings for a block-schema leaf: `{}` for the inline/plaintext
206
+ * The list bindings for a block-schema leaf: `{}` for the one-line and plain
192
207
  * schemas, which declare no list nodes.
193
208
  *
194
209
  * `Tab` / `Shift-Tab` hold the list link of the body's Tab chain. Surfaces that own
@@ -200,7 +215,8 @@ function backspaceBelow(itemType, press) {
200
215
  * shell structural keymap (VISUAL_EDITOR §Settled and open).
201
216
  *
202
217
  * `Enter` is a chain in precedence order: the escape-above gesture, then
203
- * `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
218
+ * `splitListItem` (`splitTaskItem`, which leaves a task item's new half an open task),
219
+ * then `liftListItem`. The middle link carries two behaviors of
204
220
  * its own: it splits a non-empty item, and on an empty item it either splits the
205
221
  * wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
206
222
  * so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
@@ -217,9 +233,9 @@ export function listKeymap(schema) {
217
233
  return {};
218
234
  const backspace = chainCommands(liftAtItemStart(itemType), joinOntoList(itemType));
219
235
  return {
220
- Tab: sinkListItem(itemType),
236
+ Tab: sinkWithin(itemType),
221
237
  'Shift-Tab': liftListItem(itemType),
222
- Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitListItem(itemType), liftListItem(itemType)),
238
+ Enter: chainCommands(paragraphAboveList(itemType, paragraph), splitTaskItem(itemType), liftListItem(itemType)),
223
239
  Backspace: backspace,
224
240
  Delete: backspaceBelow(itemType, backspace)
225
241
  };
@@ -0,0 +1,20 @@
1
+ import type { Node as PMNode } from 'prosemirror-model';
2
+ import { Plugin } from 'prosemirror-state';
3
+ /** The store's bound on the containers around one line: a commit nesting past it is
4
+ * refused, and `@quillmark/wasm` exports no constant to read it from. */
5
+ export declare const MAX_NESTING_DEPTH = 12;
6
+ /** The most containers any block in `doc` stands inside, counted as the store counts
7
+ * them: a `list_item` or a `blockquote` is one, the list around an item none, and no
8
+ * node holds an element (`decode.ts`). */
9
+ export declare function nestingDepth(doc: PMNode): number;
10
+ /** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
11
+ * of the doc it would dispatch. */
12
+ export declare function withinNesting(doc: PMNode): boolean;
13
+ /**
14
+ * Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
15
+ * would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
16
+ * The nesting commands ask first and keep their key; this is what reaches the rest, and
17
+ * a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
18
+ * deepest level no deeper, so a body that arrived that deep keeps its Enter.
19
+ */
20
+ export declare function nestingPlugin(): Plugin;
@@ -0,0 +1,62 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import { ReplaceAroundStep, ReplaceStep } from 'prosemirror-transform';
3
+ /** The store's bound on the containers around one line: a commit nesting past it is
4
+ * refused, and `@quillmark/wasm` exports no constant to read it from. */
5
+ export const MAX_NESTING_DEPTH = 12;
6
+ /** The most containers any block in `doc` stands inside, counted as the store counts
7
+ * them: a `list_item` or a `blockquote` is one, the list around an item none, and no
8
+ * node holds an element (`decode.ts`). */
9
+ export function nestingDepth(doc) {
10
+ const walk = (node, depth) => {
11
+ let deepest = depth;
12
+ node.forEach((child) => {
13
+ if (child.isTextblock || child.isAtom)
14
+ return;
15
+ const name = child.type.name;
16
+ const d = depth + (name === 'list_item' || name === 'blockquote' ? 1 : 0);
17
+ deepest = Math.max(deepest, walk(child, d));
18
+ });
19
+ return deepest;
20
+ };
21
+ return walk(doc, 0);
22
+ }
23
+ /** Whether `doc` nests within {@link MAX_NESTING_DEPTH}: what a command that nests asks
24
+ * of the doc it would dispatch. */
25
+ export function withinNesting(doc) {
26
+ return nestingDepth(doc) <= MAX_NESTING_DEPTH;
27
+ }
28
+ /** Whether a step of `tr` can move a block under another container: any replace but
29
+ * inline content inside one textblock, which is every keystroke's. Mark and attribute
30
+ * steps move no node. */
31
+ function mayNest(tr) {
32
+ return tr.steps.some((step, i) => {
33
+ if (step instanceof ReplaceAroundStep)
34
+ return true;
35
+ if (!(step instanceof ReplaceStep))
36
+ return false;
37
+ const doc = tr.docs[i];
38
+ const $from = doc.resolve(step.from);
39
+ if (!$from.parent.isTextblock || !$from.sameParent(doc.resolve(step.to)))
40
+ return true;
41
+ let block = false;
42
+ step.slice.content.forEach((node) => (block ||= node.isBlock));
43
+ return block;
44
+ });
45
+ }
46
+ /**
47
+ * Refuses a transaction leaving `doc` past {@link MAX_NESTING_DEPTH}, which the commit
48
+ * would lose: a paste or a drop into a deep item, whose slice the import bounded alone.
49
+ * The nesting commands ask first and keep their key; this is what reaches the rest, and
50
+ * a refused paste lands nothing. Past the bound, a transaction passes where it leaves the
51
+ * deepest level no deeper, so a body that arrived that deep keeps its Enter.
52
+ */
53
+ export function nestingPlugin() {
54
+ return new Plugin({
55
+ filterTransaction: (tr) => {
56
+ if (!tr.docChanged || !mayNest(tr))
57
+ return true;
58
+ const after = nestingDepth(tr.doc);
59
+ return after <= MAX_NESTING_DEPTH || after <= nestingDepth(tr.before);
60
+ }
61
+ });
62
+ }
@@ -148,6 +148,20 @@ li.ProseMirror-selectednode::after {
148
148
  font-size: inherit;
149
149
  }
150
150
 
151
+ /* A fence's language at the label rung in its top corner, read off the `data-lang` the
152
+ node writes (`schema.ts`). Generated content, so it is never text the caret reaches
153
+ or a copy takes; the code's lines wrap around it. Read-only: the language is changed
154
+ in source mode. */
155
+ .ProseMirror pre[data-lang]::before {
156
+ content: attr(data-lang);
157
+ float: inline-end;
158
+ margin-inline-start: var(--_qm-space-3);
159
+ font-size: var(--_qm-text-label);
160
+ color: var(--_qm-ink-label);
161
+ user-select: none;
162
+ pointer-events: none;
163
+ }
164
+
151
165
  /* A link takes the surface's ink (the UA's blue is the `#8cf` defect again) and a
152
166
  dotted underline. Dotted rather than solid because `underline` is a mark in the
153
167
  same schema, and a solid rule renders an authored `<u>` and a link identically in a
@@ -191,6 +205,59 @@ li.ProseMirror-selectednode::after {
191
205
  list-style: revert;
192
206
  }
193
207
 
208
+ /* A task item's checkbox (`tasks.ts`) stands after the item's marker and ahead of its
209
+ first line, the text wrapping back under it, as the paper sets the box. Its face is a
210
+ matrix tick's: the input drawn `appearance: none` as an edged well, held at the
211
+ accent, the check glyph over it in the surface ink. A disabled box is a held leaf's
212
+ or a copy's, and takes no press. */
213
+ .ProseMirror .qm-task {
214
+ position: relative;
215
+ float: left;
216
+ display: flex;
217
+ align-items: center;
218
+ height: 1lh;
219
+ margin-right: var(--_qm-space-2);
220
+ }
221
+ .ProseMirror .qm-task-box {
222
+ appearance: none;
223
+ margin: 0;
224
+ width: var(--_qm-glyph-control);
225
+ height: var(--_qm-glyph-control);
226
+ border: var(--_qm-border-width) solid var(--_qm-border);
227
+ border-radius: calc(var(--_qm-radius-inner) / 2);
228
+ background: var(--_qm-surface-well);
229
+ cursor: pointer;
230
+ transition:
231
+ background-color var(--_qm-duration-fast) var(--_qm-ease-reverse),
232
+ border-color var(--_qm-duration-fast) var(--_qm-ease-reverse);
233
+ }
234
+ .ProseMirror .qm-task-box:checked {
235
+ background: var(--_qm-accent);
236
+ border-color: var(--_qm-accent);
237
+ }
238
+ .ProseMirror .qm-task-box:disabled {
239
+ cursor: default;
240
+ }
241
+ .ProseMirror .qm-task-box:focus-visible {
242
+ outline: var(--_qm-ring);
243
+ outline-offset: var(--_qm-ring-offset);
244
+ }
245
+ .ProseMirror .qm-task-mark {
246
+ position: absolute;
247
+ top: 50%;
248
+ left: 0;
249
+ width: var(--_qm-glyph-control);
250
+ height: var(--_qm-glyph-control);
251
+ translate: 0 -50%;
252
+ pointer-events: none;
253
+ color: var(--_qm-surface);
254
+ opacity: 0;
255
+ transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
256
+ }
257
+ .ProseMirror .qm-task:has(:checked) .qm-task-mark {
258
+ opacity: 1;
259
+ }
260
+
194
261
  /* Six levels, one weight, no size step. The quill reads no level at all, so a size
195
262
  ramp here would be the editor predicting a hierarchy the paper does not set; the
196
263
  level stays in the model, which is where the codec round-trips it. It is also all
@@ -516,37 +583,24 @@ li.ProseMirror-selectednode::after {
516
583
  height: var(--_qm-tap-min);
517
584
  }
518
585
  /* The reveal is the control's own (CODEC §"The table island"): one comes up while the
519
- pointer is on that control, while it holds the focus, or while it is what a gesture is
520
- about — the selected line's grip, and the lifted one, which is drawn through a drag the
586
+ pointer is on that control, or while it is what a gesture is about — the selected line's grip, and the lifted one, which is drawn through a drag the
521
587
  pointer has travelled off. A cell under the pointer draws its row's and its column's
522
588
  grips (`table-view.ts` §`onHover`), one pill on each axis and none anywhere else.
523
589
 
524
590
  Opacity rather than absence, so every control keeps its box and is hit-tested wherever
525
- it sits. A grip's reach is the pointer's alone and it holds no focus, so it carries no
526
- focus rung: what a keyboard draws a line with is the cell the selection runs through
527
- (`table-view.ts` §`cellKeys`), and the wash and the selected grip's tint are what say
528
- so. The two bars are in the tab order, the Tab that declines off the last cell landing
529
- on them, so the rung there draws one a keyboard has reached. */
591
+ it sits. A grip's reach and a bar's are the pointer's alone and neither holds focus, so
592
+ neither carries a focus rung: what a keyboard draws a line with is the cell the
593
+ selection runs through (`table-view.ts` §`cellKeys`), and the wash and the selected
594
+ grip's tint are what say so. */
530
595
  :is(.qm-table-grip, .qm-table-add) {
531
596
  opacity: 0;
532
597
  transition: opacity var(--_qm-duration-fast) var(--_qm-ease-reverse);
533
598
  }
534
- .qm-table-add:is(:hover, :focus-visible),
599
+ .qm-table-add:hover,
535
600
  .qm-table-grip:is([data-near], :hover, [aria-pressed='true'], .qm-table-lifted) {
536
601
  opacity: 1;
537
602
  }
538
603
 
539
- /* The ring, on the two add bars and nowhere else in this file. A prose leaf's focus
540
- indicator is its caret and a cell's is too, so the outline is dropped there; a control
541
- that comes up out of nothing has the reveal, and a ring under a keyboard is what says
542
- the focus is there rather than on the paper behind it. Declared here rather than
543
- reached for as `.qm-focus-ring`: that recipe is the visual tier's stylesheet, and the
544
- island is inside the leaf, which is the codec's. */
545
- .qm-table-add:focus-visible {
546
- outline: var(--_qm-ring);
547
- outline-offset: var(--_qm-ring-offset);
548
- }
549
-
550
604
  /* A weighted table spans the leaf, laid out fixed, so the stored weights are the
551
605
  proportions its columns draw, as the page's fractional columns span the text block
552
606
  (`table-view.ts` §`weigh`). An auto-fit table keeps its content's width. */
@@ -610,7 +664,7 @@ li.ProseMirror-selectednode::after {
610
664
  display: none;
611
665
  }
612
666
  /* The header toggle hangs under the first row's last cell, flush with the grid's
613
- trailing edge, clear of whatever a held row raises over its first. */
667
+ trailing edge. */
614
668
  .qm-table-align[data-header] {
615
669
  margin-inline: auto 0;
616
670
  }
@@ -23,7 +23,7 @@ export declare const plaintextSchema: Schema<string, never>;
23
23
  */
24
24
  export declare const cellSchema: Schema<"hard_break", string>;
25
25
  /**
26
- * A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
26
+ * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
27
27
  * islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
28
28
  * for the reason {@link plaintextSchema} is.
29
29
  *
@@ -38,7 +38,7 @@ export declare function leafSchema(opts: {
38
38
  plaintext?: boolean;
39
39
  inline?: boolean;
40
40
  }): Schema;
41
- /** Whether `schema` can carry formatting at all: false for the two plaintext schemas.
41
+ /** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
42
42
  * What a mark command asks before it offers itself. */
43
43
  export declare function hasMarks(schema: Schema): boolean;
44
44
  /** True for the one-line schemas, which declare no `hard_break`; decode branches on it. */
@@ -3,9 +3,9 @@
3
3
  // (list_item/quote → lists/blockquote); marks mirror the content formatting set.
4
4
  // Each set is closed upstream, so the schema names its whole vocabulary and carries
5
5
  // nothing inert. `blockSchema` is the full field; `inlineSchema` is the constrained
6
- // single-textblock form for `richtext(inline)` (one paragraph, no block split, no
7
- // containers, no islands) and `plaintextSchema` is that one without marks, for
8
- // `plaintext(inline)`; `plainSchema` is a `plaintext` field without `inline`,
6
+ // single-textblock form for a one-line `richtext` (one paragraph, no block split, no
7
+ // containers, no islands) and `plaintextSchema` is that one without marks, for a
8
+ // one-line `text`; `plainSchema` is a `text` field declaring `multiline: true`,
9
9
  // paragraphs and hard breaks and nothing else; `cellSchema` is the inline form with
10
10
  // hard breaks, for a table cell. Same decode/lower/position machinery,
11
11
  // narrower shapes. Anchors are not marks here (decorations).
@@ -18,6 +18,7 @@
18
18
  // marks, a `<table>` being the block leaf's clipboard parser's (`table-paste.ts`).
19
19
  import { Schema } from 'prosemirror-model';
20
20
  import { islandBlockSpec, islandInlineSpec } from './islands.js';
21
+ import { taskBody, taskChecked, taskDOM } from './tasks.js';
21
22
  import { rendersHref, storableUrl } from './urls.js';
22
23
  // ── Marks spelled as style ──────────────────────────────────────────────────
23
24
  // A word processor writes formatting as a span's inline style rather than as a tag:
@@ -193,11 +194,19 @@ const blockNodes = {
193
194
  parseDOM: [{ tag: 'ul' }],
194
195
  toDOM: () => ['ul', 0]
195
196
  },
197
+ // `checked` is a task item's, `null` on every other item (`tasks.ts`).
196
198
  list_item: {
197
199
  content: 'block+',
198
200
  defining: true,
199
- parseDOM: [{ tag: 'li' }],
200
- toDOM: () => ['li', 0]
201
+ attrs: { checked: { default: null } },
202
+ parseDOM: [
203
+ {
204
+ tag: 'li',
205
+ getAttrs: (el) => ({ checked: taskChecked(el) }),
206
+ contentElement: taskBody
207
+ }
208
+ ],
209
+ toDOM: (node) => node.attrs.checked === null ? ['li', 0] : taskDOM(node.attrs.checked)
201
210
  },
202
211
  island_block: islandBlockSpec
203
212
  };
@@ -254,7 +263,7 @@ export const cellSchema = new Schema({
254
263
  marks
255
264
  });
256
265
  /**
257
- * A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
266
+ * A `text` field declaring `multiline: true`: paragraphs and hard breaks, no marks, no
258
267
  * islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
259
268
  * for the reason {@link plaintextSchema} is.
260
269
  *
@@ -278,7 +287,7 @@ export function leafSchema(opts) {
278
287
  return opts.inline ? plaintextSchema : plainSchema;
279
288
  return opts.inline ? inlineSchema : blockSchema;
280
289
  }
281
- /** Whether `schema` can carry formatting at all: false for the two plaintext schemas.
290
+ /** Whether `schema` can carry formatting at all: false for the two mark-free schemas.
282
291
  * What a mark command asks before it offers itself. */
283
292
  export function hasMarks(schema) {
284
293
  return Object.keys(schema.marks).length > 0;
@@ -0,0 +1,17 @@
1
+ import { Plugin } from 'prosemirror-state';
2
+ import type { TableProps } from '@quillmark/wasm';
3
+ /** A block of cells, both corners inclusive, in the row space `table.ts` names. */
4
+ export interface CellRange {
5
+ r0: number;
6
+ c0: number;
7
+ r1: number;
8
+ c1: number;
9
+ }
10
+ /** The cells of `range` as a `<table>`, the header row in a `<thead>` where the range
11
+ * holds it and the table has one. The cells carry their marks and breaks, and nothing
12
+ * of the table's layout. */
13
+ export declare function tableElement(props: TableProps, range?: CellRange): HTMLTableElement;
14
+ /** The cells of `range` as tab-separated text, a line a row. */
15
+ export declare function tableText(props: TableProps, range?: CellRange): string;
16
+ /** A block leaf's copy: a table in the slice writes as a table in both spellings. */
17
+ export declare function tableCopyPlugin(): Plugin;
@@ -0,0 +1,84 @@
1
+ // A table leaves the editor as the two spellings a table arrives in (`table-paste.ts`):
2
+ // an HTML `<table>` and tab-separated text, what Word, Docs, Excel and Sheets each read.
3
+ // Two copies write them: a held rectangle's cells (`table-view.ts`), and a selection
4
+ // holding a whole island, whose markup keeps the island's own element around the table
5
+ // so a paste inside the editor reads the island back whole.
6
+ import { DOMSerializer } from 'prosemirror-model';
7
+ import { Plugin } from 'prosemirror-state';
8
+ import { decode } from './decode.js';
9
+ import { tablePropsOfNode } from './islands.js';
10
+ import { blockSchema, cellSchema } from './schema.js';
11
+ import { cellAt, cellContent, columnCount, rowCount } from './table.js';
12
+ const whole = (props) => ({
13
+ r0: 0,
14
+ c0: 0,
15
+ r1: rowCount(props) - 1,
16
+ c1: columnCount(props) - 1
17
+ });
18
+ const cellSerializer = DOMSerializer.fromSchema(cellSchema);
19
+ function cellDOM(tag, cell) {
20
+ const dom = document.createElement(tag);
21
+ const paragraph = decode(cellContent(cell), cellSchema).firstChild;
22
+ if (paragraph)
23
+ dom.append(cellSerializer.serializeFragment(paragraph.content));
24
+ return dom;
25
+ }
26
+ /** The cells of `range` as a `<table>`, the header row in a `<thead>` where the range
27
+ * holds it and the table has one. The cells carry their marks and breaks, and nothing
28
+ * of the table's layout. */
29
+ export function tableElement(props, range = whole(props)) {
30
+ const table = document.createElement('table');
31
+ let body;
32
+ for (let r = range.r0; r <= range.r1; r++) {
33
+ const head = r === 0 && !props.headless;
34
+ const section = head ? table.createTHead() : (body ??= table.createTBody());
35
+ const tr = section.insertRow();
36
+ for (let c = range.c0; c <= range.c1; c++)
37
+ tr.append(cellDOM(head ? 'th' : 'td', cellAt(props, r, c)));
38
+ }
39
+ return table;
40
+ }
41
+ /** A cell holding a tab, a line end or a quote goes in quotes with its quotes doubled,
42
+ * as Excel and Sheets write one. */
43
+ function tsvCell(text) {
44
+ return /[\t\n"]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
45
+ }
46
+ /** The cells of `range` as tab-separated text, a line a row. */
47
+ export function tableText(props, range = whole(props)) {
48
+ const lines = [];
49
+ for (let r = range.r0; r <= range.r1; r++) {
50
+ const cells = [];
51
+ for (let c = range.c0; c <= range.c1; c++)
52
+ cells.push(tsvCell(cellAt(props, r, c).text));
53
+ lines.push(cells.join('\t'));
54
+ }
55
+ return lines.join('\n');
56
+ }
57
+ const base = DOMSerializer.fromSchema(blockSchema);
58
+ /** The island's own element with the table inside it: a paste in the editor matches the
59
+ * element and reads `props`, and every other reader finds the table. */
60
+ const islandSerializer = new DOMSerializer({
61
+ ...base.nodes,
62
+ island_block: (node) => {
63
+ const props = tablePropsOfNode(node);
64
+ const dom = DOMSerializer.renderSpec(document, blockSchema.nodes.island_block.spec.toDOM(node)).dom;
65
+ if (props)
66
+ dom.replaceChildren(tableElement(props));
67
+ return dom;
68
+ }
69
+ }, base.marks);
70
+ function leafText(node) {
71
+ const props = node.type === blockSchema.nodes.island_block ? tablePropsOfNode(node) : undefined;
72
+ if (props)
73
+ return tableText(props);
74
+ return node.type.spec.leafText?.(node) ?? '';
75
+ }
76
+ /** A block leaf's copy: a table in the slice writes as a table in both spellings. */
77
+ export function tableCopyPlugin() {
78
+ return new Plugin({
79
+ props: {
80
+ clipboardSerializer: islandSerializer,
81
+ clipboardTextSerializer: (slice) => slice.content.textBetween(0, slice.content.size, '\n\n', leafText)
82
+ }
83
+ });
84
+ }
@@ -21,18 +21,12 @@ export interface TableChromeStrings {
21
21
  /** The two trailing bars, each of which grows the table along its own axis. */
22
22
  tableAddRow: string;
23
23
  tableAddColumn: string;
24
- /** The held column's alignment cluster, a name per toggle, and the held cells'
25
- * cluster, which takes these three and the vertical three beside them. */
24
+ /** The held column's alignment cluster, a name per toggle. */
26
25
  tableAlignLeft: string;
27
26
  tableAlignCenter: string;
28
27
  tableAlignRight: string;
29
28
  /** The held first row's toggle, pressed while the row draws as the header. */
30
29
  tableHeaderToggle: string;
31
- tableAlignTop: string;
32
- tableAlignMiddle: string;
33
- tableAlignBottom: string;
34
- /** The held cells' cluster's own name. */
35
- tableCellAlignment: string;
36
30
  }
37
31
  /**
38
32
  * The package's English for the island chrome. It lives here, beside the surface