@openleaf-editor/core 0.1.0-beta.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,91 @@
1
+ /**
2
+ * Schema extensions: how a plugin contributes node and mark types.
3
+ *
4
+ * ## Why this could not be a simple registry
5
+ *
6
+ * A ProseMirror `Schema` is immutable, and `EditorState.reconfigure` -- the
7
+ * mechanism that lets a late-loading plugin add behaviour to an open editor --
8
+ * cannot change it. Verified in prosemirror-state: `reconfigure` builds
9
+ * `new Configuration(this.schema, config.plugins)`, taking the schema from the
10
+ * old state. So schema extension is not "register and the editors update"; it is
11
+ * "register before an editor is built, or wait for the next one".
12
+ *
13
+ * That constraint decides the shape of everything here.
14
+ *
15
+ * ## Append-only, and why it is not a preference
16
+ *
17
+ * Extension nodes are appended, never prepended, and there is no positioning
18
+ * hint. Measured: prepending a `group: 'block'` node makes it the document's
19
+ * `defaultType`, so `topNodeType.createAndFill()` produces
20
+ * `{"type":"doc","content":[{"type":"widget"}]}` instead of an empty paragraph.
21
+ * Every new document, and every gap the editor fills, would start with a
22
+ * plugin's widget.
23
+ *
24
+ * ## No priority field
25
+ *
26
+ * The preservation layer's catch-all rules sit at priority 0 and 1, which makes
27
+ * them the last two rules in the parse table -- so an extension rule at the
28
+ * default priority already wins, for free. Offering a priority knob would invite
29
+ * an author to set `priority: 0` to be polite and thereby tie with the
30
+ * catch-all, where the winner is decided by map insertion order. Instead,
31
+ * `createSchema` rejects any rule at priority <= 1 and says why.
32
+ *
33
+ * ## Collisions throw
34
+ *
35
+ * Deliberately the opposite of `registerToolbarItem`, which is last-wins because
36
+ * a button is UI and replacing one is a feature. A node type is a *storage
37
+ * format*: two definitions of `footnote` mean two serializations of the same
38
+ * content chosen by script-tag order, and whichever loses has already written
39
+ * documents in its shape. `replaces` is the explicit opt-in.
40
+ */
41
+ import { Schema, type MarkSpec, type NodeSpec } from 'prosemirror-model';
42
+ export interface SchemaExtension {
43
+ /** Stable and unique. Namespace it: `openleaf/footnote`. */
44
+ readonly id: string;
45
+ /** Node types by schema name, appended after the core nodes. */
46
+ readonly nodes?: Readonly<Record<string, NodeSpec>>;
47
+ /** Mark types by schema name. */
48
+ readonly marks?: Readonly<Record<string, MarkSpec>>;
49
+ /** Names this extension deliberately replaces. Absent means a clash throws. */
50
+ readonly replaces?: readonly string[];
51
+ /**
52
+ * Re-emit attributes the spec does not model. Defaults to true.
53
+ *
54
+ * Adding a node type strictly *reduces* fidelity for the tag it claims: before
55
+ * the node existed, the preservation layer kept the element and every
56
+ * attribute on it; afterwards the spec keeps only what it declares. A callout
57
+ * node modelling `class` silently drops `id` and `data-analytics` that used to
58
+ * survive.
59
+ *
60
+ * So unmodelled attributes are captured on parse and merged back on
61
+ * serialize, by default, at schema-build time -- which means an author cannot
62
+ * opt out by forgetting.
63
+ */
64
+ readonly carryUnknownAttributes?: boolean;
65
+ }
66
+ /** Where carried attributes live. Underscored: it is not for plugin authors. */
67
+ export declare const CARRIED_ATTR = "__openleafCarried";
68
+ /**
69
+ * Register a schema extension.
70
+ *
71
+ * Must happen before the editor that should have it is constructed. The element
72
+ * defers building its view until the document's scripts have run, which covers
73
+ * every documented integration; anything registering later applies to editors
74
+ * created after it, not to open ones.
75
+ */
76
+ export declare function registerSchemaExtension(extension: SchemaExtension): () => void;
77
+ export declare function registeredSchemaExtensions(): readonly SchemaExtension[];
78
+ export declare function onSchemaExtensionsChange(listener: () => void): () => void;
79
+ /** Testing seam. Not part of the public API. */
80
+ export declare function clearSchemaExtensions(): void;
81
+ export declare function createSchema(list?: readonly SchemaExtension[]): Schema;
82
+ /**
83
+ * The schema for the currently registered extensions.
84
+ *
85
+ * A function rather than a constant, and this is the point: a `const` reads as
86
+ * "bind to this" and would be captured at import by every consumer, which is
87
+ * exactly what made the schema impossible to extend. Memoized, and invalidated
88
+ * whenever the registry changes.
89
+ */
90
+ export declare function coreSchema(): Schema;
91
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,EACL,MAAM,EAEN,KAAK,QAAQ,EACb,KAAK,QAAQ,EAGd,MAAM,mBAAmB,CAAA;AAI1B,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IACnD,iCAAiC;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IACnD,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,sBAAsB,CAAC,EAAE,OAAO,CAAA;CAC1C;AAED,gFAAgF;AAChF,eAAO,MAAM,YAAY,sBAAsB,CAAA;AAmB/C;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,eAAe,GAAG,MAAM,IAAI,CAe9E;AAED,wBAAgB,0BAA0B,IAAI,SAAS,eAAe,EAAE,CAEvE;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAKzE;AAED,gDAAgD;AAChD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAgKD,wBAAgB,YAAY,CAAC,IAAI,GAAE,SAAS,eAAe,EAAO,GAAG,MAAM,CAyB1E;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAGnC"}
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Schema extensions: how a plugin contributes node and mark types.
3
+ *
4
+ * ## Why this could not be a simple registry
5
+ *
6
+ * A ProseMirror `Schema` is immutable, and `EditorState.reconfigure` -- the
7
+ * mechanism that lets a late-loading plugin add behaviour to an open editor --
8
+ * cannot change it. Verified in prosemirror-state: `reconfigure` builds
9
+ * `new Configuration(this.schema, config.plugins)`, taking the schema from the
10
+ * old state. So schema extension is not "register and the editors update"; it is
11
+ * "register before an editor is built, or wait for the next one".
12
+ *
13
+ * That constraint decides the shape of everything here.
14
+ *
15
+ * ## Append-only, and why it is not a preference
16
+ *
17
+ * Extension nodes are appended, never prepended, and there is no positioning
18
+ * hint. Measured: prepending a `group: 'block'` node makes it the document's
19
+ * `defaultType`, so `topNodeType.createAndFill()` produces
20
+ * `{"type":"doc","content":[{"type":"widget"}]}` instead of an empty paragraph.
21
+ * Every new document, and every gap the editor fills, would start with a
22
+ * plugin's widget.
23
+ *
24
+ * ## No priority field
25
+ *
26
+ * The preservation layer's catch-all rules sit at priority 0 and 1, which makes
27
+ * them the last two rules in the parse table -- so an extension rule at the
28
+ * default priority already wins, for free. Offering a priority knob would invite
29
+ * an author to set `priority: 0` to be polite and thereby tie with the
30
+ * catch-all, where the winner is decided by map insertion order. Instead,
31
+ * `createSchema` rejects any rule at priority <= 1 and says why.
32
+ *
33
+ * ## Collisions throw
34
+ *
35
+ * Deliberately the opposite of `registerToolbarItem`, which is last-wins because
36
+ * a button is UI and replacing one is a feature. A node type is a *storage
37
+ * format*: two definitions of `footnote` mean two serializations of the same
38
+ * content chosen by script-tag order, and whichever loses has already written
39
+ * documents in its shape. `replaces` is the explicit opt-in.
40
+ */
41
+ import OrderedMap from 'orderedmap';
42
+ import { Schema, } from 'prosemirror-model';
43
+ import { coreMarks, coreNodes } from './schema.js';
44
+ import { URL_ATTRIBUTES, isEventHandlerAttribute, isSafeUrl } from './url.js';
45
+ /** Where carried attributes live. Underscored: it is not for plugin authors. */
46
+ export const CARRIED_ATTR = '__openleafCarried';
47
+ /* ------------------------------------------------------------------ *
48
+ * Registry
49
+ * ------------------------------------------------------------------ */
50
+ const extensions = new Map();
51
+ const listeners = new Set();
52
+ function notify() {
53
+ for (const listener of listeners) {
54
+ try {
55
+ listener();
56
+ }
57
+ catch (error) {
58
+ console.error('@openleaf-editor/core: a schema-extension listener threw', error);
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Register a schema extension.
64
+ *
65
+ * Must happen before the editor that should have it is constructed. The element
66
+ * defers building its view until the document's scripts have run, which covers
67
+ * every documented integration; anything registering later applies to editors
68
+ * created after it, not to open ones.
69
+ */
70
+ export function registerSchemaExtension(extension) {
71
+ if (extensions.has(extension.id)) {
72
+ console.warn(`@openleaf-editor/core: schema extension "${extension.id}" is already registered. ` +
73
+ 'The second registration was ignored.');
74
+ return () => undefined;
75
+ }
76
+ extensions.set(extension.id, extension);
77
+ notify();
78
+ return () => {
79
+ // Removing an extension cannot un-extend a schema that is already built --
80
+ // a document may contain its nodes. It affects editors created afterwards.
81
+ if (extensions.delete(extension.id))
82
+ notify();
83
+ };
84
+ }
85
+ export function registeredSchemaExtensions() {
86
+ return [...extensions.values()];
87
+ }
88
+ export function onSchemaExtensionsChange(listener) {
89
+ listeners.add(listener);
90
+ return () => {
91
+ listeners.delete(listener);
92
+ };
93
+ }
94
+ /** Testing seam. Not part of the public API. */
95
+ export function clearSchemaExtensions() {
96
+ extensions.clear();
97
+ notify();
98
+ }
99
+ /* ------------------------------------------------------------------ *
100
+ * Carrying unmodelled attributes
101
+ * ------------------------------------------------------------------ */
102
+ function isPlainObject(value) {
103
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
104
+ }
105
+ /**
106
+ * Residue a spec has already encoded into a modelled attribute, and so must not
107
+ * carry a second copy of.
108
+ *
109
+ * `code_block` reads `language-js` from either `<pre>` or `<code>` and re-emits
110
+ * it on `<code>` -- read both, write one. Carrying the `<pre>`'s class verbatim
111
+ * writes it twice, so the language token is dropped from the residue while any
112
+ * other class the author put there is kept. Keyed by node name because the
113
+ * overlap is a property of the spec, not of the attribute.
114
+ */
115
+ const CARRY_SCRUB = {
116
+ code_block(carried) {
117
+ const cls = carried['class'];
118
+ if (cls === undefined)
119
+ return;
120
+ const kept = cls.split(/\s+/).filter((c) => c && !/^(?:language|lang)-/i.test(c));
121
+ if (kept.length > 0)
122
+ carried['class'] = kept.join(' ');
123
+ else
124
+ delete carried['class'];
125
+ },
126
+ };
127
+ /**
128
+ * Wrap a node spec so attributes it does not model survive the round trip.
129
+ *
130
+ * Applied to extension nodes unconditionally: they only ever claim markup the
131
+ * preservation layer previously kept in full, so carrying the residue is a pure
132
+ * improvement over the alternative of silently dropping it.
133
+ */
134
+ function withCarriedAttributes(name, spec) {
135
+ const modelled = new Set(Object.keys(spec.attrs ?? {}));
136
+ const attrs = { ...(spec.attrs ?? {}), [CARRIED_ATTR]: { default: null } };
137
+ // A node's parse rules are always tag rules -- only marks may match styles --
138
+ // so the narrower type is the accurate one and keeps the map total.
139
+ const parseDOM = (spec.parseDOM ?? []).map((rule) => {
140
+ const original = rule.getAttrs;
141
+ return {
142
+ ...rule,
143
+ getAttrs(dom) {
144
+ const base = original ? original.call(rule, dom) : (rule.attrs ?? {});
145
+ if (base === false || base === null || base === undefined)
146
+ return base;
147
+ const carried = {};
148
+ for (const attr of Array.from(dom.attributes ?? [])) {
149
+ if (modelled.has(attr.name))
150
+ continue;
151
+ // Same scrub as the preservation layer: carrying `onclick` or a
152
+ // `javascript:` URL would reintroduce exactly the executable content
153
+ // core promises to drop.
154
+ if (isEventHandlerAttribute(attr.name))
155
+ continue;
156
+ if (URL_ATTRIBUTES.has(attr.name.toLowerCase()) && !isSafeUrl(attr.value))
157
+ continue;
158
+ carried[attr.name] = attr.value;
159
+ }
160
+ CARRY_SCRUB[name]?.(carried);
161
+ return {
162
+ ...base,
163
+ [CARRIED_ATTR]: Object.keys(carried).length > 0 ? carried : null,
164
+ };
165
+ },
166
+ };
167
+ });
168
+ const originalToDOM = spec.toDOM;
169
+ const toDOM = originalToDOM
170
+ ? (node) => {
171
+ const out = originalToDOM(node);
172
+ const carried = node.attrs[CARRIED_ATTR];
173
+ if (!carried || !Array.isArray(out))
174
+ return out;
175
+ const result = [...out];
176
+ if (isPlainObject(result[1])) {
177
+ // Modelled attributes win: an author's spec is the authority on the
178
+ // names it declared.
179
+ result[1] = { ...carried, ...result[1] };
180
+ }
181
+ else {
182
+ result.splice(1, 0, carried);
183
+ }
184
+ return result;
185
+ }
186
+ : originalToDOM;
187
+ return { ...spec, attrs, parseDOM, ...(toDOM ? { toDOM } : {}) };
188
+ }
189
+ /* ------------------------------------------------------------------ *
190
+ * Building
191
+ * ------------------------------------------------------------------ */
192
+ function assertRulePriorities(extensionId, name, spec) {
193
+ for (const rule of (spec.parseDOM ?? [])) {
194
+ if (rule.priority !== undefined && rule.priority <= 1) {
195
+ throw new Error(`@openleaf-editor/core: extension "${extensionId}" gives "${name}" a parse rule at ` +
196
+ `priority ${rule.priority}. The preservation layer's catch-all rules sit at ` +
197
+ 'priority 0 and 1, so this rule would tie with them and the winner would be ' +
198
+ 'decided by insertion order. Remove the priority: the default already wins.');
199
+ }
200
+ }
201
+ }
202
+ function claim(claimed, kind, name, extension, existsInCore) {
203
+ const replaces = new Set(extension.replaces ?? []);
204
+ const previous = claimed.get(`${kind}:${name}`);
205
+ if (previous && !replaces.has(name)) {
206
+ throw new Error(`@openleaf-editor/core: extensions "${previous}" and "${extension.id}" both define the ` +
207
+ `${kind} "${name}". A ${kind} type is a storage format, not a preference: two ` +
208
+ 'definitions mean two serializations of the same content chosen by load order. ' +
209
+ `If replacing it is intended, declare replaces: ['${name}'].`);
210
+ }
211
+ if (existsInCore && !replaces.has(name)) {
212
+ throw new Error(`@openleaf-editor/core: extension "${extension.id}" defines the ${kind} "${name}", which ` +
213
+ `already exists in the base schema. If replacing it is intended, declare ` +
214
+ `replaces: ['${name}'].`);
215
+ }
216
+ claimed.set(`${kind}:${name}`, extension.id);
217
+ }
218
+ /**
219
+ * Build a schema from the base types plus these extensions.
220
+ *
221
+ * Pure: it reads no registry. That is deliberate -- a registry-reading default
222
+ * would make the fidelity suite depend on whichever other test file happened to
223
+ * register an extension first.
224
+ */
225
+ /**
226
+ * Node types whose attributes are already the whole story — wrapping them
227
+ * would duplicate markup (unknown_*) or add a phantom attr to nodes that
228
+ * never parse from the DOM (doc, text).
229
+ */
230
+ const SKIP_CARRY = new Set(['doc', 'text', 'unknown_block', 'unknown_inline']);
231
+ function coreNodesWithCarriedAttributes() {
232
+ // Claimed tags used to drop every attribute they do not model. Extension
233
+ // nodes already carry the residue; core nodes were the remaining hole, and
234
+ // it is how `<p class="lead">` became `<p>` on the first save.
235
+ let nodes = OrderedMap.from({});
236
+ for (const [name, spec] of Object.entries(coreNodes)) {
237
+ nodes = nodes.addToEnd(name, SKIP_CARRY.has(name) ? spec : withCarriedAttributes(name, spec));
238
+ }
239
+ return nodes;
240
+ }
241
+ export function createSchema(list = []) {
242
+ let nodes = coreNodesWithCarriedAttributes();
243
+ let marks = OrderedMap.from(coreMarks);
244
+ const claimed = new Map();
245
+ for (const extension of list) {
246
+ for (const [name, spec] of Object.entries(extension.nodes ?? {})) {
247
+ assertRulePriorities(extension.id, name, spec);
248
+ claim(claimed, 'node', name, extension, Object.hasOwn(coreNodes, name));
249
+ const prepared = extension.carryUnknownAttributes === false
250
+ ? spec
251
+ : withCarriedAttributes(name, spec);
252
+ // addToEnd, never prepend: a leading block node becomes the document's
253
+ // defaultType and every new document would start with it.
254
+ nodes = nodes.remove(name).addToEnd(name, prepared);
255
+ }
256
+ for (const [name, spec] of Object.entries(extension.marks ?? {})) {
257
+ assertRulePriorities(extension.id, name, spec);
258
+ claim(claimed, 'mark', name, extension, Object.hasOwn(coreMarks, name));
259
+ marks = marks.remove(name).addToEnd(name, spec);
260
+ }
261
+ }
262
+ return new Schema({ nodes, marks });
263
+ }
264
+ let cached = null;
265
+ /**
266
+ * The schema for the currently registered extensions.
267
+ *
268
+ * A function rather than a constant, and this is the point: a `const` reads as
269
+ * "bind to this" and would be captured at import by every consumer, which is
270
+ * exactly what made the schema impossible to extend. Memoized, and invalidated
271
+ * whenever the registry changes.
272
+ */
273
+ export function coreSchema() {
274
+ if (!cached)
275
+ cached = createSchema(registeredSchemaExtensions());
276
+ return cached;
277
+ }
278
+ onSchemaExtensionsChange(() => {
279
+ cached = null;
280
+ });
281
+ //# sourceMappingURL=extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"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"}
package/dist/html.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HTML in, HTML out.
3
+ *
4
+ * OpenLeaf's storage format is HTML, not a proprietary JSON document
5
+ * model. A CMS that adopts OpenLeaf and later drops it should be left
6
+ * with content it can still render, which rules out formats that require
7
+ * our code to interpret.
8
+ */
9
+ import { type Node as PMNode, type Schema } from 'prosemirror-model';
10
+ export interface HtmlIOOptions {
11
+ /** DOM implementation to use. Defaults to the global `document`. */
12
+ document?: Document;
13
+ /** Schema to parse against. Defaults to the built-in one. */
14
+ schema?: Schema;
15
+ }
16
+ /** Parse an HTML string into an OpenLeaf document. */
17
+ export declare function parseHtml(html: string, opts?: HtmlIOOptions): PMNode;
18
+ /** Serialize an OpenLeaf document back to an HTML string. */
19
+ export declare function serializeHtml(node: PMNode, opts?: HtmlIOOptions): string;
20
+ /** Convenience: one full parse/serialize cycle. */
21
+ export declare function roundTrip(html: string, opts?: HtmlIOOptions): string;
22
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAA4B,KAAK,IAAI,IAAI,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAqC9F,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAaD,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAOpE;AAqCD,6DAA6D;AAC7D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAaxE;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAEpE"}
package/dist/html.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * HTML in, HTML out.
3
+ *
4
+ * OpenLeaf's storage format is HTML, not a proprietary JSON document
5
+ * model. A CMS that adopts OpenLeaf and later drops it should be left
6
+ * with content it can still render, which rules out formats that require
7
+ * our code to interpret.
8
+ */
9
+ import { DOMParser, DOMSerializer } from 'prosemirror-model';
10
+ import { isInsidePreserved, withSerializationDocument } from './preserve.js';
11
+ import { coreSchema } from './extensions.js';
12
+ /**
13
+ * Parsers and serializers are resolved per schema rather than built once.
14
+ *
15
+ * `DOMSerializer.fromSchema` builds a map keyed by node NAME at construction, so
16
+ * a serializer built from one schema throws `this.nodes[node.type.name] is not a
17
+ * function` the moment it meets a node type a plugin added. Module-level
18
+ * instances were therefore a hard ceiling on extensibility, not just an
19
+ * optimisation.
20
+ *
21
+ * ProseMirror caches these on the schema object itself, so a WeakMap here is
22
+ * belt-and-braces -- it costs nothing and makes the intent explicit.
23
+ */
24
+ const parsers = new WeakMap();
25
+ const serializers = new WeakMap();
26
+ function parserFor(target) {
27
+ let found = parsers.get(target);
28
+ if (!found) {
29
+ found = DOMParser.fromSchema(target);
30
+ parsers.set(target, found);
31
+ }
32
+ return found;
33
+ }
34
+ function serializerFor(target) {
35
+ let found = serializers.get(target);
36
+ if (!found) {
37
+ found = DOMSerializer.fromSchema(target);
38
+ serializers.set(target, found);
39
+ }
40
+ return found;
41
+ }
42
+ function resolveDocument(opts) {
43
+ const doc = opts?.document ?? (typeof document !== 'undefined' ? document : undefined);
44
+ if (!doc) {
45
+ throw new Error('@openleaf-editor/core: no Document available. Pass { document } when ' +
46
+ 'running outside a browser.');
47
+ }
48
+ return doc;
49
+ }
50
+ /** Parse an HTML string into an OpenLeaf document. */
51
+ export function parseHtml(html, opts) {
52
+ const doc = resolveDocument(opts);
53
+ const tpl = doc.createElement('template');
54
+ tpl.innerHTML = html;
55
+ return parserFor(opts?.schema ?? coreSchema()).parse(tpl.content, {
56
+ preserveWhitespace: false,
57
+ });
58
+ }
59
+ /**
60
+ * Collapse `<td><p>text</p></td>` back to `<td>text</td>`.
61
+ *
62
+ * Table cells hold `block+` content, because real tables contain paragraphs and
63
+ * lists. The consequence is that parsing the overwhelmingly common legacy form
64
+ * `<td>text</td>` produces a cell containing a paragraph, and serializing it
65
+ * back would write `<td><p>text</p></td>` -- rewriting every cell of every table
66
+ * in a CMS the first time each post is opened and saved.
67
+ *
68
+ * That is a normalization rather than information loss, but "we changed every
69
+ * table in your archive" is not a thing this project gets to do quietly. So a
70
+ * cell holding exactly one attribute-free paragraph is unwrapped on the way out.
71
+ *
72
+ * The asymmetry is deliberate and worth stating: a cell that was authored as
73
+ * `<td><p>text</p></td>` also comes back as `<td>text</td>`. That form is rare
74
+ * in the content this editor inherits, and the alternative is rewriting the
75
+ * common case instead of the rare one.
76
+ */
77
+ function unwrapSoleCellParagraph(host) {
78
+ for (const cell of Array.from(host.querySelectorAll('td, th'))) {
79
+ // Never reach inside preserved markup. A table nested in an unrecognised
80
+ // wrapper is content we undertook to return byte-identical, and a
81
+ // normalization that is right for our own tables is a broken promise there.
82
+ if (isInsidePreserved(cell))
83
+ continue;
84
+ if (cell.childElementCount !== 1)
85
+ continue;
86
+ const only = cell.firstElementChild;
87
+ if (!only || only.nodeName !== 'P' || only.attributes.length > 0)
88
+ continue;
89
+ // Only when the paragraph is the cell's entire content; a stray text node
90
+ // beside it means the markup is doing something we should not touch.
91
+ if (cell.childNodes.length !== 1)
92
+ continue;
93
+ while (only.firstChild)
94
+ cell.insertBefore(only.firstChild, only);
95
+ cell.removeChild(only);
96
+ }
97
+ }
98
+ /** Serialize an OpenLeaf document back to an HTML string. */
99
+ export function serializeHtml(node, opts) {
100
+ const doc = resolveDocument(opts);
101
+ return withSerializationDocument(doc, () => {
102
+ // Taken from the document itself, so a document built on an extended schema
103
+ // serializes with a serializer that knows its node types. Passing the wrong
104
+ // schema explicitly is still possible, but the default is now correct.
105
+ const target = opts?.schema ?? node.type.schema;
106
+ const fragment = serializerFor(target).serializeFragment(node.content, { document: doc });
107
+ const host = doc.createElement('div');
108
+ host.appendChild(fragment);
109
+ unwrapSoleCellParagraph(host);
110
+ return host.innerHTML;
111
+ });
112
+ }
113
+ /** Convenience: one full parse/serialize cycle. */
114
+ export function roundTrip(html, opts) {
115
+ return serializeHtml(parseHtml(html, opts), opts);
116
+ }
117
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { baseSchema, coreMarks, coreNodes } from './schema.js';
2
+ export { parseHtml, serializeHtml, roundTrip, type HtmlIOOptions } from './html.js';
3
+ export { isLosslesslyUnwrappable, unknownBlock, unknownInline } from './preserve.js';
4
+ export { URL_ATTRIBUTES, isEventHandlerAttribute, isSafeUrl, safeUrlOrNull, } from './url.js';
5
+ export { activeHeadingLevel, activeLink, canInsert, canRedo, canUndo, isMarkActive, isNodeActive, toggleBold, toggleInlineCode, toggleItalic, toggleStrike, toggleUnderline, insertHorizontalRule, setHeading, setParagraph, toggleBlockquote, toggleCodeBlock, toggleHeading, wrapInBlockquote, indentListItem, outdentListItem, splitListItemCommand, toggleBulletList, toggleOrderedList, insertImage, setLink, unsetLink, type ImageAttrs, type LinkAttrs, redo, undo, } from './commands.js';
6
+ export { buildKeymap, shortcutFor, shortcuts, type Shortcut, } from './keymap.js';
7
+ export { createRegisteredPlugins, onEditorPluginsChange, registerEditorPlugin, type EditorPluginFactory, } from './plugins.js';
8
+ export { table, table_cell, table_header, table_row } from './tables.js';
9
+ export { CARRIED_ATTR, clearSchemaExtensions, coreSchema, createSchema, onSchemaExtensionsChange, registerSchemaExtension, registeredSchemaExtensions, type SchemaExtension, } from './extensions.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AACnF,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,SAAS,EACT,aAAa,GACd,MAAM,UAAU,CAAA;AACjB,OAAO,EAEL,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,OAAO,EACP,OAAO,EACP,YAAY,EACZ,YAAY,EAEZ,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,eAAe,EAEf,oBAAoB,EACpB,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,gBAAgB,EAEhB,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EAEjB,WAAW,EACX,OAAO,EACP,SAAS,EACT,KAAK,UAAU,EACf,KAAK,SAAS,EAEd,IAAI,EACJ,IAAI,GACL,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,WAAW,EACX,WAAW,EACX,SAAS,EACT,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,mBAAmB,GACzB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACxE,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,EAC1B,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ export { baseSchema, coreMarks, coreNodes } from './schema.js';
2
+ export { parseHtml, serializeHtml, roundTrip } from './html.js';
3
+ export { isLosslesslyUnwrappable, unknownBlock, unknownInline } from './preserve.js';
4
+ export { URL_ATTRIBUTES, isEventHandlerAttribute, isSafeUrl, safeUrlOrNull, } from './url.js';
5
+ export {
6
+ // predicates
7
+ activeHeadingLevel, activeLink, canInsert, canRedo, canUndo, isMarkActive, isNodeActive,
8
+ // marks
9
+ toggleBold, toggleInlineCode, toggleItalic, toggleStrike, toggleUnderline,
10
+ // blocks
11
+ insertHorizontalRule, setHeading, setParagraph, toggleBlockquote, toggleCodeBlock, toggleHeading, wrapInBlockquote,
12
+ // lists
13
+ indentListItem, outdentListItem, splitListItemCommand, toggleBulletList, toggleOrderedList,
14
+ // links and images
15
+ insertImage, setLink, unsetLink,
16
+ // history
17
+ redo, undo, } from './commands.js';
18
+ export { buildKeymap, shortcutFor, shortcuts, } from './keymap.js';
19
+ export { createRegisteredPlugins, onEditorPluginsChange, registerEditorPlugin, } from './plugins.js';
20
+ export { table, table_cell, table_header, table_row } from './tables.js';
21
+ export { CARRIED_ATTR, clearSchemaExtensions, coreSchema, createSchema, onSchemaExtensionsChange, registerSchemaExtension, registeredSchemaExtensions, } from './extensions.js';
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAsB,MAAM,WAAW,CAAA;AACnF,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AACpF,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,SAAS,EACT,aAAa,GACd,MAAM,UAAU,CAAA;AACjB,OAAO;AACL,aAAa;AACb,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,OAAO,EACP,OAAO,EACP,YAAY,EACZ,YAAY;AACZ,QAAQ;AACR,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,eAAe;AACf,SAAS;AACT,oBAAoB,EACpB,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,gBAAgB;AAChB,QAAQ;AACR,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB;AACjB,mBAAmB;AACnB,WAAW,EACX,OAAO,EACP,SAAS;AAGT,UAAU;AACV,IAAI,EACJ,IAAI,GACL,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,WAAW,EACX,WAAW,EACX,SAAS,GAEV,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,GAErB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACxE,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,UAAU,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,GAE3B,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Keyboard shortcuts.
3
+ *
4
+ * Bindings follow whatever convention the largest number of users already have
5
+ * in their fingers, which usually means Word and Google Docs rather than
6
+ * anything invented here. Where those disagree, the web convention wins,
7
+ * because this editor lives in a browser.
8
+ *
9
+ * `Mod` resolves to Cmd on macOS and Ctrl elsewhere.
10
+ *
11
+ * ## On Tab
12
+ *
13
+ * Tab is deliberately NOT bound to list indentation, even though Word does it
14
+ * and users ask for it. Inside a `contenteditable`, capturing Tab removes the
15
+ * only way a keyboard user has to leave the editor, which is a WCAG 2.1.2
16
+ * keyboard-trap failure -- and for the institutional users who most need a free
17
+ * editor, that is a procurement blocker rather than a rough edge.
18
+ *
19
+ * Indentation uses `Mod-[` and `Mod-]` instead, matching Google Docs and
20
+ * VS Code. If Tab is ever added it must come with a documented escape (Escape
21
+ * then Tab, or a first-Tab-escapes heuristic) and real screen reader testing.
22
+ */
23
+ import type { Command } from 'prosemirror-state';
24
+ /** A shortcut, its command, and a human label for the help surface. */
25
+ export interface Shortcut {
26
+ keys: string;
27
+ command: Command;
28
+ label: string;
29
+ }
30
+ /**
31
+ * The default shortcut table.
32
+ *
33
+ * Exported as data rather than as a finished keymap so that an integrator can
34
+ * remove a binding that collides with their own application, and so the help
35
+ * dialog and the toolbar tooltips can render the real bindings instead of a
36
+ * hand-maintained duplicate list that drifts.
37
+ */
38
+ export declare const shortcuts: Shortcut[];
39
+ /**
40
+ * Build the keymap bindings object.
41
+ *
42
+ * `Enter` chains: splitting a list item must be tried before ProseMirror's
43
+ * default paragraph split, or pressing Enter in a list creates a paragraph
44
+ * instead of the next bullet.
45
+ */
46
+ export declare function buildKeymap(custom?: Record<string, Command>): Record<string, Command>;
47
+ /** Human-readable shortcut for a label, with the platform's modifier symbol. */
48
+ export declare function shortcutFor(label: string, isMac?: boolean): string | null;
49
+ //# sourceMappingURL=keymap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keymap.d.ts","sourceRoot":"","sources":["../src/keymap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAmBhD,uEAAuE;AACvE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,EA+B/B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,UAAc,GAAG,MAAM,GAAG,IAAI,CAQ7E"}