@thomasfosterau/prosekit-utilities 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.
@@ -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 };
@@ -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 };