@thomasfosterau/effect-richtext 0.0.0 → 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 +117 -1
- 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 +200 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { decodeAttrs } from "./attrsCodec.js";
|
|
2
|
+
import { JSONError, isModelError } from "./errors.js";
|
|
3
|
+
import { Fragment } from "./fragment.js";
|
|
4
|
+
import { Mark } from "./mark.js";
|
|
5
|
+
import { Node } from "./node.js";
|
|
6
|
+
import { Slice } from "./replace.js";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Predicate from "effect/Predicate";
|
|
10
|
+
import * as S from "effect/Schema";
|
|
11
|
+
import * as SchemaGetter from "effect/SchemaGetter";
|
|
12
|
+
import * as SchemaIssue from "effect/SchemaIssue";
|
|
13
|
+
// src/model/json.ts
|
|
14
|
+
/**
|
|
15
|
+
* JSON (de)serialization of documents, including Effect `Schema` codecs
|
|
16
|
+
* derived from a document schema.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
const fail = (message) => {
|
|
21
|
+
throw new JSONError({ message });
|
|
22
|
+
};
|
|
23
|
+
const decodeAttrsOrFail = (defs, attrs, kind, name) => {
|
|
24
|
+
if (attrs == null) return Option.none();
|
|
25
|
+
if (typeof attrs !== "object" || Array.isArray(attrs)) return fail(`Invalid attributes for ${kind} ${name}`);
|
|
26
|
+
try {
|
|
27
|
+
return Option.some(decodeAttrs(defs, attrs));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (isModelError(e)) throw e;
|
|
30
|
+
return fail(`Invalid attributes for ${kind} ${name}: ${Predicate.hasProperty(e, "message") ? String(e.message) : String(e)}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** @internal */
|
|
34
|
+
function markFromJSON(schema, json) {
|
|
35
|
+
if (!json || typeof json !== "object") fail("Invalid input for Mark.fromJSON");
|
|
36
|
+
const type = schema.marks[json.type];
|
|
37
|
+
if (!type) return fail(`There is no mark type ${json.type} in this schema`);
|
|
38
|
+
return type.createDecodedUnsafe(decodeAttrsOrFail(type.attrs, json.attrs, "mark", json.type));
|
|
39
|
+
}
|
|
40
|
+
/** @internal */
|
|
41
|
+
function nodeFromJSON(schema, json) {
|
|
42
|
+
if (!json || typeof json !== "object") fail("Invalid input for Node.fromJSON");
|
|
43
|
+
if (json.marks && !Array.isArray(json.marks)) fail("Invalid mark data for Node.fromJSON");
|
|
44
|
+
const marks = json.marks ? json.marks.map((m) => markFromJSON(schema, m)) : Mark.none;
|
|
45
|
+
if (json.type === "text") {
|
|
46
|
+
if (typeof json.text !== "string") fail("Invalid text node in JSON");
|
|
47
|
+
if (!json.text) fail("Empty text nodes are not allowed");
|
|
48
|
+
return schema.textUnsafe(json.text, marks);
|
|
49
|
+
}
|
|
50
|
+
const content = fragmentFromJSON(schema, json.content);
|
|
51
|
+
const type = schema.nodes[json.type];
|
|
52
|
+
if (!type) return fail("Unknown node type: " + json.type);
|
|
53
|
+
return type.createDecodedUnsafe(decodeAttrsOrFail(type.attrs, json.attrs, "node", json.type), content, marks);
|
|
54
|
+
}
|
|
55
|
+
/** @internal */
|
|
56
|
+
function fragmentFromJSON(schema, value) {
|
|
57
|
+
if (!value) return Fragment.empty;
|
|
58
|
+
if (!Array.isArray(value)) fail("Invalid input for Fragment.fromJSON");
|
|
59
|
+
return Fragment.fromArray(value.map((n) => nodeFromJSON(schema, n)));
|
|
60
|
+
}
|
|
61
|
+
/** @internal */
|
|
62
|
+
function sliceFromJSON(schema, value) {
|
|
63
|
+
return Slice.fromJSONUnsafe(schema, value);
|
|
64
|
+
}
|
|
65
|
+
const NodeJSONSchema = S.suspend(() => S.Struct({
|
|
66
|
+
type: S.String,
|
|
67
|
+
attrs: S.optionalKey(S.Record(S.String, S.Unknown)),
|
|
68
|
+
content: S.optionalKey(S.Array(NodeJSONSchema)),
|
|
69
|
+
marks: S.optionalKey(S.Array(S.Struct({
|
|
70
|
+
type: S.String,
|
|
71
|
+
attrs: S.optionalKey(S.Record(S.String, S.Unknown))
|
|
72
|
+
}))),
|
|
73
|
+
text: S.optionalKey(S.String)
|
|
74
|
+
}));
|
|
75
|
+
const wrap = (f) => Effect.suspend(() => {
|
|
76
|
+
try {
|
|
77
|
+
return Effect.succeed(f());
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return Effect.fail(new SchemaIssue.InvalidValue({ message: Predicate.hasProperty(e, "message") ? String(e.message) : String(e) }));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* An Effect `Schema` that decodes the JSON representation of a node
|
|
84
|
+
* into a `Node` of the given document schema (validating structure,
|
|
85
|
+
* content, and attributes), and encodes nodes back to JSON.
|
|
86
|
+
*
|
|
87
|
+
* ```ts skip-type-checking
|
|
88
|
+
* import { Schema } from "effect"
|
|
89
|
+
* const doc = yield* Schema.decodeUnknownEffect(NodeFromJSON(mySchema))(json)
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @since 0.1.0
|
|
93
|
+
* @category schemas
|
|
94
|
+
*/
|
|
95
|
+
const NodeFromJSON = (schema) => {
|
|
96
|
+
const target = S.declare((u) => u instanceof Node && u.type.schema === schema, { title: "Node" });
|
|
97
|
+
return NodeJSONSchema.pipe(S.decodeTo(target, {
|
|
98
|
+
decode: SchemaGetter.transformEffect((json) => wrap(() => {
|
|
99
|
+
const node = nodeFromJSON(schema, json);
|
|
100
|
+
node.checkUnsafe();
|
|
101
|
+
return node;
|
|
102
|
+
})),
|
|
103
|
+
encode: SchemaGetter.transform((node) => node.toJSON())
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* An Effect `Schema` for the top-level document of a schema: like
|
|
108
|
+
* `NodeFromJSON`, but also requires the decoded node to be of the
|
|
109
|
+
* schema's top node type.
|
|
110
|
+
*
|
|
111
|
+
* @since 0.1.0
|
|
112
|
+
* @category schemas
|
|
113
|
+
*/
|
|
114
|
+
const DocFromJSON = (schema) => {
|
|
115
|
+
const target = S.declare((u) => u instanceof Node && u.type === schema.topNodeType, { title: "Document" });
|
|
116
|
+
return NodeJSONSchema.pipe(S.decodeTo(target, {
|
|
117
|
+
decode: SchemaGetter.transformEffect((json) => wrap(() => {
|
|
118
|
+
const node = nodeFromJSON(schema, json);
|
|
119
|
+
if (node.type !== schema.topNodeType) fail(`Expected a ${schema.topNodeType.name} node, got ${node.type.name}`);
|
|
120
|
+
node.checkUnsafe();
|
|
121
|
+
return node;
|
|
122
|
+
})),
|
|
123
|
+
encode: SchemaGetter.transform((node) => node.toJSON())
|
|
124
|
+
}));
|
|
125
|
+
};
|
|
126
|
+
export { DocFromJSON, NodeFromJSON, fragmentFromJSON, markFromJSON, nodeFromJSON, sliceFromJSON };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { AnySchema, MarkAttrs, MarkNames, MarkType } from "./schema.js";
|
|
2
|
+
import * as Equal from "effect/Equal";
|
|
3
|
+
import * as Hash from "effect/Hash";
|
|
4
|
+
/**
|
|
5
|
+
* A mark is a piece of information that can be attached to a node,
|
|
6
|
+
* such as it being emphasized, in code font, or a link. It has a type
|
|
7
|
+
* and optionally a set of attributes that provide further information
|
|
8
|
+
* (such as the target of the link). Marks are created through a
|
|
9
|
+
* schema, which controls which types exist and which attributes they
|
|
10
|
+
* have.
|
|
11
|
+
*
|
|
12
|
+
* Marks implement `Equal`, comparing type and attributes.
|
|
13
|
+
*
|
|
14
|
+
* @since 0.1.0
|
|
15
|
+
* @category models
|
|
16
|
+
*/
|
|
17
|
+
export declare class Mark<Sc extends AnySchema = AnySchema, out M extends MarkNames<Sc> = MarkNames<Sc>> implements Equal.Equal {
|
|
18
|
+
/** The type of this mark. */
|
|
19
|
+
readonly type: MarkType<Sc, M>;
|
|
20
|
+
/** The attributes associated with this mark. */
|
|
21
|
+
readonly attrs: MarkAttrs<Sc, M>;
|
|
22
|
+
/** @internal */
|
|
23
|
+
constructor(
|
|
24
|
+
/** The type of this mark. */
|
|
25
|
+
type: MarkType<Sc, M>,
|
|
26
|
+
/** The attributes associated with this mark. */
|
|
27
|
+
attrs: MarkAttrs<Sc, M>);
|
|
28
|
+
/** Narrow this mark to a specific mark type. */
|
|
29
|
+
is<K extends MarkNames<Sc>>(name: K): this is Mark<Sc, K>;
|
|
30
|
+
/**
|
|
31
|
+
* Given a set of marks, create a new set which contains this one as
|
|
32
|
+
* well, in the right position. If this mark is already in the set,
|
|
33
|
+
* the set itself is returned. If any marks that are set to be
|
|
34
|
+
* exclusive with this mark are present, those are replaced by this
|
|
35
|
+
* one.
|
|
36
|
+
*/
|
|
37
|
+
addToSet<Mk extends Mark<any, any>>(set: ReadonlyArray<Mk>): ReadonlyArray<Mk | this>;
|
|
38
|
+
/**
|
|
39
|
+
* Remove this mark from the given set, returning a new set. If this
|
|
40
|
+
* mark is not in the set, the set itself is returned.
|
|
41
|
+
*/
|
|
42
|
+
removeFromSet<Mk extends Mark<any, any>>(set: ReadonlyArray<Mk>): ReadonlyArray<Mk>;
|
|
43
|
+
/** Test whether this mark is in the given set of marks. */
|
|
44
|
+
isInSet(set: ReadonlyArray<Mark<any, any>>): boolean;
|
|
45
|
+
/** Test whether this mark has the same type and attributes as another mark. */
|
|
46
|
+
eq(other: Mark<any, any>): boolean;
|
|
47
|
+
/** @internal */
|
|
48
|
+
[Equal.symbol](that: Equal.Equal): boolean;
|
|
49
|
+
/** @internal */
|
|
50
|
+
[Hash.symbol](): number;
|
|
51
|
+
/** Convert this mark to a JSON-serializable representation. */
|
|
52
|
+
toJSON(): MarkJSON;
|
|
53
|
+
/** @internal */
|
|
54
|
+
toString(): string;
|
|
55
|
+
/** Test whether two sets of marks are identical. */
|
|
56
|
+
static sameSet(a: ReadonlyArray<Mark<any, any>>, b: ReadonlyArray<Mark<any, any>>): boolean;
|
|
57
|
+
/** Create a properly sorted mark set from nothing, a single mark, or an unsorted array of marks. */
|
|
58
|
+
static setFrom<Mk extends Mark<any, any>>(marks?: Mk | ReadonlyArray<Mk> | null): ReadonlyArray<Mk>;
|
|
59
|
+
/** The empty set of marks. */
|
|
60
|
+
static readonly none: ReadonlyArray<Mark<any, any>>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The JSON representation of a mark.
|
|
64
|
+
*
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
* @category models
|
|
67
|
+
*/
|
|
68
|
+
export interface MarkJSON {
|
|
69
|
+
readonly type: string;
|
|
70
|
+
readonly attrs?: {
|
|
71
|
+
readonly [name: string]: unknown;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { encodeAttrs } from "./attrsCodec.js";
|
|
2
|
+
import { compareDeep } from "./comparedeep.js";
|
|
3
|
+
import * as Arr from "effect/Array";
|
|
4
|
+
import * as Order from "effect/Order";
|
|
5
|
+
import * as Equal from "effect/Equal";
|
|
6
|
+
import * as Hash from "effect/Hash";
|
|
7
|
+
// src/model/mark.ts
|
|
8
|
+
/**
|
|
9
|
+
* Marks.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
const byRank = (a, b) => Order.Number(a.type.rank, b.type.rank);
|
|
14
|
+
/**
|
|
15
|
+
* A mark is a piece of information that can be attached to a node,
|
|
16
|
+
* such as it being emphasized, in code font, or a link. It has a type
|
|
17
|
+
* and optionally a set of attributes that provide further information
|
|
18
|
+
* (such as the target of the link). Marks are created through a
|
|
19
|
+
* schema, which controls which types exist and which attributes they
|
|
20
|
+
* have.
|
|
21
|
+
*
|
|
22
|
+
* Marks implement `Equal`, comparing type and attributes.
|
|
23
|
+
*
|
|
24
|
+
* @since 0.1.0
|
|
25
|
+
* @category models
|
|
26
|
+
*/
|
|
27
|
+
var Mark = class Mark {
|
|
28
|
+
/** @internal */
|
|
29
|
+
constructor(type, attrs) {
|
|
30
|
+
this.type = type;
|
|
31
|
+
this.attrs = attrs;
|
|
32
|
+
}
|
|
33
|
+
/** Narrow this mark to a specific mark type. */
|
|
34
|
+
is(name) {
|
|
35
|
+
return this.type.name === name;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Given a set of marks, create a new set which contains this one as
|
|
39
|
+
* well, in the right position. If this mark is already in the set,
|
|
40
|
+
* the set itself is returned. If any marks that are set to be
|
|
41
|
+
* exclusive with this mark are present, those are replaced by this
|
|
42
|
+
* one.
|
|
43
|
+
*/
|
|
44
|
+
addToSet(set) {
|
|
45
|
+
let copy;
|
|
46
|
+
let placed = false;
|
|
47
|
+
for (let i = 0; i < set.length; i++) {
|
|
48
|
+
const other = set[i];
|
|
49
|
+
if (this.eq(other)) return set;
|
|
50
|
+
if (this.type.excludes(other.type)) {
|
|
51
|
+
if (!copy) copy = set.slice(0, i);
|
|
52
|
+
} else if (other.type.excludes(this.type)) return set;
|
|
53
|
+
else {
|
|
54
|
+
if (!placed && other.type.rank > this.type.rank) {
|
|
55
|
+
if (!copy) copy = set.slice(0, i);
|
|
56
|
+
copy.push(this);
|
|
57
|
+
placed = true;
|
|
58
|
+
}
|
|
59
|
+
if (copy) copy.push(other);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!copy) copy = set.slice();
|
|
63
|
+
if (!placed) copy.push(this);
|
|
64
|
+
return copy;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove this mark from the given set, returning a new set. If this
|
|
68
|
+
* mark is not in the set, the set itself is returned.
|
|
69
|
+
*/
|
|
70
|
+
removeFromSet(set) {
|
|
71
|
+
for (let i = 0; i < set.length; i++) if (this.eq(set[i])) return set.slice(0, i).concat(set.slice(i + 1));
|
|
72
|
+
return set;
|
|
73
|
+
}
|
|
74
|
+
/** Test whether this mark is in the given set of marks. */
|
|
75
|
+
isInSet(set) {
|
|
76
|
+
for (let i = 0; i < set.length; i++) if (this.eq(set[i])) return true;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/** Test whether this mark has the same type and attributes as another mark. */
|
|
80
|
+
eq(other) {
|
|
81
|
+
return this === other || this.type === other.type && compareDeep(this.attrs, other.attrs);
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
[Equal.symbol](that) {
|
|
85
|
+
return that instanceof Mark && this.eq(that);
|
|
86
|
+
}
|
|
87
|
+
/** @internal */
|
|
88
|
+
[Hash.symbol]() {
|
|
89
|
+
return Hash.combine(Hash.string(this.type.name))(Hash.structure(this.attrs));
|
|
90
|
+
}
|
|
91
|
+
/** Convert this mark to a JSON-serializable representation. */
|
|
92
|
+
toJSON() {
|
|
93
|
+
const obj = { type: this.type.name };
|
|
94
|
+
for (const _ in this.attrs) {
|
|
95
|
+
obj.attrs = this.type.identityAttrs ? this.attrs : encodeAttrs(this.type.attrs, this.attrs);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
/** @internal */
|
|
101
|
+
toString() {
|
|
102
|
+
return this.type.name;
|
|
103
|
+
}
|
|
104
|
+
/** Test whether two sets of marks are identical. */
|
|
105
|
+
static sameSet(a, b) {
|
|
106
|
+
if (a === b) return true;
|
|
107
|
+
if (a.length !== b.length) return false;
|
|
108
|
+
for (let i = 0; i < a.length; i++) if (!a[i].eq(b[i])) return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/** Create a properly sorted mark set from nothing, a single mark, or an unsorted array of marks. */
|
|
112
|
+
static setFrom(marks) {
|
|
113
|
+
if (!marks || Array.isArray(marks) && Arr.isReadonlyArrayEmpty(marks)) return Mark.none;
|
|
114
|
+
if (marks instanceof Mark) return [marks];
|
|
115
|
+
return Arr.sort(marks, byRank);
|
|
116
|
+
}
|
|
117
|
+
static {
|
|
118
|
+
this.none = [];
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
export { Mark };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { ContentMatch } from "./content.js";
|
|
2
|
+
import { AttributeError, ContentError, PositionError, ReplaceError } from "./errors.js";
|
|
3
|
+
import { Fragment, NodeVisitor } from "./fragment.js";
|
|
4
|
+
import { Mark, MarkJSON } from "./mark.js";
|
|
5
|
+
import { Slice } from "./replace.js";
|
|
6
|
+
import { ResolvedPos } from "./resolvedpos.js";
|
|
7
|
+
import { AnySchema, Attrs, ChildNames, MarkType, NodeAttrs, NodeNames, NodeType } from "./schema.js";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as Equal from "effect/Equal";
|
|
11
|
+
import * as Hash from "effect/Hash";
|
|
12
|
+
/**
|
|
13
|
+
* The JSON representation of a node.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
* @category models
|
|
17
|
+
*/
|
|
18
|
+
export interface NodeJSON {
|
|
19
|
+
readonly type: string;
|
|
20
|
+
readonly attrs?: {
|
|
21
|
+
readonly [name: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
readonly content?: ReadonlyArray<NodeJSON>;
|
|
24
|
+
readonly marks?: ReadonlyArray<MarkJSON>;
|
|
25
|
+
readonly text?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The result of `childAfter` / `childBefore`.
|
|
29
|
+
*
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
* @category models
|
|
32
|
+
*/
|
|
33
|
+
export interface ChildInfo<Sc extends AnySchema, N extends NodeNames<Sc>> {
|
|
34
|
+
readonly node: Option.Option<Node<Sc, N>>;
|
|
35
|
+
readonly index: number;
|
|
36
|
+
readonly offset: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* This class represents a node in the tree that makes up a document.
|
|
40
|
+
* So a document is an instance of `Node`, with children that are also
|
|
41
|
+
* instances of `Node`.
|
|
42
|
+
*
|
|
43
|
+
* Nodes are persistent data structures. Instead of changing them, you
|
|
44
|
+
* create new ones with the content you want. Old ones keep pointing at
|
|
45
|
+
* the old document shape. This is made cheaper by sharing structure
|
|
46
|
+
* between the old and new data as much as possible.
|
|
47
|
+
*
|
|
48
|
+
* The type parameters track the schema and the node type name (or
|
|
49
|
+
* union of names), which determines the type of `attrs` and of the
|
|
50
|
+
* node's children. Use `is` to narrow a node to a specific type.
|
|
51
|
+
*
|
|
52
|
+
* Nodes implement `Equal`, comparing structurally.
|
|
53
|
+
*
|
|
54
|
+
* @since 0.1.0
|
|
55
|
+
* @category models
|
|
56
|
+
*/
|
|
57
|
+
export declare class Node<Sc extends AnySchema = AnySchema, out N extends NodeNames<Sc> = NodeNames<Sc>> implements Equal.Equal {
|
|
58
|
+
/** The type of node that this is. */
|
|
59
|
+
readonly type: NodeType<Sc, N>;
|
|
60
|
+
/**
|
|
61
|
+
* An object mapping attribute names to values. The kind of
|
|
62
|
+
* attributes allowed and required are determined by the node type.
|
|
63
|
+
*/
|
|
64
|
+
readonly attrs: NodeAttrs<Sc, N>;
|
|
65
|
+
/** The marks applied to this node. */
|
|
66
|
+
readonly marks: ReadonlyArray<Mark<Sc>>;
|
|
67
|
+
/** A container holding the node's children. */
|
|
68
|
+
readonly content: Fragment<Sc, ChildNames<Sc, N>>;
|
|
69
|
+
/** @internal */
|
|
70
|
+
constructor(
|
|
71
|
+
/** The type of node that this is. */
|
|
72
|
+
type: NodeType<Sc, N>,
|
|
73
|
+
/**
|
|
74
|
+
* An object mapping attribute names to values. The kind of
|
|
75
|
+
* attributes allowed and required are determined by the node type.
|
|
76
|
+
*/
|
|
77
|
+
attrs: NodeAttrs<Sc, N>, content?: Fragment<Sc, ChildNames<Sc, N>> | null,
|
|
78
|
+
/** The marks applied to this node. */
|
|
79
|
+
marks?: ReadonlyArray<Mark<Sc>>);
|
|
80
|
+
/** Narrow this node to the given node type(s). */
|
|
81
|
+
is<K extends NodeNames<Sc>>(...names: ReadonlyArray<K>): this is Node<Sc, K>;
|
|
82
|
+
/** Narrow this node to a text node. */
|
|
83
|
+
isTextNode(): this is TextNode<Sc>;
|
|
84
|
+
/** The array of this node's child nodes. */
|
|
85
|
+
get children(): ReadonlyArray<Node<Sc, ChildNames<Sc, N>>>;
|
|
86
|
+
/**
|
|
87
|
+
* The size of this node, as defined by the integer-based indexing
|
|
88
|
+
* scheme. For text nodes, this is the amount of characters. For other
|
|
89
|
+
* leaf nodes, it is one. For non-leaf nodes, it is the size of the
|
|
90
|
+
* content plus two (the start and end token).
|
|
91
|
+
*/
|
|
92
|
+
get nodeSize(): number;
|
|
93
|
+
/** The number of children that the node has. */
|
|
94
|
+
get childCount(): number;
|
|
95
|
+
/** Get the child node at the given index. Fails when the index is out of range. */
|
|
96
|
+
child(index: number): Effect.Effect<Node<Sc, ChildNames<Sc, N>>, PositionError>;
|
|
97
|
+
/** Like `child`, but throws a `PositionError` when the index is out of range. */
|
|
98
|
+
childUnsafe(index: number): Node<Sc, ChildNames<Sc, N>>;
|
|
99
|
+
/** Get the child node at the given index, if it exists. */
|
|
100
|
+
maybeChild(index: number): Option.Option<Node<Sc, ChildNames<Sc, N>>>;
|
|
101
|
+
/** Call `f` for every child node, passing the node, its offset into this parent node, and its index. */
|
|
102
|
+
forEach(f: (node: Node<Sc, ChildNames<Sc, N>>, offset: number, index: number) => void): void;
|
|
103
|
+
/**
|
|
104
|
+
* Invoke a callback for all descendant nodes recursively overlapping
|
|
105
|
+
* the given two positions that are relative to start of this node's
|
|
106
|
+
* content. This includes all ancestors of the nodes containing the
|
|
107
|
+
* two positions. The callback is invoked with the node, its position
|
|
108
|
+
* relative to the original node (method receiver), its parent node,
|
|
109
|
+
* and its child index. When the callback returns false for a given
|
|
110
|
+
* node, that node's children will not be recursed over. The last
|
|
111
|
+
* parameter can be used to specify a starting position to count
|
|
112
|
+
* from.
|
|
113
|
+
*/
|
|
114
|
+
nodesBetween(from: number, to: number, f: NodeVisitor<Sc>, startPos?: number): void;
|
|
115
|
+
/** Call the given callback for every descendant node. Doesn't descend into a node when the callback returns `false`. */
|
|
116
|
+
descendants(f: NodeVisitor<Sc>): void;
|
|
117
|
+
/** Concatenates all the text nodes found in this node and its children. */
|
|
118
|
+
get textContent(): string;
|
|
119
|
+
/**
|
|
120
|
+
* Get all text between positions `from` and `to`. When
|
|
121
|
+
* `blockSeparator` is given, it will be inserted to separate text from
|
|
122
|
+
* different block nodes. If `leafText` is given, it'll be inserted for
|
|
123
|
+
* every non-text leaf node encountered, otherwise the spec's
|
|
124
|
+
* `leafText` will be used.
|
|
125
|
+
*/
|
|
126
|
+
textBetween(from: number, to: number, blockSeparator?: string | null, leafText?: null | string | ((leafNode: Node<Sc>) => string)): string;
|
|
127
|
+
/** Returns this node's first child, if any. */
|
|
128
|
+
get firstChild(): Option.Option<Node<Sc, ChildNames<Sc, N>>>;
|
|
129
|
+
/** Returns this node's last child, if any. */
|
|
130
|
+
get lastChild(): Option.Option<Node<Sc, ChildNames<Sc, N>>>;
|
|
131
|
+
/** Test whether two nodes represent the same piece of document. */
|
|
132
|
+
eq(other: Node<any, any>): boolean;
|
|
133
|
+
/** @internal */
|
|
134
|
+
[Equal.symbol](that: Equal.Equal): boolean;
|
|
135
|
+
/** @internal */
|
|
136
|
+
[Hash.symbol](): number;
|
|
137
|
+
/** Compare the markup (type, attributes, and marks) of this node to those of another. */
|
|
138
|
+
sameMarkup(other: Node<any, any>): boolean;
|
|
139
|
+
/** Check whether this node's markup correspond to the given type, attributes, and marks. */
|
|
140
|
+
hasMarkup(type: NodeType<any, any>, attrs?: Attrs | null, marks?: ReadonlyArray<Mark<any, any>>): boolean;
|
|
141
|
+
/** Create a new node with the same markup as this node, containing the given content (or empty, if no content is given). */
|
|
142
|
+
copy(content?: Fragment<Sc, ChildNames<Sc, N>> | null): Node<Sc, N>;
|
|
143
|
+
/** Create a copy of this node, with the given set of marks instead of the node's own marks. */
|
|
144
|
+
mark(marks: ReadonlyArray<Mark<Sc>>): Node<Sc, N>;
|
|
145
|
+
/**
|
|
146
|
+
* Create a copy of this node with only the content between the given
|
|
147
|
+
* positions. If `to` is not given, it defaults to the end of the node.
|
|
148
|
+
*/
|
|
149
|
+
cut(from: number, to?: number): Node<Sc, N>;
|
|
150
|
+
/** Cut out the part of the document between the given positions, and return it as a `Slice`. */
|
|
151
|
+
slice(from: number, to?: number, includeParents?: boolean): Effect.Effect<Slice<Sc>, PositionError>;
|
|
152
|
+
/** Like `slice`, but throws a `PositionError` for invalid positions. */
|
|
153
|
+
sliceUnsafe(from: number, to?: number, includeParents?: boolean): Slice<Sc>;
|
|
154
|
+
/**
|
|
155
|
+
* Replace the part of the document between the given positions with
|
|
156
|
+
* the given slice. The slice must 'fit', meaning its open sides must
|
|
157
|
+
* be able to connect to the surrounding content, and its content
|
|
158
|
+
* nodes must be valid children for the node they are placed into. If
|
|
159
|
+
* any of this is violated, the effect fails with a `ReplaceError`.
|
|
160
|
+
*/
|
|
161
|
+
replace(from: number, to: number, slice: Slice<Sc>): Effect.Effect<Node<Sc, N>, ReplaceError | PositionError | ContentError>;
|
|
162
|
+
/** Like `replace`, but throws on failure. */
|
|
163
|
+
replaceUnsafe(from: number, to: number, slice: Slice<Sc>): Node<Sc, N>;
|
|
164
|
+
/** Find the node directly after the given position, if any. */
|
|
165
|
+
nodeAt(pos: number): Option.Option<Node<Sc>>;
|
|
166
|
+
/** @internal */
|
|
167
|
+
nodeAtOrNull(pos: number): Node<Sc> | null;
|
|
168
|
+
/**
|
|
169
|
+
* Find the (direct) child node after the given offset, if any, and
|
|
170
|
+
* return it along with its index and offset relative to this node.
|
|
171
|
+
*/
|
|
172
|
+
childAfter(pos: number): ChildInfo<Sc, ChildNames<Sc, N>>;
|
|
173
|
+
/**
|
|
174
|
+
* Find the (direct) child node before the given offset, if any, and
|
|
175
|
+
* return it along with its index and offset relative to this node.
|
|
176
|
+
*/
|
|
177
|
+
childBefore(pos: number): ChildInfo<Sc, ChildNames<Sc, N>>;
|
|
178
|
+
/**
|
|
179
|
+
* Resolve the given position in the document, returning an object
|
|
180
|
+
* with information about its context.
|
|
181
|
+
*/
|
|
182
|
+
resolve(pos: number): Effect.Effect<ResolvedPos<Sc>, PositionError>;
|
|
183
|
+
/** Like `resolve`, but throws a `PositionError` for out-of-range positions. */
|
|
184
|
+
resolveUnsafe(pos: number): ResolvedPos<Sc>;
|
|
185
|
+
/** @internal */
|
|
186
|
+
resolveNoCache(pos: number): ResolvedPos<Sc>;
|
|
187
|
+
/** Test whether a given mark or mark type occurs in this document between the two given positions. */
|
|
188
|
+
rangeHasMark(from: number, to: number, type: Mark<any, any> | MarkType<any, any>): boolean;
|
|
189
|
+
/** True when this is a block (non-inline node) */
|
|
190
|
+
get isBlock(): boolean;
|
|
191
|
+
/** True when this is a textblock node, a block node with inline content. */
|
|
192
|
+
get isTextblock(): boolean;
|
|
193
|
+
/** True when this node allows inline content. */
|
|
194
|
+
get inlineContent(): boolean;
|
|
195
|
+
/** True when this is an inline node (a text node or a node that can appear among text). */
|
|
196
|
+
get isInline(): boolean;
|
|
197
|
+
/** True when this is a text node. */
|
|
198
|
+
get isText(): boolean;
|
|
199
|
+
/** True when this is a leaf node. */
|
|
200
|
+
get isLeaf(): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* True when this is an atom, i.e. when it does not have directly
|
|
203
|
+
* editable content. This is usually the same as `isLeaf`, but can be
|
|
204
|
+
* configured with the `atom` property on a node's spec.
|
|
205
|
+
*/
|
|
206
|
+
get isAtom(): boolean;
|
|
207
|
+
/** Return a string representation of this node for debugging purposes. */
|
|
208
|
+
toString(): string;
|
|
209
|
+
/** Get the content match in this node at the given index. */
|
|
210
|
+
contentMatchAt(index: number): Effect.Effect<ContentMatch, ContentError>;
|
|
211
|
+
/** Like `contentMatchAt`, but throws when the node's content is invalid. */
|
|
212
|
+
contentMatchAtUnsafe(index: number): ContentMatch;
|
|
213
|
+
/**
|
|
214
|
+
* Test whether replacing the range between `from` and `to` (by child
|
|
215
|
+
* index) with the given replacement fragment (which defaults to the
|
|
216
|
+
* empty fragment) would leave the node's content valid. You can
|
|
217
|
+
* optionally pass `start` and `end` indices into the replacement
|
|
218
|
+
* fragment.
|
|
219
|
+
*/
|
|
220
|
+
canReplace(from: number, to: number, replacement?: Fragment<any, any>, start?: number, end?: number): boolean;
|
|
221
|
+
/**
|
|
222
|
+
* Test whether replacing the range `from` to `to` (by index) with a
|
|
223
|
+
* node of the given type would leave the node's content valid.
|
|
224
|
+
*/
|
|
225
|
+
canReplaceWith(from: number, to: number, type: NodeType<any, any>, marks?: ReadonlyArray<Mark<any, any>>): boolean;
|
|
226
|
+
/**
|
|
227
|
+
* Test whether the given node's content could be appended to this
|
|
228
|
+
* node. If that node is empty, this will only return true if there is
|
|
229
|
+
* at least one node type that can appear in both nodes (to avoid
|
|
230
|
+
* merging completely incompatible nodes).
|
|
231
|
+
*/
|
|
232
|
+
canAppend(other: Node<any, any>): boolean;
|
|
233
|
+
/**
|
|
234
|
+
* Check whether this node and its descendants conform to the schema
|
|
235
|
+
* (including attribute schemas), failing with a `ContentError` or
|
|
236
|
+
* `AttributeError` when they do not.
|
|
237
|
+
*/
|
|
238
|
+
check(): Effect.Effect<void, ContentError | AttributeError>;
|
|
239
|
+
/** Like `check`, but throws on failure. */
|
|
240
|
+
checkUnsafe(): void;
|
|
241
|
+
/** Return a JSON-serializable representation of this node. */
|
|
242
|
+
toJSON(): NodeJSON;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* A text node. Text nodes are always leaves and carry their text in
|
|
246
|
+
* `text`.
|
|
247
|
+
*
|
|
248
|
+
* @since 0.1.0
|
|
249
|
+
* @category models
|
|
250
|
+
*/
|
|
251
|
+
export declare class TextNode<Sc extends AnySchema = AnySchema> extends Node<Sc, "text" & NodeNames<Sc>> {
|
|
252
|
+
/** The text content of this node. */
|
|
253
|
+
readonly text: string;
|
|
254
|
+
/** @internal */
|
|
255
|
+
constructor(type: NodeType<Sc, any>, attrs: Attrs, content: string, marks?: ReadonlyArray<Mark<Sc>>);
|
|
256
|
+
toString(): string;
|
|
257
|
+
get textContent(): string;
|
|
258
|
+
textBetween(from: number, to: number): string;
|
|
259
|
+
get nodeSize(): number;
|
|
260
|
+
mark(marks: ReadonlyArray<Mark<Sc>>): TextNode<Sc>;
|
|
261
|
+
/** Create a copy of this text node with different text. */
|
|
262
|
+
withText(text: string): TextNode<Sc>;
|
|
263
|
+
cut(from?: number, to?: number): TextNode<Sc>;
|
|
264
|
+
eq(other: Node<any, any>): boolean;
|
|
265
|
+
[Hash.symbol](): number;
|
|
266
|
+
toJSON(): NodeJSON;
|
|
267
|
+
}
|