@quillmark/svelte 0.10.0 → 0.11.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/README.md +1 -1
- package/THEMING.md +4 -2
- package/dist/core/codec/blocks.js +21 -1
- package/dist/core/codec/breaks.d.ts +10 -2
- package/dist/core/codec/breaks.js +65 -9
- package/dist/core/codec/code.d.ts +6 -4
- package/dist/core/codec/code.js +7 -4
- package/dist/core/codec/decode.js +91 -62
- package/dist/core/codec/encode.d.ts +5 -8
- package/dist/core/codec/encode.js +55 -85
- package/dist/core/codec/field.js +43 -24
- package/dist/core/codec/index.d.ts +2 -1
- package/dist/core/codec/index.js +6 -3
- package/dist/core/codec/islands.d.ts +13 -9
- package/dist/core/codec/islands.js +75 -49
- package/dist/core/codec/keymap.js +2 -1
- package/dist/core/codec/marks.d.ts +17 -13
- package/dist/core/codec/marks.js +39 -42
- package/dist/core/codec/positions.d.ts +4 -1
- package/dist/core/codec/positions.js +5 -1
- package/dist/core/codec/prose.css +10 -0
- package/dist/core/codec/reconcile.d.ts +6 -0
- package/dist/core/codec/reconcile.js +19 -0
- package/dist/core/codec/schema.d.ts +5 -8
- package/dist/core/codec/schema.js +30 -156
- package/dist/core/codec/table-view.js +77 -26
- package/dist/core/codec/table.d.ts +6 -5
- package/dist/core/codec/table.js +14 -31
- package/dist/core/codec/urls.d.ts +20 -0
- package/dist/core/codec/urls.js +45 -0
- package/dist/preset/recipes.css +1 -1
- package/dist/preset/scale.css +7 -4
- package/dist/preview/Preview.svelte +6 -0
- package/dist/preview/bridge.d.ts +8 -1
- package/dist/preview/bridge.js +31 -25
- package/dist/preview/controller.js +13 -13
- package/dist/preview/geometry.d.ts +12 -1
- package/dist/preview/geometry.js +14 -1
- package/dist/preview/paint.js +6 -4
- package/dist/preview/strings.d.ts +0 -2
- package/dist/preview/strings.js +5 -6
- package/dist/visual/ArrayField.svelte +11 -13
- package/dist/visual/Card.svelte +5 -8
- package/dist/visual/DateField.svelte +2 -2
- package/dist/visual/Field.svelte +7 -3
- package/dist/visual/FieldHint.svelte +1 -1
- package/dist/visual/ObjectField.svelte +6 -1
- package/dist/visual/VisualEditor.svelte +1 -1
- package/dist/visual/VisualEditorInner.svelte +73 -18
- package/dist/visual/controls.css +8 -9
- package/dist/visual/ext.d.ts +12 -13
- package/dist/visual/ext.js +22 -19
- package/dist/visual/index.js +2 -3
- package/dist/visual/links.d.ts +4 -0
- package/dist/visual/links.js +15 -4
- package/dist/visual/strings.d.ts +7 -4
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +9 -1
- package/dist/visual/structure.js +26 -4
- package/package.json +2 -2
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
// Islands: the `U+FFFC` slot + `ContentIsland` entry ↔ a PM leaf node. A slot
|
|
2
2
|
// inside a `para` line is an inline island (an image); an `island`-kind line is a
|
|
3
|
-
// block island (a table). The node carries `id`, `islandType
|
|
4
|
-
// `props` verbatim, so an unknown island type round-trips untouched.
|
|
3
|
+
// block island (a table). The node carries `id`, `islandType` and `props` verbatim.
|
|
5
4
|
//
|
|
6
5
|
// A props-aware consumer reads the shape straight off the boundary:
|
|
7
6
|
// `ContentIsland.props` is the typed union (`TableProps` for `table`,
|
|
8
7
|
// `ImageProps` for `image`; DOCUMENT_MODEL §Stability seams), so the codec
|
|
9
|
-
// reads it rather than hand-rolling a shape
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// reads it rather than hand-rolling a shape. `type` is a closed set, so a
|
|
9
|
+
// discriminant check narrows it; `props` still crosses the DOM as opaque JSON,
|
|
10
|
+
// which is what §`tablePropsOfNode` guards.
|
|
12
11
|
import { Fragment, Slice } from 'prosemirror-model';
|
|
13
12
|
import { Plugin } from 'prosemirror-state';
|
|
14
|
-
import {
|
|
13
|
+
import { storableUrl } from './urls.js';
|
|
15
14
|
/** The `U+FFFC` object-replacement char that occupies one island slot in `text`. */
|
|
16
15
|
export const ISLAND_SLOT = '';
|
|
17
16
|
// The four island node attributes carry the content entry verbatim: `id` is the
|
|
18
17
|
// stable identity (rebases like an anchor), `islandType` the discriminator, `props`
|
|
19
|
-
// the
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// supplies the stored value.
|
|
18
|
+
// the payload the codec preserves byte-for-byte across a round-trip, and `loss` how
|
|
19
|
+
// faithfully markdown can carry it. `islandType` and `props` declare no default: a
|
|
20
|
+
// node standing for an island holds one, so the pair is supplied at every `create`.
|
|
21
|
+
// `loss` is authored, not derived: `applyChange` stores the class an island op gives
|
|
22
|
+
// it and re-derives nothing, so an entry that did not carry its own would promote a
|
|
23
|
+
// degraded table to lossless on the first cell edit. Its default is the weakest
|
|
24
|
+
// claim; decode always supplies the stored value.
|
|
26
25
|
const islandAttrs = {
|
|
27
26
|
id: { default: '' },
|
|
28
|
-
islandType: {
|
|
29
|
-
props: {
|
|
27
|
+
islandType: {},
|
|
28
|
+
props: {},
|
|
30
29
|
loss: { default: 'unrepresentable' }
|
|
31
30
|
};
|
|
32
31
|
// All four attributes cross the DOM: a copy and a paste inside one body run the document
|
|
@@ -44,13 +43,28 @@ function islandDOM(node) {
|
|
|
44
43
|
'data-qm-island-props': JSON.stringify(node.attrs.props ?? null)
|
|
45
44
|
};
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
// The two closed sets an element claims a value from. A `Record` over the boundary's own
|
|
47
|
+
// union rather than a literal check, so a member added upstream is a missing key here
|
|
48
|
+
// rather than a silent refusal at the one door with no decode behind it.
|
|
49
|
+
/** The island types the content model names. */
|
|
50
|
+
const ISLAND_TYPES = { table: true, image: true };
|
|
51
|
+
/** The loss classes it names, weakest last: an unrecognized claim takes that one. */
|
|
52
|
+
const LOSS_CLASSES = {
|
|
53
|
+
lossless: true,
|
|
54
|
+
degraded: true,
|
|
55
|
+
unrepresentable: true
|
|
56
|
+
};
|
|
57
|
+
/** An island node's attributes off that DOM, or `false` where the id is absent or the
|
|
58
|
+
* type is one the content vocabulary does not name — a name no read produces and no
|
|
59
|
+
* write takes, so the element's text arrives as text. A `props` that is not the JSON
|
|
60
|
+
* written above reads as `null`, and one of the wrong shape is `undefined` at its
|
|
61
|
+
* reader (§`tablePropsOfNode`): the entry survives as an island of its type rather
|
|
62
|
+
* than taking the paste down with it. */
|
|
51
63
|
function islandAttrsFromDOM(el) {
|
|
52
64
|
const id = el.getAttribute('data-qm-island-id');
|
|
53
|
-
|
|
65
|
+
// Asserted off an untrusted attribute and checked against the set on the next line.
|
|
66
|
+
const type = el.getAttribute('data-qm-island');
|
|
67
|
+
if (id == null || type == null || !ISLAND_TYPES[type])
|
|
54
68
|
return false;
|
|
55
69
|
const raw = el.getAttribute('data-qm-island-props');
|
|
56
70
|
let props = null;
|
|
@@ -60,12 +74,22 @@ function islandAttrsFromDOM(el) {
|
|
|
60
74
|
catch {
|
|
61
75
|
props = null;
|
|
62
76
|
}
|
|
63
|
-
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
return { id, islandType: type, props: storableProps(type, props), loss: lossFromDOM(el) };
|
|
78
|
+
}
|
|
79
|
+
/** The `loss` an element claims, or the weakest class for a claim outside the set:
|
|
80
|
+
* an under-claim keeps a degraded island from re-crossing as a lossless one. */
|
|
81
|
+
function lossFromDOM(el) {
|
|
82
|
+
const raw = el.getAttribute('data-qm-island-loss');
|
|
83
|
+
return raw != null && LOSS_CLASSES[raw] ? raw : 'unrepresentable';
|
|
84
|
+
}
|
|
85
|
+
/** `props` with the one key the authored lane refuses made storable: an `image`'s `url`
|
|
86
|
+
* carrying a line ending, which `IslandOp` and `overwrite` throw on. The rest crosses
|
|
87
|
+
* opaque, as it did on the way out. */
|
|
88
|
+
function storableProps(type, props) {
|
|
89
|
+
if (type !== 'image' || typeof props !== 'object' || props === null)
|
|
90
|
+
return props;
|
|
91
|
+
const url = props.url;
|
|
92
|
+
return typeof url === 'string' ? { ...props, url: storableUrl(url) } : props;
|
|
69
93
|
}
|
|
70
94
|
/** Block island node (a table): one `island`-kind content line. Atom, unselectable content. */
|
|
71
95
|
export const islandBlockSpec = {
|
|
@@ -86,7 +110,11 @@ export const islandInlineSpec = {
|
|
|
86
110
|
parseDOM: [{ tag: 'span[data-qm-island]', getAttrs: (el) => islandAttrsFromDOM(el) }],
|
|
87
111
|
toDOM: (node) => ['span', islandDOM(node), `[${node.attrs.islandType || 'island'}]`]
|
|
88
112
|
};
|
|
89
|
-
/** Build a content island entry from a PM island node's attrs (block or inline).
|
|
113
|
+
/** Build a content island entry from a PM island node's attrs (block or inline). The
|
|
114
|
+
* cast pairs a closed `type` with a `props` that crossed the DOM as opaque JSON. The
|
|
115
|
+
* store is lenient about a table's shape — it fills what it can and keeps the rest —
|
|
116
|
+
* so a payload the arm does not hold is stored rather than refused, and `undefined` at
|
|
117
|
+
* its reader is what the surface draws the placeholder for (§`tablePropsOfNode`). */
|
|
90
118
|
export function islandEntryFromNode(attrs) {
|
|
91
119
|
return {
|
|
92
120
|
id: attrs.id,
|
|
@@ -107,13 +135,13 @@ function isTableProps(props) {
|
|
|
107
135
|
return (cells(header) && Array.isArray(aligns) && Array.isArray(rows) && rows.every((row) => cells(row)));
|
|
108
136
|
}
|
|
109
137
|
/** A node's `TableProps`, or `undefined` for any other island and for a payload of the
|
|
110
|
-
* wrong shape: the boundary's own guard over the entry the node carries, so
|
|
111
|
-
*
|
|
112
|
-
* the shape says whether it can read; every caller draws the placeholder
|
|
113
|
-
* `undefined` (`table-view.ts` §`render`). */
|
|
138
|
+
* wrong shape: the boundary's own guard over the entry the node carries, so a `props`
|
|
139
|
+
* that crossed the DOM is read once here rather than at each reader. The type says
|
|
140
|
+
* which reader, the shape says whether it can read; every caller draws the placeholder
|
|
141
|
+
* for `undefined` (`table-view.ts` §`render`). */
|
|
114
142
|
export function tablePropsOfNode(node) {
|
|
115
|
-
const
|
|
116
|
-
return
|
|
143
|
+
const attrs = node.attrs;
|
|
144
|
+
return attrs.islandType === 'table' && isTableProps(attrs.props) ? attrs.props : undefined;
|
|
117
145
|
}
|
|
118
146
|
/**
|
|
119
147
|
* A minter over one document: the positional `isl-{n}` sequence continued past the
|
|
@@ -128,38 +156,35 @@ export function tablePropsOfNode(node) {
|
|
|
128
156
|
* a stale content could collide with one the same transaction places.
|
|
129
157
|
*/
|
|
130
158
|
export function islandMinter(doc) {
|
|
159
|
+
return islandState(doc).mint;
|
|
160
|
+
}
|
|
161
|
+
/** The ids a document holds and the minter continuing past them, off one sweep: both
|
|
162
|
+
* are the same walk under the same predicate, and the paste pass wants both. */
|
|
163
|
+
function islandState(doc) {
|
|
164
|
+
const held = new Set();
|
|
131
165
|
let next = 0;
|
|
132
166
|
doc.descendants((node) => {
|
|
133
167
|
if (!isIsland(node))
|
|
134
168
|
return true;
|
|
135
|
-
const
|
|
169
|
+
const id = node.attrs.id;
|
|
170
|
+
held.add(id);
|
|
171
|
+
const m = /^isl-(\d+)$/.exec(id);
|
|
136
172
|
if (m)
|
|
137
173
|
next = Math.max(next, Number(m[1]) + 1);
|
|
138
174
|
return false;
|
|
139
175
|
});
|
|
140
|
-
return () => `isl-${next++}
|
|
176
|
+
return { held, mint: () => `isl-${next++}` };
|
|
141
177
|
}
|
|
142
178
|
/** The next island id for a field: one draw from {@link islandMinter}, which is what a
|
|
143
179
|
* command placing exactly one island wants. */
|
|
144
180
|
export function mintIslandId(doc) {
|
|
145
181
|
return islandMinter(doc)();
|
|
146
182
|
}
|
|
147
|
-
/** Either island node: the test the
|
|
148
|
-
*
|
|
183
|
+
/** Either island node: the test the sweep and the paste pass share, so a third island
|
|
184
|
+
* shape is one arm rather than two. */
|
|
149
185
|
function isIsland(node) {
|
|
150
186
|
return node.type.name === 'island_block' || node.type.name === 'island_inline';
|
|
151
187
|
}
|
|
152
|
-
/** The ids a document already holds. */
|
|
153
|
-
function heldIds(doc) {
|
|
154
|
-
const held = new Set();
|
|
155
|
-
doc.descendants((node) => {
|
|
156
|
-
if (!isIsland(node))
|
|
157
|
-
return true;
|
|
158
|
-
held.add(node.attrs.id);
|
|
159
|
-
return false;
|
|
160
|
-
});
|
|
161
|
-
return held;
|
|
162
|
-
}
|
|
163
188
|
/** `fragment` with every island whose id is already `taken` re-minted, or `null` where
|
|
164
189
|
* none was: an untouched paste stays the slice it arrived as. Every surviving id joins
|
|
165
190
|
* the set, so a slice carrying one id twice comes apart in the same pass. */
|
|
@@ -205,7 +230,8 @@ export function islandPastePlugin() {
|
|
|
205
230
|
return new Plugin({
|
|
206
231
|
props: {
|
|
207
232
|
transformPasted: (slice, view) => {
|
|
208
|
-
const
|
|
233
|
+
const { held, mint } = islandState(view.state.doc);
|
|
234
|
+
const content = remintIslands(slice.content, held, mint);
|
|
209
235
|
return content ? new Slice(content, slice.openStart, slice.openEnd) : slice;
|
|
210
236
|
}
|
|
211
237
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chainCommands } from 'prosemirror-commands';
|
|
2
2
|
import { atomKeymap } from './atoms.js';
|
|
3
3
|
import { blockKeymap } from './blocks.js';
|
|
4
|
+
import { breakKeymap } from './breaks.js';
|
|
4
5
|
import { codeKeymap } from './code.js';
|
|
5
6
|
import { listKeymap } from './lists.js';
|
|
6
7
|
import { slashKeymap } from './slash.js';
|
|
@@ -27,5 +28,5 @@ function chainKeymaps(...maps) {
|
|
|
27
28
|
* no visible reason. */
|
|
28
29
|
export function bodyKeymap(schema, slash) {
|
|
29
30
|
const links = slash ? [slashKeymap()] : [];
|
|
30
|
-
return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), listKeymap(schema), atomKeymap(schema));
|
|
31
|
+
return chainKeymaps(...links, blockKeymap(schema), codeKeymap(schema), breakKeymap(schema), listKeymap(schema), atomKeymap(schema));
|
|
31
32
|
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import type { Mark, Schema } from 'prosemirror-model';
|
|
2
|
-
import type { ContentMark } from '@quillmark/wasm';
|
|
3
|
-
/** A PM mark from a content formatting
|
|
2
|
+
import type { ContentMark, ContentMarkKind } from '@quillmark/wasm';
|
|
3
|
+
/** A PM mark from a content formatting mark, or `null` for an anchor. Exhaustive over
|
|
4
|
+
* the closed mark vocabulary, so a member added upstream is a compile error here rather
|
|
5
|
+
* than an index into a PM mark this schema does not declare. */
|
|
4
6
|
export declare function pmMarkFromContent(schema: Schema, m: ContentMark): Mark | null;
|
|
5
7
|
/**
|
|
6
8
|
* A content mark descriptor from a PM mark (range-free): the `{ type, … }` half
|
|
7
|
-
* of a `ContentMark` / `MarkOp`. `
|
|
8
|
-
*
|
|
9
|
+
* of a `ContentMark` / `MarkOp`. `em` collapses to `emph` and a link's `href` rides
|
|
10
|
+
* out as the `url` its payload spells; the other four are their own content name,
|
|
11
|
+
* which is what the cast claims.
|
|
9
12
|
*/
|
|
10
|
-
export declare function contentDescriptorFromPM(mark: Mark):
|
|
13
|
+
export declare function contentDescriptorFromPM(mark: Mark): ContentMarkKind;
|
|
11
14
|
/**
|
|
12
15
|
* The range-free `{ type, … }` half of a content mark: `contentDescriptorFromPM`'s
|
|
13
|
-
* content-side twin, and what a `MarkOp` carries beside its range.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* content-side twin, and what a `MarkOp` carries beside its range. Lifted off the
|
|
17
|
+
* range envelope rather than rebuilt arm by arm, so a payload crosses whole and an
|
|
18
|
+
* arm added upstream reaches this by construction.
|
|
16
19
|
*/
|
|
17
|
-
export declare function descriptorOf(m: ContentMark):
|
|
20
|
+
export declare function descriptorOf(m: ContentMark): ContentMarkKind;
|
|
18
21
|
/**
|
|
19
22
|
* A stable grouping key for the mark diff: marks sharing a key union into one
|
|
20
|
-
* coverage set.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* coverage set. Payload-free formatting keys on its type, everything else on
|
|
24
|
+
* type+attrs, because `applyChange`'s `remove` matches type and attrs (verified),
|
|
25
|
+
* so two links differing in url are independent mark families. This is the seam's
|
|
26
|
+
* own `(type, attrs)` tie-break, which is why no arm needs a case of its own.
|
|
23
27
|
*/
|
|
24
|
-
export declare function markKey(descriptor:
|
|
28
|
+
export declare function markKey(descriptor: ContentMarkKind): string;
|
|
25
29
|
/** A held anchor position: an identity id at a USV content offset (zero-width). */
|
|
26
30
|
export interface AnchorPos {
|
|
27
31
|
id: string;
|
package/dist/core/codec/marks.js
CHANGED
|
@@ -1,64 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { canonicalJson } from './reconcile.js';
|
|
2
|
+
/** A PM mark from a content formatting mark, or `null` for an anchor. Exhaustive over
|
|
3
|
+
* the closed mark vocabulary, so a member added upstream is a compile error here rather
|
|
4
|
+
* than an index into a PM mark this schema does not declare. */
|
|
5
5
|
export function pmMarkFromContent(schema, m) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
6
|
+
switch (m.type) {
|
|
7
|
+
case 'anchor':
|
|
8
|
+
return null;
|
|
9
|
+
case 'emph':
|
|
10
|
+
return schema.marks.em.create();
|
|
11
|
+
case 'link':
|
|
12
|
+
return schema.marks.link.create({ href: m.attrs.url });
|
|
13
|
+
case 'strong':
|
|
14
|
+
case 'underline':
|
|
15
|
+
case 'strike':
|
|
16
|
+
case 'code':
|
|
17
|
+
return schema.marks[m.type].create();
|
|
18
|
+
default: {
|
|
19
|
+
const unreached = m;
|
|
20
|
+
return unreached;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
21
25
|
* A content mark descriptor from a PM mark (range-free): the `{ type, … }` half
|
|
22
|
-
* of a `ContentMark` / `MarkOp`. `
|
|
23
|
-
*
|
|
26
|
+
* of a `ContentMark` / `MarkOp`. `em` collapses to `emph` and a link's `href` rides
|
|
27
|
+
* out as the `url` its payload spells; the other four are their own content name,
|
|
28
|
+
* which is what the cast claims.
|
|
24
29
|
*/
|
|
25
30
|
export function contentDescriptorFromPM(mark) {
|
|
26
31
|
const name = mark.type.name;
|
|
27
32
|
if (name === 'em')
|
|
28
33
|
return { type: 'emph' };
|
|
29
34
|
if (name === 'link')
|
|
30
|
-
return { type: 'link', url: mark.attrs.href };
|
|
31
|
-
if (name === 'unknown')
|
|
32
|
-
return { type: mark.attrs.type, attrs: mark.attrs.attrs };
|
|
33
|
-
// strong / underline / strike / code
|
|
35
|
+
return { type: 'link', attrs: { url: mark.attrs.href } };
|
|
34
36
|
return { type: name };
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* The range-free `{ type, … }` half of a content mark: `contentDescriptorFromPM`'s
|
|
38
|
-
* content-side twin, and what a `MarkOp` carries beside its range.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* content-side twin, and what a `MarkOp` carries beside its range. Lifted off the
|
|
41
|
+
* range envelope rather than rebuilt arm by arm, so a payload crosses whole and an
|
|
42
|
+
* arm added upstream reaches this by construction.
|
|
41
43
|
*/
|
|
42
44
|
export function descriptorOf(m) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if ('attrs' in m && !isAnchorMark(m))
|
|
46
|
-
return { type: m.type, attrs: m.attrs };
|
|
47
|
-
return { type: m.type };
|
|
45
|
+
const { start: _start, end: _end, ...kind } = m;
|
|
46
|
+
return kind;
|
|
48
47
|
}
|
|
49
48
|
/**
|
|
50
49
|
* A stable grouping key for the mark diff: marks sharing a key union into one
|
|
51
|
-
* coverage set.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* coverage set. Payload-free formatting keys on its type, everything else on
|
|
51
|
+
* type+attrs, because `applyChange`'s `remove` matches type and attrs (verified),
|
|
52
|
+
* so two links differing in url are independent mark families. This is the seam's
|
|
53
|
+
* own `(type, attrs)` tie-break, which is why no arm needs a case of its own.
|
|
54
54
|
*/
|
|
55
55
|
export function markKey(descriptor) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (descriptor.attrs !== undefined)
|
|
60
|
-
return `${type}\u0000${JSON.stringify(descriptor.attrs)}`;
|
|
61
|
-
return type;
|
|
56
|
+
if ('attrs' in descriptor)
|
|
57
|
+
return `${descriptor.type}\u0000${canonicalJson(descriptor.attrs)}`;
|
|
58
|
+
return descriptor.type;
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
64
61
|
* The identity anchors of a `Content` as `{ id, pos }` in USV: the seed for the
|
|
@@ -68,7 +65,7 @@ export function markKey(descriptor) {
|
|
|
68
65
|
export function anchorsFromContent(rt) {
|
|
69
66
|
const out = [];
|
|
70
67
|
for (const m of rt.marks)
|
|
71
|
-
if (
|
|
72
|
-
out.push({ id: m.id, pos: m.start });
|
|
68
|
+
if (m.type === 'anchor')
|
|
69
|
+
out.push({ id: m.attrs.id, pos: m.start });
|
|
73
70
|
return out;
|
|
74
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
-
import { type PosRun } from './encode.js';
|
|
2
|
+
import { type PosRun, type Scan } from './encode.js';
|
|
3
3
|
/** A prebuilt line→position index for a PM doc, rebuilt on structural change. */
|
|
4
4
|
export interface LineIndex {
|
|
5
5
|
runs: PosRun[];
|
|
@@ -11,6 +11,9 @@ export interface LineIndex {
|
|
|
11
11
|
}
|
|
12
12
|
/** Build the position index for `doc` (walks it once via the shared scanner). */
|
|
13
13
|
export declare function buildLineIndex(doc: PMNode): LineIndex;
|
|
14
|
+
/** The index a scan's runs make. The commit path takes this one: it wants the
|
|
15
|
+
* content projection off the same walk (`encode.ts`, `scanContent`). */
|
|
16
|
+
export declare function lineIndexOf(s: Scan): LineIndex;
|
|
14
17
|
/** USV content offset → PM position. */
|
|
15
18
|
export declare function usvToPM(index: LineIndex, usvPos: number): number;
|
|
16
19
|
/** PM position → USV content offset. */
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { scanDoc } from './encode.js';
|
|
2
2
|
/** Build the position index for `doc` (walks it once via the shared scanner). */
|
|
3
3
|
export function buildLineIndex(doc) {
|
|
4
|
-
|
|
4
|
+
return lineIndexOf(scanDoc(doc));
|
|
5
|
+
}
|
|
6
|
+
/** The index a scan's runs make. The commit path takes this one: it wants the
|
|
7
|
+
* content projection off the same walk (`encode.ts`, `scanContent`). */
|
|
8
|
+
export function lineIndexOf(s) {
|
|
5
9
|
const runs = s.runs;
|
|
6
10
|
let pmEndContent = 1;
|
|
7
11
|
let firstContentStart = 1;
|
|
@@ -228,6 +228,16 @@ li.ProseMirror-selectednode::after {
|
|
|
228
228
|
.ProseMirror > .qm-table-island + * {
|
|
229
229
|
margin-top: 0;
|
|
230
230
|
}
|
|
231
|
+
/* Width from above, never from the columns. The scroller below caps at `100%` of this
|
|
232
|
+
box, and a percentage is no cap while an ancestor is being sized to what it holds: a
|
|
233
|
+
leaf taking its width from its own contents is asked for the table's max-content width
|
|
234
|
+
and the cap never applies. The surface closes the same question at its edge
|
|
235
|
+
(`visual/VisualEditorInner.svelte` §`.qm-editor`); this end of it is what makes the cap
|
|
236
|
+
below true wherever the island lands. `inline-size`, not `size`: an island is as tall
|
|
237
|
+
as its rows. */
|
|
238
|
+
.qm-table-island {
|
|
239
|
+
contain: inline-size;
|
|
240
|
+
}
|
|
231
241
|
.qm-table-scroller {
|
|
232
242
|
/* The one horizontally scrolling box in a leaf: a table is as wide as its
|
|
233
243
|
columns, and a wide one must not push the card's own layout. `overflow-y` is
|
|
@@ -24,3 +24,9 @@ export declare function contentEqual(a: Content, b: Content): boolean;
|
|
|
24
24
|
* on one side and a PM node attribute on the other.
|
|
25
25
|
*/
|
|
26
26
|
export declare function valueEqual(a: unknown, b: unknown): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* A value's canonical string: key-sorted at every depth, so two spellings of one value
|
|
29
|
+
* mint one key whatever order each side's producer wrote them in. What the key-minting
|
|
30
|
+
* half of the seam stringifies with, for the reason the equalities deep-compare.
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -62,3 +62,22 @@ function deepEqual(a, b) {
|
|
|
62
62
|
}
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* A value's canonical string: key-sorted at every depth, so two spellings of one value
|
|
67
|
+
* mint one key whatever order each side's producer wrote them in. What the key-minting
|
|
68
|
+
* half of the seam stringifies with, for the reason the equalities deep-compare.
|
|
69
|
+
*/
|
|
70
|
+
export function canonicalJson(value) {
|
|
71
|
+
return JSON.stringify(canonical(value));
|
|
72
|
+
}
|
|
73
|
+
function canonical(v) {
|
|
74
|
+
if (Array.isArray(v))
|
|
75
|
+
return v.map(canonical);
|
|
76
|
+
if (typeof v !== 'object' || v === null)
|
|
77
|
+
return v;
|
|
78
|
+
const o = v;
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const k of Object.keys(o).sort())
|
|
81
|
+
out[k] = canonical(o[k]);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { Schema } from 'prosemirror-model';
|
|
2
|
-
|
|
3
|
-
* Whether a link carrying `href` renders as one. A value with no scheme is relative
|
|
4
|
-
* to the embedding page and has none to refuse.
|
|
5
|
-
*
|
|
6
|
-
* A refused href is unchanged: it stays on the mark and round-trips, so a document
|
|
7
|
-
* survives an editor that declines to make it clickable.
|
|
8
|
-
*/
|
|
9
|
-
export declare function rendersHref(href: string): boolean;
|
|
2
|
+
import type { NodeType } from 'prosemirror-model';
|
|
10
3
|
/** The full field schema: every block kind, container, mark, and island. */
|
|
11
4
|
export declare const blockSchema: Schema<string, string>;
|
|
12
5
|
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
@@ -28,3 +21,7 @@ export declare const plaintextSchema: Schema<string, never>;
|
|
|
28
21
|
export declare function hasMarks(schema: Schema): boolean;
|
|
29
22
|
/** True for the constrained inline schema (no block containers); decode branches on it. */
|
|
30
23
|
export declare function isInlineSchema(schema: Schema): boolean;
|
|
24
|
+
/** Whether `type` can hold a within-block line break, which a heading cannot (§`heading`).
|
|
25
|
+
* What a `\n` landing in a textblock is rewritten to turns on it (`breaks.ts`), and so
|
|
26
|
+
* does what decode puts between a block's continued segments. */
|
|
27
|
+
export declare function takesLineBreak(type: NodeType): boolean;
|