@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,827 @@
1
+ import { i as isSafeHref, n as toEditorDocument } from "./envelope-DQS7tt8f.js";
2
+ import { d as attributeOverlay, o as Registry, p as required, s as createRegistryCache } from "./derive-BtbsOqS7.js";
3
+ import * as Array from "effect/Array";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import * as Predicate from "effect/Predicate";
7
+ import * as Result from "effect/Result";
8
+ import * as Schema from "effect/Schema";
9
+ import * as Record from "effect/Record";
10
+ import { createEditor, defineBaseCommands, defineBaseKeymap, defineHistory, defineMarkSpec, defineNodeSpec, htmlFromJSON, union } from "prosekit/core";
11
+ import { defineBold, defineBold as defineBold$1 } from "prosekit/extensions/bold";
12
+ import { defineCode, defineCode as defineCode$1 } from "prosekit/extensions/code";
13
+ import { defineItalic, defineItalic as defineItalic$1 } from "prosekit/extensions/italic";
14
+ import { defineLink, defineLink as defineLink$1 } from "prosekit/extensions/link";
15
+ import { defineStrike, defineStrike as defineStrike$1 } from "prosekit/extensions/strike";
16
+ import { defineUnderline, defineUnderline as defineUnderline$1 } from "prosekit/extensions/underline";
17
+ import { defineDoc } from "prosekit/extensions/doc";
18
+ import { defineText } from "prosekit/extensions/text";
19
+ import { defineParagraph } from "prosekit/extensions/paragraph";
20
+ import { defineHeading } from "prosekit/extensions/heading";
21
+ import { defineList } from "prosekit/extensions/list";
22
+ import { defineBlockquote } from "prosekit/extensions/blockquote";
23
+ import { defineImage } from "prosekit/extensions/image";
24
+ import { defineHorizontalRule } from "prosekit/extensions/horizontal-rule";
25
+ import { defineHardBreak } from "prosekit/extensions/hard-break";
26
+ import { defineTable } from "prosekit/extensions/table";
27
+ import { defineCodeBlock } from "prosekit/extensions/code-block";
28
+ import { defineGapCursor } from "prosekit/extensions/gap-cursor";
29
+ import { defineModClickPrevention } from "prosekit/extensions/mod-click-prevention";
30
+ import { defineVirtualSelection } from "prosekit/extensions/virtual-selection";
31
+ // src/prosekit/attribute-overrides.ts
32
+ /**
33
+ * The attribute overlay this library ships, and how to extend it.
34
+ *
35
+ * ProseMirror exposes a schema's *structure* at runtime but not its attribute
36
+ * *value types*: an `AttributeSpec` carries a `default` and an optional
37
+ * `validate` hint, nothing more. The derivation in
38
+ * `@thomasfosterau/prosekit-utilities/derive` recovers what it can from those
39
+ * and leaves a seam — the attribute overlay — for the rest. This module is
40
+ * that seam pre-filled for ProseKit's stock nodes, plus the merge helper an
41
+ * application uses to add its own entries.
42
+ *
43
+ * @module
44
+ */
45
+ /** The value schema both table cell types' `colwidth` attribute carries. */
46
+ const TABLE_CELL_COLWIDTH = /* @__PURE__ */ Schema.NullOr(/* @__PURE__ */ Schema.Array(Schema.Finite));
47
+ /**
48
+ * The overlay for ProseKit's stock nodes — the default when
49
+ * `defineDocumentSchema` is given no `attributes`.
50
+ *
51
+ * Every entry here is either a value type ProseMirror cannot express or a
52
+ * required-ness correction, and nothing else needs one: `link`'s
53
+ * `href`/`target`/`rel`, `list`'s `checked`/`collapsed` and the table cells'
54
+ * `colspan`/`rowspan` all derive correctly from ProseMirror's own `validate`
55
+ * string or default value.
56
+ *
57
+ * **Why `required(…)`.** ProseMirror's rule is that *any* attribute with a
58
+ * `default` — even one this overlay supplies a value type for — becomes an
59
+ * omissible key. But `heading.level`, `list.kind`, `image.src`/`width`/`height`
60
+ * and `codeBlock.language` are attributes a live editor always serialises:
61
+ * `Node.toJSON()` includes every attr, defaulted or not, so an
62
+ * editor-produced document always carries them. Leaving them omissible would
63
+ * let a machine-authored document skip the very value it was supposed to
64
+ * specify — a heading with no level, an image with no source — and still
65
+ * validate. `required(…)` closes that gap. `colwidth` is deliberately *not*
66
+ * wrapped: a cell that has never been resized genuinely has none.
67
+ */
68
+ const STANDARD_ATTRIBUTE_OVERRIDES = {
69
+ heading: { level: /* @__PURE__ */ required(/* @__PURE__ */ Schema.Literals([
70
+ 1,
71
+ 2,
72
+ 3,
73
+ 4,
74
+ 5,
75
+ 6
76
+ ])) },
77
+ list: {
78
+ kind: /* @__PURE__ */ required(/* @__PURE__ */ Schema.Literals([
79
+ "bullet",
80
+ "ordered",
81
+ "task",
82
+ "toggle"
83
+ ])),
84
+ order: /* @__PURE__ */ Schema.NullOr(Schema.Finite)
85
+ },
86
+ image: {
87
+ src: /* @__PURE__ */ required(/* @__PURE__ */ Schema.NullOr(Schema.String)),
88
+ width: /* @__PURE__ */ required(/* @__PURE__ */ Schema.NullOr(Schema.Finite)),
89
+ height: /* @__PURE__ */ required(/* @__PURE__ */ Schema.NullOr(Schema.Finite))
90
+ },
91
+ codeBlock: { language: /* @__PURE__ */ required(Schema.String) },
92
+ tableCell: { colwidth: TABLE_CELL_COLWIDTH },
93
+ tableHeaderCell: { colwidth: TABLE_CELL_COLWIDTH }
94
+ };
95
+ /**
96
+ * The overlay entry for `defineMention()`, to merge over
97
+ * {@link STANDARD_ATTRIBUTE_OVERRIDES} when a grammar includes the mention
98
+ * node.
99
+ *
100
+ * Both attributes are `required(…)` for the reason above: a mention the editor
101
+ * produced always carries an id and a label, and a mention without an id
102
+ * resolves to nothing.
103
+ */
104
+ const MENTION_ATTRIBUTE_OVERRIDES = { mention: {
105
+ id: /* @__PURE__ */ required(Schema.String),
106
+ label: /* @__PURE__ */ required(Schema.String)
107
+ } };
108
+ /**
109
+ * Merges overlays left to right, per node and then per attribute; a later
110
+ * entry wins.
111
+ *
112
+ * ```ts
113
+ * const attributes = mergeAttributeOverrides(
114
+ * STANDARD_ATTRIBUTE_OVERRIDES,
115
+ * MENTION_ATTRIBUTE_OVERRIDES,
116
+ * );
117
+ * ```
118
+ *
119
+ * The merge is shallow *per node*: two overlays that both name `image` end up
120
+ * with the union of their attribute entries rather than one replacing the
121
+ * other, so an application can override `image.src` alone without restating
122
+ * `width` and `height`. Nothing is mutated — every call returns a new record.
123
+ */
124
+ const mergeAttributeOverrides = (...overrides) => {
125
+ return Array.reduce(overrides, {}, (merged, current) => Record.union(merged, current, (left, right) => ({
126
+ ...left,
127
+ ...right
128
+ })));
129
+ };
130
+ // src/prosekit/lazy-schema.ts
131
+ /**
132
+ * Compile a ProseMirror schema once per isolate, on first use.
133
+ *
134
+ * @module
135
+ */
136
+ /**
137
+ * Wraps an extension factory in a compile-once, cache-forever thunk.
138
+ *
139
+ * ```ts
140
+ * const schema = lazySchema(() => defineBlockDocumentExtension(defineMention()));
141
+ * schema() === schema(); // the same compiled instance, for the life of the isolate
142
+ * ```
143
+ *
144
+ * **Why lazy.** Compiling a ProseMirror schema builds the whole node and mark
145
+ * registry and the content-match automata for every content expression in it.
146
+ * It is expensive, and the result never changes at runtime. Doing it at module
147
+ * scope would charge that cost to every bundle that so much as names a
148
+ * document type; doing it per call charges it per use.
149
+ *
150
+ * **Why this matters more than it sounds (PERF-CPU-001 in the source
151
+ * applications).** Before this cache existed there, schema validation called
152
+ * `createEditor(...)` on every call — once per jsonb document column on every
153
+ * database row read, and once per server-side document render. An O(1) cost
154
+ * had become O(rows). Anything on a per-row or per-render path must go
155
+ * through a thunk like this one, never `createEditor` directly.
156
+ *
157
+ * The cache is an `Option` rather than a nullable so a schema is never
158
+ * recompiled because a falsy-looking value was cached, and the thunk is a
159
+ * closure rather than a module-scope slot so two grammars never share one.
160
+ */
161
+ function lazySchema(define) {
162
+ let compiled = Option.none();
163
+ return () => {
164
+ if (Option.isNone(compiled)) {
165
+ const schema = createEditor({ extension: define() }).schema;
166
+ compiled = Option.some(schema);
167
+ return schema;
168
+ }
169
+ return compiled.value;
170
+ };
171
+ }
172
+ // src/prosekit/document-schema.ts
173
+ /**
174
+ * The document wire schema, derived from the editor's own ProseMirror schema.
175
+ *
176
+ * ## What a document schema is for
177
+ *
178
+ * One `DocumentSchema` is a single Effect Schema doing both jobs a document
179
+ * contract has to do:
180
+ *
181
+ * - The persistence and `{@html}`-render **trust boundary**. A jsonb document
182
+ * column (`@thomasfosterau/prosekit-utilities/drizzle`) decodes and encodes
183
+ * through `Document` on every read and write, and that is the first half of
184
+ * the SEC-XSS-001 defence in the source applications — the HTML serialiser's
185
+ * attribute escaping and link allow-list (`./html.ts`) is the second.
186
+ * - The **generative contract** for whole-document machine emission (agentic
187
+ * editing, a re-validation guard around a translation service), and the
188
+ * source for a JSON Schema via `Schema.toJsonSchemaDocument`.
189
+ *
190
+ * ## Where the grammar comes from
191
+ *
192
+ * Node and mark names, groups, the content model — including each node's
193
+ * `+`/`*` non-emptiness and any by-name content restriction, such as the
194
+ * sidenote's `(text | hardBreak)*` — and the admissible marks are read
195
+ * straight off the compiled ProseMirror schema by the `Registry` in
196
+ * `@thomasfosterau/prosekit-utilities/derive`, which walks the compiled
197
+ * `ContentMatch` automaton. There is no hand-transcribed grammar to drift out
198
+ * of sync with the live editor. Attribute value types, which ProseMirror
199
+ * cannot expose, come from the attribute overlay — see
200
+ * `./attribute-overrides.ts`.
201
+ *
202
+ * ## The two halves
203
+ *
204
+ * A `DocumentShape` (the root entry) is the browser-cheap half: it names the
205
+ * surface and checks the envelope structurally, with no ProseKit import.
206
+ * `defineDocumentSchema(shape, …)` is the server half, and `install()` is what
207
+ * joins them — from then on the shape rejects exactly what the derived schema
208
+ * rejects, for the life of the isolate.
209
+ *
210
+ * @module
211
+ */
212
+ /**
213
+ * A decode that failed, as a tagged error rather than a thrown
214
+ * `SchemaError` — for a caller that wants the failure in an `Effect` channel
215
+ * it can catch by tag.
216
+ *
217
+ * `document` is the shape's name, so one handler can tell a rejected title
218
+ * from a rejected body. `issue` is the underlying `SchemaError` carried
219
+ * through `Schema.Defect()`, which is JSON-encodable, so the error survives a
220
+ * wire hop to a client or a log sink.
221
+ */
222
+ var DocumentDecodeError = class extends (/* @__PURE__ */ Schema.TaggedError()("DocumentDecodeError", {
223
+ document: Schema.String,
224
+ issue: /* @__PURE__ */ Schema.Defect()
225
+ })) {};
226
+ /**
227
+ * Declares the ProseKit half of a document surface.
228
+ *
229
+ * ```ts
230
+ * const blockDocument = defineDocumentShape({ name: 'blockDocument' }); // root entry
231
+ * const cache = createRegistryCache(); // ./derive
232
+ *
233
+ * export const blockDocumentSchema = defineDocumentSchema(blockDocument, {
234
+ * extension: () => defineBlockDocumentExtension(defineMention()),
235
+ * attributes: mergeAttributeOverrides(STANDARD_ATTRIBUTE_OVERRIDES, MENTION_ATTRIBUTE_OVERRIDES),
236
+ * registryCache: cache,
237
+ * });
238
+ * ```
239
+ *
240
+ * ## Lazy, all the way down
241
+ *
242
+ * Nothing here compiles or derives anything at declaration time.
243
+ * {@link DocumentSchema.schema} is a `lazySchema` thunk, and `Document` is
244
+ * wrapped in `Schema.suspend`, which defers its body until the schema is first
245
+ * used and memoises it thereafter. So importing the module that declares a
246
+ * document surface — as a model layer or a route module does — never pays for
247
+ * a ProseMirror compile or for building the derived Effect Schema graph unless
248
+ * a document is actually validated (PERF-CPU-001 in the source applications).
249
+ *
250
+ * ## Sharing derivation without coupling
251
+ *
252
+ * Two document schemas built from overlapping extensions — a block grammar and
253
+ * an inline grammar both composing `defineStandardMarks()` and `defineText()` —
254
+ * derive the same mark schemas and the same `text` node schema. Passing both
255
+ * the same `registryCache` means whichever is forced *first* builds that shared
256
+ * structure and the other reuses it.
257
+ *
258
+ * They nonetheless stay **independently lazy and independent failure
259
+ * domains**: validating one never forces compiling or deriving the other, and
260
+ * a defect in one's derivation cannot break the other. That is why this is not
261
+ * `deriveDocumentSchemas`, which batches N schemas in one eager call — it would
262
+ * force compiling every grammar up front and couple their failure domains, for
263
+ * exactly the same sharing benefit this gets without either cost.
264
+ *
265
+ * ## When two grammars may share a cache
266
+ *
267
+ * Only when their **mark sets match** and every node or mark type they both
268
+ * name is defined identically (the same spec and the same attribute overlay).
269
+ * The cache memoises derived schemas by type *name*, and an inline node's
270
+ * derived `marks` field is the union of every mark its grammar's textblocks
271
+ * admit. So two grammars that compose the same `text` node but different marks
272
+ * do not derive the same `text` schema, and whichever is forced first decides
273
+ * it for both.
274
+ *
275
+ * Concretely: a block grammar that adds `defineCitation()` and an inline
276
+ * grammar that does not must not share a cache. If they do, forcing the block
277
+ * schema first makes the inline schema accept a `citation` mark it never
278
+ * declared, and forcing the inline schema first makes the block schema reject
279
+ * its own `citation` marks. Neither order raises an error. Give such grammars
280
+ * separate caches (or none: the default is a private cache).
281
+ */
282
+ function defineDocumentSchema(shape, options) {
283
+ const schema = lazySchema(options.extension);
284
+ const overlay = attributeOverlay(options.attributes ?? STANDARD_ATTRIBUTE_OVERRIDES);
285
+ const shared = options.registryCache;
286
+ let own = Option.none();
287
+ const cacheFor = () => {
288
+ if (shared !== void 0) return shared;
289
+ if (Option.isSome(own)) return own.value;
290
+ const created = createRegistryCache();
291
+ own = Option.some(created);
292
+ return created;
293
+ };
294
+ const Document = Schema.suspend(() => {
295
+ const compiled = schema();
296
+ return new Registry(compiled, { attributes: overlay }, cacheFor()).nodeAs(compiled.topNodeType, shape.name);
297
+ });
298
+ const guard = Schema.is(Document);
299
+ const decodeUnknown = Schema.decodeUnknownEffect(Document);
300
+ return {
301
+ shape,
302
+ extension: options.extension,
303
+ schema,
304
+ Document,
305
+ is: (value) => Result.getOrElse(Result.try(() => guard(value)), () => false),
306
+ decode: (value) => Effect.mapError(decodeUnknown(value), (issue) => new DocumentDecodeError({
307
+ document: shape.name,
308
+ issue
309
+ })),
310
+ install: () => {
311
+ shape.install((value) => guard(value));
312
+ }
313
+ };
314
+ }
315
+ /**
316
+ * Installs every schema's grammar guard onto its shape — the persistence
317
+ * layer's one call.
318
+ *
319
+ * ```ts
320
+ * installDocumentGrammars(blockDocumentSchema, inlineDocumentSchema);
321
+ * ```
322
+ *
323
+ * Put it at module scope in the module a server runtime loads before handling
324
+ * a request and no browser bundle can reach (the one that declares the
325
+ * document columns). Installing is cheap — it stores a function reference and
326
+ * forces nothing — and every shape stays permissive until it happens, so a
327
+ * runtime that never installs under-checks rather than wrongly rejects.
328
+ */
329
+ const installDocumentGrammars = (...schemas) => {
330
+ Array.forEach(schemas, (schema) => schema.install());
331
+ };
332
+ // src/prosekit/html.ts
333
+ /**
334
+ * HTML generation for editor documents, through ProseKit and a DOM.
335
+ *
336
+ * ProseKit's `htmlFromJSON` serialises through real DOM nodes, so the renderer
337
+ * needs a `Document`. In a browser that is the global one; on a server it is
338
+ * whatever the runtime can provide. See {@link createHtmlRenderer} for how one
339
+ * is chosen and why it must be linkedom rather than happy-dom.
340
+ *
341
+ * Security: {@link sanitiseDocument} runs first on every render, dropping
342
+ * `link` marks and `image` nodes whose URL is not in the safe-URL allow-list.
343
+ * Together with the derived wire schema's decode (`./document-schema.ts`) that
344
+ * is the SEC-XSS-001 defence in the source applications: the schema decides
345
+ * what a document may contain, this decides what a URL may be.
346
+ *
347
+ * @module
348
+ */
349
+ /**
350
+ * A render that failed.
351
+ *
352
+ * `dom-unavailable` means no `Document` could be obtained: there is no global
353
+ * one and `linkedom` could not be imported (or a caller-supplied `document`
354
+ * thunk rejected). `serialise` means the document itself could not be turned
355
+ * into HTML — most often a node or mark the compiled schema does not know,
356
+ * which is what `htmlFromJSON` throws on. `cause` carries the original through
357
+ * `Schema.Defect()`, so the error is JSON-encodable and survives a log sink.
358
+ */
359
+ var HtmlRenderError = class extends (/* @__PURE__ */ Schema.TaggedError()("HtmlRenderError", {
360
+ reason: /* @__PURE__ */ Schema.Literals(["dom-unavailable", "serialise"]),
361
+ cause: /* @__PURE__ */ Schema.Defect()
362
+ })) {};
363
+ /**
364
+ * A copy of a document tree with every unsafe URL removed: `link` marks whose
365
+ * `href` fails {@link isSafeHref} are dropped from the node that carries them,
366
+ * and `image` children whose `src` fails it are dropped from their parent's
367
+ * content. Recursive, and non-mutating.
368
+ *
369
+ * Dropping the mark rather than the text keeps the prose — an unsafe link
370
+ * renders as plain words — while dropping the whole `image` node is the only
371
+ * option for a leaf whose entire content is its URL.
372
+ *
373
+ * Exported because the same pass belongs at any boundary that hands a document
374
+ * to something which will render it, not only to {@link createHtmlRenderer}.
375
+ */
376
+ function sanitiseDocument(node) {
377
+ const copy = { ...node };
378
+ const marked = Array.isArray(node.marks) ? {
379
+ ...copy,
380
+ marks: node.marks.filter((mark) => mark.type !== "link" || isSafeHref(mark.attrs?.href))
381
+ } : copy;
382
+ return Array.isArray(node.content) ? {
383
+ ...marked,
384
+ content: node.content.filter((child) => child.type !== "image" || isSafeHref(child.attrs?.src)).map(sanitiseDocument)
385
+ } : marked;
386
+ }
387
+ /**
388
+ * The default `Document` source, resolved once per isolate.
389
+ *
390
+ * **Capability detection, not an environment flag.** A browser — and vitest's
391
+ * happy-dom environment — provides a global `document`; every server runtime
392
+ * (Node, workerd) does not. `import.meta.env.SSR` is `false` under vitest's
393
+ * *node* environment, so flag-based branching picks the browser path exactly
394
+ * where it must not.
395
+ *
396
+ * **On the server this must be linkedom, not happy-dom.** happy-dom pulls in
397
+ * `node:child_process` and `node:vm`, which workerd does not provide: the
398
+ * import fails at request time and takes down every page that renders prose.
399
+ * linkedom is pure JavaScript and runs anywhere. It is an optional peer for
400
+ * exactly this reason — a browser-only consumer never installs it, and the
401
+ * import below is never reached.
402
+ *
403
+ * The promise, not the document, is cached: concurrent first renders share one
404
+ * import rather than racing several.
405
+ */
406
+ let defaultDocument = /* @__PURE__ */ Option.none();
407
+ const resolveDefaultDocument = () => {
408
+ if (Option.isNone(defaultDocument)) {
409
+ const loading = (async () => {
410
+ if (!Predicate.isUndefined(globalThis.document)) return globalThis.document;
411
+ const { parseHTML } = await import("linkedom");
412
+ return parseHTML("<!doctype html><html><body></body></html>").document;
413
+ })();
414
+ defaultDocument = Option.some(loading);
415
+ return loading;
416
+ }
417
+ return defaultDocument.value;
418
+ };
419
+ /**
420
+ * Builds a renderer bound to one compiled schema.
421
+ *
422
+ * ```ts
423
+ * const renderer = createHtmlRenderer({ schema: blockDocumentSchema.schema });
424
+ * const html = await renderer.renderPromise(document);
425
+ * ```
426
+ *
427
+ * A renderer is cheap to create and holds the resolved `Document` promise, so
428
+ * create one per grammar at module scope rather than one per render.
429
+ *
430
+ * Every render sanitises first ({@link sanitiseDocument}), then rewrites the
431
+ * envelope's `type` back to `doc` — the surface name is a persistence rename,
432
+ * and a compiled schema's top node is always `doc`, so feeding the stored
433
+ * shape straight in throws `RangeError: Unknown node type`.
434
+ */
435
+ function createHtmlRenderer(options) {
436
+ const documentOf = options.document ?? resolveDefaultDocument;
437
+ const postProcess = options.postProcess;
438
+ let resolved = Option.none();
439
+ const documentPromise = () => {
440
+ if (Option.isNone(resolved)) {
441
+ const loading = documentOf();
442
+ resolved = Option.some(loading);
443
+ return loading;
444
+ }
445
+ return resolved.value;
446
+ };
447
+ const serialise = (doc, host) => {
448
+ const editorDocument = toEditorDocument(sanitiseDocument(doc));
449
+ const html = htmlFromJSON(editorDocument, {
450
+ schema: options.schema(),
451
+ document: host
452
+ });
453
+ return postProcess === void 0 ? html : postProcess(html, host);
454
+ };
455
+ const hostPromise = () => Result.match(Result.try(documentPromise), {
456
+ onSuccess: (loading) => loading,
457
+ onFailure: (cause) => Promise.reject(cause)
458
+ });
459
+ const renderPromise = (doc) => hostPromise().then((host) => Result.match(Result.try(() => serialise(doc, host)), {
460
+ onSuccess: (html) => html,
461
+ onFailure: (cause) => Promise.reject(new HtmlRenderError({
462
+ reason: "serialise",
463
+ cause
464
+ }))
465
+ }), (cause) => Promise.reject(new HtmlRenderError({
466
+ reason: "dom-unavailable",
467
+ cause
468
+ })));
469
+ return {
470
+ renderPromise,
471
+ render: (doc) => Effect.tryPromise({
472
+ try: () => renderPromise(doc),
473
+ catch: (cause) => cause instanceof HtmlRenderError ? cause : new HtmlRenderError({
474
+ reason: "serialise",
475
+ cause
476
+ })
477
+ })
478
+ };
479
+ }
480
+ // src/prosekit/marks.ts
481
+ /**
482
+ * Mark extensions: ProseKit's stock set, re-exported, plus the three marks
483
+ * this library defines itself and the opt-in `citation` mark.
484
+ *
485
+ * `highlight`, `subscript` and `superscript` are **not** taken from
486
+ * `prosekit/extensions/*` even though ProseKit ships same-named extensions:
487
+ * theirs carry different attribute shapes, and the specs here are
488
+ * wire-identical to documents the source applications have already persisted.
489
+ * Since the derived wire schema reads its mark set off the compiled
490
+ * ProseMirror schema, swapping in the stock versions would change what
491
+ * already-stored rows decode as. They are exported individually as well as
492
+ * through {@link defineStandardMarks} so an application composing a narrower
493
+ * grammar can pick them up one at a time.
494
+ *
495
+ * The mark set is half of what constrains rendered HTML — a mark is what
496
+ * turns a run of text into an element — so it is part of the same defence as
497
+ * the HTML serialiser's link allow-list (SEC-XSS-001 in the source
498
+ * applications).
499
+ *
500
+ * @module
501
+ */
502
+ /** The `highlight` mark: a run of text marked for attention, rendered `<mark>`. */
503
+ function defineHighlight() {
504
+ return defineMarkSpec({
505
+ name: "highlight",
506
+ parseDOM: [{ tag: "mark" }],
507
+ toDOM() {
508
+ return ["mark", 0];
509
+ }
510
+ });
511
+ }
512
+ /**
513
+ * The `subscript` mark, rendered `<sub>`.
514
+ *
515
+ * Excludes {@link defineSuperscript}'s mark: a run of text is one or the
516
+ * other, never both, and ProseMirror enforces that at the model level rather
517
+ * than leaving it to the toolbar.
518
+ */
519
+ function defineSubscript() {
520
+ return defineMarkSpec({
521
+ name: "subscript",
522
+ excludes: "superscript",
523
+ parseDOM: [{ tag: "sub" }],
524
+ toDOM() {
525
+ return ["sub", 0];
526
+ }
527
+ });
528
+ }
529
+ /** The `superscript` mark, rendered `<sup>`; excludes `subscript` — see {@link defineSubscript}. */
530
+ function defineSuperscript() {
531
+ return defineMarkSpec({
532
+ name: "superscript",
533
+ excludes: "subscript",
534
+ parseDOM: [{ tag: "sup" }],
535
+ toDOM() {
536
+ return ["sup", 0];
537
+ }
538
+ });
539
+ }
540
+ /**
541
+ * An inline mark binding a run of text to a cited work by id, rendered
542
+ * `<cite data-work-id>`.
543
+ *
544
+ * A mark rather than a node, deliberately: marks carry no content, so a
545
+ * citation annotates a run of prose and can never swallow structure the way a
546
+ * footnote node could. `workId` is a caller-owned handle and is emitted as a
547
+ * data attribute, never as a URL.
548
+ *
549
+ * Opt-in: not part of {@link defineStandardMarks}, because adding it changes
550
+ * what the derived wire schema accepts.
551
+ */
552
+ function defineCitation() {
553
+ return defineMarkSpec({
554
+ name: "citation",
555
+ attrs: { workId: { default: "" } },
556
+ parseDOM: [{
557
+ tag: "cite[data-work-id]",
558
+ getAttrs: (dom) => ({ workId: dom.getAttribute("data-work-id") ?? "" })
559
+ }],
560
+ toDOM(mark) {
561
+ return [
562
+ "cite",
563
+ { "data-work-id": mark.attrs.workId },
564
+ 0
565
+ ];
566
+ }
567
+ });
568
+ }
569
+ /**
570
+ * The nine marks both presets in `./presets.ts` compose: ProseKit's `bold`,
571
+ * `italic`, `underline`, `strike`, `code` and `link`, plus this library's
572
+ * `highlight`, `subscript` and `superscript`.
573
+ *
574
+ * `citation` is not in the set — it is opt-in, and an application that wants
575
+ * it passes `defineCitation()` as an extra to the preset.
576
+ */
577
+ function defineStandardMarks() {
578
+ return union(defineBold$1(), defineItalic$1(), defineUnderline$1(), defineStrike$1(), defineCode$1(), defineLink$1(), defineHighlight(), defineSubscript(), defineSuperscript());
579
+ }
580
+ // src/prosekit/nodes.ts
581
+ /**
582
+ * Node extensions: ProseKit's stock set, re-exported, plus the opt-in node
583
+ * definitions this library adds.
584
+ *
585
+ * The stock re-exports exist so a consumer composes one grammar from one
586
+ * import rather than a dozen `prosekit/extensions/*` specifiers, and so the
587
+ * presets in `./presets.ts` and an application's own extension are provably
588
+ * built from the same definitions.
589
+ *
590
+ * The opt-in nodes below are **not** in either preset. Each one changes what
591
+ * the derived wire schema accepts, and a document written under a grammar
592
+ * that includes `mention` does not decode under one that does not, so adding
593
+ * one is a deliberate act at the application's own `defineBlockDocumentExtension(…)`
594
+ * call site. Every `toDOM` here emits data attributes only — no `href`, no
595
+ * `src` — so the one URL-valued attribute in the stock grammar stays the
596
+ * `image` node's `src`, which `sanitiseDocument` and `isSafeHref` already
597
+ * gate (SEC-XSS-001 in the source applications).
598
+ *
599
+ * @module
600
+ */
601
+ /**
602
+ * A `doc` top node that accepts inline content only (`content: "inline*"`).
603
+ *
604
+ * The shape a single-line rich-text field wants: marks and text, no
605
+ * paragraphs, no blocks. Each compiled schema owns its own node registry, so
606
+ * this never conflicts with the stock `defineDoc()` (`content: "block+"`) —
607
+ * the two simply belong to different extensions.
608
+ */
609
+ function defineInlineDoc() {
610
+ return defineNodeSpec({
611
+ name: "doc",
612
+ content: "inline*",
613
+ topNode: true
614
+ });
615
+ }
616
+ /**
617
+ * An inline atom that points at an entity by id and carries the text to
618
+ * render for it — the `@name` shape.
619
+ *
620
+ * `id` is the stable handle a consumer resolves (a notification fan-out, a
621
+ * profile link); `label` is display text and is untrusted, which is why
622
+ * `toDOM` emits it as a text child rather than as markup. The node is an
623
+ * atom, so nothing can be typed inside it and the label only ever changes by
624
+ * replacing the whole node.
625
+ *
626
+ * This is deliberately **not** `prosekit/extensions/mention`: ProseKit's stock
627
+ * mention carries a different attribute shape, and this one is wire-identical
628
+ * to documents the source applications have already persisted.
629
+ */
630
+ function defineMention() {
631
+ return defineNodeSpec({
632
+ name: "mention",
633
+ group: "inline",
634
+ inline: true,
635
+ atom: true,
636
+ attrs: {
637
+ id: { default: "" },
638
+ label: { default: "" }
639
+ },
640
+ parseDOM: [{
641
+ tag: "span[data-mention-id]",
642
+ getAttrs: (dom) => ({
643
+ id: dom.getAttribute("data-mention-id") ?? "",
644
+ label: dom.getAttribute("data-mention-label") ?? ""
645
+ })
646
+ }],
647
+ toDOM(node) {
648
+ const { id, label } = node.attrs;
649
+ return [
650
+ "span",
651
+ {
652
+ "data-mention-id": id,
653
+ "data-mention-label": label,
654
+ class: "mention"
655
+ },
656
+ `@${label}`
657
+ ];
658
+ }
659
+ });
660
+ }
661
+ /**
662
+ * The caption half of a figure: inline content only.
663
+ *
664
+ * `content: "inline*"` rather than `block+` is what stops a caption nesting a
665
+ * paragraph, a list, or another figure — a caption is a line of prose, and
666
+ * the derived wire schema enforces exactly that because it reads this content
667
+ * expression off the compiled schema.
668
+ *
669
+ * The derived wire schema does not, however, require a figure to carry a
670
+ * caption: see {@link defineFigureNode} for what it does and does not enforce
671
+ * about the pair.
672
+ */
673
+ function defineFigcaption() {
674
+ return defineNodeSpec({
675
+ name: "figcaption",
676
+ content: "inline*",
677
+ parseDOM: [{ tag: "figcaption" }],
678
+ toDOM() {
679
+ return ["figcaption", 0];
680
+ }
681
+ });
682
+ }
683
+ /**
684
+ * A captioned image block: in the editor, exactly one `image` followed by
685
+ * exactly one `figcaption`.
686
+ *
687
+ * The fixed content expression is the point — a figure is a pair, not a
688
+ * container, so ProseMirror's DOM parser and its editing transactions never
689
+ * let either half be dropped or duplicated. `defining` keeps the figure as its
690
+ * own context on paste rather than dissolving into the surrounding block.
691
+ *
692
+ * Loading JSON is not checked the same way: ProseMirror's
693
+ * `schema.nodeFromJSON` validates attributes but not content, and ProseKit's
694
+ * `defaultContent` goes through it, so a stored captionless figure loads into
695
+ * an editor unchecked. A consumer that wants the content expression enforced
696
+ * on a loaded document calls `node.check()`, which ProseKit's
697
+ * `editor.setContent` already does.
698
+ *
699
+ * The derived wire schema is deliberately laxer. Derivation reads the set of
700
+ * admissible children and the empty-vs-non-empty distinction off the compiled
701
+ * content expression, not the ordered sequence (see `derive/Content`), so a
702
+ * stored `figure` decodes when its `content` is any non-empty array of `image`
703
+ * and `figcaption` nodes: a figure without a figcaption, a figcaption alone,
704
+ * the halves reordered or one duplicated all decode. An empty figure and any
705
+ * other child type are still rejected. A consumer that needs the pair enforced
706
+ * at the wire adds its own check, for example `node.check()` on the node it
707
+ * builds from the decoded JSON.
708
+ *
709
+ * Use {@link defineFigure} unless you are composing the two halves yourself:
710
+ * this node is unusable without {@link defineFigcaption} in the same
711
+ * extension, and the stock `image` node besides.
712
+ */
713
+ function defineFigureNode() {
714
+ return defineNodeSpec({
715
+ name: "figure",
716
+ group: "block",
717
+ content: "image figcaption",
718
+ defining: true,
719
+ parseDOM: [{ tag: "figure" }],
720
+ toDOM() {
721
+ return ["figure", 0];
722
+ }
723
+ });
724
+ }
725
+ /** {@link defineFigureNode} together with its {@link defineFigcaption} child. */
726
+ function defineFigure() {
727
+ return union(defineFigureNode(), defineFigcaption());
728
+ }
729
+ /**
730
+ * An inline aside that sits alongside the body prose — a reference note the
731
+ * reader can take or leave, rendered in the margin on wide viewports and as a
732
+ * footnote-like inline block on narrow ones.
733
+ *
734
+ * The note holds **rich inline content** (text plus the surrounding marks),
735
+ * not a plain-text attribute, so a link or emphasis inside a note round-trips
736
+ * through the node's own children on every surface: the editor, the HTML
737
+ * serialiser, the Markdown walk and paste.
738
+ *
739
+ * It is `inline: true` but deliberately **not** `atom`: that is what lets the
740
+ * cursor enter the note and edit its content in place with the same toolbar
741
+ * that styles body prose, with no nested editor and no node view. An `atom`
742
+ * inline node cannot hold editable inline children without one. `selectable`
743
+ * makes the whole note pickable; `defining` keeps its boundary on paste.
744
+ *
745
+ * Its content is `(text | hardBreak)*` — the two text-like inline nodes by
746
+ * name, not the broad `inline` group — so **a note can never nest a note**.
747
+ * `sidenote` is itself in group `inline`, so `content: "inline*"` would admit
748
+ * `^[…^[…]…]`, which has no valid inline-footnote rendering in Markdown; the
749
+ * derived wire schema reads this very expression off the compiled schema and
750
+ * rejects the nesting for free.
751
+ *
752
+ * Placement is a render-time, CSS-only concern: the node is layout-agnostic
753
+ * and ships no styles.
754
+ */
755
+ function defineSidenote() {
756
+ return defineNodeSpec({
757
+ name: "sidenote",
758
+ group: "inline",
759
+ inline: true,
760
+ content: "(text | hardBreak)*",
761
+ defining: true,
762
+ selectable: true,
763
+ parseDOM: [{ tag: "span[data-sidenote]" }],
764
+ toDOM: () => [
765
+ "span",
766
+ { "data-sidenote": "" },
767
+ 0
768
+ ]
769
+ });
770
+ }
771
+ // src/prosekit/presets.ts
772
+ /**
773
+ * The two composite extensions an application starts from, and the editor
774
+ * behaviour they share.
775
+ *
776
+ * Each preset takes `...extra` extensions and unions them in last, which is
777
+ * how an opt-in node or mark joins a grammar:
778
+ *
779
+ * ```ts
780
+ * const extension = () =>
781
+ * defineBlockDocumentExtension(defineMention(), defineFigure(), defineCitation());
782
+ * ```
783
+ *
784
+ * The presets are deliberately not configurable beyond that. A grammar is a
785
+ * wire contract: adding an extension widens what decodes, and an options bag
786
+ * that could *remove* a stock node would silently narrow it for documents
787
+ * already written. Composing a narrower grammar means calling `union` over
788
+ * the node and mark definitions directly.
789
+ *
790
+ * @module
791
+ */
792
+ /**
793
+ * The interaction tail every editor wants and no document shape depends on:
794
+ * the base keymap, the base commands, undo/redo history, the gap cursor, the
795
+ * virtual selection and mod-click prevention.
796
+ *
797
+ * None of these registers a node or a mark, so including them changes nothing
798
+ * about what a derived wire schema accepts — which is why compiling a schema
799
+ * for validation alone can include them without consequence, and why they are
800
+ * a separate export rather than baked into the node lists.
801
+ */
802
+ function defineEditorBehaviour() {
803
+ return union(defineBaseKeymap(), defineBaseCommands(), defineHistory(), defineGapCursor(), defineVirtualSelection(), defineModClickPrevention());
804
+ }
805
+ /**
806
+ * The block-document grammar: a `block+` top node, text, ProseKit's stock
807
+ * block set (paragraph, heading, list, blockquote, image, horizontal rule,
808
+ * hard break, table, code block), {@link defineStandardMarks} and
809
+ * {@link defineEditorBehaviour}, plus whatever `extra` extensions the caller
810
+ * composes in.
811
+ */
812
+ function defineBlockDocumentExtension(...extra) {
813
+ return union(defineDoc(), defineText(), defineParagraph(), defineHeading(), defineList(), defineBlockquote(), defineImage(), defineHorizontalRule(), defineHardBreak(), defineTable(), defineCodeBlock(), defineStandardMarks(), defineEditorBehaviour(), ...extra);
814
+ }
815
+ /**
816
+ * The inline-document grammar: an `inline*` top node
817
+ * ({@link defineInlineDoc}), text, {@link defineStandardMarks} and
818
+ * {@link defineEditorBehaviour}, plus whatever `extra` extensions the caller
819
+ * composes in.
820
+ *
821
+ * No block node is registered at all, so a paragraph is not merely
822
+ * unreachable from the toolbar — it fails to decode.
823
+ */
824
+ function defineInlineDocumentExtension(...extra) {
825
+ return union(defineInlineDoc(), defineText(), defineStandardMarks(), defineEditorBehaviour(), ...extra);
826
+ }
827
+ export { DocumentDecodeError, HtmlRenderError, MENTION_ATTRIBUTE_OVERRIDES, STANDARD_ATTRIBUTE_OVERRIDES, createHtmlRenderer, defineBlockDocumentExtension, defineBlockquote, defineBold, defineCitation, defineCode, defineCodeBlock, defineDoc, defineDocumentSchema, defineEditorBehaviour, defineFigcaption, defineFigure, defineFigureNode, defineHardBreak, defineHeading, defineHighlight, defineHorizontalRule, defineImage, defineInlineDoc, defineInlineDocumentExtension, defineItalic, defineLink, defineList, defineMention, defineParagraph, defineSidenote, defineStandardMarks, defineStrike, defineSubscript, defineSuperscript, defineTable, defineText, defineUnderline, installDocumentGrammars, lazySchema, mergeAttributeOverrides, sanitiseDocument };