@thomasfosterau/effect-richtext 0.0.0-stage → 0.1.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 +48 -0
- package/README.md +118 -2
- package/dist/_virtual/_rolldown/runtime.js +12 -0
- package/dist/commands/commands.d.ts +323 -0
- package/dist/commands/commands.js +722 -0
- package/dist/commands/internal.js +43 -0
- package/dist/commands/keymaps.d.ts +46 -0
- package/dist/commands/keymaps.js +64 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +3 -0
- package/dist/compat/commands.d.ts +250 -0
- package/dist/compat/commands.js +275 -0
- package/dist/compat/internal/core.d.ts +20 -0
- package/dist/compat/internal/core.js +148 -0
- package/dist/compat/internal/props.js +82 -0
- package/dist/compat/keymap.d.ts +21 -0
- package/dist/compat/keymap.js +38 -0
- package/dist/compat/model.d.ts +893 -0
- package/dist/compat/model.js +1212 -0
- package/dist/compat/orderedmap.d.ts +74 -0
- package/dist/compat/orderedmap.js +124 -0
- package/dist/compat/schema-basic.d.ts +38 -0
- package/dist/compat/schema-basic.js +238 -0
- package/dist/compat/schema-list.d.ts +81 -0
- package/dist/compat/schema-list.js +156 -0
- package/dist/compat/state.d.ts +415 -0
- package/dist/compat/state.js +689 -0
- package/dist/compat/test-builder.d.ts +98 -0
- package/dist/compat/test-builder.js +197 -0
- package/dist/compat/transform.d.ts +458 -0
- package/dist/compat/transform.js +803 -0
- package/dist/compat/view.d.ts +411 -0
- package/dist/compat/view.js +409 -0
- package/dist/dropcursor.d.ts +46 -0
- package/dist/dropcursor.js +176 -0
- package/dist/extensions/defaults.d.ts +716 -0
- package/dist/extensions/defaults.js +209 -0
- package/dist/extensions/editor.d.ts +246 -0
- package/dist/extensions/editor.js +271 -0
- package/dist/extensions/extension.d.ts +375 -0
- package/dist/extensions/extension.js +233 -0
- package/dist/extensions.d.ts +4 -0
- package/dist/extensions.js +4 -0
- package/dist/gapcursor.d.ts +66 -0
- package/dist/gapcursor.js +273 -0
- package/dist/history.d.ts +178 -0
- package/dist/history.js +412 -0
- package/dist/html.d.ts +158 -0
- package/dist/html.js +164 -0
- package/dist/inputrules.d.ts +271 -0
- package/dist/inputrules.js +321 -0
- package/dist/internal/attempt.js +15 -0
- package/dist/internal/json.js +29 -0
- package/dist/keymap/keymap.d.ts +130 -0
- package/dist/keymap/keymap.js +159 -0
- package/dist/keymap.d.ts +2 -0
- package/dist/keymap.js +1 -0
- package/dist/markdown/errors.d.ts +23 -0
- package/dist/markdown/errors.js +26 -0
- package/dist/markdown/from_markdown.d.ts +185 -0
- package/dist/markdown/from_markdown.js +266 -0
- package/dist/markdown/schema.d.ts +522 -0
- package/dist/markdown/schema.js +292 -0
- package/dist/markdown/to_markdown.d.ts +251 -0
- package/dist/markdown/to_markdown.js +413 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +5 -0
- package/dist/model/attrsCodec.js +48 -0
- package/dist/model/comparedeep.js +17 -0
- package/dist/model/content.d.ts +232 -0
- package/dist/model/content.js +630 -0
- package/dist/model/diff.js +66 -0
- package/dist/model/dom.d.ts +3 -0
- package/dist/model/errors.d.ts +90 -0
- package/dist/model/errors.js +76 -0
- package/dist/model/fragment.d.ts +131 -0
- package/dist/model/fragment.js +262 -0
- package/dist/model/from_dom.d.ts +299 -0
- package/dist/model/from_dom.js +618 -0
- package/dist/model/json.d.ts +37 -0
- package/dist/model/json.js +126 -0
- package/dist/model/mark.d.ts +73 -0
- package/dist/model/mark.js +121 -0
- package/dist/model/node.d.ts +267 -0
- package/dist/model/node.js +434 -0
- package/dist/model/replace.d.ts +78 -0
- package/dist/model/replace.js +207 -0
- package/dist/model/resolvedpos.d.ts +196 -0
- package/dist/model/resolvedpos.js +305 -0
- package/dist/model/schema.d.ts +740 -0
- package/dist/model/schema.js +451 -0
- package/dist/model/to_dom.d.ts +169 -0
- package/dist/model/to_dom.js +238 -0
- package/dist/model.d.ts +12 -0
- package/dist/model.js +12 -0
- package/dist/placeholder.d.ts +53 -0
- package/dist/placeholder.js +79 -0
- package/dist/reactive/observer.d.ts +54 -0
- package/dist/reactive/observer.js +61 -0
- package/dist/reactive/selectors.d.ts +22 -0
- package/dist/reactive/selectors.js +61 -0
- package/dist/reactive/stateref.d.ts +72 -0
- package/dist/reactive/stateref.js +71 -0
- package/dist/reactive/streams.d.ts +83 -0
- package/dist/reactive/streams.js +85 -0
- package/dist/reactive.d.ts +16 -0
- package/dist/reactive.js +26 -0
- package/dist/schema-basic.d.ts +448 -0
- package/dist/schema-basic.js +269 -0
- package/dist/schema-list/commands.d.ts +66 -0
- package/dist/schema-list/commands.js +203 -0
- package/dist/schema-list.d.ts +92 -0
- package/dist/schema-list.js +100 -0
- package/dist/state/errors.d.ts +39 -0
- package/dist/state/errors.js +32 -0
- package/dist/state/plugin.d.ts +232 -0
- package/dist/state/plugin.js +82 -0
- package/dist/state/selection.d.ts +297 -0
- package/dist/state/selection.js +486 -0
- package/dist/state/state.d.ts +212 -0
- package/dist/state/state.js +489 -0
- package/dist/state/transaction.d.ts +145 -0
- package/dist/state/transaction.js +195 -0
- package/dist/state.d.ts +6 -0
- package/dist/state.js +6 -0
- package/dist/test-builder.d.ts +3582 -0
- package/dist/test-builder.js +239 -0
- package/dist/transform/attr_step.d.ts +60 -0
- package/dist/transform/attr_step.js +105 -0
- package/dist/transform/errors.d.ts +50 -0
- package/dist/transform/errors.js +35 -0
- package/dist/transform/map.d.ts +182 -0
- package/dist/transform/map.js +280 -0
- package/dist/transform/mark.js +98 -0
- package/dist/transform/mark_set_step.js +69 -0
- package/dist/transform/mark_step.d.ts +113 -0
- package/dist/transform/mark_step.js +225 -0
- package/dist/transform/replace.d.ts +18 -0
- package/dist/transform/replace.js +381 -0
- package/dist/transform/replace_step.d.ts +109 -0
- package/dist/transform/replace_step.js +175 -0
- package/dist/transform/step.d.ts +157 -0
- package/dist/transform/step.js +168 -0
- package/dist/transform/structure.d.ts +89 -0
- package/dist/transform/structure.js +350 -0
- package/dist/transform/transform.d.ts +248 -0
- package/dist/transform/transform.js +373 -0
- package/dist/transform.d.ts +10 -0
- package/dist/transform.js +10 -0
- package/dist/view/browser.js +39 -0
- package/dist/view/capturekeys.js +282 -0
- package/dist/view/clipboard.d.ts +8 -0
- package/dist/view/clipboard.js +245 -0
- package/dist/view/decoration.d.ts +339 -0
- package/dist/view/decoration.js +566 -0
- package/dist/view/dom.d.ts +18 -0
- package/dist/view/dom.js +140 -0
- package/dist/view/domchange.js +295 -0
- package/dist/view/domcoords.d.ts +22 -0
- package/dist/view/domcoords.js +448 -0
- package/dist/view/domobserver.d.ts +47 -0
- package/dist/view/domobserver.js +328 -0
- package/dist/view/errors.d.ts +18 -0
- package/dist/view/errors.js +21 -0
- package/dist/view/input.d.ts +58 -0
- package/dist/view/input.js +704 -0
- package/dist/view/selection.js +198 -0
- package/dist/view/view.d.ts +699 -0
- package/dist/view/view.js +623 -0
- package/dist/view/viewdesc.d.ts +236 -0
- package/dist/view/viewdesc.js +1205 -0
- package/dist/view-css.d.ts +43 -0
- package/dist/view-css.js +124 -0
- package/dist/view.d.ts +6 -0
- package/dist/view.js +4 -0
- package/package.json +206 -3
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { attempt } from "../internal/attempt.js";
|
|
2
|
+
import { showJson } from "../internal/json.js";
|
|
3
|
+
import { encodeAttrs } from "./attrsCodec.js";
|
|
4
|
+
import { compareDeep } from "./comparedeep.js";
|
|
5
|
+
import { ContentError, PositionError, isModelError } from "./errors.js";
|
|
6
|
+
import { Fragment } from "./fragment.js";
|
|
7
|
+
import { Mark } from "./mark.js";
|
|
8
|
+
import { Slice, replace } from "./replace.js";
|
|
9
|
+
import { ResolvedPos } from "./resolvedpos.js";
|
|
10
|
+
import * as Option from "effect/Option";
|
|
11
|
+
import * as Equal from "effect/Equal";
|
|
12
|
+
import * as Hash from "effect/Hash";
|
|
13
|
+
// src/model/node.ts
|
|
14
|
+
const emptyAttrs = Object.create(null);
|
|
15
|
+
const isPositionError = (u) => u instanceof PositionError;
|
|
16
|
+
/**
|
|
17
|
+
* This class represents a node in the tree that makes up a document.
|
|
18
|
+
* So a document is an instance of `Node`, with children that are also
|
|
19
|
+
* instances of `Node`.
|
|
20
|
+
*
|
|
21
|
+
* Nodes are persistent data structures. Instead of changing them, you
|
|
22
|
+
* create new ones with the content you want. Old ones keep pointing at
|
|
23
|
+
* the old document shape. This is made cheaper by sharing structure
|
|
24
|
+
* between the old and new data as much as possible.
|
|
25
|
+
*
|
|
26
|
+
* The type parameters track the schema and the node type name (or
|
|
27
|
+
* union of names), which determines the type of `attrs` and of the
|
|
28
|
+
* node's children. Use `is` to narrow a node to a specific type.
|
|
29
|
+
*
|
|
30
|
+
* Nodes implement `Equal`, comparing structurally.
|
|
31
|
+
*
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
* @category models
|
|
34
|
+
*/
|
|
35
|
+
var Node = class Node {
|
|
36
|
+
/** @internal */
|
|
37
|
+
constructor(type, attrs, content, marks = Mark.none) {
|
|
38
|
+
this.type = type;
|
|
39
|
+
this.attrs = attrs;
|
|
40
|
+
this.marks = marks;
|
|
41
|
+
this.content = content || Fragment.empty;
|
|
42
|
+
}
|
|
43
|
+
/** Narrow this node to the given node type(s). */
|
|
44
|
+
is(...names) {
|
|
45
|
+
return names.indexOf(this.type.name) > -1;
|
|
46
|
+
}
|
|
47
|
+
/** Narrow this node to a text node. */
|
|
48
|
+
isTextNode() {
|
|
49
|
+
return this.type.isText;
|
|
50
|
+
}
|
|
51
|
+
/** The array of this node's child nodes. */
|
|
52
|
+
get children() {
|
|
53
|
+
return this.content.content;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The size of this node, as defined by the integer-based indexing
|
|
57
|
+
* scheme. For text nodes, this is the amount of characters. For other
|
|
58
|
+
* leaf nodes, it is one. For non-leaf nodes, it is the size of the
|
|
59
|
+
* content plus two (the start and end token).
|
|
60
|
+
*/
|
|
61
|
+
get nodeSize() {
|
|
62
|
+
return this.isLeaf ? 1 : 2 + this.content.size;
|
|
63
|
+
}
|
|
64
|
+
/** The number of children that the node has. */
|
|
65
|
+
get childCount() {
|
|
66
|
+
return this.content.childCount;
|
|
67
|
+
}
|
|
68
|
+
/** Get the child node at the given index. Fails when the index is out of range. */
|
|
69
|
+
child(index) {
|
|
70
|
+
return this.content.child(index);
|
|
71
|
+
}
|
|
72
|
+
/** Like `child`, but throws a `PositionError` when the index is out of range. */
|
|
73
|
+
childUnsafe(index) {
|
|
74
|
+
return this.content.childUnsafe(index);
|
|
75
|
+
}
|
|
76
|
+
/** Get the child node at the given index, if it exists. */
|
|
77
|
+
maybeChild(index) {
|
|
78
|
+
return this.content.maybeChild(index);
|
|
79
|
+
}
|
|
80
|
+
/** Call `f` for every child node, passing the node, its offset into this parent node, and its index. */
|
|
81
|
+
forEach(f) {
|
|
82
|
+
this.content.forEach(f);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Invoke a callback for all descendant nodes recursively overlapping
|
|
86
|
+
* the given two positions that are relative to start of this node's
|
|
87
|
+
* content. This includes all ancestors of the nodes containing the
|
|
88
|
+
* two positions. The callback is invoked with the node, its position
|
|
89
|
+
* relative to the original node (method receiver), its parent node,
|
|
90
|
+
* and its child index. When the callback returns false for a given
|
|
91
|
+
* node, that node's children will not be recursed over. The last
|
|
92
|
+
* parameter can be used to specify a starting position to count
|
|
93
|
+
* from.
|
|
94
|
+
*/
|
|
95
|
+
nodesBetween(from, to, f, startPos = 0) {
|
|
96
|
+
this.content.nodesBetween(from, to, f, startPos, this);
|
|
97
|
+
}
|
|
98
|
+
/** Call the given callback for every descendant node. Doesn't descend into a node when the callback returns `false`. */
|
|
99
|
+
descendants(f) {
|
|
100
|
+
this.nodesBetween(0, this.content.size, f);
|
|
101
|
+
}
|
|
102
|
+
/** Concatenates all the text nodes found in this node and its children. */
|
|
103
|
+
get textContent() {
|
|
104
|
+
return this.isLeaf && this.type.spec.leafText ? this.type.spec.leafText(this) : this.textBetween(0, this.content.size, "");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get all text between positions `from` and `to`. When
|
|
108
|
+
* `blockSeparator` is given, it will be inserted to separate text from
|
|
109
|
+
* different block nodes. If `leafText` is given, it'll be inserted for
|
|
110
|
+
* every non-text leaf node encountered, otherwise the spec's
|
|
111
|
+
* `leafText` will be used.
|
|
112
|
+
*/
|
|
113
|
+
textBetween(from, to, blockSeparator, leafText) {
|
|
114
|
+
return this.content.textBetween(from, to, blockSeparator, leafText);
|
|
115
|
+
}
|
|
116
|
+
/** Returns this node's first child, if any. */
|
|
117
|
+
get firstChild() {
|
|
118
|
+
return this.content.firstChild;
|
|
119
|
+
}
|
|
120
|
+
/** Returns this node's last child, if any. */
|
|
121
|
+
get lastChild() {
|
|
122
|
+
return this.content.lastChild;
|
|
123
|
+
}
|
|
124
|
+
/** Test whether two nodes represent the same piece of document. */
|
|
125
|
+
eq(other) {
|
|
126
|
+
return this === other || this.sameMarkup(other) && this.content.eq(other.content);
|
|
127
|
+
}
|
|
128
|
+
/** @internal */
|
|
129
|
+
[Equal.symbol](that) {
|
|
130
|
+
return that instanceof Node && this.eq(that);
|
|
131
|
+
}
|
|
132
|
+
/** @internal */
|
|
133
|
+
[Hash.symbol]() {
|
|
134
|
+
return Hash.combine(Hash.string(this.type.name))(Hash.combine(Hash.structure(this.attrs))(Hash.combine(Hash.array(this.marks))(Hash.hash(this.content))));
|
|
135
|
+
}
|
|
136
|
+
/** Compare the markup (type, attributes, and marks) of this node to those of another. */
|
|
137
|
+
sameMarkup(other) {
|
|
138
|
+
return this.hasMarkup(other.type, other.attrs, other.marks);
|
|
139
|
+
}
|
|
140
|
+
/** Check whether this node's markup correspond to the given type, attributes, and marks. */
|
|
141
|
+
hasMarkup(type, attrs, marks) {
|
|
142
|
+
return this.type === type && compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) && Mark.sameSet(this.marks, marks || Mark.none);
|
|
143
|
+
}
|
|
144
|
+
/** Create a new node with the same markup as this node, containing the given content (or empty, if no content is given). */
|
|
145
|
+
copy(content = null) {
|
|
146
|
+
if (content === this.content) return this;
|
|
147
|
+
return new Node(this.type, this.attrs, content, this.marks);
|
|
148
|
+
}
|
|
149
|
+
/** Create a copy of this node, with the given set of marks instead of the node's own marks. */
|
|
150
|
+
mark(marks) {
|
|
151
|
+
return marks === this.marks ? this : new Node(this.type, this.attrs, this.content, marks);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create a copy of this node with only the content between the given
|
|
155
|
+
* positions. If `to` is not given, it defaults to the end of the node.
|
|
156
|
+
*/
|
|
157
|
+
cut(from, to = this.content.size) {
|
|
158
|
+
if (from === 0 && to === this.content.size) return this;
|
|
159
|
+
return this.copy(this.content.cut(from, to));
|
|
160
|
+
}
|
|
161
|
+
/** Cut out the part of the document between the given positions, and return it as a `Slice`. */
|
|
162
|
+
slice(from, to = this.content.size, includeParents = false) {
|
|
163
|
+
return attempt(() => this.sliceUnsafe(from, to, includeParents), isPositionError);
|
|
164
|
+
}
|
|
165
|
+
/** Like `slice`, but throws a `PositionError` for invalid positions. */
|
|
166
|
+
sliceUnsafe(from, to = this.content.size, includeParents = false) {
|
|
167
|
+
if (from === to) return Slice.empty;
|
|
168
|
+
const $from = this.resolveUnsafe(from), $to = this.resolveUnsafe(to);
|
|
169
|
+
const depth = includeParents ? 0 : $from.sharedDepth(to);
|
|
170
|
+
const start = $from.start(depth);
|
|
171
|
+
const content = $from.node(depth).content.cut($from.pos - start, $to.pos - start);
|
|
172
|
+
return new Slice(content, $from.depth - depth, $to.depth - depth);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Replace the part of the document between the given positions with
|
|
176
|
+
* the given slice. The slice must 'fit', meaning its open sides must
|
|
177
|
+
* be able to connect to the surrounding content, and its content
|
|
178
|
+
* nodes must be valid children for the node they are placed into. If
|
|
179
|
+
* any of this is violated, the effect fails with a `ReplaceError`.
|
|
180
|
+
*/
|
|
181
|
+
replace(from, to, slice) {
|
|
182
|
+
return attempt(() => this.replaceUnsafe(from, to, slice), isModelError);
|
|
183
|
+
}
|
|
184
|
+
/** Like `replace`, but throws on failure. */
|
|
185
|
+
replaceUnsafe(from, to, slice) {
|
|
186
|
+
return replace(this.resolveUnsafe(from), this.resolveUnsafe(to), slice);
|
|
187
|
+
}
|
|
188
|
+
/** Find the node directly after the given position, if any. */
|
|
189
|
+
nodeAt(pos) {
|
|
190
|
+
return Option.fromNullishOr(this.nodeAtOrNull(pos));
|
|
191
|
+
}
|
|
192
|
+
/** @internal */
|
|
193
|
+
nodeAtOrNull(pos) {
|
|
194
|
+
if (pos < 0 || pos > this.content.size) return null;
|
|
195
|
+
for (let node = this;;) {
|
|
196
|
+
const { index, offset } = node.content.findIndex(pos);
|
|
197
|
+
node = node.content.content[index];
|
|
198
|
+
if (!node) return null;
|
|
199
|
+
if (offset === pos || node.isText) return node;
|
|
200
|
+
pos -= offset + 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Find the (direct) child node after the given offset, if any, and
|
|
205
|
+
* return it along with its index and offset relative to this node.
|
|
206
|
+
*/
|
|
207
|
+
childAfter(pos) {
|
|
208
|
+
const { index, offset } = this.content.findIndex(pos);
|
|
209
|
+
return {
|
|
210
|
+
node: this.content.maybeChild(index),
|
|
211
|
+
index,
|
|
212
|
+
offset
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Find the (direct) child node before the given offset, if any, and
|
|
217
|
+
* return it along with its index and offset relative to this node.
|
|
218
|
+
*/
|
|
219
|
+
childBefore(pos) {
|
|
220
|
+
if (pos === 0) return {
|
|
221
|
+
node: Option.none(),
|
|
222
|
+
index: 0,
|
|
223
|
+
offset: 0
|
|
224
|
+
};
|
|
225
|
+
const { index, offset } = this.content.findIndex(pos);
|
|
226
|
+
if (offset < pos) return {
|
|
227
|
+
node: Option.some(this.content.content[index]),
|
|
228
|
+
index,
|
|
229
|
+
offset
|
|
230
|
+
};
|
|
231
|
+
const node = this.content.content[index - 1];
|
|
232
|
+
return {
|
|
233
|
+
node: Option.some(node),
|
|
234
|
+
index: index - 1,
|
|
235
|
+
offset: offset - node.nodeSize
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the given position in the document, returning an object
|
|
240
|
+
* with information about its context.
|
|
241
|
+
*/
|
|
242
|
+
resolve(pos) {
|
|
243
|
+
return attempt(() => this.resolveUnsafe(pos), isPositionError);
|
|
244
|
+
}
|
|
245
|
+
/** Like `resolve`, but throws a `PositionError` for out-of-range positions. */
|
|
246
|
+
resolveUnsafe(pos) {
|
|
247
|
+
return ResolvedPos.resolveCached(this, pos);
|
|
248
|
+
}
|
|
249
|
+
/** @internal */
|
|
250
|
+
resolveNoCache(pos) {
|
|
251
|
+
return ResolvedPos.resolve(this, pos);
|
|
252
|
+
}
|
|
253
|
+
/** Test whether a given mark or mark type occurs in this document between the two given positions. */
|
|
254
|
+
rangeHasMark(from, to, type) {
|
|
255
|
+
let found = false;
|
|
256
|
+
if (to > from) this.nodesBetween(from, to, (node) => {
|
|
257
|
+
if (type instanceof Mark ? type.isInSet(node.marks) : type.findInSet(node.marks)) found = true;
|
|
258
|
+
return !found;
|
|
259
|
+
});
|
|
260
|
+
return found;
|
|
261
|
+
}
|
|
262
|
+
/** True when this is a block (non-inline node) */
|
|
263
|
+
get isBlock() {
|
|
264
|
+
return this.type.isBlock;
|
|
265
|
+
}
|
|
266
|
+
/** True when this is a textblock node, a block node with inline content. */
|
|
267
|
+
get isTextblock() {
|
|
268
|
+
return this.type.isTextblock;
|
|
269
|
+
}
|
|
270
|
+
/** True when this node allows inline content. */
|
|
271
|
+
get inlineContent() {
|
|
272
|
+
return this.type.inlineContent;
|
|
273
|
+
}
|
|
274
|
+
/** True when this is an inline node (a text node or a node that can appear among text). */
|
|
275
|
+
get isInline() {
|
|
276
|
+
return this.type.isInline;
|
|
277
|
+
}
|
|
278
|
+
/** True when this is a text node. */
|
|
279
|
+
get isText() {
|
|
280
|
+
return this.type.isText;
|
|
281
|
+
}
|
|
282
|
+
/** True when this is a leaf node. */
|
|
283
|
+
get isLeaf() {
|
|
284
|
+
return this.type.isLeaf;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* True when this is an atom, i.e. when it does not have directly
|
|
288
|
+
* editable content. This is usually the same as `isLeaf`, but can be
|
|
289
|
+
* configured with the `atom` property on a node's spec.
|
|
290
|
+
*/
|
|
291
|
+
get isAtom() {
|
|
292
|
+
return this.type.isAtom;
|
|
293
|
+
}
|
|
294
|
+
/** Return a string representation of this node for debugging purposes. */
|
|
295
|
+
toString() {
|
|
296
|
+
if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this);
|
|
297
|
+
let name = this.type.name;
|
|
298
|
+
if (this.content.size) name += "(" + this.content.toStringInner() + ")";
|
|
299
|
+
return wrapMarks(this.marks, name);
|
|
300
|
+
}
|
|
301
|
+
/** Get the content match in this node at the given index. */
|
|
302
|
+
contentMatchAt(index) {
|
|
303
|
+
return attempt(() => this.contentMatchAtUnsafe(index), (u) => u instanceof ContentError);
|
|
304
|
+
}
|
|
305
|
+
/** Like `contentMatchAt`, but throws when the node's content is invalid. */
|
|
306
|
+
contentMatchAtUnsafe(index) {
|
|
307
|
+
const match = this.type.contentMatch.matchFragmentOrNull(this.content, 0, index);
|
|
308
|
+
if (!match) throw new ContentError({ message: "Called contentMatchAt on a node with invalid content" });
|
|
309
|
+
return match;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Test whether replacing the range between `from` and `to` (by child
|
|
313
|
+
* index) with the given replacement fragment (which defaults to the
|
|
314
|
+
* empty fragment) would leave the node's content valid. You can
|
|
315
|
+
* optionally pass `start` and `end` indices into the replacement
|
|
316
|
+
* fragment.
|
|
317
|
+
*/
|
|
318
|
+
canReplace(from, to, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
|
|
319
|
+
const one = this.contentMatchAtUnsafe(from).matchFragmentOrNull(replacement, start, end);
|
|
320
|
+
const two = one && one.matchFragmentOrNull(this.content, to);
|
|
321
|
+
if (!two || !two.validEnd) return false;
|
|
322
|
+
for (let i = start; i < end; i++) if (!this.type.allowsMarks(replacement.content[i].marks)) return false;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Test whether replacing the range `from` to `to` (by index) with a
|
|
327
|
+
* node of the given type would leave the node's content valid.
|
|
328
|
+
*/
|
|
329
|
+
canReplaceWith(from, to, type, marks) {
|
|
330
|
+
if (marks && !this.type.allowsMarks(marks)) return false;
|
|
331
|
+
const start = this.contentMatchAtUnsafe(from).matchTypeOrNull(type);
|
|
332
|
+
const end = start && start.matchFragmentOrNull(this.content, to);
|
|
333
|
+
return end ? end.validEnd : false;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Test whether the given node's content could be appended to this
|
|
337
|
+
* node. If that node is empty, this will only return true if there is
|
|
338
|
+
* at least one node type that can appear in both nodes (to avoid
|
|
339
|
+
* merging completely incompatible nodes).
|
|
340
|
+
*/
|
|
341
|
+
canAppend(other) {
|
|
342
|
+
if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content);
|
|
343
|
+
return this.type.compatibleContent(other.type);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Check whether this node and its descendants conform to the schema
|
|
347
|
+
* (including attribute schemas), failing with a `ContentError` or
|
|
348
|
+
* `AttributeError` when they do not.
|
|
349
|
+
*/
|
|
350
|
+
check() {
|
|
351
|
+
return attempt(() => this.checkUnsafe(), isModelError);
|
|
352
|
+
}
|
|
353
|
+
/** Like `check`, but throws on failure. */
|
|
354
|
+
checkUnsafe() {
|
|
355
|
+
this.type.checkContent(this.content);
|
|
356
|
+
this.type.checkAttrs(this.attrs);
|
|
357
|
+
let copy = Mark.none;
|
|
358
|
+
for (let i = 0; i < this.marks.length; i++) {
|
|
359
|
+
const mark = this.marks[i];
|
|
360
|
+
mark.type.checkAttrs(mark.attrs);
|
|
361
|
+
copy = mark.addToSet(copy);
|
|
362
|
+
}
|
|
363
|
+
if (!Mark.sameSet(copy, this.marks)) throw new ContentError({ message: `Invalid collection of marks for node ${this.type.name}: ${this.marks.map((m) => m.type.name).join(",")}` });
|
|
364
|
+
this.content.forEach((node) => node.checkUnsafe());
|
|
365
|
+
}
|
|
366
|
+
/** Return a JSON-serializable representation of this node. */
|
|
367
|
+
toJSON() {
|
|
368
|
+
const obj = { type: this.type.name };
|
|
369
|
+
for (const _ in this.attrs) {
|
|
370
|
+
obj.attrs = this.type.identityAttrs ? this.attrs : encodeAttrs(this.type.attrs, this.attrs);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (this.content.size) obj.content = this.content.toJSON();
|
|
374
|
+
if (this.marks.length) obj.marks = this.marks.map((n) => n.toJSON());
|
|
375
|
+
return obj;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* A text node. Text nodes are always leaves and carry their text in
|
|
380
|
+
* `text`.
|
|
381
|
+
*
|
|
382
|
+
* @since 0.1.0
|
|
383
|
+
* @category models
|
|
384
|
+
*/
|
|
385
|
+
var TextNode = class TextNode extends Node {
|
|
386
|
+
/** @internal */
|
|
387
|
+
constructor(type, attrs, content, marks) {
|
|
388
|
+
super(type, attrs, null, marks);
|
|
389
|
+
if (!content) throw new ContentError({ message: "Empty text nodes are not allowed" });
|
|
390
|
+
this.text = content;
|
|
391
|
+
}
|
|
392
|
+
toString() {
|
|
393
|
+
if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this);
|
|
394
|
+
return wrapMarks(this.marks, showJson(this.text));
|
|
395
|
+
}
|
|
396
|
+
get textContent() {
|
|
397
|
+
return this.text;
|
|
398
|
+
}
|
|
399
|
+
textBetween(from, to) {
|
|
400
|
+
return this.text.slice(from, to);
|
|
401
|
+
}
|
|
402
|
+
get nodeSize() {
|
|
403
|
+
return this.text.length;
|
|
404
|
+
}
|
|
405
|
+
mark(marks) {
|
|
406
|
+
return marks === this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks);
|
|
407
|
+
}
|
|
408
|
+
/** Create a copy of this text node with different text. */
|
|
409
|
+
withText(text) {
|
|
410
|
+
if (text === this.text) return this;
|
|
411
|
+
return new TextNode(this.type, this.attrs, text, this.marks);
|
|
412
|
+
}
|
|
413
|
+
cut(from = 0, to = this.text.length) {
|
|
414
|
+
if (from === 0 && to === this.text.length) return this;
|
|
415
|
+
return this.withText(this.text.slice(from, to));
|
|
416
|
+
}
|
|
417
|
+
eq(other) {
|
|
418
|
+
return this.sameMarkup(other) && other instanceof TextNode && this.text === other.text;
|
|
419
|
+
}
|
|
420
|
+
[Hash.symbol]() {
|
|
421
|
+
return Hash.combine(Hash.string(this.text))(Hash.array(this.marks));
|
|
422
|
+
}
|
|
423
|
+
toJSON() {
|
|
424
|
+
return {
|
|
425
|
+
...super.toJSON(),
|
|
426
|
+
text: this.text
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
function wrapMarks(marks, str) {
|
|
431
|
+
for (let i = marks.length - 1; i >= 0; i--) str = marks[i].type.name + "(" + str + ")";
|
|
432
|
+
return str;
|
|
433
|
+
}
|
|
434
|
+
export { Node, TextNode };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment } from "./fragment.js";
|
|
2
|
+
import { Node, NodeJSON } from "./node.js";
|
|
3
|
+
import { ResolvedPos } from "./resolvedpos.js";
|
|
4
|
+
import { AnySchema } from "./schema.js";
|
|
5
|
+
import * as Equal from "effect/Equal";
|
|
6
|
+
import * as Hash from "effect/Hash";
|
|
7
|
+
/**
|
|
8
|
+
* The JSON representation of a slice.
|
|
9
|
+
*
|
|
10
|
+
* @since 0.1.0
|
|
11
|
+
* @category models
|
|
12
|
+
*/
|
|
13
|
+
export interface SliceJSON {
|
|
14
|
+
readonly content: ReadonlyArray<NodeJSON>;
|
|
15
|
+
readonly openStart?: number;
|
|
16
|
+
readonly openEnd?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A slice represents a piece cut out of a larger document. It stores
|
|
20
|
+
* not only a fragment, but also the depth up to which nodes on both
|
|
21
|
+
* side are ‘open’ (cut through).
|
|
22
|
+
*
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
* @category models
|
|
25
|
+
*/
|
|
26
|
+
export declare class Slice<Sc extends AnySchema = AnySchema> implements Equal.Equal {
|
|
27
|
+
/** The slice's content. */
|
|
28
|
+
readonly content: Fragment<Sc>;
|
|
29
|
+
/** The open depth at the start of the fragment. */
|
|
30
|
+
readonly openStart: number;
|
|
31
|
+
/** The open depth at the end. */
|
|
32
|
+
readonly openEnd: number;
|
|
33
|
+
/**
|
|
34
|
+
* Create a slice. When specifying a non-zero open depth, you must
|
|
35
|
+
* make sure that there are nodes of at least that depth at the
|
|
36
|
+
* appropriate side of the fragment—i.e. if the fragment is an empty
|
|
37
|
+
* paragraph node, `openStart` and `openEnd` can't be greater than 1.
|
|
38
|
+
*
|
|
39
|
+
* It is not necessary for the content of open nodes to conform to the
|
|
40
|
+
* schema's content constraints, though it should be a valid
|
|
41
|
+
* start/end/middle for such a node, depending on which sides are
|
|
42
|
+
* open.
|
|
43
|
+
*/
|
|
44
|
+
constructor(
|
|
45
|
+
/** The slice's content. */
|
|
46
|
+
content: Fragment<Sc>,
|
|
47
|
+
/** The open depth at the start of the fragment. */
|
|
48
|
+
openStart: number,
|
|
49
|
+
/** The open depth at the end. */
|
|
50
|
+
openEnd: number);
|
|
51
|
+
/** The size this slice would add when inserted into a document. */
|
|
52
|
+
get size(): number;
|
|
53
|
+
/** @internal */
|
|
54
|
+
insertAt(pos: number, fragment: Fragment<Sc>): Slice<Sc> | null;
|
|
55
|
+
/** @internal */
|
|
56
|
+
removeBetween(from: number, to: number): Slice<Sc>;
|
|
57
|
+
/** Tests whether this slice is equal to another slice. */
|
|
58
|
+
eq(other: Slice<any>): boolean;
|
|
59
|
+
/** @internal */
|
|
60
|
+
[Equal.symbol](that: Equal.Equal): boolean;
|
|
61
|
+
/** @internal */
|
|
62
|
+
[Hash.symbol](): number;
|
|
63
|
+
/** @internal */
|
|
64
|
+
toString(): string;
|
|
65
|
+
/** Convert a slice to a JSON-serializable representation. */
|
|
66
|
+
toJSON(): SliceJSON | null;
|
|
67
|
+
/** @internal */
|
|
68
|
+
static fromJSONUnsafe<Sc extends AnySchema>(schema: Sc, json: any): Slice<Sc>;
|
|
69
|
+
/**
|
|
70
|
+
* Create a slice from a fragment by taking the maximum possible open
|
|
71
|
+
* value on both side of the fragment.
|
|
72
|
+
*/
|
|
73
|
+
static maxOpen<Sc extends AnySchema>(fragment: Fragment<Sc>, openIsolating?: boolean): Slice<Sc>;
|
|
74
|
+
/** The empty slice. */
|
|
75
|
+
static readonly empty: Slice<any>;
|
|
76
|
+
}
|
|
77
|
+
/** @internal */
|
|
78
|
+
export declare function replace<Sc extends AnySchema>($from: ResolvedPos<Sc>, $to: ResolvedPos<Sc>, slice: Slice<Sc>): Node<Sc>;
|