@thomasfosterau/prosekit-utilities 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 +21 -0
- package/README.md +447 -2
- package/dist/derive-BtbsOqS7.js +479 -0
- package/dist/derive.d.ts +2 -0
- package/dist/derive.js +1 -0
- package/dist/document-shape-CrKjPIEf.d.ts +195 -0
- package/dist/drizzle.d.ts +76 -0
- package/dist/drizzle.js +95 -0
- package/dist/envelope-DQS7tt8f.js +87 -0
- package/dist/index-DlgceBHe.d.ts +266 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.js +723 -0
- package/dist/prosekit.d.ts +3218 -0
- package/dist/prosekit.js +827 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/package.json +71 -3
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
|
|
2
|
+
import * as Array from "effect/Array";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as String from "effect/String";
|
|
8
|
+
import * as Record from "effect/Record";
|
|
9
|
+
// src/derive/Options.ts
|
|
10
|
+
/**
|
|
11
|
+
* @copyright Thomas Foster
|
|
12
|
+
*
|
|
13
|
+
* Public option and result types for the derivation entry points.
|
|
14
|
+
*
|
|
15
|
+
* ProseMirror exposes a schema's *structure* at runtime — node and mark
|
|
16
|
+
* names, groups, the compiled content model, which marks each node admits,
|
|
17
|
+
* and per-attribute defaults — but it does **not** expose attribute *value
|
|
18
|
+
* types*: an {@link AttributeSpec} carries only a `default` and an optional
|
|
19
|
+
* `validate` hint. So a faithful derivation recovers the structure exactly and
|
|
20
|
+
* makes a best effort at attributes, leaving a seam — the {@link AttributeOverlay}
|
|
21
|
+
* — through which a caller supplies the value schemas ProseMirror cannot. The
|
|
22
|
+
* derived schema complements, rather than replaces, a hand-written generative
|
|
23
|
+
* document contract, which still owns the attribute value types.
|
|
24
|
+
*
|
|
25
|
+
* @module @thomasfosterau/prosekit-utilities/derive/Options
|
|
26
|
+
*/
|
|
27
|
+
var Options_exports = /* @__PURE__ */ __exportAll({
|
|
28
|
+
attributeOverlay: () => attributeOverlay,
|
|
29
|
+
isRequiredAttribute: () => isRequiredAttribute,
|
|
30
|
+
required: () => required
|
|
31
|
+
});
|
|
32
|
+
const RequiredAttributeTypeId = Symbol.for("@thomasfosterau/prosekit-utilities/RequiredAttribute");
|
|
33
|
+
/** Wraps `schema` as a {@link RequiredAttribute}. */
|
|
34
|
+
const required = (schema) => ({
|
|
35
|
+
[RequiredAttributeTypeId]: true,
|
|
36
|
+
schema
|
|
37
|
+
});
|
|
38
|
+
/** True when `value` is a {@link RequiredAttribute} produced by {@link required}. */
|
|
39
|
+
const isRequiredAttribute = (value) => Predicate.isObjectOrArray(value) && RequiredAttributeTypeId in value;
|
|
40
|
+
/**
|
|
41
|
+
* Builds an {@link AttributeOverlay} from a nested record keyed by owner name
|
|
42
|
+
* then attribute name. A convenience over writing the function by hand when the
|
|
43
|
+
* overrides are static:
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* const overlay = attributeOverlay({
|
|
47
|
+
* heading: { level: Schema.Literals([1, 2, 3, 4, 5, 6]) },
|
|
48
|
+
* image: { src: Schema.NullOr(Schema.String) },
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Keys match on the owning type's name regardless of whether it is a node or a
|
|
53
|
+
* mark; a node and a mark sharing a name (unusual) would share overrides.
|
|
54
|
+
*/
|
|
55
|
+
function attributeOverlay(overrides) {
|
|
56
|
+
return (owner, attribute) => Option.fromNullishOr(overrides[owner.name]?.[attribute]);
|
|
57
|
+
}
|
|
58
|
+
// src/derive/Attribute.ts
|
|
59
|
+
/**
|
|
60
|
+
* @copyright Thomas Foster
|
|
61
|
+
*
|
|
62
|
+
* Attribute derivation — the part of a ProseMirror schema that is only
|
|
63
|
+
* *partially* recoverable at runtime.
|
|
64
|
+
*
|
|
65
|
+
* An {@link AttributeSpec} carries a `default` and an optional `validate`
|
|
66
|
+
* hint, nothing more. So this module recovers what it can, in priority order:
|
|
67
|
+
*
|
|
68
|
+
* 1. A caller {@link AttributeOverlay} override, if one is supplied.
|
|
69
|
+
* 2. The `validate` string grammar (`"number | string | null"` …), parsed
|
|
70
|
+
* into a union of primitives — this is the one place ProseMirror records an
|
|
71
|
+
* attribute's value type.
|
|
72
|
+
* 3. The `typeof` of a non-null `default` value.
|
|
73
|
+
* 4. `Schema.Unknown` as the last resort.
|
|
74
|
+
*
|
|
75
|
+
* Key optionality follows ProseMirror's own rule: an attribute with a
|
|
76
|
+
* `default` (of any value, `null` included) may be omitted and is emitted as
|
|
77
|
+
* an optional key; an attribute with no default is required — the same split
|
|
78
|
+
* `NodeType.hasRequiredAttrs()` reports.
|
|
79
|
+
*
|
|
80
|
+
* @module @thomasfosterau/prosekit-utilities/derive/Attribute
|
|
81
|
+
*/
|
|
82
|
+
var Attribute_exports = /* @__PURE__ */ __exportAll({
|
|
83
|
+
deriveAttribute: () => deriveAttribute,
|
|
84
|
+
deriveAttrsStruct: () => deriveAttrsStruct,
|
|
85
|
+
parseValidateGrammar: () => parseValidateGrammar
|
|
86
|
+
});
|
|
87
|
+
const PRIMITIVE_BY_NAME = {
|
|
88
|
+
string: Schema.String,
|
|
89
|
+
number: Schema.Finite,
|
|
90
|
+
boolean: Schema.Boolean,
|
|
91
|
+
null: Schema.Null
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parses ProseMirror's `validate` string grammar — a `|`-separated list of the
|
|
95
|
+
* primitive names `"string"`, `"number"`, `"boolean"`, `"null"`, and
|
|
96
|
+
* `"undefined"` — into an Effect schema.
|
|
97
|
+
*
|
|
98
|
+
* The `"undefined"` member is not represented as a value (there is no
|
|
99
|
+
* first-class `undefined` schema for a present key); it is reported via the
|
|
100
|
+
* returned `optional` flag so the caller can drop the key instead.
|
|
101
|
+
*
|
|
102
|
+
* An empty or all-unknown grammar yields `Schema.Unknown`.
|
|
103
|
+
*/
|
|
104
|
+
function parseValidateGrammar(validate) {
|
|
105
|
+
const tokens = validate.split("|").map((t) => t.trim()).filter(String.isNonEmpty);
|
|
106
|
+
const optional = tokens.includes("undefined");
|
|
107
|
+
const keys = Array.dedupe(Array.filterMap(tokens, (token) => token === "undefined" ? Result.failVoid : Result.succeed(token in PRIMITIVE_BY_NAME ? token : "")));
|
|
108
|
+
const members = Array.map(keys, (key) => PRIMITIVE_BY_NAME[key] ?? Schema.Unknown);
|
|
109
|
+
return Array.match(members, {
|
|
110
|
+
onEmpty: () => ({
|
|
111
|
+
schema: Schema.Unknown,
|
|
112
|
+
optional
|
|
113
|
+
}),
|
|
114
|
+
onNonEmpty: (present) => present.length === 1 ? {
|
|
115
|
+
schema: Array.headNonEmpty(present),
|
|
116
|
+
optional
|
|
117
|
+
} : {
|
|
118
|
+
schema: Schema.Union(members),
|
|
119
|
+
optional
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Best-effort schema for an attribute value inferred from its default. */
|
|
124
|
+
function schemaFromDefault(value) {
|
|
125
|
+
if (Predicate.isString(value)) return Schema.String;
|
|
126
|
+
if (Predicate.isNumber(value)) return Schema.Finite;
|
|
127
|
+
if (Predicate.isBoolean(value)) return Schema.Boolean;
|
|
128
|
+
return value === null ? Schema.NullOr(Schema.Unknown) : Schema.Unknown;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Derives the schema and key-optionality for one node or mark attribute.
|
|
132
|
+
*
|
|
133
|
+
* @param owner The node or mark type the attribute belongs to (for the overlay).
|
|
134
|
+
* @param name The attribute name.
|
|
135
|
+
* @param spec The ProseMirror attribute spec.
|
|
136
|
+
* @param overlay Optional caller override consulted first.
|
|
137
|
+
*/
|
|
138
|
+
function deriveAttribute(owner, name, spec, overlay) {
|
|
139
|
+
const hasDefault = spec.default !== void 0;
|
|
140
|
+
const override = overlay === void 0 ? Option.none() : overlay(owner, name, spec);
|
|
141
|
+
if (Option.isSome(override)) {
|
|
142
|
+
const value = override.value;
|
|
143
|
+
if (isRequiredAttribute(value)) return {
|
|
144
|
+
schema: value.schema,
|
|
145
|
+
optional: false
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
schema: value,
|
|
149
|
+
optional: hasDefault
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (Predicate.isString(spec.validate)) {
|
|
153
|
+
const { schema, optional } = parseValidateGrammar(spec.validate);
|
|
154
|
+
return {
|
|
155
|
+
schema,
|
|
156
|
+
optional: optional || hasDefault
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
schema: hasDefault ? schemaFromDefault(spec.default) : Schema.Unknown,
|
|
161
|
+
optional: hasDefault
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Derives the `attrs` struct schema for a node or mark from its attribute
|
|
166
|
+
* specs, or `Option.none()` when the type declares no attributes.
|
|
167
|
+
*
|
|
168
|
+
* `attrs` is an `Option` because ProseMirror's own `NodeSpec`/`MarkSpec` leaves
|
|
169
|
+
* `attrs` absent for an attribute-less type — the caller converts that foreign
|
|
170
|
+
* `undefined` at the boundary (`Option.fromNullishOr(nodeType.spec.attrs)`).
|
|
171
|
+
*/
|
|
172
|
+
function deriveAttrsStruct(owner, attrs, overlay) {
|
|
173
|
+
if (Option.isNone(attrs) || Record.isEmptyReadonlyRecord(attrs.value)) return Option.none();
|
|
174
|
+
const fields = Record.map(attrs.value, (spec, name) => {
|
|
175
|
+
const { schema, optional } = deriveAttribute(owner, name, spec, overlay);
|
|
176
|
+
return optional ? Schema.optionalKey(schema) : schema;
|
|
177
|
+
});
|
|
178
|
+
return Option.some(Schema.Struct(fields));
|
|
179
|
+
}
|
|
180
|
+
// src/derive/Content.ts
|
|
181
|
+
/**
|
|
182
|
+
* @copyright Thomas Foster
|
|
183
|
+
*
|
|
184
|
+
* Content-model derivation — reading a node type's allowed children and
|
|
185
|
+
* cardinality straight from ProseMirror's compiled content expression.
|
|
186
|
+
*
|
|
187
|
+
* A node's content expression (`"block+"`, `"(tableCell | tableHeaderCell)*"`,
|
|
188
|
+
* …) is compiled into a {@link ContentMatch} finite automaton at schema-build
|
|
189
|
+
* time. Rather than re-parse the source string — which ProseMirror also
|
|
190
|
+
* exposes as `spec.content`, but only as an opaque string — we walk that
|
|
191
|
+
* automaton. Every edge names a {@link NodeType} that may appear, and the
|
|
192
|
+
* start state's `validEnd` tells us whether empty content is legal (`*`/`?`
|
|
193
|
+
* style) or at least one child is required (`+`).
|
|
194
|
+
*
|
|
195
|
+
* Walking the DFA is robust for arbitrary expressions and needs no grammar
|
|
196
|
+
* parser of our own; the trade-off is that we recover the *set* of admissible
|
|
197
|
+
* child types and the empty-vs-non-empty distinction, not the full ordered
|
|
198
|
+
* sequence. That is exactly the granularity the derived Effect schema models
|
|
199
|
+
* (an array of a child union). A fixed sequence such as the figure node's
|
|
200
|
+
* `image figcaption` therefore derives as a non-empty array of
|
|
201
|
+
* `image | figcaption`: a stored figure without its caption, or with the two
|
|
202
|
+
* halves reordered, decodes. This is intended: order and per-type
|
|
203
|
+
* multiplicity within a content expression are left to ProseMirror's DOM
|
|
204
|
+
* parser and editing transactions, which enforce them. Loading JSON does not:
|
|
205
|
+
* `schema.nodeFromJSON` (and so ProseKit's `defaultContent`) checks attributes
|
|
206
|
+
* but not content, so such a figure loads unchecked unless the consumer calls
|
|
207
|
+
* `node.check()`, as ProseKit's `editor.setContent` already does.
|
|
208
|
+
*
|
|
209
|
+
* @module @thomasfosterau/prosekit-utilities/derive/Content
|
|
210
|
+
*/
|
|
211
|
+
var Content_exports = /* @__PURE__ */ __exportAll({
|
|
212
|
+
allowedContentTypes: () => allowedContentTypes,
|
|
213
|
+
contentCardinality: () => contentCardinality
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* Collects the distinct set of node types that may appear anywhere in a node
|
|
217
|
+
* type's content, by breadth-first walk of its content-match automaton.
|
|
218
|
+
*
|
|
219
|
+
* Order follows first-discovery, which for the typical single-group
|
|
220
|
+
* expression (`block+`, `inline*`) matches the schema's node declaration
|
|
221
|
+
* order.
|
|
222
|
+
*/
|
|
223
|
+
function allowedContentTypes(nodeType) {
|
|
224
|
+
const result = [];
|
|
225
|
+
const seenTypes = /* @__PURE__ */ new Set();
|
|
226
|
+
const visited = /* @__PURE__ */ new Set();
|
|
227
|
+
const queue = [nodeType.contentMatch];
|
|
228
|
+
while (Array.isArrayNonEmpty(queue)) {
|
|
229
|
+
const match = Array.headNonEmpty(queue);
|
|
230
|
+
queue.shift();
|
|
231
|
+
if (visited.has(match)) continue;
|
|
232
|
+
visited.add(match);
|
|
233
|
+
for (let i = 0; i < match.edgeCount; i++) {
|
|
234
|
+
const edge = match.edge(i);
|
|
235
|
+
if (!seenTypes.has(edge.type.name)) {
|
|
236
|
+
seenTypes.add(edge.type.name);
|
|
237
|
+
result.push(edge.type);
|
|
238
|
+
}
|
|
239
|
+
queue.push(edge.next);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Classifies a node type's content cardinality from its content-match
|
|
246
|
+
* automaton: `none` when it admits no children, `optional` when the empty
|
|
247
|
+
* fragment is a valid end (`*` / `?`), and `required` otherwise (`+`).
|
|
248
|
+
*/
|
|
249
|
+
function contentCardinality(nodeType) {
|
|
250
|
+
const match = nodeType.contentMatch;
|
|
251
|
+
if (match.edgeCount === 0) return "none";
|
|
252
|
+
return match.validEnd ? "optional" : "required";
|
|
253
|
+
}
|
|
254
|
+
// src/derive/Registry.ts
|
|
255
|
+
/**
|
|
256
|
+
* @copyright Thomas Foster
|
|
257
|
+
*
|
|
258
|
+
* The derivation engine: a memoised builder that turns one ProseMirror
|
|
259
|
+
* {@link PmSchema} plus a set of {@link DeriveOptions} into Effect schemas for
|
|
260
|
+
* its marks and (mutually recursive) nodes.
|
|
261
|
+
*
|
|
262
|
+
* Nodes reference each other through their content models, so the node
|
|
263
|
+
* schemas form a mutually recursive graph. The graph is expressed with
|
|
264
|
+
* `Schema.suspend`: a node's content field holds a union of *deferred*
|
|
265
|
+
* references to its admissible child schemas, and each reference resolves
|
|
266
|
+
* through {@link Registry.node}, which memoises so the graph is built once and
|
|
267
|
+
* cycles terminate. `suspend` also keeps construction lazy — building one node
|
|
268
|
+
* never forces its children — so declaration order is irrelevant and recursive
|
|
269
|
+
* grammars (a list inside a list) are fine.
|
|
270
|
+
*
|
|
271
|
+
* A {@link Registry} is internal plumbing; the public entry points in
|
|
272
|
+
* `./Derive` create one and read a single schema out of it. Several
|
|
273
|
+
* `Registry` instances — one per ProseMirror schema — can share a single
|
|
274
|
+
* {@link RegistryCache} so structure the schemas have in common (e.g. two
|
|
275
|
+
* ProseKit extensions both composing the same `defineAllMarks()`) is built
|
|
276
|
+
* once rather than once per schema; {@link import("./Derive.ts").deriveDocumentSchemas}
|
|
277
|
+
* is the public entry point that does this. Memoization is by mark/node
|
|
278
|
+
* *name*, so sharing a cache across schemas is only correct when same-named
|
|
279
|
+
* types across those schemas are structurally identical — true by
|
|
280
|
+
* construction when the schemas are assembled from the same extension
|
|
281
|
+
* definitions, which is the only scenario `deriveDocumentSchemas` targets.
|
|
282
|
+
*
|
|
283
|
+
* That includes the **mark set**. An inline node's derived `marks` field is
|
|
284
|
+
* the schema-wide union of inline marks (see `#inlineMarks`), so two schemas
|
|
285
|
+
* that both define `text` identically but admit different marks still derive
|
|
286
|
+
* different `text` schemas, and the cache keeps whichever was built first. A
|
|
287
|
+
* schema with an extra mark that is derived first makes the other accept that
|
|
288
|
+
* mark; derived second, it rejects its own mark. Nothing reports the clash.
|
|
289
|
+
*
|
|
290
|
+
* @module @thomasfosterau/prosekit-utilities/derive/Registry
|
|
291
|
+
*/
|
|
292
|
+
/** A fresh, empty {@link RegistryCache} — private to one `Registry` unless explicitly shared. */
|
|
293
|
+
function createRegistryCache() {
|
|
294
|
+
return {
|
|
295
|
+
nodeCache: /* @__PURE__ */ new Map(),
|
|
296
|
+
markCache: /* @__PURE__ */ new Map()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** Builds and memoises the derived schemas for a single ProseMirror schema. */
|
|
300
|
+
var Registry = class {
|
|
301
|
+
#schema;
|
|
302
|
+
#options;
|
|
303
|
+
#cache;
|
|
304
|
+
constructor(schema, options = {}, cache = createRegistryCache()) {
|
|
305
|
+
this.#schema = schema;
|
|
306
|
+
this.#options = options;
|
|
307
|
+
this.#cache = cache;
|
|
308
|
+
}
|
|
309
|
+
/** The derived schema for a mark type, built once and memoised by name. */
|
|
310
|
+
mark(markType) {
|
|
311
|
+
const cached = this.#cache.markCache.get(markType.name);
|
|
312
|
+
if (cached) return cached;
|
|
313
|
+
const built = this.#buildMark(markType);
|
|
314
|
+
this.#cache.markCache.set(markType.name, built);
|
|
315
|
+
return built;
|
|
316
|
+
}
|
|
317
|
+
/** The derived schema for a node type, built once and memoised by name. */
|
|
318
|
+
node(nodeType) {
|
|
319
|
+
const cached = this.#cache.nodeCache.get(nodeType.name);
|
|
320
|
+
if (cached) return cached;
|
|
321
|
+
const built = this.#buildNode(nodeType, nodeType.name);
|
|
322
|
+
this.#cache.nodeCache.set(nodeType.name, built);
|
|
323
|
+
return built;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The derived schema for a node type with its `type` literal overridden —
|
|
327
|
+
* used for a renamed top node (e.g. `doc` → `blockDocument`). Not memoised,
|
|
328
|
+
* since the override makes it distinct from the canonical node schema.
|
|
329
|
+
*/
|
|
330
|
+
nodeAs(nodeType, typeName) {
|
|
331
|
+
return this.#buildNode(nodeType, typeName);
|
|
332
|
+
}
|
|
333
|
+
#buildMark(markType) {
|
|
334
|
+
const fields = { type: Schema.Literal(markType.name) };
|
|
335
|
+
const attrs = deriveAttrsStruct({
|
|
336
|
+
kind: "mark",
|
|
337
|
+
name: markType.name
|
|
338
|
+
}, Option.fromNullishOr(markType.spec.attrs), this.#options.attributes);
|
|
339
|
+
if (Option.isSome(attrs)) fields.attrs = attrs.value;
|
|
340
|
+
return Schema.Struct(fields);
|
|
341
|
+
}
|
|
342
|
+
#inlineMarkCache = Option.none();
|
|
343
|
+
/**
|
|
344
|
+
* The marks admissible on inline content anywhere in the schema.
|
|
345
|
+
*
|
|
346
|
+
* In ProseMirror JSON marks are serialised on the inline nodes, but the
|
|
347
|
+
* *permission* to carry a mark comes from the containing textblock's
|
|
348
|
+
* `markSet` (a node's own `markSet` — `text`'s included — governs what it may
|
|
349
|
+
* hold, and a textblock's governs its inline children). Rather than thread
|
|
350
|
+
* parent context through the memoised node graph, we take the schema-wide
|
|
351
|
+
* union: a mark that some textblock (`inlineContent`) admits may appear on
|
|
352
|
+
* inline nodes. A `null` `markSet` on any textblock means all marks.
|
|
353
|
+
*/
|
|
354
|
+
#inlineMarks() {
|
|
355
|
+
if (Option.isSome(this.#inlineMarkCache)) return this.#inlineMarkCache.value;
|
|
356
|
+
const inlineNodes = Array.filter(Record.values(this.#schema.nodes), (node) => node.inlineContent);
|
|
357
|
+
if (Array.some(inlineNodes, (node) => node.markSet === null)) {
|
|
358
|
+
const all = Record.values(this.#schema.marks);
|
|
359
|
+
this.#inlineMarkCache = Option.some(all);
|
|
360
|
+
return all;
|
|
361
|
+
}
|
|
362
|
+
const deduped = Array.dedupeWith(Array.flatMap(inlineNodes, (node) => node.markSet ?? []), (a, b) => a.name === b.name);
|
|
363
|
+
this.#inlineMarkCache = Option.some(deduped);
|
|
364
|
+
return deduped;
|
|
365
|
+
}
|
|
366
|
+
/** The union of mark schemas a node may carry in JSON, or `Option.none()`. */
|
|
367
|
+
#marksFor(nodeType) {
|
|
368
|
+
if (this.#options.includeMarks === false) return Option.none();
|
|
369
|
+
if (!nodeType.isInline) return Option.none();
|
|
370
|
+
const markTypes = this.#inlineMarks();
|
|
371
|
+
if (!Array.isReadonlyArrayNonEmpty(markTypes)) return Option.none();
|
|
372
|
+
const members = Array.map(markTypes, (m) => this.mark(m));
|
|
373
|
+
return Option.some(members.length === 1 ? Array.headNonEmpty(members) : Schema.Union(members));
|
|
374
|
+
}
|
|
375
|
+
/** The content field schema for a node, or `Option.none()` for a leaf. */
|
|
376
|
+
#contentFor(nodeType) {
|
|
377
|
+
const cardinality = contentCardinality(nodeType);
|
|
378
|
+
if (cardinality === "none") return Option.none();
|
|
379
|
+
const childTypes = allowedContentTypes(nodeType);
|
|
380
|
+
if (!Array.isReadonlyArrayNonEmpty(childTypes)) return Option.none();
|
|
381
|
+
const childMembers = Array.map(childTypes, (child) => Schema.suspend(() => this.node(child)));
|
|
382
|
+
const child = childMembers.length === 1 ? Array.headNonEmpty(childMembers) : Schema.Union(childMembers);
|
|
383
|
+
if (cardinality === "required" && this.#options.nonEmptyRequiredContent !== false) return Option.some(Schema.NonEmptyArray(child));
|
|
384
|
+
return Option.some(Schema.Array(child));
|
|
385
|
+
}
|
|
386
|
+
#buildNode(nodeType, typeName) {
|
|
387
|
+
const fields = { type: Schema.Literal(typeName) };
|
|
388
|
+
const attrs = deriveAttrsStruct({
|
|
389
|
+
kind: "node",
|
|
390
|
+
name: nodeType.name
|
|
391
|
+
}, Option.fromNullishOr(nodeType.spec.attrs), this.#options.attributes);
|
|
392
|
+
if (Option.isSome(attrs)) fields.attrs = attrs.value;
|
|
393
|
+
if (nodeType.isText) fields.text = Schema.String;
|
|
394
|
+
else {
|
|
395
|
+
const content = this.#contentFor(nodeType);
|
|
396
|
+
if (Option.isSome(content)) fields.content = contentCardinality(nodeType) === "optional" ? Schema.optionalKey(content.value) : content.value;
|
|
397
|
+
}
|
|
398
|
+
const marks = this.#marksFor(nodeType);
|
|
399
|
+
if (Option.isSome(marks)) fields.marks = Schema.optionalKey(Schema.Array(marks.value));
|
|
400
|
+
return Schema.Struct(fields);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
// src/derive/Derive.ts
|
|
404
|
+
var Derive_exports = /* @__PURE__ */ __exportAll({
|
|
405
|
+
deriveDocumentSchema: () => deriveDocumentSchema,
|
|
406
|
+
deriveDocumentSchemas: () => deriveDocumentSchemas,
|
|
407
|
+
deriveMarkSchema: () => deriveMarkSchema,
|
|
408
|
+
deriveNodeSchema: () => deriveNodeSchema
|
|
409
|
+
});
|
|
410
|
+
/**
|
|
411
|
+
* Derives an Effect schema for a single ProseMirror mark type: a struct with a
|
|
412
|
+
* `type` literal and, when the mark declares attributes, an `attrs` field.
|
|
413
|
+
*/
|
|
414
|
+
function deriveMarkSchema(markType, options) {
|
|
415
|
+
return new Registry(markType.schema, options).mark(markType);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Derives an Effect schema for a single ProseMirror node type, recursively
|
|
419
|
+
* modelling its content via the node's own schema. The result validates a JSON
|
|
420
|
+
* subtree rooted at this node.
|
|
421
|
+
*/
|
|
422
|
+
function deriveNodeSchema(nodeType, options) {
|
|
423
|
+
return new Registry(nodeType.schema, options).node(nodeType);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Derives an Effect schema for a whole document from a ProseMirror schema,
|
|
427
|
+
* rooted at its top node (`schema.topNodeType`, usually `doc`).
|
|
428
|
+
*
|
|
429
|
+
* Pass `topTypeName` to rename the top node's `type` literal — for an
|
|
430
|
+
* application that persists the top node under a different name than
|
|
431
|
+
* ProseMirror's internal `doc` (e.g. `blockDocument`).
|
|
432
|
+
*/
|
|
433
|
+
function deriveDocumentSchema(schema, options) {
|
|
434
|
+
return documentNode(new Registry(schema, options), schema, Option.fromNullishOr(options?.topTypeName));
|
|
435
|
+
}
|
|
436
|
+
/** The document-envelope node a {@link deriveDocumentSchema} / {@link deriveDocumentSchemas} entry resolves to. */
|
|
437
|
+
function documentNode(registry, schema, topTypeName) {
|
|
438
|
+
return Option.isNone(topTypeName) ? registry.node(schema.topNodeType) : registry.nodeAs(schema.topNodeType, topTypeName.value);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Derives Effect schemas for two or more related ProseMirror document
|
|
442
|
+
* schemas in one call, sharing a single {@link Registry} — and so its
|
|
443
|
+
* mark/node memoization — across all of them.
|
|
444
|
+
*
|
|
445
|
+
* Use this instead of separate {@link deriveDocumentSchema} calls when the
|
|
446
|
+
* schemas are assembled from overlapping ProseKit extensions (e.g. two
|
|
447
|
+
* document types that both compose the same `defineAllMarks()` or
|
|
448
|
+
* `defineText()`): the marks and nodes the schemas have in common are then
|
|
449
|
+
* derived once rather than once per document. The results stay fully
|
|
450
|
+
* independent `Schema.Codec`s — this shares derivation machinery, not the
|
|
451
|
+
* documents' wire shapes.
|
|
452
|
+
*
|
|
453
|
+
* **Precondition:** sharing is by mark/node *name*, so this is only correct
|
|
454
|
+
* when every schema in the call defines same-named marks/nodes identically
|
|
455
|
+
* (the same extension composed into each schema, as opposed to two schemas
|
|
456
|
+
* that coincidentally both declare a differently-shaped type under the same
|
|
457
|
+
* name) **and admits the same set of marks** — an inline node's derived
|
|
458
|
+
* `marks` field is the schema's whole inline mark union, so a schema with an
|
|
459
|
+
* extra mark derives a different `text`. A mismatch would silently resolve
|
|
460
|
+
* to whichever schema's version was derived first, for every schema after
|
|
461
|
+
* it, with no error.
|
|
462
|
+
*
|
|
463
|
+
* All entries are built eagerly in this one call — pass already-compiled
|
|
464
|
+
* schemas. A caller that needs each document to stay independently lazy
|
|
465
|
+
* (e.g. never compiling schema B just because schema A was validated) should
|
|
466
|
+
* instead share a {@link import("./Registry.ts").RegistryCache} across
|
|
467
|
+
* separate `Registry` instances directly, one per schema, each built inside
|
|
468
|
+
* its own deferred call site — the pattern
|
|
469
|
+
* `@thomasfosterau/prosekit-utilities/prosekit`'s `defineDocumentSchema`
|
|
470
|
+
* implements.
|
|
471
|
+
*
|
|
472
|
+
* `options` applies to every schema in the call; use each entry's own
|
|
473
|
+
* `topTypeName` for a per-document top-node rename.
|
|
474
|
+
*/
|
|
475
|
+
function deriveDocumentSchemas(schemas, options) {
|
|
476
|
+
const cache = createRegistryCache();
|
|
477
|
+
return Record.map(schemas, ({ schema, topTypeName }) => documentNode(new Registry(schema, options, cache), schema, Option.fromNullishOr(topTypeName)));
|
|
478
|
+
}
|
|
479
|
+
export { deriveNodeSchema as a, Content_exports as c, attributeOverlay as d, isRequiredAttribute as f, deriveMarkSchema as i, Attribute_exports as l, deriveDocumentSchema as n, Registry as o, required as p, deriveDocumentSchemas as r, createRegistryCache as s, Derive_exports as t, Options_exports as u };
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { S as required, _ as DerivedNodeJSON, a as Attribute_d_exports, b as attributeOverlay, c as deriveDocumentSchema, d as deriveNodeSchema, f as AttributeOverlay, g as DerivedMarkJSON, h as DeriveOptions, i as Content_d_exports, l as deriveDocumentSchemas, m as DeriveDocumentOptions, n as RegistryCache, o as Derive_d_exports, p as AttributeOwner, r as createRegistryCache, s as DocumentSchemaSpec, t as Registry, u as deriveMarkSchema, v as Options_d_exports, x as isRequiredAttribute, y as RequiredAttribute } from "./index-DlgceBHe.js";
|
|
2
|
+
export { Attribute_d_exports as Attribute, type AttributeOverlay, type AttributeOwner, Content_d_exports as Content, Derive_d_exports as Derive, type DeriveDocumentOptions, type DeriveOptions, type DerivedMarkJSON, type DerivedNodeJSON, type DocumentSchemaSpec, Options_d_exports as Options, Registry, type RegistryCache, type RequiredAttribute, attributeOverlay, createRegistryCache, deriveDocumentSchema, deriveDocumentSchemas, deriveMarkSchema, deriveNodeSchema, isRequiredAttribute, required };
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { l as Attribute, c as Content, t as Derive, u as Options, o as Registry, d as attributeOverlay, s as createRegistryCache, n as deriveDocumentSchema, r as deriveDocumentSchemas, i as deriveMarkSchema, a as deriveNodeSchema, f as isRequiredAttribute, p as required } from "./derive-BtbsOqS7.js";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as HashSet from "effect/HashSet";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
6
|
+
/**
|
|
7
|
+
* The ProseMirror JSON shapes, declared here rather than imported.
|
|
8
|
+
*
|
|
9
|
+
* `prosekit/core` exports a `NodeJSON` interface of exactly this shape, but
|
|
10
|
+
* importing it — even as a type — would put `prosekit` in this entry's
|
|
11
|
+
* import graph, and a `verbatimModuleSyntax` type-only import is easy to
|
|
12
|
+
* turn into a value import by accident. The root entry's whole purpose is
|
|
13
|
+
* that a model layer can name a document type without the editor runtime
|
|
14
|
+
* reaching the bundle, so the types are restated here instead.
|
|
15
|
+
*
|
|
16
|
+
* These are structural twins: `prosekit/core`'s `NodeJSON` is assignable to
|
|
17
|
+
* {@link NodeJSON} and vice versa where the mutability allows, so a document
|
|
18
|
+
* read out of an editor passes straight into anything here.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* One ProseMirror mark in its JSON form — a type name and its attributes.
|
|
24
|
+
*
|
|
25
|
+
* `attrs` is `unknown`-valued rather than `any`-valued: a serialiser that
|
|
26
|
+
* reads `mark.attrs?.href` has to narrow it, which is what keeps the
|
|
27
|
+
* `isSafeHref` check from being accidentally skipped.
|
|
28
|
+
*/
|
|
29
|
+
interface MarkJSON {
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly attrs?: Record<string, unknown> | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One ProseMirror node in its JSON form.
|
|
35
|
+
*
|
|
36
|
+
* Every field but `type` is optional, exactly as ProseMirror's own
|
|
37
|
+
* serialisation is: a text leaf carries `text`, a container carries
|
|
38
|
+
* `content`, an atom carries neither.
|
|
39
|
+
*/
|
|
40
|
+
interface NodeJSON {
|
|
41
|
+
readonly type: string;
|
|
42
|
+
readonly attrs?: Record<string, unknown> | undefined;
|
|
43
|
+
readonly content?: ReadonlyArray<NodeJSON> | undefined;
|
|
44
|
+
readonly marks?: ReadonlyArray<MarkJSON> | undefined;
|
|
45
|
+
readonly text?: string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The persisted document envelope: a ProseMirror document whose top-level
|
|
49
|
+
* `type` is the application's own name for the surface (`blockDocument`,
|
|
50
|
+
* `inlineDocument`, …) rather than ProseMirror's `doc`.
|
|
51
|
+
*
|
|
52
|
+
* Storing the surface name in the envelope is what lets one column's rows
|
|
53
|
+
* say which grammar they were written under; {@link NodeJSON} is what the
|
|
54
|
+
* editor itself wants, and `toEditorDocument` / `fromEditorDocument` move
|
|
55
|
+
* between the two.
|
|
56
|
+
*/
|
|
57
|
+
interface DocumentJSON<Name extends string = string> {
|
|
58
|
+
readonly type: Name;
|
|
59
|
+
readonly content: ReadonlyArray<NodeJSON>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One surface's vocabulary: the node and mark type names it admits, whether
|
|
63
|
+
* link `href`s are scheme-checked, and the name used in violation messages.
|
|
64
|
+
*
|
|
65
|
+
* `label` reads as a noun phrase in `node type "x" is not permitted in
|
|
66
|
+
* <label>` — "a comment", "an article section".
|
|
67
|
+
*/
|
|
68
|
+
interface AllowList {
|
|
69
|
+
readonly label: string;
|
|
70
|
+
readonly nodes: HashSet.HashSet<string>;
|
|
71
|
+
readonly marks: HashSet.HashSet<string>;
|
|
72
|
+
readonly checkLinkHref: boolean;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Builds an {@link AllowList} from any iterables of node and mark type
|
|
76
|
+
* names — an array literal, a `Set`, another list's `HashSet`.
|
|
77
|
+
*
|
|
78
|
+
* `checkLinkHref` defaults to `true`: an allow-list that admits the `link`
|
|
79
|
+
* mark is opting into untrusted URLs, and the scheme check is the cheap half
|
|
80
|
+
* of that. Pass `false` only where something else already decides.
|
|
81
|
+
*/
|
|
82
|
+
declare const defineAllowList: (options: {
|
|
83
|
+
readonly label: string;
|
|
84
|
+
readonly nodes: Iterable<string>;
|
|
85
|
+
readonly marks: Iterable<string>;
|
|
86
|
+
readonly checkLinkHref?: boolean | undefined;
|
|
87
|
+
}) => AllowList;
|
|
88
|
+
declare const DocumentViolation_base: Schema.Class<DocumentViolation, Schema.TaggedStruct<"DocumentViolation", {
|
|
89
|
+
readonly label: Schema.String;
|
|
90
|
+
readonly kind: Schema.Literals<readonly ["node", "mark", "href"]>;
|
|
91
|
+
readonly name: Schema.String;
|
|
92
|
+
readonly message: Schema.String;
|
|
93
|
+
}>, import("effect/Cause").YieldableError>;
|
|
94
|
+
/**
|
|
95
|
+
* The first thing an allow-list walk found outside its vocabulary, as data
|
|
96
|
+
* rather than as a bare string.
|
|
97
|
+
*
|
|
98
|
+
* `kind` says which of the three checks failed and `name` says what tripped
|
|
99
|
+
* it (the offending node or mark type), so a caller can map a violation onto
|
|
100
|
+
* its own error envelope without re-parsing `message`. `message` carries the
|
|
101
|
+
* human-readable form both source applications emitted, unchanged, because
|
|
102
|
+
* it is what an API returns to the author.
|
|
103
|
+
*/
|
|
104
|
+
declare class DocumentViolation extends /* @__PURE__ */ DocumentViolation_base {}
|
|
105
|
+
/** A document as the walk needs to see it: an envelope with `content`. */
|
|
106
|
+
interface WithContent {
|
|
107
|
+
readonly content: ReadonlyArray<NodeJSON>;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The widest thing {@link allowListFilter} can be pointed at: anything whose
|
|
111
|
+
* `content` is an array, whatever the element type. A document shape's
|
|
112
|
+
* `content` is `ReadonlyArray<Json>` at the schema level — a `null` entry
|
|
113
|
+
* gets past the structural check — so the walk below never assumes an entry
|
|
114
|
+
* is an object.
|
|
115
|
+
*/
|
|
116
|
+
interface ContentCarrier {
|
|
117
|
+
readonly content: ReadonlyArray<unknown>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The first node or mark type outside `allowList`'s vocabulary — or the
|
|
121
|
+
* first unsafe `link` `href` — in an already shape-valid document.
|
|
122
|
+
*
|
|
123
|
+
* Depth-first in document order, so the violation names the earliest
|
|
124
|
+
* offender, and `Option.none()` when the whole tree is admissible. The
|
|
125
|
+
* envelope's own `type` is not checked against the node list; only its
|
|
126
|
+
* `content` children are.
|
|
127
|
+
*/
|
|
128
|
+
declare const allowListViolation: (allowList: AllowList, doc: WithContent) => Option.Option<DocumentViolation>;
|
|
129
|
+
/**
|
|
130
|
+
* The Effect-shaped write seam: succeeds with the document it was given, or
|
|
131
|
+
* fails with the first {@link DocumentViolation}.
|
|
132
|
+
*
|
|
133
|
+
* Curried so an application can name the check once
|
|
134
|
+
* (`const checkComment = checkAllowList(COMMENT)`) and apply it per write,
|
|
135
|
+
* and generic in the document so the value that comes out is the exact type
|
|
136
|
+
* that went in — including a branded or narrowed one.
|
|
137
|
+
*/
|
|
138
|
+
declare const checkAllowList: (allowList: AllowList) => <D extends WithContent>(doc: D) => Effect.Effect<D, DocumentViolation>;
|
|
139
|
+
/**
|
|
140
|
+
* The same walk as a `Schema.check`-compatible filter, so an allow-list can
|
|
141
|
+
* be composed onto a schema rather than applied beside it.
|
|
142
|
+
*
|
|
143
|
+
* This is what `DocumentShape.narrow` uses. The failure message is the
|
|
144
|
+
* violation's own `message`, so a decode error reads exactly like the
|
|
145
|
+
* write-seam error for the same document.
|
|
146
|
+
*/
|
|
147
|
+
declare const allowListFilter: (allowList: AllowList) => SchemaAST.Filter<ContentCarrier>;
|
|
148
|
+
/**
|
|
149
|
+
* A whole-document type guard — what the ProseKit entry point's `DocumentSchema.is`
|
|
150
|
+
* provides and what {@link DocumentShape.install} accepts.
|
|
151
|
+
*/
|
|
152
|
+
type DocumentGrammarGuard = (value: unknown) => boolean;
|
|
153
|
+
/**
|
|
154
|
+
* One document surface, declared once and shared by the model layer, the
|
|
155
|
+
* persistence layer and the editor.
|
|
156
|
+
*
|
|
157
|
+
* `Schema` is the browser-cheap codec; `narrow` composes an allow-list onto
|
|
158
|
+
* it; `install` upgrades every one of them, for the life of the isolate, to
|
|
159
|
+
* the full grammar check.
|
|
160
|
+
*/
|
|
161
|
+
interface DocumentShape<Name extends string> {
|
|
162
|
+
/** The envelope's top-level `type` — `blockDocument`, `inlineDocument`, … */
|
|
163
|
+
readonly name: Name;
|
|
164
|
+
/** The human-readable noun phrase used in validation messages. */
|
|
165
|
+
readonly label: string;
|
|
166
|
+
/** The structural codec, grammar-checked wherever {@link install} has run. */
|
|
167
|
+
readonly Schema: Schema.Codec<DocumentJSON<Name>>;
|
|
168
|
+
/** `Schema.is` over {@link Schema}, with a defect net — see below. */
|
|
169
|
+
readonly is: (value: unknown) => value is DocumentJSON<Name>;
|
|
170
|
+
/** Installs the grammar guard for the life of the isolate. Idempotent in effect. */
|
|
171
|
+
readonly install: (guard: DocumentGrammarGuard) => void;
|
|
172
|
+
/** Whether a grammar guard is installed. For tests; production code should not branch on it. */
|
|
173
|
+
readonly isInstalled: () => boolean;
|
|
174
|
+
/** An empty document of this shape — the value a fresh editor field starts from. */
|
|
175
|
+
readonly empty: () => DocumentJSON<Name>;
|
|
176
|
+
/** {@link Schema} with an allow-list filter appended. */
|
|
177
|
+
readonly narrow: (allowList: AllowList) => Schema.Codec<DocumentJSON<Name>>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Declares a document surface.
|
|
181
|
+
*
|
|
182
|
+
* ```ts
|
|
183
|
+
* const blockDocument = defineDocumentShape({ name: 'blockDocument' });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* Each call gets its own guard slot, so two shapes never share an install.
|
|
187
|
+
* `label` defaults to the name split into words (`blockDocument` → `block
|
|
188
|
+
* document`), which is what the validation message reads with: `must be a
|
|
189
|
+
* valid block document`.
|
|
190
|
+
*/
|
|
191
|
+
declare function defineDocumentShape<Name extends string>(options: {
|
|
192
|
+
readonly name: Name;
|
|
193
|
+
readonly label?: string | undefined;
|
|
194
|
+
}): DocumentShape<Name>;
|
|
195
|
+
export { DocumentViolation as a, checkAllowList as c, MarkJSON as d, NodeJSON as f, AllowList as i, defineAllowList as l, DocumentShape as n, allowListFilter as o, defineDocumentShape as r, allowListViolation as s, DocumentGrammarGuard as t, DocumentJSON as u };
|