@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
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ const doc = quill.seedDocument();
|
|
|
40
40
|
|
|
41
41
|
// An EXISTING document: parsed back from what you stored.
|
|
42
42
|
const doc = Document.fromMarkdown(markdown); // canonical Quillmark markdown
|
|
43
|
-
const doc = Document.
|
|
43
|
+
const doc = Document.fromStored(json); // the versioned storage DTO (`doc.toStored()`)
|
|
44
44
|
|
|
45
45
|
const session = await new Engine().open(quill, doc);
|
|
46
46
|
// free() on teardown the handles you MINTED: this quill, this doc, this session.
|
package/THEMING.md
CHANGED
|
@@ -24,6 +24,8 @@ Nothing to import and nothing to set. The package pulls its own stylesheet, and
|
|
|
24
24
|
|
|
25
25
|
**Placing and sizing**: each surface carries a stable root class (`.qm-editor`, `.qm-preview`), so you can place it from your own stylesheet without a wrapper. Each also takes a `class` prop that merges onto the same element, which is the better handle when you have one to give.
|
|
26
26
|
|
|
27
|
+
A surface's width is the one you give it. Each contains its own inline size, so what a document holds — a table is as wide as its columns — scrolls inside the mount rather than widening the column the mount stands in.
|
|
28
|
+
|
|
27
29
|
**Taking the column back**: our selectors compute at specificity zero (below), so `padding: 0` or a `background` of your own on `.qm-editor` wins outright. Nothing is locked.
|
|
28
30
|
|
|
29
31
|
## Make it yours
|
|
@@ -121,7 +123,7 @@ import '@quillmark/svelte/preset';
|
|
|
121
123
|
```html
|
|
122
124
|
<body class="qm-page">
|
|
123
125
|
<p class="qm-label">quill</p>
|
|
124
|
-
<pre class="qm-readout">
|
|
126
|
+
<pre class="qm-readout">showcase@1.0.0</pre>
|
|
125
127
|
</body>
|
|
126
128
|
```
|
|
127
129
|
|
|
@@ -191,7 +193,7 @@ A blanket `:focus-visible` of yours outranks ours, including on the controls tha
|
|
|
191
193
|
|
|
192
194
|
## What is deliberately not public
|
|
193
195
|
|
|
194
|
-
The
|
|
196
|
+
The two root classes and `.qm-pane` are the whole class contract on the surfaces. Every other `qm-*` class inside them is internal and renames without notice: a class contract over the interior would freeze the DOM shape the surfaces are free to re-cut, and the dials already reach the values a restyle wants. The preset's classes are a contract of their own: they land on _your_ DOM, so they freeze nothing of ours.
|
|
195
197
|
|
|
196
198
|
The derived surface scale (surface / border / ink rungs, the blur radius, the popover's translucency ratio, the recede-opacity ladder, the two leading rungs, the overlay ring widths, the scroll tail) is internal on the same terms, and declared **on** each root element, so setting a rung from an ancestor does nothing. A dial appears when a real consumer needs one; every dial is one more thing a reader has to hold.
|
|
197
199
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Selection } from 'prosemirror-state';
|
|
2
2
|
import { chainCommands, splitBlock } from 'prosemirror-commands';
|
|
3
3
|
import { canJoin, findWrapping } from 'prosemirror-transform';
|
|
4
|
+
import { takesLineBreak } from './schema.js';
|
|
4
5
|
/** The head of a textblock: where a shorthand fires, and where a pick lands once its
|
|
5
6
|
* run is consumed. */
|
|
6
7
|
function atHead(state) {
|
|
@@ -33,10 +34,29 @@ function retype(name, attrs) {
|
|
|
33
34
|
const { $from } = state.selection;
|
|
34
35
|
if (!type || !atHead(state) || !fits($from, type))
|
|
35
36
|
return false;
|
|
36
|
-
|
|
37
|
+
const tr = state.tr;
|
|
38
|
+
spaceBreaks(tr, $from, type);
|
|
39
|
+
dispatch?.(tr.setBlockType($from.pos, $from.pos, type, attrs));
|
|
37
40
|
return true;
|
|
38
41
|
};
|
|
39
42
|
}
|
|
43
|
+
/** Replace every `hard_break` in the caret's block with a space, ahead of a retype into
|
|
44
|
+
* a type that holds none. `setBlockType` clears what the new type cannot hold by
|
|
45
|
+
* deleting it, which joins the text either side with nothing and glues the words; a
|
|
46
|
+
* space is what the break becomes at every other door (`breaks.ts`, `decode.ts`). A
|
|
47
|
+
* break and a space are both one position wide, so an earlier replacement leaves every
|
|
48
|
+
* later one addressed. */
|
|
49
|
+
function spaceBreaks(tr, $from, type) {
|
|
50
|
+
const br = tr.doc.type.schema.nodes.hard_break;
|
|
51
|
+
if (!br || takesLineBreak(type))
|
|
52
|
+
return;
|
|
53
|
+
let at = $from.start();
|
|
54
|
+
$from.parent.forEach((child) => {
|
|
55
|
+
if (child.type === br)
|
|
56
|
+
tr.replaceWith(at, at + child.nodeSize, type.schema.text(' ', type.allowedMarks(child.marks)));
|
|
57
|
+
at += child.nodeSize;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
40
60
|
/** `wrappingInputRule`'s body: wrap the block range at `pos`, then join a preceding
|
|
41
61
|
* sibling of the same type — the one boundary the wrap itself opens (`lists.ts`
|
|
42
62
|
* §cleanup: command-local, never a global pass). */
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { Schema } from 'prosemirror-model';
|
|
2
|
-
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { Plugin, type Command } from 'prosemirror-state';
|
|
3
3
|
/**
|
|
4
|
-
* Rewrite every `\n` a transaction leaves in a non-`code` textblock to a `hard_break
|
|
4
|
+
* Rewrite every `\n` a transaction leaves in a non-`code` textblock: to a `hard_break`
|
|
5
|
+
* where the block takes one, to a space where it does not.
|
|
5
6
|
*
|
|
6
7
|
* Reads the schema rather than a flag: the inline schemas declare neither node, and
|
|
7
8
|
* nothing puts a `\n` in one — a paste parses its whitespace away and the keys type
|
|
8
9
|
* characters.
|
|
9
10
|
*/
|
|
10
11
|
export declare function linebreakPlugin(schema: Schema): Plugin;
|
|
12
|
+
/**
|
|
13
|
+
* The line-break link of the body's key chains: `{}` for the inline schemas, which
|
|
14
|
+
* declare no break node. A `richtext(inline)` field is one line by construction
|
|
15
|
+
* (`Content::is_inline`), so there the key is swallowed beside Enter (`field.ts`)
|
|
16
|
+
* rather than answered here.
|
|
17
|
+
*/
|
|
18
|
+
export declare function breakKeymap(schema: Schema): Record<string, Command>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { Plugin } from 'prosemirror-state';
|
|
1
|
+
import { Plugin, TextSelection } from 'prosemirror-state';
|
|
2
|
+
import { takesLineBreak } from './schema.js';
|
|
2
3
|
/**
|
|
3
|
-
* Rewrite every `\n` a transaction leaves in a non-`code` textblock to a `hard_break
|
|
4
|
+
* Rewrite every `\n` a transaction leaves in a non-`code` textblock: to a `hard_break`
|
|
5
|
+
* where the block takes one, to a space where it does not.
|
|
4
6
|
*
|
|
5
7
|
* Reads the schema rather than a flag: the inline schemas declare neither node, and
|
|
6
8
|
* nothing puts a `\n` in one — a paste parses its whitespace away and the keys type
|
|
@@ -13,25 +15,79 @@ export function linebreakPlugin(schema) {
|
|
|
13
15
|
if (!br || !trs.some((tr) => tr.docChanged))
|
|
14
16
|
return null;
|
|
15
17
|
const at = [];
|
|
16
|
-
state.doc.descendants((node, pos) => {
|
|
18
|
+
state.doc.descendants((node, pos, parent) => {
|
|
17
19
|
// A code block's newlines are its content (`whitespace: 'pre'`).
|
|
18
20
|
if (node.type.spec.code)
|
|
19
21
|
return false;
|
|
20
|
-
if (!node.isText)
|
|
22
|
+
if (!node.isText || !parent)
|
|
21
23
|
return true;
|
|
24
|
+
// A space carries the marks the `\n` it stands in for was under, the way
|
|
25
|
+
// PM's own `clearIncompatible` fills one: without them a bolded break
|
|
26
|
+
// leaves an unbolded space, and one mark becomes two in the store.
|
|
27
|
+
const fill = takesLineBreak(parent.type)
|
|
28
|
+
? br.create()
|
|
29
|
+
: schema.text(' ', parent.type.allowedMarks(node.marks));
|
|
22
30
|
const text = node.text ?? '';
|
|
23
31
|
for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1))
|
|
24
|
-
at.push(pos + i);
|
|
32
|
+
at.push({ pos: pos + i, fill });
|
|
25
33
|
return true;
|
|
26
34
|
});
|
|
27
35
|
if (!at.length)
|
|
28
36
|
return null;
|
|
29
|
-
// A break and the `\n`
|
|
30
|
-
// replacement leaves every later one addressed.
|
|
37
|
+
// A break, a space and the `\n` either replaces are all one position wide, so
|
|
38
|
+
// an earlier replacement leaves every later one addressed.
|
|
31
39
|
const tr = state.tr;
|
|
32
|
-
for (const
|
|
33
|
-
tr.replaceWith(pos, pos + 1,
|
|
40
|
+
for (const it of at)
|
|
41
|
+
tr.replaceWith(it.pos, it.pos + 1, it.fill);
|
|
34
42
|
return tr;
|
|
35
43
|
}
|
|
36
44
|
});
|
|
37
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Shift-Enter: a line break inside the caret's own block — the `continues` line a hard
|
|
48
|
+
* break is, never a second block.
|
|
49
|
+
*
|
|
50
|
+
* A selection that is not a text range is swallowed outright: an island is the subject of
|
|
51
|
+
* the next command, never a thing armed for replacement (`atoms.ts`). Otherwise the
|
|
52
|
+
* caret's own block decides. It **declines** in a code block, where the newline Enter
|
|
53
|
+
* takes already *is* that line and the code link binds the key one surface in
|
|
54
|
+
* (`code.ts`); it **swallows** the key where the block holds no break (`takesLineBreak`),
|
|
55
|
+
* a heading being the one of those; everywhere else it inserts the break.
|
|
56
|
+
*
|
|
57
|
+
* **Swallowing is the guard, not a spelling of declining.** A key the leaf declines is
|
|
58
|
+
* the browser's, a contenteditable answers this one with a `<br>`, and a heading that
|
|
59
|
+
* merely declined would take the space a `<br>` parses to.
|
|
60
|
+
*
|
|
61
|
+
* The break takes no marks and the caret keeps them: `decode` mints an unmarked break,
|
|
62
|
+
* so an inherited mark would leave the leaf holding a shape a re-hydrate swaps out
|
|
63
|
+
* (CODEC §Encode, one spelling of each shape), while dropping the marks from the
|
|
64
|
+
* *caret* would end a bold run at a line break the author only meant to wrap at.
|
|
65
|
+
*/
|
|
66
|
+
function insertBreak(schema) {
|
|
67
|
+
const br = schema.nodes.hard_break;
|
|
68
|
+
return (state, dispatch) => {
|
|
69
|
+
if (!(state.selection instanceof TextSelection))
|
|
70
|
+
return true;
|
|
71
|
+
const { parent } = state.selection.$from;
|
|
72
|
+
if (parent.type.spec.code)
|
|
73
|
+
return false;
|
|
74
|
+
if (!takesLineBreak(parent.type))
|
|
75
|
+
return true;
|
|
76
|
+
if (dispatch) {
|
|
77
|
+
const marks = state.storedMarks ?? state.selection.$from.marks();
|
|
78
|
+
dispatch(state.tr.replaceSelectionWith(br.create(), false).setStoredMarks(marks).scrollIntoView());
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The line-break link of the body's key chains: `{}` for the inline schemas, which
|
|
85
|
+
* declare no break node. A `richtext(inline)` field is one line by construction
|
|
86
|
+
* (`Content::is_inline`), so there the key is swallowed beside Enter (`field.ts`)
|
|
87
|
+
* rather than answered here.
|
|
88
|
+
*/
|
|
89
|
+
export function breakKeymap(schema) {
|
|
90
|
+
if (!schema.nodes.hard_break)
|
|
91
|
+
return {};
|
|
92
|
+
return { 'Shift-Enter': insertBreak(schema) };
|
|
93
|
+
}
|
|
@@ -6,9 +6,11 @@ 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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
10
|
+
* the same command because it is the same edit: the line break the key means
|
|
11
|
+
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
12
|
+
* Every command here declines away from a code block — inside one for the four keys
|
|
13
|
+
* about its text, against one for the two about its edge — so the list links keep the
|
|
14
|
+
* keys elsewhere.
|
|
13
15
|
*/
|
|
14
16
|
export declare function codeKeymap(schema: Schema): Record<string, Command>;
|
package/dist/core/codec/code.js
CHANGED
|
@@ -158,10 +158,12 @@ function retypeAtProseEdge(side) {
|
|
|
158
158
|
*
|
|
159
159
|
* `Enter` is upstream's `newlineInCode`, and it is load-bearing rather than
|
|
160
160
|
* cosmetic: inside a `list_item > code_block` the list link's `splitListItem`
|
|
161
|
-
* otherwise splits the item in two, each half holding a code block.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
162
|
+
* the same command because it is the same edit: the line break the key means
|
|
163
|
+
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
164
|
+
* Every command here declines away from a code block — inside one for the four keys
|
|
165
|
+
* about its text, against one for the two about its edge — so the list links keep the
|
|
166
|
+
* keys elsewhere.
|
|
165
167
|
*/
|
|
166
168
|
export function codeKeymap(schema) {
|
|
167
169
|
if (!schema.nodes.code_block)
|
|
@@ -170,6 +172,7 @@ export function codeKeymap(schema) {
|
|
|
170
172
|
Tab: indentInCode,
|
|
171
173
|
'Shift-Tab': outdentInCode,
|
|
172
174
|
Enter: newlineInCode,
|
|
175
|
+
'Shift-Enter': newlineInCode,
|
|
173
176
|
Backspace: retypeAtProseEdge(-1),
|
|
174
177
|
Delete: retypeAtProseEdge(1)
|
|
175
178
|
};
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
11
|
import { ISLAND_SLOT } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
|
-
import { hasMarks, isInlineSchema } from './schema.js';
|
|
14
|
-
import { isAnchorMark, isCodeLine, isHeadingLine, isListItemContainer } from '@quillmark/wasm';
|
|
13
|
+
import { hasMarks, isInlineSchema, takesLineBreak } from './schema.js';
|
|
15
14
|
/** Code points of `s` (USV units): the iteration granularity the content speaks. */
|
|
16
15
|
export function codePoints(s) {
|
|
17
16
|
return Array.from(s);
|
|
@@ -50,7 +49,7 @@ export function decode(rt, schema) {
|
|
|
50
49
|
// plaintext field that acquired one anyway — an older build's popover, a hand-
|
|
51
50
|
// written document — is exactly the content that must open and then heal on the
|
|
52
51
|
// next commit, not throw here.
|
|
53
|
-
const marks = hasMarks(schema) ? rt.marks.filter((m) =>
|
|
52
|
+
const marks = hasMarks(schema) ? rt.marks.filter((m) => m.type !== 'anchor') : [];
|
|
54
53
|
const cursor = { i: 0, rt };
|
|
55
54
|
if (isInlineSchema(schema)) {
|
|
56
55
|
return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
|
|
@@ -96,22 +95,21 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
96
95
|
continue;
|
|
97
96
|
}
|
|
98
97
|
const here = path[depth];
|
|
99
|
-
|
|
100
|
-
// narrow, so the two arms this schema builds are claimed by the boundary's
|
|
101
|
-
// guard and a literal check, and everything else is an unknown the inert
|
|
102
|
-
// wrapper carries (CODEC §Open sets); no branch degrades silently.
|
|
103
|
-
if (isListItemContainer(here)) {
|
|
98
|
+
if (here.container === 'list_item') {
|
|
104
99
|
// Gather the maximal run of sibling `list_item` leaves at this depth
|
|
105
100
|
// (same ordered/start), then split it into items by `ordinal`.
|
|
106
|
-
const { ordered, start
|
|
101
|
+
const { ordered, start } = here.attrs;
|
|
102
|
+
const instance = instanceOf(here);
|
|
107
103
|
let j = i + 1;
|
|
108
104
|
while (j < leaves.length) {
|
|
109
105
|
const c = atDepth(leaves[j], depth);
|
|
110
|
-
if (!c ||
|
|
106
|
+
if (!c || c.container !== 'list_item')
|
|
107
|
+
break;
|
|
108
|
+
if (c.attrs.ordered !== ordered || c.attrs.start !== start)
|
|
111
109
|
break;
|
|
112
110
|
// `instance` is the boundary between two adjacent lists; the normalizer
|
|
113
111
|
// numbers a run's `ordinal`s gaplessly from 0, so a reset carries none.
|
|
114
|
-
if (c
|
|
112
|
+
if (instanceOf(c) !== instance)
|
|
115
113
|
break;
|
|
116
114
|
j++;
|
|
117
115
|
}
|
|
@@ -131,9 +129,11 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
131
129
|
i = j;
|
|
132
130
|
continue;
|
|
133
131
|
}
|
|
134
|
-
// quote and
|
|
135
|
-
//
|
|
136
|
-
//
|
|
132
|
+
// The quote, and the whole of what is left: `satisfies` is the exhaustiveness
|
|
133
|
+
// check, so a container added upstream is a compile error here rather than a
|
|
134
|
+
// blockquote it is not. A wrapper over the run of leaves carrying the identical
|
|
135
|
+
// container here, identity by `containerKey`.
|
|
136
|
+
here.container;
|
|
137
137
|
const key = containerKey(here);
|
|
138
138
|
let j = i + 1;
|
|
139
139
|
while (j < leaves.length) {
|
|
@@ -143,9 +143,7 @@ function groupBlocks(schema, leaves, depth, marks, cursor) {
|
|
|
143
143
|
j++;
|
|
144
144
|
}
|
|
145
145
|
const inner = groupBlocks(schema, leaves.slice(i, j), depth + 1, marks, cursor);
|
|
146
|
-
out.push(
|
|
147
|
-
? schema.nodes.blockquote.create(null, inner)
|
|
148
|
-
: schema.nodes.unknown_container.create({ container: here.container, attrs: 'attrs' in here ? here.attrs : null }, inner));
|
|
146
|
+
out.push(schema.nodes.blockquote.create(null, inner));
|
|
149
147
|
i = j;
|
|
150
148
|
}
|
|
151
149
|
return out;
|
|
@@ -154,61 +152,73 @@ function atDepth(leaf, depth) {
|
|
|
154
152
|
return leaf.containers[depth];
|
|
155
153
|
}
|
|
156
154
|
/** The `ordinal` of a leaf's `list_item` at `depth`: only called inside a run the
|
|
157
|
-
*
|
|
155
|
+
* discriminant has already established, so a miss is unreachable. */
|
|
158
156
|
function ordinalAt(leaf, depth) {
|
|
159
157
|
const c = atDepth(leaf, depth);
|
|
160
|
-
return c
|
|
158
|
+
return c?.container === 'list_item' ? c.attrs.ordinal : -1;
|
|
159
|
+
}
|
|
160
|
+
/** A container's `instance`, absent meaning the zero a canonical read omits. It is what
|
|
161
|
+
* tells one container from an adjacent sibling of identical shape, which contiguity
|
|
162
|
+
* alone reads as one, so absent and `0` have to answer alike. */
|
|
163
|
+
function instanceOf(c) {
|
|
164
|
+
return c.instance ?? 0;
|
|
161
165
|
}
|
|
162
|
-
/** Identity of a container for run gathering: its name
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* shape, which contiguity alone reads as one. Every arm but `list_item` (which has its
|
|
166
|
-
* own run rule) keys here. */
|
|
166
|
+
/** Identity of a container for run gathering: its name and its `instance`, NUL-joined
|
|
167
|
+
* as `markKey` joins a mark's. `list_item` carries its own run rule, so the quote is
|
|
168
|
+
* what keys here. */
|
|
167
169
|
function containerKey(c) {
|
|
168
|
-
|
|
169
|
-
return 'attrs' in c ? `${id}\u0000${JSON.stringify(c.attrs)}` : id;
|
|
170
|
+
return `${c.container}\u0000${instanceOf(c)}`;
|
|
170
171
|
}
|
|
171
|
-
/** A single leaf block node
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* compare literally, and anything left is unknown. */
|
|
172
|
+
/** A single leaf block node from its segments. Exhaustive over the closed line
|
|
173
|
+
* vocabulary, so a kind added upstream is a compile error rather than a silent
|
|
174
|
+
* flattening to `para` that the next commit would store. */
|
|
175
175
|
function makeLeaf(schema, leaf, marks, cursor) {
|
|
176
176
|
const line = leaf.line;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
177
|
+
switch (line.kind) {
|
|
178
|
+
case 'rule':
|
|
179
|
+
return schema.nodes.horizontal_rule.create();
|
|
180
|
+
case 'island': {
|
|
181
|
+
const attrs = islandAttrs(cursor);
|
|
182
|
+
return attrs ? schema.nodes.island_block.create(attrs) : schema.nodes.paragraph.create();
|
|
183
|
+
}
|
|
184
|
+
case 'code': {
|
|
185
|
+
// One code_block: the segments' texts joined by literal `\n`, no marks.
|
|
186
|
+
const text = leaf.segments.map((s) => s.text).join('\n');
|
|
187
|
+
const content = text.length ? [schema.text(text)] : [];
|
|
188
|
+
return schema.nodes.code_block.create({ lang: line.attrs?.lang ?? null }, content);
|
|
189
|
+
}
|
|
190
|
+
case 'para':
|
|
191
|
+
case 'heading': {
|
|
192
|
+
// Inline content, the segment boundary a `hard_break` in a paragraph and a space
|
|
193
|
+
// in a heading, which takes none (`schema.ts` §`takesLineBreak`). A stored heading
|
|
194
|
+
// carries no continuation — the store clears one after a block of a single line —
|
|
195
|
+
// so the space is what keeps a hand-built content decodable.
|
|
196
|
+
const type = line.kind === 'heading' ? schema.nodes.heading : schema.nodes.paragraph;
|
|
197
|
+
const inline = [];
|
|
198
|
+
leaf.segments.forEach((seg, idx) => {
|
|
199
|
+
if (idx > 0)
|
|
200
|
+
inline.push(takesLineBreak(type) ? schema.nodes.hard_break.create() : schema.text(' '));
|
|
201
|
+
inline.push(...buildInline(schema, seg.text, seg.startUSV, marks, cursor, false));
|
|
202
|
+
});
|
|
203
|
+
return line.kind === 'heading'
|
|
204
|
+
? type.create({ level: line.attrs.level }, inline)
|
|
205
|
+
: type.create(null, inline);
|
|
206
|
+
}
|
|
207
|
+
default: {
|
|
208
|
+
const unreached = line;
|
|
209
|
+
return unreached;
|
|
210
|
+
}
|
|
197
211
|
}
|
|
198
|
-
// An unknown kind renders as a paragraph and rides on it, so it re-encodes
|
|
199
|
-
// verbatim rather than flattening to `para` on the field's first edit.
|
|
200
|
-
const unknown = line.kind === 'para' ? null : { kind: line.kind, attrs: 'attrs' in line ? line.attrs : null };
|
|
201
|
-
return schema.nodes.paragraph.create({ unknown }, inline);
|
|
202
212
|
}
|
|
203
213
|
/** Consume the next island entry as PM node attrs (text-order matched to slots):
|
|
204
214
|
* the whole entry, `loss` included, which an island edit has to write back
|
|
205
|
-
* (`islands.ts`).
|
|
206
|
-
*
|
|
215
|
+
* (`islands.ts`). `null` where a slot has no entry behind it, which is a malformed
|
|
216
|
+
* content: the slot draws an empty paragraph or nothing at all, so the decode stays
|
|
217
|
+
* total without minting a node whose `type` the vocabulary would refuse on the way
|
|
218
|
+
* back. */
|
|
207
219
|
function islandAttrs(cursor) {
|
|
208
220
|
const isl = cursor.rt.islands[cursor.i++];
|
|
209
|
-
|
|
210
|
-
return { id: '', islandType: '', props: null, loss: 'unrepresentable' };
|
|
211
|
-
return { id: isl.id, islandType: isl.type, props: isl.props, loss: isl.loss };
|
|
221
|
+
return isl ? { id: isl.id, islandType: isl.type, props: isl.props, loss: isl.loss } : null;
|
|
212
222
|
}
|
|
213
223
|
/**
|
|
214
224
|
* Inline nodes for one segment: split text into maximal runs of a constant mark
|
|
@@ -227,20 +237,39 @@ function buildInline(schema, segText, segStartUSV, marks, cursor, stripIslands)
|
|
|
227
237
|
out.push(schema.text(runText, runMarks));
|
|
228
238
|
runText = '';
|
|
229
239
|
};
|
|
240
|
+
// A mark set changes only where a mark starts or ends. `marks` is the field's whole
|
|
241
|
+
// list, so resolving one per code point costs the text against every mark in the
|
|
242
|
+
// document.
|
|
243
|
+
const segEndUSV = segStartUSV + cps.length;
|
|
244
|
+
const spanning = marks.filter((m) => m.start < segEndUSV && m.end > segStartUSV);
|
|
245
|
+
const edges = new Set();
|
|
246
|
+
for (const m of spanning) {
|
|
247
|
+
if (m.start > segStartUSV)
|
|
248
|
+
edges.add(m.start);
|
|
249
|
+
if (m.end < segEndUSV)
|
|
250
|
+
edges.add(m.end);
|
|
251
|
+
}
|
|
252
|
+
let active = [];
|
|
253
|
+
let key = '';
|
|
254
|
+
let unresolved = true;
|
|
230
255
|
for (let k = 0; k < cps.length; k++) {
|
|
231
256
|
const cp = cps[k];
|
|
232
257
|
const pos = segStartUSV + k;
|
|
233
258
|
if (cp === ISLAND_SLOT) {
|
|
234
259
|
flush();
|
|
235
260
|
const attrs = islandAttrs(cursor);
|
|
236
|
-
if (!stripIslands && schema.nodes.island_inline) {
|
|
261
|
+
if (attrs && !stripIslands && schema.nodes.island_inline) {
|
|
237
262
|
out.push(schema.nodes.island_inline.create(attrs));
|
|
238
263
|
}
|
|
239
264
|
runKey = '\0slot'; // force a fresh run after a slot
|
|
265
|
+
unresolved = true; // the slot's own position is skipped, edge or not
|
|
240
266
|
continue;
|
|
241
267
|
}
|
|
242
|
-
|
|
243
|
-
|
|
268
|
+
if (unresolved || edges.has(pos)) {
|
|
269
|
+
active = spanning.filter((m) => m.start <= pos && pos < m.end);
|
|
270
|
+
key = markSetKey(active);
|
|
271
|
+
unresolved = false;
|
|
272
|
+
}
|
|
244
273
|
if (key !== runKey && runText.length)
|
|
245
274
|
flush();
|
|
246
275
|
if (key !== runKey) {
|
|
@@ -21,8 +21,6 @@ export interface Scan {
|
|
|
21
21
|
marks: ContentMark[];
|
|
22
22
|
islands: ContentIsland[];
|
|
23
23
|
runs: PosRun[];
|
|
24
|
-
/** Total PM position at the end of the document content (doc.content.size). */
|
|
25
|
-
pmEnd: number;
|
|
26
24
|
/** Total USV length of `text`. */
|
|
27
25
|
usvEnd: number;
|
|
28
26
|
}
|
|
@@ -32,15 +30,14 @@ export interface Scan {
|
|
|
32
30
|
* source both `pmToContent` and the position map draw from.
|
|
33
31
|
*/
|
|
34
32
|
export declare function scanDoc(doc: PMNode): Scan;
|
|
33
|
+
/** The content projection a scan carries (drops the position runs): what a caller
|
|
34
|
+
* holding a `Scan` reads instead of walking the document a second time. */
|
|
35
|
+
export declare function scanContent(s: Scan): Content;
|
|
35
36
|
/** `pmToContent`: the pure inverse of decode (drops the position runs). */
|
|
36
37
|
export declare function pmToContent(doc: PMNode): Content;
|
|
37
38
|
interface LowerOpts {
|
|
38
|
-
/** Anchor decoration positions
|
|
39
|
-
|
|
40
|
-
id: string;
|
|
41
|
-
pos: number;
|
|
42
|
-
}[];
|
|
43
|
-
/** Anchor decoration positions after the edit, in final (new) USV coords. */
|
|
39
|
+
/** Anchor decoration positions after the edit, in final (new) USV coords. The
|
|
40
|
+
* pre-edit set is `oldRt`'s own, so only the projection's is passed in. */
|
|
44
41
|
newAnchors?: {
|
|
45
42
|
id: string;
|
|
46
43
|
pos: number;
|