@openleaf-editor/core 0.1.0-beta.2 → 0.1.0-beta.4

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.
Files changed (104) hide show
  1. package/README.md +127 -0
  2. package/dist/autolink.d.ts +7 -4
  3. package/dist/autolink.d.ts.map +1 -1
  4. package/dist/autolink.js +90 -11
  5. package/dist/autolink.js.map +1 -1
  6. package/dist/command-helpers.d.ts +8 -0
  7. package/dist/command-helpers.d.ts.map +1 -0
  8. package/dist/command-helpers.js +36 -0
  9. package/dist/command-helpers.js.map +1 -0
  10. package/dist/commands.d.ts +21 -63
  11. package/dist/commands.d.ts.map +1 -1
  12. package/dist/commands.js +430 -346
  13. package/dist/commands.js.map +1 -1
  14. package/dist/css.d.ts +1 -175
  15. package/dist/css.d.ts.map +1 -1
  16. package/dist/css.js +1 -516
  17. package/dist/css.js.map +1 -1
  18. package/dist/disclosure.d.ts +21 -0
  19. package/dist/disclosure.d.ts.map +1 -0
  20. package/dist/disclosure.js +144 -0
  21. package/dist/disclosure.js.map +1 -0
  22. package/dist/elements.d.ts +2 -0
  23. package/dist/elements.d.ts.map +1 -0
  24. package/dist/elements.js +2 -0
  25. package/dist/elements.js.map +1 -0
  26. package/dist/embed.d.ts +1 -51
  27. package/dist/embed.d.ts.map +1 -1
  28. package/dist/embed.js +1 -115
  29. package/dist/embed.js.map +1 -1
  30. package/dist/errors.d.ts +38 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +40 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/extensions.d.ts +46 -5
  35. package/dist/extensions.d.ts.map +1 -1
  36. package/dist/extensions.js +272 -52
  37. package/dist/extensions.js.map +1 -1
  38. package/dist/figure-drag.d.ts +26 -0
  39. package/dist/figure-drag.d.ts.map +1 -0
  40. package/dist/figure-drag.js +75 -0
  41. package/dist/figure-drag.js.map +1 -0
  42. package/dist/formats.d.ts.map +1 -1
  43. package/dist/formats.js +19 -7
  44. package/dist/formats.js.map +1 -1
  45. package/dist/gapcursor.d.ts +14 -0
  46. package/dist/gapcursor.d.ts.map +1 -0
  47. package/dist/gapcursor.js +17 -0
  48. package/dist/gapcursor.js.map +1 -0
  49. package/dist/html.d.ts +32 -2
  50. package/dist/html.d.ts.map +1 -1
  51. package/dist/html.js +185 -31
  52. package/dist/html.js.map +1 -1
  53. package/dist/index.d.ts +23 -6
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +34 -6
  56. package/dist/index.js.map +1 -1
  57. package/dist/insert-commands.d.ts +172 -0
  58. package/dist/insert-commands.d.ts.map +1 -0
  59. package/dist/insert-commands.js +560 -0
  60. package/dist/insert-commands.js.map +1 -0
  61. package/dist/isolating-selection.d.ts +59 -0
  62. package/dist/isolating-selection.d.ts.map +1 -0
  63. package/dist/isolating-selection.js +251 -0
  64. package/dist/isolating-selection.js.map +1 -0
  65. package/dist/noneditable.d.ts.map +1 -1
  66. package/dist/noneditable.js +18 -4
  67. package/dist/noneditable.js.map +1 -1
  68. package/dist/plugins.d.ts +20 -2
  69. package/dist/plugins.d.ts.map +1 -1
  70. package/dist/plugins.js +27 -2
  71. package/dist/plugins.js.map +1 -1
  72. package/dist/preserve.d.ts +48 -17
  73. package/dist/preserve.d.ts.map +1 -1
  74. package/dist/preserve.js +214 -43
  75. package/dist/preserve.js.map +1 -1
  76. package/dist/schema.d.ts +15 -0
  77. package/dist/schema.d.ts.map +1 -1
  78. package/dist/schema.js +157 -28
  79. package/dist/schema.js.map +1 -1
  80. package/dist/structure.d.ts +32 -0
  81. package/dist/structure.d.ts.map +1 -1
  82. package/dist/structure.js +53 -10
  83. package/dist/structure.js.map +1 -1
  84. package/dist/tables.d.ts +47 -1
  85. package/dist/tables.d.ts.map +1 -1
  86. package/dist/tables.js +417 -40
  87. package/dist/tables.js.map +1 -1
  88. package/dist/testing.d.ts +3 -0
  89. package/dist/testing.d.ts.map +1 -0
  90. package/dist/testing.js +3 -0
  91. package/dist/testing.js.map +1 -0
  92. package/dist/tokens.d.ts +8 -0
  93. package/dist/tokens.d.ts.map +1 -1
  94. package/dist/tokens.js +54 -6
  95. package/dist/tokens.js.map +1 -1
  96. package/dist/url.d.ts +1 -25
  97. package/dist/url.d.ts.map +1 -1
  98. package/dist/url.js +1 -77
  99. package/dist/url.js.map +1 -1
  100. package/dist/visual-aids.d.ts +11 -1
  101. package/dist/visual-aids.d.ts.map +1 -1
  102. package/dist/visual-aids.js +124 -36
  103. package/dist/visual-aids.js.map +1 -1
  104. package/package.json +17 -1
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Make `<details>` open and close while editing.
3
+ *
4
+ * Outside a contenteditable the browser toggles a `<details>` when its
5
+ * `<summary>` is clicked, and nothing has to be written for it. Inside one that
6
+ * default action does not happen: the click is spent placing a caret, so the
7
+ * element stays in whatever state it was parsed in.
8
+ *
9
+ * The consequence is worse than a control that does nothing. A `<details>` parsed
10
+ * without `open` renders collapsed, and its contents are then unreachable --
11
+ * there is no gesture that expands it, so an author cannot edit or even read the
12
+ * body of a collapsible section they already have in a document.
13
+ *
14
+ * So the toggle is written by hand, against the node's own `open` attribute
15
+ * rather than the DOM's. That is what makes the state part of the document and
16
+ * therefore part of what round-trips; toggling the element directly would be
17
+ * undone by the next redraw.
18
+ */
19
+ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
20
+ const key = new PluginKey('openleaf-disclosure');
21
+ export function disclosurePlugin() {
22
+ return new Plugin({
23
+ key,
24
+ props: {
25
+ handleDOMEvents: {
26
+ /*
27
+ * A DOM listener rather than `handleClickOn`.
28
+ *
29
+ * ProseMirror calls `handleClickOn` for each node around the click from
30
+ * the inside out, and whether the summary arrives as the direct node
31
+ * depends on whether the pointer landed on its text, its marker or its
32
+ * padding. That made the toggle fire on some clicks and not others --
33
+ * the section appeared stuck open, which is worse than not working at
34
+ * all, because it looks like the state is wrong rather than the control.
35
+ *
36
+ * The target's nearest `<summary>` is not ambiguous.
37
+ */
38
+ click(view, event) {
39
+ const target = event.target;
40
+ if (!target)
41
+ return false;
42
+ const from = target.nodeType === 1 ? target : target.parentElement;
43
+ const summary = from?.closest?.('summary');
44
+ if (!summary || !view.dom.contains(summary))
45
+ return false;
46
+ /*
47
+ * `handleDOMEvents` runs before ProseMirror's `view.editable` check
48
+ * (`runCustomHandler` is first in `initInput`). Typing, paste, drop
49
+ * and the keymaps get that gate for free; this click does not, so a
50
+ * read-only editor still flipped `open` and fired `openleaf:change`.
51
+ *
52
+ * `view.editable` rather than the host's `readonly` attribute, same
53
+ * as the table context menu and the media resize handle: it is the
54
+ * flag ProseMirror itself consults, the element derives it from the
55
+ * attribute, and a view mounted without the custom element can set
56
+ * it directly.
57
+ *
58
+ * Returning false, not swallowing the event: outside contenteditable
59
+ * the browser toggles a `<details>` natively, so collapsed sections
60
+ * stay readable without writing the node. `preventDefault` below is
61
+ * only needed when we own the toggle.
62
+ */
63
+ if (!view.editable)
64
+ return false;
65
+ const $pos = view.state.doc.resolve(view.posAtDOM(summary, 0));
66
+ for (let depth = $pos.depth; depth >= 0; depth -= 1) {
67
+ const node = $pos.node(depth);
68
+ if (node.type.name !== 'details')
69
+ continue;
70
+ // Once the element carries `open`, the browser's own activation
71
+ // behaviour for a summary works again and fires alongside this --
72
+ // two toggles that cancel. Only one may act, and it has to be this
73
+ // one: the DOM's idea of open is discarded on the next redraw, the
74
+ // node's is what round-trips.
75
+ event.preventDefault();
76
+ /*
77
+ * Put the caret in the label explicitly.
78
+ *
79
+ * `setNodeMarkup` replaces the details node, and a selection mapped
80
+ * through a replacement does not reliably survive it: Chromium kept
81
+ * the caret and Firefox dropped it, which left the label uneditable
82
+ * in one browser and not the other. Where the click already landed
83
+ * inside the label is preferred, so clicking a word still puts the
84
+ * caret at that word; otherwise it goes to the end of the label.
85
+ */
86
+ const detailsPos = $pos.before(depth);
87
+ const summaryNode = node.firstChild;
88
+ const labelFrom = detailsPos + 2;
89
+ const labelTo = labelFrom + (summaryNode?.content.size ?? 0);
90
+ const clicked = view.state.selection.from;
91
+ const caret = clicked >= labelFrom && clicked <= labelTo ? clicked : labelTo;
92
+ const tr = view.state.tr.setNodeMarkup(detailsPos, undefined, {
93
+ ...node.attrs,
94
+ open: !node.attrs['open'],
95
+ });
96
+ tr.setSelection(TextSelection.near(tr.doc.resolve(tr.mapping.map(caret))));
97
+ /*
98
+ * Keep the toggle off the undo stack.
99
+ *
100
+ * `open` lives in the document, so flipping it is by every mechanical
101
+ * measure a document change: it is a step, it maps positions, it
102
+ * serializes on save. History would take it on those grounds alone.
103
+ *
104
+ * But the undo stack does not model changes to the document, it
105
+ * models an author's intent to have made them, and expanding a
106
+ * section in order to read it is not an edit -- it is how you look at
107
+ * a document whose parts are folded away. An author who opened three
108
+ * collapsed sections while hunting for a paragraph, fixed a typo in
109
+ * the third and pressed Ctrl+Z got the typo back and the section
110
+ * closed under them; the correction they meant to take back was four
111
+ * undos away, behind their own navigation. Losing the ability to undo
112
+ * a fold costs nothing -- the gesture that made it is one click away
113
+ * and visible on screen -- while losing the edit next to it costs the
114
+ * work.
115
+ *
116
+ * `addToHistory: false` is the key `prosemirror-history` reads to
117
+ * exclude a transaction; the attribute still changes, and still
118
+ * round-trips.
119
+ */
120
+ tr.setMeta('addToHistory', false);
121
+ view.dispatch(tr);
122
+ /*
123
+ * And take the focus, because clicking a summary does not give it.
124
+ *
125
+ * `<summary>` is a natively focusable widget, and Firefox declines to
126
+ * put a caret in one inside a contenteditable -- focus stayed on
127
+ * `<body>`, so the selection above was correct and every keystroke
128
+ * still went nowhere. Handling the click means owning the focus that
129
+ * the default action would otherwise have arranged.
130
+ */
131
+ if (!view.hasFocus())
132
+ view.focus();
133
+ // False, not true: ProseMirror still gets to finish its own handling
134
+ // of the click. Swallowing it here changed nothing for the better and
135
+ // made the interaction harder to reason about.
136
+ return false;
137
+ }
138
+ return false;
139
+ },
140
+ },
141
+ },
142
+ });
143
+ }
144
+ //# sourceMappingURL=disclosure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disclosure.js","sourceRoot":"","sources":["../src/disclosure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from '@openleaf-editor/content-policy/elements';
2
+ //# sourceMappingURL=elements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,cAAc,0CAA0C,CAAA"}
@@ -0,0 +1,2 @@
1
+ export * from '@openleaf-editor/content-policy/elements';
2
+ //# sourceMappingURL=elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements.js","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,cAAc,0CAA0C,CAAA"}
package/dist/embed.d.ts CHANGED
@@ -1,52 +1,2 @@
1
- /**
2
- * Which `<iframe>` sources the schema will store.
3
- *
4
- * An iframe is a nested browsing context. Allowing an arbitrary `src` is
5
- * allowing a third-party page to run in yours, which is why the preservation
6
- * layer used to drop every iframe it saw. Modelling embeds at all means
7
- * answering "which hosts, on which paths, over which scheme" in one place --
8
- * here -- so the schema, the insert command and `@openleaf-editor/sanitize`
9
- * cannot drift.
10
- *
11
- * Relative iframe URLs are refused: a CMS that later hosts an uploaded HTML
12
- * file at a relative path would otherwise turn a stored embed into a stored
13
- * XSS. `http:` is refused for the same reason every other URL check prefers
14
- * an allowlist of schemes: mixed content is not an embed, it is a warning.
15
- */
16
- export interface EmbedHostRule {
17
- /** Hostname, compared case-insensitively, with a leading `www.` ignored. */
18
- readonly host: string;
19
- /** Path that must match. Absent means any path on that host. */
20
- readonly path?: RegExp;
21
- }
22
- /**
23
- * Hosts an author may embed.
24
- *
25
- * Each rule is a known player page, not a whole site. `youtube.com/` would
26
- * let an author embed `youtube.com/watch` as a nested YouTube UI; `/embed/`
27
- * is the documented iframe URL.
28
- */
29
- export declare const EMBED_HOSTS: readonly EmbedHostRule[];
30
- /** True when this URL is an https embed the schema will store. */
31
- export declare function isAllowedEmbedSrc(value: string | null | undefined): boolean;
32
- /** The URL, or null when it must not be stored as an iframe `src`. */
33
- export declare function safeEmbedSrc(value: string | null | undefined): string | null;
34
- /**
35
- * Permissions policy tokens an iframe may advertise.
36
- *
37
- * Anything not in the closed set is dropped rather than stored: `allow` is how
38
- * an embed asks to skip the sandbox the rest of the page lives in.
39
- *
40
- * The attribute is a **semicolon**-delimited list of directives, each one a
41
- * feature name followed by an optional origin allowlist -- `camera 'self';
42
- * fullscreen *`. Splitting on whitespace instead read the standard YouTube
43
- * string `allow="autoplay; fullscreen; picture-in-picture"` as the tokens
44
- * "autoplay;" and "fullscreen;", so only the last survived, and a trailing
45
- * semicolon lost every permission the author had.
46
- *
47
- * Only the feature name is kept. Dropping the origin allowlist leaves the
48
- * attribute's default, which is the frame's own origin -- so `camera *` narrows
49
- * to the embed itself rather than being stored as written.
50
- */
51
- export declare function safeAllowList(value: string | null | undefined): string | null;
1
+ export * from '@openleaf-editor/content-policy/embed';
52
2
  //# sourceMappingURL=embed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,aAAa;IAC5B,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,aAAa,EAS/C,CAAA;AA8BD,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAY3E;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAW7E"}
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAA"}
package/dist/embed.js CHANGED
@@ -1,116 +1,2 @@
1
- /**
2
- * Which `<iframe>` sources the schema will store.
3
- *
4
- * An iframe is a nested browsing context. Allowing an arbitrary `src` is
5
- * allowing a third-party page to run in yours, which is why the preservation
6
- * layer used to drop every iframe it saw. Modelling embeds at all means
7
- * answering "which hosts, on which paths, over which scheme" in one place --
8
- * here -- so the schema, the insert command and `@openleaf-editor/sanitize`
9
- * cannot drift.
10
- *
11
- * Relative iframe URLs are refused: a CMS that later hosts an uploaded HTML
12
- * file at a relative path would otherwise turn a stored embed into a stored
13
- * XSS. `http:` is refused for the same reason every other URL check prefers
14
- * an allowlist of schemes: mixed content is not an embed, it is a warning.
15
- */
16
- import { isSafeUrl } from './url.js';
17
- /**
18
- * Hosts an author may embed.
19
- *
20
- * Each rule is a known player page, not a whole site. `youtube.com/` would
21
- * let an author embed `youtube.com/watch` as a nested YouTube UI; `/embed/`
22
- * is the documented iframe URL.
23
- */
24
- export const EMBED_HOSTS = [
25
- { host: 'youtube.com', path: /^\/embed\// },
26
- { host: 'youtube-nocookie.com', path: /^\/embed\// },
27
- { host: 'player.vimeo.com', path: /^\/video\// },
28
- { host: 'dailymotion.com', path: /^\/embed\// },
29
- { host: 'player.twitch.tv' },
30
- { host: 'w.soundcloud.com', path: /^\/player\/?/ },
31
- { host: 'open.spotify.com', path: /^\/embed\// },
32
- { host: 'google.com', path: /^\/maps\/embed/ },
33
- ];
34
- const ALLOW_TOKENS = new Set([
35
- 'accelerometer',
36
- 'autoplay',
37
- 'clipboard-write',
38
- 'encrypted-media',
39
- 'fullscreen',
40
- 'gyroscope',
41
- 'picture-in-picture',
42
- 'web-share',
43
- ]);
44
- function hostnameOf(value) {
45
- try {
46
- const url = new URL(value);
47
- return url.hostname.replace(/^www\./i, '').toLowerCase();
48
- }
49
- catch {
50
- return '';
51
- }
52
- }
53
- function parsed(value) {
54
- try {
55
- return new URL(value);
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- /** True when this URL is an https embed the schema will store. */
62
- export function isAllowedEmbedSrc(value) {
63
- if (value === null || value === undefined)
64
- return false;
65
- if (!isSafeUrl(value))
66
- return false;
67
- const url = parsed(value.trim());
68
- if (!url)
69
- return false;
70
- if (url.protocol !== 'https:')
71
- return false;
72
- const host = hostnameOf(value);
73
- return EMBED_HOSTS.some((rule) => {
74
- if (rule.host !== host)
75
- return false;
76
- if (!rule.path)
77
- return true;
78
- return rule.path.test(url.pathname);
79
- });
80
- }
81
- /** The URL, or null when it must not be stored as an iframe `src`. */
82
- export function safeEmbedSrc(value) {
83
- return isAllowedEmbedSrc(value) ? value : null;
84
- }
85
- /**
86
- * Permissions policy tokens an iframe may advertise.
87
- *
88
- * Anything not in the closed set is dropped rather than stored: `allow` is how
89
- * an embed asks to skip the sandbox the rest of the page lives in.
90
- *
91
- * The attribute is a **semicolon**-delimited list of directives, each one a
92
- * feature name followed by an optional origin allowlist -- `camera 'self';
93
- * fullscreen *`. Splitting on whitespace instead read the standard YouTube
94
- * string `allow="autoplay; fullscreen; picture-in-picture"` as the tokens
95
- * "autoplay;" and "fullscreen;", so only the last survived, and a trailing
96
- * semicolon lost every permission the author had.
97
- *
98
- * Only the feature name is kept. Dropping the origin allowlist leaves the
99
- * attribute's default, which is the frame's own origin -- so `camera *` narrows
100
- * to the embed itself rather than being stored as written.
101
- */
102
- export function safeAllowList(value) {
103
- if (value === null || value === undefined)
104
- return null;
105
- const kept = [];
106
- const seen = new Set();
107
- for (const directive of value.split(';')) {
108
- const name = (directive.trim().split(/\s+/)[0] ?? '').toLowerCase();
109
- if (!ALLOW_TOKENS.has(name) || seen.has(name))
110
- continue;
111
- seen.add(name);
112
- kept.push(name);
113
- }
114
- return kept.length > 0 ? kept.join('; ') : null;
115
- }
1
+ export * from '@openleaf-editor/content-policy/embed';
116
2
  //# sourceMappingURL=embed.js.map
package/dist/embed.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The one error type OpenLeaf throws on purpose.
3
+ *
4
+ * Before this there were four styles and no way to tell them apart: `parseHtml`
5
+ * coerced anything to an empty document, `serializeHtml(null)` surfaced a raw
6
+ * ProseMirror `TypeError` with nothing naming OpenLeaf in it, a bad
7
+ * `registerEditorPlugin` argument was accepted and failed much later through
8
+ * `console.error`, and a schema collision threw on some unrelated later call.
9
+ * A `catch` block could not distinguish "the integrator passed the wrong thing"
10
+ * from "the library has a bug", so integrators either swallowed everything or
11
+ * nothing.
12
+ *
13
+ * `code` is the stable part. Messages are for people and may be reworded; the
14
+ * code is what a `catch` should switch on.
15
+ */
16
+ export type OpenLeafErrorCode =
17
+ /** A public function was called with a value it cannot accept. */
18
+ 'invalid-argument'
19
+ /** Input nests deeper than the recursive DOM walks can survive. */
20
+ | 'depth-limit'
21
+ /** Two schema extensions define the same node or mark name. */
22
+ | 'schema-conflict'
23
+ /** No `Document` was available and none was supplied. */
24
+ | 'no-document';
25
+ export declare class OpenLeafError extends Error {
26
+ readonly code: OpenLeafErrorCode;
27
+ constructor(code: OpenLeafErrorCode, message: string, options?: ErrorOptions);
28
+ }
29
+ /**
30
+ * A duck check rather than `instanceof`.
31
+ *
32
+ * Two copies of `@openleaf-editor/core` on one page -- which the plugin bundles
33
+ * exist to prevent but a mis-pinned install still produces -- have two distinct
34
+ * `OpenLeafError` classes, and `instanceof` is false across them. The code is
35
+ * what callers actually branch on, so that is what this checks.
36
+ */
37
+ export declare function isOpenLeafError(value: unknown, code?: OpenLeafErrorCode): boolean;
38
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,iBAAiB;AAC3B,kEAAkE;AAChE,kBAAkB;AACpB,mEAAmE;GACjE,aAAa;AACf,+DAA+D;GAC7D,iBAAiB;AACnB,yDAAyD;GACvD,aAAa,CAAA;AAEjB,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;gBAEpB,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAQ7E;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAGjF"}
package/dist/errors.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The one error type OpenLeaf throws on purpose.
3
+ *
4
+ * Before this there were four styles and no way to tell them apart: `parseHtml`
5
+ * coerced anything to an empty document, `serializeHtml(null)` surfaced a raw
6
+ * ProseMirror `TypeError` with nothing naming OpenLeaf in it, a bad
7
+ * `registerEditorPlugin` argument was accepted and failed much later through
8
+ * `console.error`, and a schema collision threw on some unrelated later call.
9
+ * A `catch` block could not distinguish "the integrator passed the wrong thing"
10
+ * from "the library has a bug", so integrators either swallowed everything or
11
+ * nothing.
12
+ *
13
+ * `code` is the stable part. Messages are for people and may be reworded; the
14
+ * code is what a `catch` should switch on.
15
+ */
16
+ export class OpenLeafError extends Error {
17
+ code;
18
+ constructor(code, message, options) {
19
+ super(message, options);
20
+ this.code = code;
21
+ // Set explicitly rather than via a class field: a field would be an own
22
+ // property on every instance, and `name` is conventionally read off the
23
+ // prototype chain by anything formatting an error.
24
+ this.name = 'OpenLeafError';
25
+ }
26
+ }
27
+ /**
28
+ * A duck check rather than `instanceof`.
29
+ *
30
+ * Two copies of `@openleaf-editor/core` on one page -- which the plugin bundles
31
+ * exist to prevent but a mis-pinned install still produces -- have two distinct
32
+ * `OpenLeafError` classes, and `instanceof` is false across them. The code is
33
+ * what callers actually branch on, so that is what this checks.
34
+ */
35
+ export function isOpenLeafError(value, code) {
36
+ if (!(value instanceof Error) || value.name !== 'OpenLeafError')
37
+ return false;
38
+ return code === undefined || value.code === code;
39
+ }
40
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAYH,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAmB;IAEhC,YAAY,IAAuB,EAAE,OAAe,EAAE,OAAsB;QAC1E,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,wEAAwE;QACxE,wEAAwE;QACxE,mDAAmD;QACnD,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;IAC7B,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,IAAwB;IACtE,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe;QAAE,OAAO,KAAK,CAAA;IAC7E,OAAO,IAAI,KAAK,SAAS,IAAK,KAAuB,CAAC,IAAI,KAAK,IAAI,CAAA;AACrE,CAAC"}
@@ -51,15 +51,28 @@ export interface SchemaExtension {
51
51
  /**
52
52
  * Re-emit attributes the spec does not model. Defaults to true.
53
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.
54
+ * Adding a node or mark type strictly *reduces* fidelity for the tag it
55
+ * claims: before the type existed, the preservation layer kept the element
56
+ * and every attribute on it; afterwards the spec keeps only what it declares.
57
+ * A callout node modelling `class` silently drops `id` and `data-analytics`
58
+ * that used to survive, and a `strong` mark used to drop `class` the same way.
59
59
  *
60
60
  * So unmodelled attributes are captured on parse and merged back on
61
61
  * serialize, by default, at schema-build time -- which means an author cannot
62
62
  * opt out by forgetting.
63
+ *
64
+ * ## Setting this to `false` is security-relevant
65
+ *
66
+ * The capture is also where `on*` handlers and unsafe URL schemes are
67
+ * filtered out of the residue -- see `withCarriedAttributes` below. Turning
68
+ * the carry off skips the wrapper entirely, and with it that filter, so a
69
+ * spec's own `getAttrs` becomes the only thing standing between pasted
70
+ * `onclick` and the stored document.
71
+ *
72
+ * It exists for specs that already model every attribute they claim, where
73
+ * carrying would emit the same value twice. It is not a performance knob, and
74
+ * a plugin that sets it is making a decision `SECURITY.md` documents under
75
+ * "Plugin trust model". If you set it, your `getAttrs` owns the scrub.
63
76
  */
64
77
  readonly carryUnknownAttributes?: boolean;
65
78
  }
@@ -86,6 +99,34 @@ export declare function createSchema(list?: readonly SchemaExtension[]): Schema;
86
99
  * "bind to this" and would be captured at import by every consumer, which is
87
100
  * exactly what made the schema impossible to extend. Memoized, and invalidated
88
101
  * whenever the registry changes.
102
+ *
103
+ * ## Why the subscription is in here and not at module scope
104
+ *
105
+ * This package declares `"sideEffects": false`, which is a literal promise that
106
+ * evaluating a module for its own sake changes nothing observable. The bare
107
+ * top-level `onSchemaExtensionsChange(() => { cached = null })` that used to sit
108
+ * below this function broke that promise: it mutates the listener set in the
109
+ * registry above, which is observable by anything that calls `notify()`.
110
+ *
111
+ * What it did *not* do, measured rather than assumed, is actually get dropped.
112
+ * Both rollup 4 (`treeshake.moduleSideEffects: false`) and webpack 5
113
+ * (`optimization.sideEffects`) keep the statement, because the arrow function
114
+ * writes `cached` and `cached` is read by this function -- so the tree-shaker
115
+ * can see the statement contributes to a live export and retains it. A probe
116
+ * that warms the memo, registers an extension and re-reads the schema returns
117
+ * the plugin's node type under both bundlers, before and after this change.
118
+ *
119
+ * It is moved anyway, because the retention depended entirely on that linkage
120
+ * being visible. It is not a property of the invalidator; it is a property of
121
+ * this particular pair of statements, and it would quietly stop holding the
122
+ * moment the memo is refactored -- at which point the failure is silent and
123
+ * total, since `coreSchema()` would keep handing back a schema built before the
124
+ * first `registerSchemaExtension` call. Registering on the first call costs a
125
+ * boolean and makes the flag true instead of true-by-luck.
126
+ *
127
+ * There is no window in which the invalidator is missing: the subscription is
128
+ * installed before the value it guards ever exists, and nothing can reach the
129
+ * memo except through here.
89
130
  */
90
131
  export declare function coreSchema(): Schema;
91
132
  //# sourceMappingURL=extensions.d.ts.map
@@ -1 +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;AAW1B,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;AA8PD,wBAAgB,YAAY,CAAC,IAAI,GAAE,SAAS,eAAe,EAAO,GAAG,MAAM,CAyB1E;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAGnC"}
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,EAEL,MAAM,EAEN,KAAK,QAAQ,EACb,KAAK,QAAQ,EAGd,MAAM,mBAAmB,CAAA;AAqB1B,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;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;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,CAoB9E;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;AAmdD,wBAAgB,YAAY,CAAC,IAAI,GAAE,SAAS,eAAe,EAAO,GAAG,MAAM,CA4B1E;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,UAAU,IAAI,MAAM,CASnC"}