@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.
- package/NOTICE +4 -2
- package/dist/core/codec/blocks.d.ts +11 -2
- package/dist/core/codec/blocks.js +45 -14
- package/dist/core/codec/breaks.d.ts +3 -2
- package/dist/core/codec/breaks.js +3 -2
- package/dist/core/codec/decode.d.ts +7 -3
- package/dist/core/codec/decode.js +34 -8
- package/dist/core/codec/field.d.ts +11 -5
- package/dist/core/codec/field.js +11 -8
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +1 -1
- package/dist/core/codec/islands.d.ts +3 -1
- package/dist/core/codec/islands.js +8 -5
- package/dist/core/codec/markdown-paste.d.ts +9 -0
- package/dist/core/codec/markdown-paste.js +49 -0
- package/dist/core/codec/prose.css +109 -1
- package/dist/core/codec/schema.d.ts +8 -2
- package/dist/core/codec/schema.js +16 -5
- package/dist/core/codec/slash.d.ts +9 -6
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-paste.d.ts +7 -0
- package/dist/core/codec/table-paste.js +132 -0
- package/dist/core/codec/table-view.d.ts +12 -0
- package/dist/core/codec/table-view.js +401 -57
- package/dist/core/codec/table.d.ts +38 -16
- package/dist/core/codec/table.js +106 -35
- package/dist/core/theme.css +1 -1
- package/dist/visual/ArrayField.svelte +4 -0
- package/dist/visual/Field.svelte +8 -2
- package/dist/visual/Field.svelte.d.ts +1 -1
- package/dist/visual/FieldHint.svelte +62 -10
- package/dist/visual/FormatPopover.svelte +27 -31
- package/dist/visual/FormatPopover.svelte.d.ts +2 -2
- package/dist/visual/MatrixAdd.svelte +13 -132
- package/dist/visual/MatrixAdd.svelte.d.ts +2 -5
- package/dist/visual/MatrixField.svelte +5 -63
- package/dist/visual/MatrixField.svelte.d.ts +0 -3
- package/dist/visual/ObjectField.svelte +7 -3
- package/dist/visual/ObjectField.svelte.d.ts +4 -2
- package/dist/visual/ProseField.svelte +2 -1
- package/dist/visual/ProseValue.svelte +113 -40
- package/dist/visual/ProseValue.svelte.d.ts +8 -4
- package/dist/visual/VisualEditorInner.svelte +22 -22
- package/dist/visual/VisualEditorInner.svelte.d.ts +2 -2
- package/dist/visual/checklists.d.ts +0 -79
- package/dist/visual/checklists.js +0 -164
- package/dist/visual/guidance.d.ts +15 -0
- package/dist/visual/guidance.js +37 -0
- package/dist/visual/leaves.d.ts +27 -1
- package/dist/visual/leaves.js +46 -0
- package/dist/visual/strings.d.ts +3 -8
- package/dist/visual/strings.js +0 -3
- package/dist/visual/structure.js +1 -1
- package/dist/visual/tips.js +1 -1
- package/package.json +2 -2
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
// single-textblock form for `richtext(inline)` (one paragraph, no block split, no
|
|
7
7
|
// containers, no islands) and `plaintextSchema` is that one without marks, for
|
|
8
8
|
// `plaintext(inline)`; `plainSchema` is a `plaintext` field without `inline`,
|
|
9
|
-
// paragraphs and hard breaks and nothing else
|
|
9
|
+
// paragraphs and hard breaks and nothing else; `cellSchema` is the inline form with
|
|
10
|
+
// hard breaks, for a table cell. Same decode/lower/position machinery,
|
|
10
11
|
// narrower shapes. Anchors are not marks here (decorations).
|
|
11
12
|
//
|
|
12
13
|
// `toDOM` and `parseDOM` are one tier, not two halves of a rendering: a copy and a
|
|
13
14
|
// paste inside one body run the whole document through them (CODEC §"Markdown at the
|
|
14
15
|
// edges"), so every attribute written here is read back here. Foreign HTML spells none
|
|
15
16
|
// of this package's `data-qm-*` names, so what a paste takes off the web is unchanged;
|
|
16
|
-
// the one rule that widens that door on purpose is the fence's
|
|
17
|
+
// the one rule here that widens that door on purpose is the fence's, a `<table>` being
|
|
18
|
+
// the block leaf's clipboard parser's (`table-paste.ts`).
|
|
17
19
|
import { Schema } from 'prosemirror-model';
|
|
18
20
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
19
21
|
import { rendersHref, storableUrl } from './urls.js';
|
|
@@ -176,7 +178,7 @@ export const blockSchema = new Schema({
|
|
|
176
178
|
nodes: { ...blockNodes, ...inlineLeafNodes },
|
|
177
179
|
marks
|
|
178
180
|
});
|
|
179
|
-
// ── The constrained inline nodes (
|
|
181
|
+
// ── The constrained inline nodes (the inline schemas and the cell's) ────
|
|
180
182
|
// `doc: "paragraph"` (exactly one child) is what makes an Enter a no-op at the
|
|
181
183
|
// model level: there is no second block for a split to land.
|
|
182
184
|
const inlineNodes = {
|
|
@@ -185,8 +187,7 @@ const inlineNodes = {
|
|
|
185
187
|
text: { group: 'inline' }
|
|
186
188
|
};
|
|
187
189
|
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
188
|
-
* containers, no islands, and the full mark set (a `richtext(inline)` field
|
|
189
|
-
* a table cell, which is the same content unit). */
|
|
190
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field). */
|
|
190
191
|
export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
191
192
|
/**
|
|
192
193
|
* The inline schema with no mark types at all: a `plaintext(inline)` field, whose
|
|
@@ -198,6 +199,16 @@ export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
|
198
199
|
* carrying (CODEC §Inline mode).
|
|
199
200
|
*/
|
|
200
201
|
export const plaintextSchema = new Schema({ nodes: inlineNodes, marks: {} });
|
|
202
|
+
/**
|
|
203
|
+
* A table cell: the inline schema plus `hard_break`, which a `TableCell` stores as a
|
|
204
|
+
* `\n` in its `text`. Still one paragraph, so Enter has no block to split; a break is
|
|
205
|
+
* Shift-Enter's (`breaks.ts`). Declaring the break routes `decode` through the plain
|
|
206
|
+
* path, which folds a cell's `continues` lines into that one paragraph.
|
|
207
|
+
*/
|
|
208
|
+
export const cellSchema = new Schema({
|
|
209
|
+
nodes: { ...inlineNodes, hard_break: inlineLeafNodes.hard_break },
|
|
210
|
+
marks
|
|
211
|
+
});
|
|
201
212
|
/**
|
|
202
213
|
* A `plaintext` field without `inline`: paragraphs and hard breaks, no marks, no
|
|
203
214
|
* islands, no containers, which is the shape `Content::is_plain` admits. Mark-free
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EditorState, Plugin, type Command } from 'prosemirror-state';
|
|
2
|
-
import type
|
|
2
|
+
import { type EditorView } from 'prosemirror-view';
|
|
3
3
|
import { type RangeAnchor } from './anchor.js';
|
|
4
4
|
/** The menu as the chrome sees it: the command names the query matched, the keyboard's
|
|
5
5
|
* cursor, and the trigger's anchor to hang off. `undefined` is a closed menu.
|
|
@@ -16,10 +16,13 @@ export interface SlashState {
|
|
|
16
16
|
* open menu outlives any one scroll position of the leaf. */
|
|
17
17
|
anchor: RangeAnchor;
|
|
18
18
|
}
|
|
19
|
-
/** The menu's wording, which is its accessible name and
|
|
20
|
-
* command names, and a name is not translated. */
|
|
19
|
+
/** The menu's wording, which is its accessible name and the empty line's hint: the
|
|
20
|
+
* offers are command names, and a name is not translated. */
|
|
21
21
|
export interface SlashStrings {
|
|
22
22
|
slashLabel: string;
|
|
23
|
+
/** Drawn on the empty line a focused caret holds, naming the key that opens the
|
|
24
|
+
* menu there. */
|
|
25
|
+
slashHint: string;
|
|
23
26
|
}
|
|
24
27
|
/** The package's English, beside the surface that draws it: the same rule the island
|
|
25
28
|
* chrome's wording follows, and the visual `strings` set extends both. */
|
|
@@ -28,8 +31,8 @@ export declare const DEFAULT_SLASH_STRINGS: SlashStrings;
|
|
|
28
31
|
* The vocabulary a caret can run, in menu order: the names `SLASH_COMMANDS` is keyed
|
|
29
32
|
* by, each asked whether it would fire here. A row that declines when picked is worse
|
|
30
33
|
* than an absent one, and asking the command rather than restating its guards is what
|
|
31
|
-
* keeps the menu from drifting off what
|
|
32
|
-
*
|
|
34
|
+
* keeps the menu from drifting off what a pick does. So the offers narrow as the caret
|
|
35
|
+
* moves: the lists and the island alone mid-paragraph, no list at an item's head.
|
|
33
36
|
*
|
|
34
37
|
* `state` is the state a pick leaves behind — the run already consumed — since that is
|
|
35
38
|
* the caret every command answers for (`blocks.ts`).
|
|
@@ -54,7 +57,7 @@ interface SlashRun {
|
|
|
54
57
|
* below and close the menu without a listener of their own. A dismissal edits no
|
|
55
58
|
* text; only a pick does, and it consumes exactly the run.
|
|
56
59
|
*/
|
|
57
|
-
export declare function slashPlugin(onState: (state: SlashState | undefined) => void): Plugin<SlashRun | null>;
|
|
60
|
+
export declare function slashPlugin(onState: (state: SlashState | undefined) => void, hint?: () => string | undefined): Plugin<SlashRun | null>;
|
|
58
61
|
/**
|
|
59
62
|
* The menu's keys, the innermost link of the leaf's chain (`keymap.ts`). Every one
|
|
60
63
|
* declines on an empty offer list rather than on a dead run (§`SlashState`): a key an
|
package/dist/core/codec/slash.js
CHANGED
|
Binary file
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
/**
|
|
3
|
+
* A block leaf's table paste: the clipboard's parse for HTML, and for text that is
|
|
4
|
+
* tab-separated. A paste as plain text keeps the text, tabs and all. A cell's own view
|
|
5
|
+
* mounts none of this, so a paste inside a cell is inline.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tablePastePlugin(): Plugin;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// A table on the clipboard lands as a table island (CODEC §"Markdown at the edges"). Two
|
|
2
|
+
// spellings carry one: an HTML `<table>`, and the tab-separated text a spreadsheet writes
|
|
3
|
+
// beside it, which ProseMirror hands over only where the clipboard holds no HTML. Either
|
|
4
|
+
// builds its props through `normalizeTable` and arrives with no id, which the paste's
|
|
5
|
+
// island pass mints (`islands.ts`). Nothing the source styled a table with has a reader
|
|
6
|
+
// here, so a pasted table takes the document's face.
|
|
7
|
+
import { DOMParser, Fragment, Slice } from 'prosemirror-model';
|
|
8
|
+
import { Plugin } from 'prosemirror-state';
|
|
9
|
+
import { usvLength } from './decode.js';
|
|
10
|
+
import { pmToContent } from './encode.js';
|
|
11
|
+
import { blockSchema } from './schema.js';
|
|
12
|
+
import { emptyCell, normalizeTable } from './table.js';
|
|
13
|
+
const schemaRules = DOMParser.fromSchema(blockSchema).rules;
|
|
14
|
+
/** What a cell's markup is read with: the schema's rules less the islands', so an island
|
|
15
|
+
* element inside a cell is the text it holds. */
|
|
16
|
+
const cellParser = new DOMParser(blockSchema, schemaRules.filter((rule) => !('node' in rule && rule.node?.startsWith('island_'))));
|
|
17
|
+
/** A `<td>` or `<th>` as a cell: its blocks are lines and a `<br>` a break, each one `\n`
|
|
18
|
+
* in the text (`cellContent`), and its marks the ones the schema's rules read. Whitespace
|
|
19
|
+
* at the cell's edges is dropped, which is where a source's empty paragraph lands. */
|
|
20
|
+
function cellFromDOM(el) {
|
|
21
|
+
const { text, marks } = pmToContent(cellParser.parse(el));
|
|
22
|
+
// Every whitespace character is in the BMP, so `lead` counts USVs as well.
|
|
23
|
+
const lead = text.length - text.trimStart().length;
|
|
24
|
+
const kept = text.trim();
|
|
25
|
+
const end = usvLength(kept);
|
|
26
|
+
return {
|
|
27
|
+
text: kept,
|
|
28
|
+
marks: marks.flatMap((m) => {
|
|
29
|
+
const start = Math.max(0, m.start - lead);
|
|
30
|
+
const stop = Math.min(end, m.end - lead);
|
|
31
|
+
return start < stop ? [{ ...m, start, end: stop }] : [];
|
|
32
|
+
})
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A `<table>` as table props, or `null` where it holds no cell. Rows run in the order the
|
|
37
|
+
* DOM's `rows` gives them, a `<thead>`'s first, and the first is the header. A merged
|
|
38
|
+
* cell splits: its content in the first slot it covers and an empty cell in each other,
|
|
39
|
+
* so the cells beside and under it keep their columns.
|
|
40
|
+
*/
|
|
41
|
+
function tableFromDOM(table) {
|
|
42
|
+
const rows = Array.from(table.rows);
|
|
43
|
+
const grid = rows.map(() => []);
|
|
44
|
+
rows.forEach((tr, r) => {
|
|
45
|
+
let c = 0;
|
|
46
|
+
for (const td of Array.from(tr.cells)) {
|
|
47
|
+
while (grid[r][c])
|
|
48
|
+
c++;
|
|
49
|
+
const across = Math.max(1, td.colSpan);
|
|
50
|
+
const down = Math.min(td.rowSpan || rows.length, rows.length - r);
|
|
51
|
+
for (let dr = 0; dr < down; dr++)
|
|
52
|
+
for (let dc = 0; dc < across; dc++)
|
|
53
|
+
grid[r + dr][c + dc] = dr || dc ? emptyCell() : cellFromDOM(td);
|
|
54
|
+
c += across;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!grid.some((row) => row.length))
|
|
58
|
+
return null;
|
|
59
|
+
// A row a merge reached into past its own cells has holes.
|
|
60
|
+
const [header = [], ...body] = grid.map((row) => Array.from(row, (cell) => cell ?? emptyCell()));
|
|
61
|
+
return normalizeTable({ header, rows: body, aligns: [] });
|
|
62
|
+
}
|
|
63
|
+
const QUOTED = /"((?:[^"]|"")*)"(?=[\t\r\n]|$)/y;
|
|
64
|
+
const BARE = /[^\t\r\n]*/y;
|
|
65
|
+
/**
|
|
66
|
+
* Tab-separated text as table props, the first line the header, or `null` unless every
|
|
67
|
+
* line holds a tab and some cell holds more than whitespace. A cell in double quotes is
|
|
68
|
+
* one cell whose tabs, line ends and doubled quotes are its text, which is how Excel and
|
|
69
|
+
* Sheets write a cell holding any of the three; a quote not closed that way is literal.
|
|
70
|
+
* The line ends closing the text are the spreadsheet's terminator, not an empty row.
|
|
71
|
+
*/
|
|
72
|
+
function tableFromText(text) {
|
|
73
|
+
const body = text.replace(/[\r\n]+$/, '');
|
|
74
|
+
const lines = [[]];
|
|
75
|
+
let at = 0;
|
|
76
|
+
for (;;) {
|
|
77
|
+
QUOTED.lastIndex = BARE.lastIndex = at;
|
|
78
|
+
const quoted = QUOTED.exec(body);
|
|
79
|
+
const cell = quoted
|
|
80
|
+
? quoted[1].replace(/""/g, '"').replace(/\r\n?/g, '\n')
|
|
81
|
+
: BARE.exec(body)[0];
|
|
82
|
+
lines.at(-1).push(cell);
|
|
83
|
+
at = quoted ? QUOTED.lastIndex : BARE.lastIndex;
|
|
84
|
+
if (at >= body.length)
|
|
85
|
+
break;
|
|
86
|
+
if (body[at] === '\t')
|
|
87
|
+
at += 1;
|
|
88
|
+
else {
|
|
89
|
+
at += body.startsWith('\r\n', at) ? 2 : 1;
|
|
90
|
+
lines.push([]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!lines.every((cells) => cells.length > 1))
|
|
94
|
+
return null;
|
|
95
|
+
if (!lines.some((cells) => cells.some((cell) => cell.trim())))
|
|
96
|
+
return null;
|
|
97
|
+
const [header = [], ...rows] = lines.map((cells) => cells.map((t) => ({ text: t, marks: [] })));
|
|
98
|
+
return normalizeTable({ header, rows, aligns: [] });
|
|
99
|
+
}
|
|
100
|
+
/** The schema's parse with a `<table>` read as an island rather than as its cells' text. */
|
|
101
|
+
const clipboardParser = new DOMParser(blockSchema, [
|
|
102
|
+
{
|
|
103
|
+
tag: 'table',
|
|
104
|
+
node: 'island_block',
|
|
105
|
+
getAttrs: (el) => {
|
|
106
|
+
const props = tableFromDOM(el);
|
|
107
|
+
return props ? { islandType: 'table', props } : false;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
...schemaRules
|
|
111
|
+
]);
|
|
112
|
+
/**
|
|
113
|
+
* A block leaf's table paste: the clipboard's parse for HTML, and for text that is
|
|
114
|
+
* tab-separated. A paste as plain text keeps the text, tabs and all. A cell's own view
|
|
115
|
+
* mounts none of this, so a paste inside a cell is inline.
|
|
116
|
+
*/
|
|
117
|
+
export function tablePastePlugin() {
|
|
118
|
+
return new Plugin({
|
|
119
|
+
props: {
|
|
120
|
+
clipboardParser,
|
|
121
|
+
// A falsy return declines, handing the text to ProseMirror's own split into
|
|
122
|
+
// paragraphs; the prop's type has no spelling for it.
|
|
123
|
+
clipboardTextParser: ((text, _context, plain) => {
|
|
124
|
+
const props = plain ? null : tableFromText(text);
|
|
125
|
+
if (!props)
|
|
126
|
+
return null;
|
|
127
|
+
const island = blockSchema.nodes.island_block.create({ islandType: 'table', props });
|
|
128
|
+
return new Slice(Fragment.from(island), 0, 0);
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -21,6 +21,18 @@ 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. */
|
|
26
|
+
tableAlignLeft: string;
|
|
27
|
+
tableAlignCenter: string;
|
|
28
|
+
tableAlignRight: string;
|
|
29
|
+
/** The held first row's toggle, pressed while the row draws as the header. */
|
|
30
|
+
tableHeaderToggle: string;
|
|
31
|
+
tableAlignTop: string;
|
|
32
|
+
tableAlignMiddle: string;
|
|
33
|
+
tableAlignBottom: string;
|
|
34
|
+
/** The held cells' cluster's own name. */
|
|
35
|
+
tableCellAlignment: string;
|
|
24
36
|
}
|
|
25
37
|
/**
|
|
26
38
|
* The package's English for the island chrome. It lives here, beside the surface
|