@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
package/dist/prosekit.js
ADDED
|
@@ -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 };
|