@quillmark/svelte 0.0.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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +241 -0
- package/THEMING.md +153 -0
- package/dist/core/address.d.ts +56 -0
- package/dist/core/address.js +95 -0
- package/dist/core/bloom.d.ts +37 -0
- package/dist/core/bloom.js +116 -0
- package/dist/core/codec/anchor.d.ts +19 -0
- package/dist/core/codec/anchor.js +36 -0
- package/dist/core/codec/code.d.ts +12 -0
- package/dist/core/codec/code.js +118 -0
- package/dist/core/codec/decode.d.ts +24 -0
- package/dist/core/codec/decode.js +270 -0
- package/dist/core/codec/encode.d.ts +71 -0
- package/dist/core/codec/encode.js +480 -0
- package/dist/core/codec/field.d.ts +171 -0
- package/dist/core/codec/field.js +493 -0
- package/dist/core/codec/index.d.ts +22 -0
- package/dist/core/codec/index.js +43 -0
- package/dist/core/codec/inputrules.d.ts +7 -0
- package/dist/core/codec/inputrules.js +168 -0
- package/dist/core/codec/islands.d.ts +36 -0
- package/dist/core/codec/islands.js +82 -0
- package/dist/core/codec/keymap.d.ts +13 -0
- package/dist/core/codec/keymap.js +29 -0
- package/dist/core/codec/lists.d.ts +22 -0
- package/dist/core/codec/lists.js +104 -0
- package/dist/core/codec/marks.d.ts +37 -0
- package/dist/core/codec/marks.js +74 -0
- package/dist/core/codec/positions.d.ts +17 -0
- package/dist/core/codec/positions.js +108 -0
- package/dist/core/codec/prose.css +458 -0
- package/dist/core/codec/reconcile.d.ts +26 -0
- package/dist/core/codec/reconcile.js +64 -0
- package/dist/core/codec/schema.d.ts +13 -0
- package/dist/core/codec/schema.js +155 -0
- package/dist/core/codec/slash.d.ts +79 -0
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +50 -0
- package/dist/core/codec/table-view.js +928 -0
- package/dist/core/codec/table.d.ts +85 -0
- package/dist/core/codec/table.js +224 -0
- package/dist/core/errors.d.ts +73 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lifecycle.d.ts +7 -0
- package/dist/core/lifecycle.js +21 -0
- package/dist/core/rebind.svelte.d.ts +18 -0
- package/dist/core/rebind.svelte.js +44 -0
- package/dist/core/teardown.d.ts +20 -0
- package/dist/core/teardown.js +37 -0
- package/dist/core/theme.css +276 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9 -0
- package/dist/preset/index.css +10 -0
- package/dist/preset/recipes.css +188 -0
- package/dist/preset/scale.css +97 -0
- package/dist/preview/Preview.svelte +118 -0
- package/dist/preview/Preview.svelte.d.ts +39 -0
- package/dist/preview/bridge.d.ts +13 -0
- package/dist/preview/bridge.js +88 -0
- package/dist/preview/controller.d.ts +40 -0
- package/dist/preview/controller.js +141 -0
- package/dist/preview/geometry.d.ts +37 -0
- package/dist/preview/geometry.js +45 -0
- package/dist/preview/index.d.ts +5 -0
- package/dist/preview/index.js +14 -0
- package/dist/preview/overlay.d.ts +10 -0
- package/dist/preview/overlay.js +82 -0
- package/dist/preview/paint.d.ts +17 -0
- package/dist/preview/paint.js +262 -0
- package/dist/preview/strings.d.ts +14 -0
- package/dist/preview/strings.js +22 -0
- package/dist/visual/ArrayField.svelte +362 -0
- package/dist/visual/ArrayField.svelte.d.ts +24 -0
- package/dist/visual/BooleanField.svelte +88 -0
- package/dist/visual/BooleanField.svelte.d.ts +20 -0
- package/dist/visual/Card.svelte +849 -0
- package/dist/visual/Card.svelte.d.ts +43 -0
- package/dist/visual/CardControls.svelte +92 -0
- package/dist/visual/CardControls.svelte.d.ts +11 -0
- package/dist/visual/DateField.svelte +203 -0
- package/dist/visual/DateField.svelte.d.ts +28 -0
- package/dist/visual/DiagnosticList.svelte +50 -0
- package/dist/visual/DiagnosticList.svelte.d.ts +7 -0
- package/dist/visual/EnumField.svelte +169 -0
- package/dist/visual/EnumField.svelte.d.ts +25 -0
- package/dist/visual/Field.svelte +270 -0
- package/dist/visual/Field.svelte.d.ts +37 -0
- package/dist/visual/FieldHint.svelte +185 -0
- package/dist/visual/FieldHint.svelte.d.ts +13 -0
- package/dist/visual/FieldLabel.svelte +141 -0
- package/dist/visual/FieldLabel.svelte.d.ts +19 -0
- package/dist/visual/FormatPopover.svelte +396 -0
- package/dist/visual/FormatPopover.svelte.d.ts +9 -0
- package/dist/visual/NumberField.svelte +72 -0
- package/dist/visual/NumberField.svelte.d.ts +19 -0
- package/dist/visual/ObjectField.svelte +131 -0
- package/dist/visual/ObjectField.svelte.d.ts +17 -0
- package/dist/visual/ProseArrayElement.svelte +98 -0
- package/dist/visual/ProseArrayElement.svelte.d.ts +19 -0
- package/dist/visual/ProseField.svelte +213 -0
- package/dist/visual/ProseField.svelte.d.ts +50 -0
- package/dist/visual/SlashMenu.svelte +126 -0
- package/dist/visual/SlashMenu.svelte.d.ts +14 -0
- package/dist/visual/TextField.svelte +63 -0
- package/dist/visual/TextField.svelte.d.ts +23 -0
- package/dist/visual/TipsCard.svelte +191 -0
- package/dist/visual/TipsCard.svelte.d.ts +13 -0
- package/dist/visual/VisualEditor.svelte +96 -0
- package/dist/visual/VisualEditor.svelte.d.ts +14 -0
- package/dist/visual/VisualEditorInner.svelte +1032 -0
- package/dist/visual/VisualEditorInner.svelte.d.ts +17 -0
- package/dist/visual/controls.css +272 -0
- package/dist/visual/diagnostics.d.ts +52 -0
- package/dist/visual/diagnostics.js +83 -0
- package/dist/visual/domid.d.ts +21 -0
- package/dist/visual/domid.js +36 -0
- package/dist/visual/ext.d.ts +19 -0
- package/dist/visual/ext.js +29 -0
- package/dist/visual/icons/Icon.svelte +39 -0
- package/dist/visual/icons/Icon.svelte.d.ts +12 -0
- package/dist/visual/icons/nodes.d.ts +103 -0
- package/dist/visual/icons/nodes.js +55 -0
- package/dist/visual/index.d.ts +9 -0
- package/dist/visual/index.js +24 -0
- package/dist/visual/motion.d.ts +18 -0
- package/dist/visual/motion.js +53 -0
- package/dist/visual/props.d.ts +77 -0
- package/dist/visual/props.js +1 -0
- package/dist/visual/signals.d.ts +59 -0
- package/dist/visual/signals.js +1 -0
- package/dist/visual/strings.d.ts +123 -0
- package/dist/visual/strings.js +80 -0
- package/dist/visual/structure.d.ts +224 -0
- package/dist/visual/structure.js +270 -0
- package/dist/visual/synced.svelte.d.ts +15 -0
- package/dist/visual/synced.svelte.js +34 -0
- package/dist/visual/tips.d.ts +24 -0
- package/dist/visual/tips.js +50 -0
- package/package.json +102 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Content, TableCell, TableProps } from '@quillmark/wasm';
|
|
2
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
3
|
+
/** One column's alignment, read off the boundary rather than restated. */
|
|
4
|
+
export type TableAlign = TableProps['aligns'][number];
|
|
5
|
+
/** The alignments a column can take: the four the content declares, `none` first
|
|
6
|
+
* because it is what a column arrives at and what a set is cleared back to. */
|
|
7
|
+
export declare const ALIGNS: readonly TableAlign[];
|
|
8
|
+
/** The zero cell: empty text, no marks. */
|
|
9
|
+
export declare function emptyCell(): TableCell;
|
|
10
|
+
/**
|
|
11
|
+
* The row index space the chrome and the ops share: **0 is the header**, 1…n the
|
|
12
|
+
* body rows. A table always has a header (`header` is a separate field, and a
|
|
13
|
+
* header-less table is not expressible), so the header is a row that never
|
|
14
|
+
* deletes rather than a toggle.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rowCount(props: TableProps): number;
|
|
17
|
+
/** The cells of row `r` in that space. */
|
|
18
|
+
export declare function rowCells(props: TableProps, r: number): TableCell[];
|
|
19
|
+
/** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
|
|
20
|
+
export declare function cellAt(props: TableProps, r: number, c: number): TableCell;
|
|
21
|
+
/** The table's column count: the one number `header`, every row and `aligns` share. */
|
|
22
|
+
export declare function columnCount(props: TableProps): number;
|
|
23
|
+
/**
|
|
24
|
+
* The rectangularizing constructor: one column count across `header`, every row and
|
|
25
|
+
* `aligns`, padding with empty cells and `none`. The column count is the widest
|
|
26
|
+
* thing present, so a caller that appended to one axis alone still gets a rectangle
|
|
27
|
+
* back, and a table is never narrower than one column.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeTable(props: TableProps): TableProps;
|
|
30
|
+
/** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
|
|
31
|
+
* Growth is cheap (Tab at the last cell appends a row), so the default is small. */
|
|
32
|
+
export declare function newTable(cols?: number, bodyRows?: number): TableProps;
|
|
33
|
+
/** `props` with the cell at `(r, c)` replaced. */
|
|
34
|
+
export declare function withCell(props: TableProps, r: number, c: number, cell: TableCell): TableProps;
|
|
35
|
+
/** A new empty body row below row `r` (row 0, the header, opens the first body row). */
|
|
36
|
+
export declare function insertRow(props: TableProps, r: number): TableProps;
|
|
37
|
+
/** Drop body row `r`. The header (`r === 0`) has no delete: `header: []` is not a
|
|
38
|
+
* table, so the asymmetry is the model's rather than a guard's. */
|
|
39
|
+
export declare function deleteRow(props: TableProps, r: number): TableProps;
|
|
40
|
+
/** A new empty column after column `c`. */
|
|
41
|
+
export declare function insertColumn(props: TableProps, c: number): TableProps;
|
|
42
|
+
/** Drop column `c`; a no-op at one column (a table has at least one). */
|
|
43
|
+
export declare function deleteColumn(props: TableProps, c: number): TableProps;
|
|
44
|
+
/** Body row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a
|
|
45
|
+
* no-op rather than an error, since the drop target is geometry and geometry runs
|
|
46
|
+
* past the last row. The header is refused because it has no destination: it is a
|
|
47
|
+
* separate field, not row 0 of one array. */
|
|
48
|
+
export declare function moveRow(props: TableProps, r: number, by: number): TableProps;
|
|
49
|
+
/** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
|
|
50
|
+
* alignment is the column's property, so a move that left it behind would retint the
|
|
51
|
+
* column that took the index. */
|
|
52
|
+
export declare function moveColumn(props: TableProps, c: number, by: number): TableProps;
|
|
53
|
+
/** Column `c` with every cell emptied, alignment kept: what a delete gesture means at
|
|
54
|
+
* the LAST column, which the model keeps. There is no row analogue: the header is the
|
|
55
|
+
* only row a delete is refused for, and it carries no handle to select it from. */
|
|
56
|
+
export declare function clearColumn(props: TableProps, c: number): TableProps;
|
|
57
|
+
/** Set column `c`'s alignment: the one table capability the content round-trips
|
|
58
|
+
* today and nothing in the editor could reach. */
|
|
59
|
+
export declare function setAlign(props: TableProps, c: number, align: TableAlign): TableProps;
|
|
60
|
+
/**
|
|
61
|
+
* A cell as a one-line `Content`: what `decode` takes under the inline schema. The
|
|
62
|
+
* cell's marks ARE that content's marks, because both are offsets into the same
|
|
63
|
+
* text: the cell-local coordinate space is a `Content`'s coordinate space with
|
|
64
|
+
* one line in it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function cellContent(cell: TableCell): Content;
|
|
67
|
+
/**
|
|
68
|
+
* The cell a nested PM doc projects, carrying `prior`'s identity anchors through.
|
|
69
|
+
*
|
|
70
|
+
* Anchors inside a cell are **preserved verbatim, never minted**: they are not in
|
|
71
|
+
* the field plugin's coordinate space (the field's position map holds one `atom`
|
|
72
|
+
* run for the whole table), so nothing here can mint one and the popover withholds
|
|
73
|
+
* its `anchor` button while the caret is in a cell. Preserving them still means
|
|
74
|
+
* rebasing: a cell edit is a splice like any other, so each held anchor maps through
|
|
75
|
+
* the cell's own text delta by the rule `applyChange` uses on a field's
|
|
76
|
+
* (start-assoc `after`), and a mark the splice deleted through lands where the
|
|
77
|
+
* splice left it rather than pointing past the text.
|
|
78
|
+
*/
|
|
79
|
+
export declare function cellFromDoc(doc: PMNode, prior: TableCell): TableCell;
|
|
80
|
+
/** Whether two cells are the same value: what tells an own edit (the projection the
|
|
81
|
+
* view just produced) from an external one (an undo, a re-hydrate) without a flag. */
|
|
82
|
+
export declare function cellEqual(a: TableCell, b: TableCell): boolean;
|
|
83
|
+
/** Whether two tables have the same rectangle and alignment: the change that forces
|
|
84
|
+
* a rebuild rather than a per-cell reseed. */
|
|
85
|
+
export declare function shapeEqual(a: TableProps, b: TableProps): boolean;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// The table island's model: the cell codec (a `TableCell` ↔ one inline-schema PM
|
|
2
|
+
// doc) and the rectangularizing constructors every row and column op goes through
|
|
3
|
+
// (CODEC §Islands, §"The table island").
|
|
4
|
+
//
|
|
5
|
+
// `TableProps` normalizes to ONE column count shared by `header`, every row, and
|
|
6
|
+
// `aligns`, so a ragged table is not a state the content can hold. The editor keeps
|
|
7
|
+
// its optimistic PM and re-hydrates only on an EXTERNAL change (CODEC
|
|
8
|
+
// §Reconciliation), so an op that emitted a ragged table would leave the store
|
|
9
|
+
// rectangularized and PM ragged with no error channel and no repair. Every
|
|
10
|
+
// constructor here therefore ends in {@link normalizeTable}, and a caller reaches
|
|
11
|
+
// the shape only through them.
|
|
12
|
+
//
|
|
13
|
+
// A cell is its own content unit: `marks` are USV offsets into that cell's `text`,
|
|
14
|
+
// not into `Content.text`. That is the second coordinate space, and the inline mode
|
|
15
|
+
// the codec already runs (one paragraph, no containers, no islands) is exactly its
|
|
16
|
+
// shape, so a cell decodes and projects through the same machinery a
|
|
17
|
+
// `richtext(inline)` field does.
|
|
18
|
+
import { isAnchorMark, mapPos } from '@quillmark/wasm';
|
|
19
|
+
import { contentEdit, pmToContent } from './encode.js';
|
|
20
|
+
/** The alignments a column can take: the four the content declares, `none` first
|
|
21
|
+
* because it is what a column arrives at and what a set is cleared back to. */
|
|
22
|
+
export const ALIGNS = ['none', 'left', 'center', 'right'];
|
|
23
|
+
/** The zero cell: empty text, no marks. */
|
|
24
|
+
export function emptyCell() {
|
|
25
|
+
return { text: '', marks: [] };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The row index space the chrome and the ops share: **0 is the header**, 1…n the
|
|
29
|
+
* body rows. A table always has a header (`header` is a separate field, and a
|
|
30
|
+
* header-less table is not expressible), so the header is a row that never
|
|
31
|
+
* deletes rather than a toggle.
|
|
32
|
+
*/
|
|
33
|
+
export function rowCount(props) {
|
|
34
|
+
return props.rows.length + 1;
|
|
35
|
+
}
|
|
36
|
+
/** The cells of row `r` in that space. */
|
|
37
|
+
export function rowCells(props, r) {
|
|
38
|
+
return r === 0 ? props.header : (props.rows[r - 1] ?? []);
|
|
39
|
+
}
|
|
40
|
+
/** The cell at `(row, column)`, or the zero cell where the rectangle does not reach. */
|
|
41
|
+
export function cellAt(props, r, c) {
|
|
42
|
+
return rowCells(props, r)[c] ?? emptyCell();
|
|
43
|
+
}
|
|
44
|
+
/** The table's column count: the one number `header`, every row and `aligns` share. */
|
|
45
|
+
export function columnCount(props) {
|
|
46
|
+
return props.aligns.length;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rectangularizing constructor: one column count across `header`, every row and
|
|
50
|
+
* `aligns`, padding with empty cells and `none`. The column count is the widest
|
|
51
|
+
* thing present, so a caller that appended to one axis alone still gets a rectangle
|
|
52
|
+
* back, and a table is never narrower than one column.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeTable(props) {
|
|
55
|
+
const cols = Math.max(1, props.header.length, props.aligns.length, ...props.rows.map((row) => row.length));
|
|
56
|
+
const fit = (cells) => Array.from({ length: cols }, (_, c) => cells[c] ?? emptyCell());
|
|
57
|
+
return {
|
|
58
|
+
header: fit(props.header),
|
|
59
|
+
rows: props.rows.map(fit),
|
|
60
|
+
aligns: Array.from({ length: cols }, (_, c) => props.aligns[c] ?? 'none')
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** A fresh table: a header plus `bodyRows` empty rows, every column unaligned.
|
|
64
|
+
* Growth is cheap (Tab at the last cell appends a row), so the default is small. */
|
|
65
|
+
export function newTable(cols = 3, bodyRows = 2) {
|
|
66
|
+
return normalizeTable({
|
|
67
|
+
header: Array.from({ length: cols }, emptyCell),
|
|
68
|
+
rows: Array.from({ length: bodyRows }, () => Array.from({ length: cols }, emptyCell)),
|
|
69
|
+
aligns: Array.from({ length: cols }, () => 'none')
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** `props` with the cell at `(r, c)` replaced. */
|
|
73
|
+
export function withCell(props, r, c, cell) {
|
|
74
|
+
const replace = (cells) => cells.map((old, i) => (i === c ? cell : old));
|
|
75
|
+
return normalizeTable({
|
|
76
|
+
header: r === 0 ? replace(props.header) : props.header,
|
|
77
|
+
rows: props.rows.map((row, i) => (i === r - 1 ? replace(row) : row)),
|
|
78
|
+
aligns: props.aligns
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** A new empty body row below row `r` (row 0, the header, opens the first body row). */
|
|
82
|
+
export function insertRow(props, r) {
|
|
83
|
+
const at = Math.max(0, Math.min(r, props.rows.length));
|
|
84
|
+
const fresh = Array.from({ length: columnCount(props) }, emptyCell);
|
|
85
|
+
return normalizeTable({
|
|
86
|
+
...props,
|
|
87
|
+
rows: [...props.rows.slice(0, at), fresh, ...props.rows.slice(at)]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Drop body row `r`. The header (`r === 0`) has no delete: `header: []` is not a
|
|
91
|
+
* table, so the asymmetry is the model's rather than a guard's. */
|
|
92
|
+
export function deleteRow(props, r) {
|
|
93
|
+
if (r === 0)
|
|
94
|
+
return normalizeTable(props);
|
|
95
|
+
return normalizeTable({ ...props, rows: props.rows.filter((_, i) => i !== r - 1) });
|
|
96
|
+
}
|
|
97
|
+
/** A new empty column after column `c`. */
|
|
98
|
+
export function insertColumn(props, c) {
|
|
99
|
+
const at = Math.max(0, Math.min(c + 1, columnCount(props)));
|
|
100
|
+
const splice = (cells) => [...cells.slice(0, at), emptyCell(), ...cells.slice(at)];
|
|
101
|
+
return normalizeTable({
|
|
102
|
+
header: splice(props.header),
|
|
103
|
+
rows: props.rows.map(splice),
|
|
104
|
+
aligns: [...props.aligns.slice(0, at), 'none', ...props.aligns.slice(at)]
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Drop column `c`; a no-op at one column (a table has at least one). */
|
|
108
|
+
export function deleteColumn(props, c) {
|
|
109
|
+
if (columnCount(props) <= 1)
|
|
110
|
+
return normalizeTable(props);
|
|
111
|
+
const drop = (xs) => xs.filter((_, i) => i !== c);
|
|
112
|
+
return normalizeTable({
|
|
113
|
+
header: drop(props.header),
|
|
114
|
+
rows: props.rows.map(drop),
|
|
115
|
+
aligns: drop(props.aligns)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** One element moved within an array, out of place. The whole of what both move ops
|
|
119
|
+
* do to their axis, which is why they can only disagree about the index. */
|
|
120
|
+
function move(xs, from, to) {
|
|
121
|
+
const next = [...xs];
|
|
122
|
+
const [moved] = next.splice(from, 1);
|
|
123
|
+
next.splice(to, 0, moved);
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
/** Body row `r` moved by `by` places, clamped: a drag that leaves the rectangle is a
|
|
127
|
+
* no-op rather than an error, since the drop target is geometry and geometry runs
|
|
128
|
+
* past the last row. The header is refused because it has no destination: it is a
|
|
129
|
+
* separate field, not row 0 of one array. */
|
|
130
|
+
export function moveRow(props, r, by) {
|
|
131
|
+
const from = r - 1;
|
|
132
|
+
const to = Math.max(0, Math.min(from + by, props.rows.length - 1));
|
|
133
|
+
if (r === 0 || from === to || from >= props.rows.length)
|
|
134
|
+
return normalizeTable(props);
|
|
135
|
+
return normalizeTable({ ...props, rows: move(props.rows, from, to) });
|
|
136
|
+
}
|
|
137
|
+
/** Column `c` moved by `by` places, clamped. `aligns` travels with the column: an
|
|
138
|
+
* alignment is the column's property, so a move that left it behind would retint the
|
|
139
|
+
* column that took the index. */
|
|
140
|
+
export function moveColumn(props, c, by) {
|
|
141
|
+
const to = Math.max(0, Math.min(c + by, columnCount(props) - 1));
|
|
142
|
+
if (c === to || c < 0 || c >= columnCount(props))
|
|
143
|
+
return normalizeTable(props);
|
|
144
|
+
const shift = (xs) => move(xs, c, to);
|
|
145
|
+
return normalizeTable({
|
|
146
|
+
header: shift(props.header),
|
|
147
|
+
rows: props.rows.map(shift),
|
|
148
|
+
aligns: shift(props.aligns)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Column `c` with every cell emptied, alignment kept: what a delete gesture means at
|
|
152
|
+
* the LAST column, which the model keeps. There is no row analogue: the header is the
|
|
153
|
+
* only row a delete is refused for, and it carries no handle to select it from. */
|
|
154
|
+
export function clearColumn(props, c) {
|
|
155
|
+
const blank = (cells) => cells.map((cell, i) => (i === c ? emptyCell() : cell));
|
|
156
|
+
return normalizeTable({
|
|
157
|
+
header: blank(props.header),
|
|
158
|
+
rows: props.rows.map(blank),
|
|
159
|
+
aligns: props.aligns
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Set column `c`'s alignment: the one table capability the content round-trips
|
|
163
|
+
* today and nothing in the editor could reach. */
|
|
164
|
+
export function setAlign(props, c, align) {
|
|
165
|
+
return normalizeTable({
|
|
166
|
+
...props,
|
|
167
|
+
aligns: props.aligns.map((a, i) => (i === c ? align : a))
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// ── The cell codec ──────────────────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* A cell as a one-line `Content`: what `decode` takes under the inline schema. The
|
|
173
|
+
* cell's marks ARE that content's marks, because both are offsets into the same
|
|
174
|
+
* text: the cell-local coordinate space is a `Content`'s coordinate space with
|
|
175
|
+
* one line in it.
|
|
176
|
+
*/
|
|
177
|
+
export function cellContent(cell) {
|
|
178
|
+
return {
|
|
179
|
+
text: cell.text,
|
|
180
|
+
lines: [{ containers: [], kind: 'para' }],
|
|
181
|
+
marks: cell.marks,
|
|
182
|
+
islands: []
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The cell a nested PM doc projects, carrying `prior`'s identity anchors through.
|
|
187
|
+
*
|
|
188
|
+
* Anchors inside a cell are **preserved verbatim, never minted**: they are not in
|
|
189
|
+
* the field plugin's coordinate space (the field's position map holds one `atom`
|
|
190
|
+
* run for the whole table), so nothing here can mint one and the popover withholds
|
|
191
|
+
* its `anchor` button while the caret is in a cell. Preserving them still means
|
|
192
|
+
* rebasing: a cell edit is a splice like any other, so each held anchor maps through
|
|
193
|
+
* the cell's own text delta by the rule `applyChange` uses on a field's
|
|
194
|
+
* (start-assoc `after`), and a mark the splice deleted through lands where the
|
|
195
|
+
* splice left it rather than pointing past the text.
|
|
196
|
+
*/
|
|
197
|
+
export function cellFromDoc(doc, prior) {
|
|
198
|
+
const projected = pmToContent(doc);
|
|
199
|
+
const text = projected.text;
|
|
200
|
+
const anchors = prior.marks.filter(isAnchorMark);
|
|
201
|
+
if (!anchors.length)
|
|
202
|
+
return { text, marks: projected.marks };
|
|
203
|
+
const delta = contentEdit(cellContent(prior), cellContent({ text, marks: [] })).delta;
|
|
204
|
+
const rebased = anchors.map((a) => {
|
|
205
|
+
const pos = delta ? mapPos(delta, a.start, 'after') : a.start;
|
|
206
|
+
return { ...a, start: pos, end: pos };
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
text,
|
|
210
|
+
marks: [...projected.marks, ...rebased].sort((a, b) => a.start - b.start || a.end - b.end)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Whether two cells are the same value: what tells an own edit (the projection the
|
|
214
|
+
* view just produced) from an external one (an undo, a re-hydrate) without a flag. */
|
|
215
|
+
export function cellEqual(a, b) {
|
|
216
|
+
return a.text === b.text && JSON.stringify(a.marks) === JSON.stringify(b.marks);
|
|
217
|
+
}
|
|
218
|
+
/** Whether two tables have the same rectangle and alignment: the change that forces
|
|
219
|
+
* a rebuild rather than a per-cell reseed. */
|
|
220
|
+
export function shapeEqual(a, b) {
|
|
221
|
+
return (a.rows.length === b.rows.length &&
|
|
222
|
+
a.aligns.length === b.aligns.length &&
|
|
223
|
+
a.aligns.every((x, i) => x === b.aligns[i]));
|
|
224
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DocPath } from './address.js';
|
|
2
|
+
/**
|
|
3
|
+
* What failed. Stable strings rather than an enum: a consumer switches on them,
|
|
4
|
+
* and a new surface adds a code without moving the ones an app already handles.
|
|
5
|
+
*/
|
|
6
|
+
export type EditorErrorCode =
|
|
7
|
+
/** A scalar/array/object write the boundary refused. Also pinned as a
|
|
8
|
+
* diagnostic on its own field; editing continues, the value is unwritten. */
|
|
9
|
+
'commit-refused'
|
|
10
|
+
/** A prose commit whose op path the boundary refused: the leaf overwrote
|
|
11
|
+
* the whole projection instead, so the store is correct and this field's
|
|
12
|
+
* identity anchors were paid. */
|
|
13
|
+
| 'commit-fallback'
|
|
14
|
+
/** The overwrite fallback ALSO failed: the optimistic PM state stands and the
|
|
15
|
+
* store is stale for this field. The one code here that leaves damage. */
|
|
16
|
+
| 'commit-lost'
|
|
17
|
+
/** A card operation (add, move, remove, retype) that threw. The document is
|
|
18
|
+
* unchanged; the boundary's mutators are transactional. */
|
|
19
|
+
| 'card-op-failed'
|
|
20
|
+
/** `quill.validate` threw: this derive contributes no validation diagnostics. */
|
|
21
|
+
| 'validate-failed'
|
|
22
|
+
/** `quill.resolve` threw: ghosted `default:`s fall back to none for this derive. */
|
|
23
|
+
| 'resolve-failed'
|
|
24
|
+
/** A page paint the backend refused. The preview shows its error message state. */
|
|
25
|
+
| 'paint-failed'
|
|
26
|
+
/** A tip's markdown did not render: the tip shows as literal text. */
|
|
27
|
+
| 'tip-render-failed'
|
|
28
|
+
/**
|
|
29
|
+
* A prop a surface binds ONCE was swapped in place, and the surface neither
|
|
30
|
+
* re-keyed on it nor observed it. The mounted surface still names the previous
|
|
31
|
+
* handle. `dev` throughout: the fix is a remount at the call site, so this is
|
|
32
|
+
* aimed at whoever wired it and never at a running app's telemetry.
|
|
33
|
+
*/
|
|
34
|
+
| 'rebind-ignored'
|
|
35
|
+
/**
|
|
36
|
+
* An instance verb named a card or a field the surface does not hold: a `cardId`
|
|
37
|
+
* from a previous session or an already-removed card, a `DocPath` naming no
|
|
38
|
+
* mounted leaf. The call is a no-op and the document is untouched. `dev`: the
|
|
39
|
+
* editor's own chrome cannot mint a bad target, so this only ever reports a host
|
|
40
|
+
* driving the verbs from outside.
|
|
41
|
+
*/
|
|
42
|
+
| 'target-unknown';
|
|
43
|
+
/** A failure a surface recovered from. */
|
|
44
|
+
export interface EditorError {
|
|
45
|
+
code: EditorErrorCode;
|
|
46
|
+
/**
|
|
47
|
+
* `error` is a runtime failure an app routes to telemetry; `dev` is a contract
|
|
48
|
+
* violation aimed at whoever is building against the package. Without the
|
|
49
|
+
* split every consumer's sink filters the dev codes by hand.
|
|
50
|
+
*/
|
|
51
|
+
severity: 'error' | 'dev';
|
|
52
|
+
/** English, for a log. Not for display: nothing here has a place on screen. */
|
|
53
|
+
message: string;
|
|
54
|
+
/** Whatever was thrown, unwrapped: a `QuillmarkError` arrives intact. */
|
|
55
|
+
cause?: unknown;
|
|
56
|
+
/** The field the failure belongs to, where it has one. */
|
|
57
|
+
path?: DocPath;
|
|
58
|
+
/** The page a paint failed on. */
|
|
59
|
+
page?: number;
|
|
60
|
+
}
|
|
61
|
+
/** The `onError` hook every surface takes. */
|
|
62
|
+
export type EditorErrorHandler = (err: EditorError) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Report `err`, or log it when nothing is listening: a failure the package
|
|
65
|
+
* recovered from is worth seeing in a console.
|
|
66
|
+
*
|
|
67
|
+
* A throwing handler is the consumer's bug and must not become the package's: it
|
|
68
|
+
* is caught, because every call site sits on a recovery path where a second throw
|
|
69
|
+
* would undo the recovery.
|
|
70
|
+
*/
|
|
71
|
+
export declare function reportError(handler: EditorErrorHandler | undefined, err: EditorError): void;
|
|
72
|
+
/** A thrown value's message, for the `message` field. */
|
|
73
|
+
export declare function errorMessage(e: unknown): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report `err`, or log it when nothing is listening: a failure the package
|
|
3
|
+
* recovered from is worth seeing in a console.
|
|
4
|
+
*
|
|
5
|
+
* A throwing handler is the consumer's bug and must not become the package's: it
|
|
6
|
+
* is caught, because every call site sits on a recovery path where a second throw
|
|
7
|
+
* would undo the recovery.
|
|
8
|
+
*/
|
|
9
|
+
export function reportError(handler, err) {
|
|
10
|
+
if (!handler) {
|
|
11
|
+
console.error(`[quillmark/ui] ${err.code}: ${err.message}`, err.cause);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
handler(err);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
console.error(`[quillmark/ui] onError handler threw while reporting ${err.code}`, e);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** A thrown value's message, for the `message` field. */
|
|
22
|
+
export function errorMessage(e) {
|
|
23
|
+
return e instanceof Error ? e.message : String(e);
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import './theme.css';
|
|
2
|
+
export { init } from './lifecycle.js';
|
|
3
|
+
export type { DocPath, Place } from './address.js';
|
|
4
|
+
export { fieldPathForAddr, addrForFieldPath } from './address.js';
|
|
5
|
+
export type { EditorError, EditorErrorCode, EditorErrorHandler } from './errors.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The theme derivation: the public dials → the private `--_qm-*` scale, applied
|
|
2
|
+
// to every `data-qm-root` element. Imported HERE because `core/` is the one module
|
|
3
|
+
// both `preview/` and `visual/` already pull, so a consumer cannot forget it and
|
|
4
|
+
// the derivation is minted once rather than re-declared per detached root
|
|
5
|
+
// (THEMING.md).
|
|
6
|
+
import './theme.css';
|
|
7
|
+
// `@quillmark/svelte/core`: what this package OWNS at the substrate seam.
|
|
8
|
+
//
|
|
9
|
+
// The `@quillmark/wasm` API is not re-exported: import it straight from the peer
|
|
10
|
+
// dependency, the single source of truth (quiver states the same rule). The
|
|
11
|
+
// handles cross this package's surfaces untouched (no wrapper types, per
|
|
12
|
+
// DOCUMENT_MODEL §What the editor owns), so the boundary needs no second door
|
|
13
|
+
// here. What earns a line is what more than one surface speaks and no other
|
|
14
|
+
// package declares: the address vocabulary and the error channel, plus the WASM
|
|
15
|
+
// init gate.
|
|
16
|
+
export { init } from './lifecycle.js';
|
|
17
|
+
// The hop between that vocabulary and the `Addr` the document verbs take, both
|
|
18
|
+
// directions: a host handed a path by a hook and holding a mutator needs it.
|
|
19
|
+
export { fieldPathForAddr, addrForFieldPath } from './address.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instantiate the core WASM build. Await once before any other boundary verb,
|
|
3
|
+
* from any surface's setup path; the promise is memoized, so concurrent callers
|
|
4
|
+
* share one instantiation and repeat calls are free. A failed init clears the
|
|
5
|
+
* memo, leaving a retry possible.
|
|
6
|
+
*/
|
|
7
|
+
export declare function init(): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// `@quillmark/wasm` ships wasm-bindgen's web target: the core build is
|
|
2
|
+
// instantiated by an explicit `await init()`, and every export throws
|
|
3
|
+
// `runtime::not_initialized` until it resolves. Instantiation carries the panic
|
|
4
|
+
// hook on its start section, so a Rust panic reaches JS as a stack trace.
|
|
5
|
+
import { init as initWasm } from '@quillmark/wasm';
|
|
6
|
+
let started;
|
|
7
|
+
/**
|
|
8
|
+
* Instantiate the core WASM build. Await once before any other boundary verb,
|
|
9
|
+
* from any surface's setup path; the promise is memoized, so concurrent callers
|
|
10
|
+
* share one instantiation and repeat calls are free. A failed init clears the
|
|
11
|
+
* memo, leaving a retry possible.
|
|
12
|
+
*/
|
|
13
|
+
export function init() {
|
|
14
|
+
if (started)
|
|
15
|
+
return started;
|
|
16
|
+
started = initWasm().catch((err) => {
|
|
17
|
+
started = undefined;
|
|
18
|
+
throw err;
|
|
19
|
+
});
|
|
20
|
+
return started;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type EditorErrorHandler } from './errors.js';
|
|
2
|
+
/** The once-bound props by name, `onError` among them. */
|
|
3
|
+
export type Bound = Record<string, unknown> & {
|
|
4
|
+
onError?: EditorErrorHandler;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
|
|
8
|
+
* in place. `remount` closes the message with the surface's own remount spelling
|
|
9
|
+
* (`Remount the preview ({#key session}) to rebind.`).
|
|
10
|
+
*
|
|
11
|
+
* Mounted identities are recorded on the effect's FIRST run, the flush the surface
|
|
12
|
+
* binds in; every later comparison reads them through the same `snapshot`.
|
|
13
|
+
*
|
|
14
|
+
* The report goes to the SNAPSHOT's `onError`: every other error these surfaces
|
|
15
|
+
* report reaches the handler they bound, and a swapped `onError` is itself one of
|
|
16
|
+
* the reports.
|
|
17
|
+
*/
|
|
18
|
+
export declare function guardRebind(snapshot: () => Bound, remount: string): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The remount contract, enforced. `Preview` binds its props ONCE, in `onMount`, and
|
|
2
|
+
// observes nothing after: the vanilla core below it owns scroll position, mounted
|
|
3
|
+
// slots and observer sets that a remount would discard on every apply. So a prop
|
|
4
|
+
// swapped in place is reported, not dropped in silence.
|
|
5
|
+
//
|
|
6
|
+
// A guard over ONE prop of a set is worse than none: a consumer who learns the
|
|
7
|
+
// surface complains when it ignores a prop reads silence on the rest as reactivity.
|
|
8
|
+
// The snapshot is every once-bound prop, and one report names whichever went stale.
|
|
9
|
+
import { reportError } from './errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* Report `rebind-ignored` once, at `dev`, when any prop `snapshot` names is swapped
|
|
12
|
+
* in place. `remount` closes the message with the surface's own remount spelling
|
|
13
|
+
* (`Remount the preview ({#key session}) to rebind.`).
|
|
14
|
+
*
|
|
15
|
+
* Mounted identities are recorded on the effect's FIRST run, the flush the surface
|
|
16
|
+
* binds in; every later comparison reads them through the same `snapshot`.
|
|
17
|
+
*
|
|
18
|
+
* The report goes to the SNAPSHOT's `onError`: every other error these surfaces
|
|
19
|
+
* report reaches the handler they bound, and a swapped `onError` is itself one of
|
|
20
|
+
* the reports.
|
|
21
|
+
*/
|
|
22
|
+
export function guardRebind(snapshot, remount) {
|
|
23
|
+
let mounted;
|
|
24
|
+
let reported = false;
|
|
25
|
+
$effect(() => {
|
|
26
|
+
const live = snapshot();
|
|
27
|
+
if (mounted === undefined) {
|
|
28
|
+
mounted = live;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (reported)
|
|
32
|
+
return;
|
|
33
|
+
const bound = mounted;
|
|
34
|
+
const stale = Object.keys(bound).filter((k) => live[k] !== bound[k]);
|
|
35
|
+
if (stale.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
reported = true;
|
|
38
|
+
reportError(bound.onError, {
|
|
39
|
+
code: 'rebind-ignored',
|
|
40
|
+
severity: 'dev',
|
|
41
|
+
message: `${stale.join(', ')} swapped in place; the mounted surface still holds what it bound at mount. ${remount}`
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A surface's lifespan: alive from construction until {@link Lifespan.end}. */
|
|
2
|
+
export interface Lifespan {
|
|
3
|
+
/** False once the span has ended. */
|
|
4
|
+
readonly alive: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Register work to undo at the end: a registry entry to drop, a timer or frame
|
|
7
|
+
* to cancel, a handle to free. Cancellers run in REGISTRATION order, which is
|
|
8
|
+
* where the order lives. Registering on an ended span runs the canceller at
|
|
9
|
+
* once: there is no later run to hold it for.
|
|
10
|
+
*/
|
|
11
|
+
onEnd(cancel: () => void): void;
|
|
12
|
+
/**
|
|
13
|
+
* Await `pending`, then report whether the surface survived the window. `false`
|
|
14
|
+
* is a continuation to drop: what it captured before the await is gone.
|
|
15
|
+
*/
|
|
16
|
+
resumes(pending: Promise<unknown>): Promise<boolean>;
|
|
17
|
+
/** Run the cancellers once and end the span. Idempotent. */
|
|
18
|
+
end(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createLifespan(): Lifespan;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The teardown order every surface holds: unregister, cancel, then free.
|
|
2
|
+
//
|
|
3
|
+
// An order rather than a list, because work is deferred: a surface that frees
|
|
4
|
+
// first leaves a scheduled continuation holding what nobody owns, and one that
|
|
5
|
+
// cancels first but stays registered still takes the next event.
|
|
6
|
+
//
|
|
7
|
+
// `Lifespan` carries it. A surface builds one, registers its cancellers in the
|
|
8
|
+
// order they must run, and ends the span once; continuations that cross an await
|
|
9
|
+
// ask `resumes()` rather than a flag they captured before it.
|
|
10
|
+
export function createLifespan() {
|
|
11
|
+
let alive = true;
|
|
12
|
+
const cancels = [];
|
|
13
|
+
return {
|
|
14
|
+
get alive() {
|
|
15
|
+
return alive;
|
|
16
|
+
},
|
|
17
|
+
onEnd(cancel) {
|
|
18
|
+
if (!alive)
|
|
19
|
+
return void cancel();
|
|
20
|
+
cancels.push(cancel);
|
|
21
|
+
},
|
|
22
|
+
async resumes(pending) {
|
|
23
|
+
await pending;
|
|
24
|
+
return alive;
|
|
25
|
+
},
|
|
26
|
+
end() {
|
|
27
|
+
if (!alive)
|
|
28
|
+
return;
|
|
29
|
+
// Dead BEFORE the cancellers run: one of them may synchronously reach
|
|
30
|
+
// something that asks, and a span mid-teardown is not alive.
|
|
31
|
+
alive = false;
|
|
32
|
+
for (const cancel of cancels)
|
|
33
|
+
cancel();
|
|
34
|
+
cancels.length = 0;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|