@quillmark/svelte 0.1.1 → 0.3.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 (124) hide show
  1. package/README.md +16 -12
  2. package/THEMING.md +48 -25
  3. package/dist/core/address.d.ts +42 -7
  4. package/dist/core/address.js +49 -21
  5. package/dist/core/bloom.d.ts +5 -5
  6. package/dist/core/bloom.js +8 -8
  7. package/dist/core/codec/anchor.d.ts +1 -1
  8. package/dist/core/codec/anchor.js +1 -1
  9. package/dist/core/codec/atoms.d.ts +10 -0
  10. package/dist/core/codec/atoms.js +78 -0
  11. package/dist/core/codec/clipboard.d.ts +59 -0
  12. package/dist/core/codec/clipboard.js +202 -0
  13. package/dist/core/codec/code.d.ts +1 -1
  14. package/dist/core/codec/code.js +4 -4
  15. package/dist/core/codec/decode.d.ts +1 -1
  16. package/dist/core/codec/decode.js +8 -8
  17. package/dist/core/codec/encode.d.ts +4 -4
  18. package/dist/core/codec/encode.js +14 -11
  19. package/dist/core/codec/field.d.ts +22 -27
  20. package/dist/core/codec/field.js +33 -32
  21. package/dist/core/codec/index.js +5 -8
  22. package/dist/core/codec/inputrules.js +56 -18
  23. package/dist/core/codec/islands.d.ts +11 -7
  24. package/dist/core/codec/islands.js +16 -10
  25. package/dist/core/codec/keymap.d.ts +5 -6
  26. package/dist/core/codec/keymap.js +8 -7
  27. package/dist/core/codec/lists.d.ts +3 -3
  28. package/dist/core/codec/lists.js +32 -13
  29. package/dist/core/codec/marks.d.ts +1 -1
  30. package/dist/core/codec/marks.js +1 -1
  31. package/dist/core/codec/positions.js +1 -1
  32. package/dist/core/codec/prose.css +266 -228
  33. package/dist/core/codec/schema.js +4 -4
  34. package/dist/core/codec/slash.d.ts +39 -41
  35. package/dist/core/codec/slash.js +0 -0
  36. package/dist/core/codec/table-view.d.ts +12 -16
  37. package/dist/core/codec/table-view.js +488 -292
  38. package/dist/core/codec/table.d.ts +42 -15
  39. package/dist/core/codec/table.js +73 -27
  40. package/dist/core/errors.d.ts +17 -7
  41. package/dist/core/index.d.ts +1 -1
  42. package/dist/core/index.js +6 -5
  43. package/dist/core/lifecycle.d.ts +21 -5
  44. package/dist/core/lifecycle.js +34 -8
  45. package/dist/core/rebind.svelte.d.ts +18 -4
  46. package/dist/core/rebind.svelte.js +30 -12
  47. package/dist/core/teardown.d.ts +1 -1
  48. package/dist/core/teardown.js +1 -1
  49. package/dist/core/theme.css +173 -58
  50. package/dist/preset/index.css +1 -1
  51. package/dist/preset/recipes.css +79 -30
  52. package/dist/preset/scale.css +40 -28
  53. package/dist/preview/Preview.svelte +22 -19
  54. package/dist/preview/Preview.svelte.d.ts +8 -8
  55. package/dist/preview/bridge.d.ts +6 -4
  56. package/dist/preview/bridge.js +19 -9
  57. package/dist/preview/controller.d.ts +13 -6
  58. package/dist/preview/controller.js +8 -5
  59. package/dist/preview/geometry.d.ts +17 -2
  60. package/dist/preview/geometry.js +21 -1
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/index.js +9 -2
  63. package/dist/preview/overlay.d.ts +2 -1
  64. package/dist/preview/overlay.js +26 -11
  65. package/dist/preview/paint.js +19 -17
  66. package/dist/visual/ArrayField.svelte +103 -83
  67. package/dist/visual/ArrayField.svelte.d.ts +5 -4
  68. package/dist/visual/BooleanField.svelte +8 -6
  69. package/dist/visual/BooleanField.svelte.d.ts +2 -2
  70. package/dist/visual/Card.svelte +161 -333
  71. package/dist/visual/Card.svelte.d.ts +5 -4
  72. package/dist/visual/CardControls.svelte +1 -1
  73. package/dist/visual/DateField.svelte +19 -19
  74. package/dist/visual/DateField.svelte.d.ts +3 -3
  75. package/dist/visual/DiagnosticList.svelte +2 -2
  76. package/dist/visual/EnumField.svelte +48 -28
  77. package/dist/visual/EnumField.svelte.d.ts +5 -4
  78. package/dist/visual/Field.svelte +207 -132
  79. package/dist/visual/Field.svelte.d.ts +11 -7
  80. package/dist/visual/FieldHint.svelte +79 -48
  81. package/dist/visual/FieldHint.svelte.d.ts +1 -1
  82. package/dist/visual/FieldLabel.svelte +8 -8
  83. package/dist/visual/FormatPopover.svelte +82 -35
  84. package/dist/visual/NumberField.svelte +5 -5
  85. package/dist/visual/NumberField.svelte.d.ts +1 -1
  86. package/dist/visual/ObjectField.svelte +28 -9
  87. package/dist/visual/ObjectField.svelte.d.ts +4 -2
  88. package/dist/visual/ProseArrayElement.svelte +12 -19
  89. package/dist/visual/ProseArrayElement.svelte.d.ts +1 -2
  90. package/dist/visual/ProseField.svelte +62 -85
  91. package/dist/visual/ProseField.svelte.d.ts +14 -17
  92. package/dist/visual/SlashMenu.svelte +42 -16
  93. package/dist/visual/TextField.svelte +5 -5
  94. package/dist/visual/TextField.svelte.d.ts +1 -1
  95. package/dist/visual/TipsCard.svelte +43 -28
  96. package/dist/visual/VisualEditor.svelte +18 -19
  97. package/dist/visual/VisualEditor.svelte.d.ts +2 -3
  98. package/dist/visual/VisualEditorInner.svelte +251 -184
  99. package/dist/visual/VisualEditorInner.svelte.d.ts +2 -3
  100. package/dist/visual/controls.css +85 -38
  101. package/dist/visual/diagnostics.d.ts +13 -21
  102. package/dist/visual/diagnostics.js +7 -19
  103. package/dist/visual/domid.js +3 -3
  104. package/dist/visual/ext.d.ts +1 -1
  105. package/dist/visual/ext.js +1 -1
  106. package/dist/visual/icons/nodes.d.ts +3 -0
  107. package/dist/visual/icons/nodes.js +2 -1
  108. package/dist/visual/index.d.ts +1 -0
  109. package/dist/visual/index.js +10 -3
  110. package/dist/visual/leaves.d.ts +47 -0
  111. package/dist/visual/leaves.js +31 -0
  112. package/dist/visual/links.d.ts +24 -0
  113. package/dist/visual/links.js +67 -0
  114. package/dist/visual/props.d.ts +33 -12
  115. package/dist/visual/signals.d.ts +6 -6
  116. package/dist/visual/strings.d.ts +14 -19
  117. package/dist/visual/strings.js +5 -7
  118. package/dist/visual/structure.d.ts +24 -17
  119. package/dist/visual/structure.js +8 -8
  120. package/dist/visual/synced.svelte.d.ts +1 -1
  121. package/dist/visual/synced.svelte.js +4 -4
  122. package/dist/visual/tips.d.ts +4 -2
  123. package/dist/visual/tips.js +7 -4
  124. package/package.json +2 -2
@@ -0,0 +1,59 @@
1
+ import { DOMParser as PMDOMParser, Slice } from 'prosemirror-model';
2
+ import type { Node as PMNode, Schema } from 'prosemirror-model';
3
+ import { Plugin } from 'prosemirror-state';
4
+ import type { TableCell, TableProps } from '@quillmark/wasm';
5
+ import { type TableAlign } from './table.js';
6
+ /** What a clipboard event hands over, which is all either arm needs of it: jsdom has no
7
+ * `DataTransfer`, and the suite drives these paths with the two methods themselves. */
8
+ interface Clipboard {
9
+ getData: (type: string) => string;
10
+ setData: (type: string, data: string) => void;
11
+ }
12
+ /**
13
+ * A `<table>` element as props. `rowspan` is not carried — a merged cell is not a
14
+ * rectangle, so a table with one arrives with the rows below it short and
15
+ * {@link normalizeTable} pads them at the end. `colspan` is, as the empty cells it
16
+ * covers, so the columns after it keep their index.
17
+ *
18
+ * Only the table's own rows: a `<tr>` inside a nested table belongs to a cell this
19
+ * reader is about to flatten, not to a rank of this one.
20
+ */
21
+ export declare function tableFromDOM(dom: Element): TableProps;
22
+ /** The table a paste is carrying, or `undefined` for a paste that holds none. The first
23
+ * one: a clipboard holding a table among other blocks is a document paste, and only a
24
+ * surface that takes nothing but a table (a cell) reads it this way. */
25
+ export declare function tableFromClipboard(data: Clipboard | null | undefined): TableProps | undefined;
26
+ /**
27
+ * A rectangle as a `<table>`: the wire the HTML arm writes and reads. Its first row is
28
+ * a `<th>` row whatever row of the source it came from, that being the row index 0 a
29
+ * paste lands it at.
30
+ */
31
+ export declare function tableToDOM(block: TableCell[][], aligns: TableAlign[], owner: Document): Element;
32
+ /**
33
+ * A rectangle as pipe rows, delimiter row included: what a plain-text target reads as a
34
+ * table and what markdown carries. The delimiter follows the first row for the reason
35
+ * that row serializes as `<th>` — a pasted rectangle's row 0 is a header.
36
+ */
37
+ export declare function tableToPipe(block: TableCell[][], aligns: TableAlign[]): string;
38
+ /** Put a rectangle on the clipboard, both arms. The caller is a `copy` / `cut` handler
39
+ * over a selection its own surface holds, so the rectangle arrives already cut out and
40
+ * claiming the event is the caller's. */
41
+ export declare function writeRectangle(data: Clipboard, block: TableCell[][], aligns: TableAlign[], owner: Document): void;
42
+ /** The clipboard's parser: the schema's own rules with the table rule ahead of them.
43
+ * Ahead, because a `<table>` otherwise matches nothing and its cells' text falls
44
+ * through to the enclosing block. */
45
+ export declare function tableClipboardParser(schema: Schema): PMDOMParser;
46
+ /** Mint an id for every island a paste is carrying. An id is part of the document's
47
+ * canonical bytes and the channel that addresses an island (CODEC §Islands), so a
48
+ * pasted island with none reaches the store as an insert nothing can name afterwards.
49
+ * One minter for the whole slice, or a paste placing two tables hands them one id. */
50
+ export declare function stampIslandIds(slice: Slice, doc: PMNode): Slice;
51
+ /**
52
+ * The block leaf's clipboard plugin: the table rule on the way in, and the id mint over
53
+ * what it produced. A `clipboardParser` rather than the schema's own `parseDOM`,
54
+ * because what a `<table>` in the DOM means is a question only the clipboard asks — the
55
+ * schema's parse rules are also what a re-parse of the editor's own DOM runs through,
56
+ * where an island is already a node and a `<table>` is the chrome drawing one.
57
+ */
58
+ export declare function tableClipboardPlugin(schema: Schema): Plugin;
59
+ export {};
@@ -0,0 +1,202 @@
1
+ // The table island's clipboard door, both directions: what a copy writes, a paste
2
+ // reads. The design and the two wire formats are CODEC §"The table island" and
3
+ // §"Markdown at the edges"; what is here is the grammar.
4
+ //
5
+ // What crosses is A rectangle, never a cell. A `<table>` read in is one; the held cell
6
+ // selection written out is one; a rectangle overlaid onto a table at a cell is one. The
7
+ // wire therefore carries `TableCell[][]` and no coordinates: each end names its own
8
+ // corner.
9
+ //
10
+ // A row 0 is A header, on both sides, which is the model's own rule (`table.ts`
11
+ // §`allRows`): whichever row holds index 0 is the header. So a `<table>` with no `<th>`
12
+ // promotes its first row, and a copied rectangle's first row is the header of what it
13
+ // pastes.
14
+ import { DOMParser as PMDOMParser, DOMSerializer, Fragment, Slice } from 'prosemirror-model';
15
+ import { Plugin } from 'prosemirror-state';
16
+ import { islandMinter } from './islands.js';
17
+ import { inlineSchema } from './schema.js';
18
+ import { ALIGNS, cellContent, cellFromDoc, emptyCell, normalizeTable } from './table.js';
19
+ import { decode } from './decode.js';
20
+ /** The two keys, spelled once: a reader and a writer that disagreed about which holds
21
+ * the table would each work alone and never round-trip. */
22
+ const HTML = 'text/html';
23
+ const PLAIN = 'text/plain';
24
+ // ── Reading a table in ──────────────────────────────────────────────────────
25
+ /**
26
+ * One cell from its `<td>` / `<th>`. The inline schema is the whole normalization: it
27
+ * declares one paragraph, no containers and no islands, so a cell holding blocks
28
+ * flattens to its text and an image in one drops out rather than leaving a slot char
29
+ * behind. Whitespace collapses and the ends trim, which is PM's own parse.
30
+ */
31
+ function cellFromDOM(dom) {
32
+ const para = PMDOMParser.fromSchema(inlineSchema).parse(dom, {
33
+ topNode: inlineSchema.nodes.paragraph.create()
34
+ });
35
+ return cellFromDoc(inlineSchema.nodes.doc.create(null, para), emptyCell());
36
+ }
37
+ /** A column's alignment off its header cell, which is where a pipe table's delimiter
38
+ * row puts it. Anything else (`start`, `justify`, absent) is `none`. */
39
+ function alignFromDOM(dom) {
40
+ const raw = (dom.getAttribute('align') ??
41
+ dom.style?.textAlign ??
42
+ '').toLowerCase();
43
+ return ALIGNS.includes(raw) ? raw : 'none';
44
+ }
45
+ /**
46
+ * A `<table>` element as props. `rowspan` is not carried — a merged cell is not a
47
+ * rectangle, so a table with one arrives with the rows below it short and
48
+ * {@link normalizeTable} pads them at the end. `colspan` is, as the empty cells it
49
+ * covers, so the columns after it keep their index.
50
+ *
51
+ * Only the table's own rows: a `<tr>` inside a nested table belongs to a cell this
52
+ * reader is about to flatten, not to a rank of this one.
53
+ */
54
+ export function tableFromDOM(dom) {
55
+ const rows = [];
56
+ const aligns = [];
57
+ for (const tr of dom.querySelectorAll('tr')) {
58
+ if (tr.closest('table') !== dom)
59
+ continue;
60
+ const cells = [];
61
+ for (const box of tr.children) {
62
+ if (box.tagName !== 'TD' && box.tagName !== 'TH')
63
+ continue;
64
+ if (!rows.length)
65
+ aligns.push(alignFromDOM(box));
66
+ cells.push(cellFromDOM(box));
67
+ const span = Number(box.getAttribute('colspan')) || 1;
68
+ for (let i = 1; i < span; i++) {
69
+ if (!rows.length)
70
+ aligns.push('none');
71
+ cells.push(emptyCell());
72
+ }
73
+ }
74
+ rows.push(cells);
75
+ }
76
+ const [header = [], ...body] = rows;
77
+ return normalizeTable({ header, rows: body, aligns });
78
+ }
79
+ /** The table a paste is carrying, or `undefined` for a paste that holds none. The first
80
+ * one: a clipboard holding a table among other blocks is a document paste, and only a
81
+ * surface that takes nothing but a table (a cell) reads it this way. */
82
+ export function tableFromClipboard(data) {
83
+ const html = data?.getData(HTML);
84
+ if (!html || !/<table[\s>]/i.test(html))
85
+ return undefined;
86
+ const dom = new DOMParser().parseFromString(html, HTML).querySelector('table');
87
+ return dom ? tableFromDOM(dom) : undefined;
88
+ }
89
+ // ── Writing a rectangle out ─────────────────────────────────────────────────
90
+ /** A cell's inline DOM, serialized by the schema that declares its marks, which is what
91
+ * makes {@link cellFromDOM} its exact inverse. */
92
+ function cellToDOM(cell, owner) {
93
+ const doc = decode(cellContent(cell), inlineSchema);
94
+ const line = doc.firstChild;
95
+ return DOMSerializer.fromSchema(inlineSchema).serializeFragment(line ? line.content : Fragment.empty, { document: owner });
96
+ }
97
+ /**
98
+ * A rectangle as a `<table>`: the wire the HTML arm writes and reads. Its first row is
99
+ * a `<th>` row whatever row of the source it came from, that being the row index 0 a
100
+ * paste lands it at.
101
+ */
102
+ export function tableToDOM(block, aligns, owner) {
103
+ const table = owner.createElement('table');
104
+ block.forEach((cells, r) => {
105
+ const tr = owner.createElement('tr');
106
+ cells.forEach((cell, c) => {
107
+ const box = owner.createElement(r === 0 ? 'th' : 'td');
108
+ const align = aligns[c] ?? 'none';
109
+ if (align !== 'none')
110
+ box.style.textAlign = align;
111
+ box.appendChild(cellToDOM(cell, owner));
112
+ tr.appendChild(box);
113
+ });
114
+ table.appendChild(tr);
115
+ });
116
+ return table;
117
+ }
118
+ /** A cell's text as one pipe-table field: the delimiter escaped, since a `|` inside a
119
+ * cell would otherwise open a column that is not there. Marks are dropped, markdown
120
+ * being the lossy arm. */
121
+ const pipeCell = (cell) => cell.text.replaceAll('|', '\\|');
122
+ /**
123
+ * A rectangle as pipe rows, delimiter row included: what a plain-text target reads as a
124
+ * table and what markdown carries. The delimiter follows the first row for the reason
125
+ * that row serializes as `<th>` — a pasted rectangle's row 0 is a header.
126
+ */
127
+ export function tableToPipe(block, aligns) {
128
+ const width = block[0]?.length ?? 0;
129
+ const rule = Array.from({ length: width }, (_, c) => {
130
+ const align = aligns[c] ?? 'none';
131
+ if (align === 'center')
132
+ return ':---:';
133
+ return align === 'left' ? ':---' : align === 'right' ? '---:' : '---';
134
+ });
135
+ const line = (cells) => `| ${cells.join(' | ')} |`;
136
+ const [head = [], ...body] = block;
137
+ return [line(head.map(pipeCell)), line(rule), ...body.map((row) => line(row.map(pipeCell)))].join('\n');
138
+ }
139
+ /** Put a rectangle on the clipboard, both arms. The caller is a `copy` / `cut` handler
140
+ * over a selection its own surface holds, so the rectangle arrives already cut out and
141
+ * claiming the event is the caller's. */
142
+ export function writeRectangle(data, block, aligns, owner) {
143
+ data.setData(HTML, tableToDOM(block, aligns, owner).outerHTML);
144
+ data.setData(PLAIN, tableToPipe(block, aligns));
145
+ }
146
+ // ── The paste door ──────────────────────────────────────────────────────────
147
+ /** The `<table>` rule: a clipboard table becomes the island the model already holds
148
+ * rather than the paragraph its text flattens to. `id` is left empty for
149
+ * {@link stampIslandIds}, a parse rule having no document to mint against. `loss` is
150
+ * authored: what this reader produces is a pipe table, so markdown carries it whole. */
151
+ function tableParseRule() {
152
+ return {
153
+ tag: 'table',
154
+ node: 'island_block',
155
+ getAttrs: (dom) => ({
156
+ id: '',
157
+ islandType: 'table',
158
+ props: tableFromDOM(dom),
159
+ loss: 'lossless'
160
+ })
161
+ };
162
+ }
163
+ /** The clipboard's parser: the schema's own rules with the table rule ahead of them.
164
+ * Ahead, because a `<table>` otherwise matches nothing and its cells' text falls
165
+ * through to the enclosing block. */
166
+ export function tableClipboardParser(schema) {
167
+ return new PMDOMParser(schema, [tableParseRule(), ...PMDOMParser.fromSchema(schema).rules]);
168
+ }
169
+ /** An island node a parse rule produced, which is the only kind carrying no id. */
170
+ const unstamped = (node) => (node.type.name === 'island_block' || node.type.name === 'island_inline') && !node.attrs.id;
171
+ /** Mint an id for every island a paste is carrying. An id is part of the document's
172
+ * canonical bytes and the channel that addresses an island (CODEC §Islands), so a
173
+ * pasted island with none reaches the store as an insert nothing can name afterwards.
174
+ * One minter for the whole slice, or a paste placing two tables hands them one id. */
175
+ export function stampIslandIds(slice, doc) {
176
+ const mint = islandMinter(doc);
177
+ const walk = (fragment) => {
178
+ const out = [];
179
+ fragment.forEach((node) => {
180
+ const next = node.content.size ? node.copy(walk(node.content)) : node;
181
+ out.push(unstamped(next) ? next.type.create({ ...next.attrs, id: mint() }, next.content) : next);
182
+ });
183
+ return Fragment.fromArray(out);
184
+ };
185
+ return new Slice(walk(slice.content), slice.openStart, slice.openEnd);
186
+ }
187
+ /**
188
+ * The block leaf's clipboard plugin: the table rule on the way in, and the id mint over
189
+ * what it produced. A `clipboardParser` rather than the schema's own `parseDOM`,
190
+ * because what a `<table>` in the DOM means is a question only the clipboard asks — the
191
+ * schema's parse rules are also what a re-parse of the editor's own DOM runs through,
192
+ * where an island is already a node and a `<table>` is the chrome drawing one.
193
+ */
194
+ export function tableClipboardPlugin(schema) {
195
+ const parser = tableClipboardParser(schema);
196
+ return new Plugin({
197
+ props: {
198
+ clipboardParser: parser,
199
+ transformPasted: (slice, view) => stampIslandIds(slice, view.state.doc)
200
+ }
201
+ });
202
+ }
@@ -6,7 +6,7 @@ import { type Command } from 'prosemirror-state';
6
6
  *
7
7
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
8
8
  * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
9
- * otherwise splits the ITEM in two, each half holding a code block. Every command
9
+ * otherwise splits the item in two, each half holding a code block. Every command
10
10
  * here declines outside a code block, so the list links keep the keys elsewhere.
11
11
  */
12
12
  export declare function codeKeymap(schema: Schema): Record<string, Command>;
@@ -16,7 +16,7 @@ function codeBlockAt(state) {
16
16
  /**
17
17
  * Start offsets of the lines the range `[f, t)` covers, in document order.
18
18
  *
19
- * A line whose start IS `t` is excluded on a non-empty range: a selection ending at
19
+ * A line whose start is `t` is excluded on a non-empty range: a selection ending at
20
20
  * a line start does not reach into that line, the editor convention (else selecting
21
21
  * whole lines by dragging to the next line's start indents one line too many).
22
22
  */
@@ -44,7 +44,7 @@ function outdentWidth(text, s) {
44
44
  }
45
45
  /**
46
46
  * Tab in a code block: insert one indent unit, or indent every covered line when the
47
- * selection spans a newline. A single-line selection is REPLACED by the unit: Tab
47
+ * selection spans a newline. A single-line selection is replaced by the unit: Tab
48
48
  * types, the way any other key does.
49
49
  */
50
50
  const indentInCode = (state, dispatch) => {
@@ -75,7 +75,7 @@ const indentInCode = (state, dispatch) => {
75
75
  /**
76
76
  * Shift-Tab in a code block: remove one indent level from every covered line.
77
77
  *
78
- * Declines when no covered line carries an indent, so the key is NOT swallowed: it
78
+ * Declines when no covered line carries an indent, so the key is not swallowed: it
79
79
  * falls through to the list link and then out of the leaf, which is the body's
80
80
  * keyboard exit from inside a code block (VISUAL_EDITOR §Chrome).
81
81
  */
@@ -104,7 +104,7 @@ const outdentInCode = (state, dispatch) => {
104
104
  *
105
105
  * `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
106
106
  * cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
107
- * otherwise splits the ITEM in two, each half holding a code block. Every command
107
+ * otherwise splits the item in two, each half holding a code block. Every command
108
108
  * here declines outside a code block, so the list links keep the keys elsewhere.
109
109
  */
110
110
  export function codeKeymap(schema) {
@@ -9,7 +9,7 @@ interface DecodeOpts {
9
9
  plaintext?: boolean;
10
10
  }
11
11
  /**
12
- * A `Content` as READ-ONLY DOM: decode under `schema`, then the nodes' own `toDOM`.
12
+ * A `Content` as read-only DOM: decode under `schema`, then the nodes' own `toDOM`.
13
13
  * The rendering half of the codec's job with no editing attached (no PM view, no
14
14
  * plugins, no `contenteditable`) for chrome that must show content in the same
15
15
  * mark vocabulary a leaf edits it in (the tips card). A second renderer over the
@@ -4,7 +4,7 @@
4
4
  // prefix (`list_item`/`quote` → lists/blockquote), select the block node by
5
5
  // `kind`, apply marks over their `[start,end)` USV ranges (PM splits inline nodes
6
6
  // at mark boundaries), and lower island slots to leaf nodes (inline in a `para`
7
- // line, block on an `island` line). Anchors are NOT applied here; they are
7
+ // line, block on an `island` line). Anchors are not applied here; they are
8
8
  // decorations (field.ts). Positions throughout are USV; `Array.from` iterates by
9
9
  // code point so an astral char is one unit, never a surrogate half.
10
10
  import { DOMSerializer } from 'prosemirror-model';
@@ -24,7 +24,7 @@ export function usvLength(s) {
24
24
  return n;
25
25
  }
26
26
  /**
27
- * A `Content` as READ-ONLY DOM: decode under `schema`, then the nodes' own `toDOM`.
27
+ * A `Content` as read-only DOM: decode under `schema`, then the nodes' own `toDOM`.
28
28
  * The rendering half of the codec's job with no editing attached (no PM view, no
29
29
  * plugins, no `contenteditable`) for chrome that must show content in the same
30
30
  * mark vocabulary a leaf edits it in (the tips card). A second renderer over the
@@ -92,7 +92,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
92
92
  continue;
93
93
  }
94
94
  const here = path[depth];
95
- // `ContentContainer` is an OPEN set: a bare `here.container === 'x'` does not
95
+ // `ContentContainer` is an open set: a bare `here.container === 'x'` does not
96
96
  // narrow, so the two arms this schema builds are claimed by the boundary's
97
97
  // guard and a literal check, and everything else is an unknown the inert
98
98
  // wrapper carries (CODEC §Open sets); no branch degrades silently.
@@ -106,7 +106,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
106
106
  const c = atDepth(leaves[j], depth);
107
107
  if (!c || !isListItemContainer(c) || c.ordered !== ordered || c.start !== start)
108
108
  break;
109
- // An ordinal reset is an ADJACENT SIBLING list (content preserves the
109
+ // An ordinal reset is an adjacent sibling list (content preserves the
110
110
  // shape): merging it here would re-encode `[0,1,0]` as `[0,1,2]`,
111
111
  // breaking the decode round-trip on the first edit.
112
112
  if (c.ordinal < prevOrd)
@@ -131,7 +131,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
131
131
  continue;
132
132
  }
133
133
  // quote and every unknown container share one shape: a wrapper over the run
134
- // of leaves carrying the IDENTICAL container here (identity by `containerKey`,
134
+ // of leaves carrying the identical container here (identity by `containerKey`,
135
135
  // so two adjacent unknowns differing only in `attrs` stay two wrappers).
136
136
  const key = containerKey(here);
137
137
  let j = i + 1;
@@ -165,7 +165,7 @@ function containerKey(c) {
165
165
  return 'attrs' in c ? `${c.container}\u0000${JSON.stringify(c.attrs)}` : c.container;
166
166
  }
167
167
  /** A single leaf block node (para/heading/code/rule/island, or an unknown kind
168
- * carried on a paragraph) from its segments. `kind` is an OPEN set, so the two
168
+ * carried on a paragraph) from its segments. `kind` is an open set, so the two
169
169
  * payload-carrying arms read through the boundary's guards; the payload-free arms
170
170
  * compare literally, and anything left is unknown. */
171
171
  function makeLeaf(schema, leaf, marks, cursor) {
@@ -191,13 +191,13 @@ function makeLeaf(schema, leaf, marks, cursor) {
191
191
  if (isHeadingLine(line)) {
192
192
  return schema.nodes.heading.create({ level: line.level }, inline);
193
193
  }
194
- // An unknown kind renders as a paragraph AND rides on it, so it re-encodes
194
+ // An unknown kind renders as a paragraph and rides on it, so it re-encodes
195
195
  // verbatim rather than flattening to `para` on the field's first edit.
196
196
  const unknown = line.kind === 'para' ? null : { kind: line.kind, attrs: 'attrs' in line ? line.attrs : null };
197
197
  return schema.nodes.paragraph.create({ unknown }, inline);
198
198
  }
199
199
  /** Consume the next island entry as PM node attrs (text-order matched to slots):
200
- * the WHOLE entry, `loss` included, which an island edit has to write back
200
+ * the whole entry, `loss` included, which an island edit has to write back
201
201
  * (`islands.ts`). A slot with no entry behind it is a malformed content; the empty
202
202
  * attrs keep the decode total rather than throwing on a read. */
203
203
  function islandAttrs(cursor) {
@@ -27,7 +27,7 @@ export interface Scan {
27
27
  usvEnd: number;
28
28
  }
29
29
  /**
30
- * Walk a PM document once, producing the content projection AND the position-map
30
+ * Walk a PM document once, producing the content projection and the position-map
31
31
  * runs (they share the traversal so they can never disagree). This is the single
32
32
  * source both `pmToContent` and the position map draw from.
33
33
  */
@@ -40,14 +40,14 @@ interface LowerOpts {
40
40
  id: string;
41
41
  pos: number;
42
42
  }[];
43
- /** Anchor decoration positions after the edit, in FINAL (new) USV coords. */
43
+ /** Anchor decoration positions after the edit, in final (new) USV coords. */
44
44
  newAnchors?: {
45
45
  id: string;
46
46
  pos: number;
47
47
  }[];
48
48
  }
49
49
  /**
50
- * One old→new content edit with the text splice it implies. BOTH sides are content,
50
+ * One old→new content edit with the text splice it implies. Both sides are content,
51
51
  * never a PM doc: the projection of the new doc (`pmToContent`) is the caller's to
52
52
  * hold, because a doc-taking overload re-walks the whole tree, once per keystroke,
53
53
  * for a value the caller has in hand.
@@ -56,7 +56,7 @@ interface LowerOpts {
56
56
  * and the commit path holds the edit across the lowering. `contentEdit` being its
57
57
  * only constructor is what keeps a `delta` from disagreeing with the contents it
58
58
  * was diffed from; `lower` is where a slot inside that splice moves to the island
59
- * channel, so this `delta` is the RAW splice, not always the one committed.
59
+ * channel, so this `delta` is the raw splice, not always the one committed.
60
60
  */
61
61
  export interface ContentEdit {
62
62
  readonly oldRt: Content;
@@ -1,4 +1,5 @@
1
- import { mapPos, isAnchorMark } from '@quillmark/wasm';
1
+ import { isAnchorMark } from '@quillmark/wasm';
2
+ import { core } from '../lifecycle.js';
2
3
  import { codePoints, usvLength } from './decode.js';
3
4
  import { ISLAND_SLOT, islandEntryFromNode } from './islands.js';
4
5
  import { contentDescriptorFromPM, descriptorOf, markKey } from './marks.js';
@@ -9,7 +10,7 @@ function appendText(acc, s) {
9
10
  acc.usvEnd += usvLength(s);
10
11
  }
11
12
  /**
12
- * Walk a PM document once, producing the content projection AND the position-map
13
+ * Walk a PM document once, producing the content projection and the position-map
13
14
  * runs (they share the traversal so they can never disagree). This is the single
14
15
  * source both `pmToContent` and the position map draw from.
15
16
  */
@@ -49,7 +50,7 @@ function scanBlock(acc, node, nodePos, containers) {
49
50
  case 'paragraph':
50
51
  case 'heading': {
51
52
  // One kind value per block: the opening line and its hard-break
52
- // continuations read the SAME object, so they cannot disagree.
53
+ // continuations read the same object, so they cannot disagree.
53
54
  const kind = textblockKind(node);
54
55
  beginLine(acc, contentStart, containers, kind);
55
56
  scanInline(acc, node, contentStart, containers, kind);
@@ -255,7 +256,7 @@ function diffLines(oldRt, newRt) {
255
256
  }
256
257
  /** A line minus its `containers` / `continues` envelope: exactly `setKind`'s
257
258
  * payload, which the boundary names `ContentLineKind`. Lifting it whole is what
258
- * keeps this arm-agnostic: `kind` is an OPEN set, so an arm-by-arm switch would
259
+ * keeps this arm-agnostic: `kind` is an open set, so an arm-by-arm switch would
259
260
  * have to guess at the unknown arm's payload and would drift on every arm
260
261
  * upstream adds. */
261
262
  function kindPart(l) {
@@ -285,14 +286,14 @@ function kindKey(l) {
285
286
  }
286
287
  /**
287
288
  * Split the edit's islands off the text splice. `applyChange` throws
288
- * `IslandSlotInInsert` on a `delta` that types a slot, so a slot NEVER rides the
289
+ * `IslandSlotInInsert` on a `delta` that types a slot, so a slot never rides the
289
290
  * delta: every slot inside the splice's inserted region is stripped from it and
290
291
  * placed by `{ op: 'insert', at }`, which carries the backing entry in the same
291
292
  * op. Every other island kept its slot, so a changed payload is `{ op: 'set' }`,
292
- * addressed by the id both sides carry. A DELETED island needs no op at all: the
293
+ * addressed by the id both sides carry. A deleted island needs no op at all: the
293
294
  * delta that removes its slot drops the entry.
294
295
  *
295
- * `at` is the slot's position in the FINAL text: inserts apply left to right, so
296
+ * `at` is the slot's position in the final text: inserts apply left to right, so
296
297
  * by the time the k-th lands, the k slots before it are back in place.
297
298
  *
298
299
  * The pairing is positional for `insert` and by id for `set`, which is what each
@@ -337,7 +338,7 @@ function slotPositions(text) {
337
338
  }
338
339
  return out;
339
340
  }
340
- /** The splice's inserted region `[start, end)` in FINAL coords. `diffText` emits
341
+ /** The splice's inserted region `[start, end)` in final coords. `diffText` emits
341
342
  * one `[retain?][delete?][insert?][retain?]`, so the leading retain is where the
342
343
  * insert lands and its length is how far it reaches. */
343
344
  function insertedRegion(delta) {
@@ -349,7 +350,7 @@ function insertedRegion(delta) {
349
350
  return { start, end: start + (insert ? usvLength(insert.insert) : 0) };
350
351
  }
351
352
  /** The delta with every island slot removed from its insert, or `undefined` when
352
- * that leaves nothing but retains. Retain/delete arithmetic reads the OLD text,
353
+ * that leaves nothing but retains. Retain/delete arithmetic reads the old text,
353
354
  * so dropping inserted characters leaves the rest of the splice intact. */
354
355
  function withoutSlots(delta) {
355
356
  const ops = [];
@@ -374,7 +375,7 @@ function withoutSlots(delta) {
374
375
  * A post-delta position carried through the bundle's island inserts, which move
375
376
  * text exactly as the delta does and rebase marks by the same rule (start-assoc
376
377
  * `after`, end-assoc `before`; an anchor is a point and holds `before`). Ascending
377
- * `at`s apply left to right, so each comparison reads the RUNNING position.
378
+ * `at`s apply left to right, so each comparison reads the running position.
378
379
  */
379
380
  function shiftPastIslands(pos, insertedAt, assoc) {
380
381
  let p = pos;
@@ -386,7 +387,9 @@ function shiftPastIslands(pos, insertedAt, assoc) {
386
387
  /** Formatting/unknown marks → add/remove ops; anchors → add/removeAnchor by id. */
387
388
  function diffMarks(oldRt, newRt, delta, insertedAt, opts) {
388
389
  const ops = [];
389
- // Rebase old marks through BOTH text-moving channels to final coords, exactly
390
+ // Read the gate once per diff rather than once per mark.
391
+ const { mapPos } = core();
392
+ // Rebase old marks through both text-moving channels to final coords, exactly
390
393
  // as `applyChange` does internally (start assoc `after`, end assoc `before`).
391
394
  const rebase = (pos, assoc) => shiftPastIslands(delta ? mapPos(delta, pos, assoc) : pos, insertedAt, assoc);
392
395
  // Group both sides by descriptor key (excluding anchors).
@@ -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 SlashItem, type SlashState, type SlashStrings } from './slash.js';
7
+ import { type SlashState } from './slash.js';
8
8
  /** Options for {@link createField}. */
9
9
  export interface CreateFieldOpts {
10
10
  doc: Document;
@@ -20,24 +20,22 @@ export interface CreateFieldOpts {
20
20
  plaintext?: boolean;
21
21
  /** Suppress the markdown-shorthand input rules. */
22
22
  noInputRules?: boolean;
23
- /** The island chrome's wording, read LIVE (per render) so a consumer swapping
23
+ /** The island chrome's wording, read live (per render) so a consumer swapping
24
24
  * locale mid-session re-renders rather than freezing it at mount. Absent leaves
25
25
  * the package's English; an inline leaf never asks (it holds no island). */
26
26
  tableStrings?: () => TableChromeStrings;
27
- /** The slash menu's wording, read live for the same reason. */
28
- slashStrings?: () => SlashStrings;
29
27
  /**
30
28
  * The slash menu's live state, or `undefined` when it is closed: the channel the
31
29
  * chrome draws from (`visual/SlashMenu.svelte`).
32
30
  *
33
- * Its PRESENCE is what mounts the menu at all. The trigger and its keys are the
31
+ * Its presence is what mounts the menu at all. The trigger and its keys are the
34
32
  * leaf's, but a surface only a keyboard can reach (claiming Enter, Escape and the
35
33
  * arrows with nothing on screen) is worse than no surface, so the door exists
36
34
  * exactly where something can draw it. A constrained inline leaf never has one:
37
35
  * it holds no island and no block to convert.
38
36
  */
39
37
  onSlash?(state: SlashState | undefined): void;
40
- /** Accessible name → `aria-label` on the `contenteditable`. For a leaf NOTHING
38
+ /** Accessible name → `aria-label` on the `contenteditable`. For a leaf nothing
41
39
  * else names: an array element (the field label plus its 1-based index), the
42
40
  * card body (no visible label at all). A leaf with a field label takes
43
41
  * `labelledBy` instead: `for` cannot reach a `contenteditable`, which is not a
@@ -48,7 +46,7 @@ export interface CreateFieldOpts {
48
46
  /** The parked `description` → `aria-describedby`; announced after the name. */
49
47
  describedBy?: string;
50
48
  /** Ghost text shown on the empty leaf: the resolved `default:` a field ghosts,
51
- * or a body's invitation. The INITIAL value; {@link
49
+ * or a body's invitation. The initial value; {@link
52
50
  * FieldController.setPlaceholder} moves it after mount. Empty/absent
53
51
  * shows no ghost. */
54
52
  placeholder?: string;
@@ -56,7 +54,7 @@ export interface CreateFieldOpts {
56
54
  /** Fired with the new USV caret after an edit or a selection move. */
57
55
  onCaretMove?(addr: Addr, pos: number): void;
58
56
  /**
59
- * Fired after an edit COMMITTED to the document: the leaf's change signal, and
57
+ * Fired after an edit committed to the document: the leaf's change signal, and
60
58
  * the reason it is not `onCaretMove`. That one also fires on a bare selection
61
59
  * move, so a host driving a recompile off it recompiles on every arrow key.
62
60
  *
@@ -74,7 +72,7 @@ export interface FieldController {
74
72
  /** The mounted `container`: the leaf's element, for a caller that has to reach the
75
73
  * DOM the view sits in (the arrival wash, `core/bloom.ts`) rather than re-find it. */
76
74
  readonly el: HTMLElement;
77
- /** Place the caret at USV `pos` (preview onCaretPick → usvToPM → here). */
75
+ /** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
78
76
  setCaret(pos: number): void;
79
77
  /** External content change → re-hydrate this leaf (gated by reconcile). */
80
78
  applyExternal(): void;
@@ -83,7 +81,7 @@ export interface FieldController {
83
81
  * its new kind's wording without remounting, which it must not do (the leaf key
84
82
  * is the card's session id, so a remount would cost the caret).
85
83
  *
86
- * Refreshes the decoration WITHOUT a transaction: a placeholder is chrome, and
84
+ * Refreshes the decoration without a transaction: a placeholder is chrome, and
87
85
  * a transaction would fire `onCaretMove` at a moment the caret did not move.
88
86
  */
89
87
  setPlaceholder(text: string | undefined): void;
@@ -103,14 +101,14 @@ export interface FieldController {
103
101
  /** Ids of the anchors within USV range `[from, to]`: a selection's anchor state. */
104
102
  anchorsInRange(from: number, to: number): string[];
105
103
  /**
106
- * Move the slash menu's cursor onto item `id`: what a POINTER entering an item
107
- * calls. The keyboard cursor and the pointer's highlight are one state, so the
104
+ * Move the slash menu's cursor onto the command `name`: what a pointer entering an
105
+ * item calls. The keyboard cursor and the pointer's highlight are one state, so the
108
106
  * chrome moves the one the keys already drive rather than painting a second.
109
107
  */
110
- slashFocus(id: string): void;
111
- /** Run slash item `id` (a click on one): the same path Enter takes. A no-op with
112
- * no menu open, which is the state a stale click lands in. */
113
- slashPick(id: string): void;
108
+ slashFocus(name: string): void;
109
+ /** Run the command `name` (a click on its item): the same path Enter takes. A no-op
110
+ * with no menu open, which is the state a stale click lands in. */
111
+ slashPick(name: string): void;
114
112
  /** The current PM selection as a USV `{ from, to }` range (the boundary currency). */
115
113
  selectionRange(): {
116
114
  from: number;
@@ -123,9 +121,9 @@ export interface FieldController {
123
121
  * of {@link FieldController}: they are PM internals, and a consumer's contract is the
124
122
  * controller's verbs. The format popover is the one caller and it needs both:
125
123
  * `view` to name the leaf's own document, `focusedView` to act on wherever the caret
126
- * actually is, which inside a table island is a NESTED cell view (`table-view.ts`).
124
+ * actually is, which inside a table island is a nested cell view (`table-view.ts`).
127
125
  * The two being different is exactly what tells the popover to withhold its
128
- * `anchor` button: an anchor is the FIELD's coordinate space, and a cell is not in it.
126
+ * `anchor` button: an anchor is the field's coordinate space, and a cell is not in it.
129
127
  */
130
128
  export interface LeafViews {
131
129
  view: EditorView;
@@ -144,24 +142,21 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
144
142
  * leaf-specific plugins (`afterHistory`: the addressed leaf passes its
145
143
  * anchor-position plugin; a by-value array element passes none), the
146
144
  * markdown-shorthand input rules, then the field keymap over the base keymap, and
147
- * last the two that answer to a caret or a selection BESIDE a block: the gap cursor
145
+ * last the two that answer to a caret or a selection beside a block: the gap cursor
148
146
  * and {@link pastAtomPlugin}.
149
147
  *
150
148
  * A `plaintext` field carries no marks (decode strips them, the keymap suppresses
151
149
  * Mod-b/i/u), so it also skips the input rules: `inlineSchema` still declares the
152
- * mark types, so a `**bold**` rule would apply a strong mark AND eat the literal
153
- * delimiters. The inline schema has no block nodes, so those rules are the ONLY
150
+ * mark types, so a `**bold**` rule would apply a strong mark and eat the literal
151
+ * delimiters. The inline schema has no block nodes, so those rules are the only
154
152
  * ones it would add: skip all.
155
153
  */
156
154
  export declare function proseLeafPlugins(schema: Schema, opts: {
157
155
  inline: boolean;
158
156
  plaintext: boolean;
159
- /** The slash menu's model and its report channel; absent mounts no menu and
160
- * leaves Enter/Escape/the arrows to the links below (`keymap.ts`). */
161
- slash?: {
162
- items: () => SlashItem[];
163
- onState: (state: SlashState | undefined) => void;
164
- };
157
+ /** The slash menu's report channel; absent mounts no menu and leaves
158
+ * Enter/Escape/the arrows to the links below (`keymap.ts`). */
159
+ slash?: (state: SlashState | undefined) => void;
165
160
  noInputRules?: boolean;
166
161
  /** Read live, not captured: the ghost can move after mount
167
162
  * ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this