@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.
- package/README.md +127 -0
- package/dist/autolink.d.ts +7 -4
- package/dist/autolink.d.ts.map +1 -1
- package/dist/autolink.js +90 -11
- package/dist/autolink.js.map +1 -1
- package/dist/command-helpers.d.ts +8 -0
- package/dist/command-helpers.d.ts.map +1 -0
- package/dist/command-helpers.js +36 -0
- package/dist/command-helpers.js.map +1 -0
- package/dist/commands.d.ts +21 -63
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +430 -346
- package/dist/commands.js.map +1 -1
- package/dist/css.d.ts +1 -175
- package/dist/css.d.ts.map +1 -1
- package/dist/css.js +1 -516
- package/dist/css.js.map +1 -1
- package/dist/disclosure.d.ts +21 -0
- package/dist/disclosure.d.ts.map +1 -0
- package/dist/disclosure.js +144 -0
- package/dist/disclosure.js.map +1 -0
- package/dist/elements.d.ts +2 -0
- package/dist/elements.d.ts.map +1 -0
- package/dist/elements.js +2 -0
- package/dist/elements.js.map +1 -0
- package/dist/embed.d.ts +1 -51
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +1 -115
- package/dist/embed.js.map +1 -1
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions.d.ts +46 -5
- package/dist/extensions.d.ts.map +1 -1
- package/dist/extensions.js +272 -52
- package/dist/extensions.js.map +1 -1
- package/dist/figure-drag.d.ts +26 -0
- package/dist/figure-drag.d.ts.map +1 -0
- package/dist/figure-drag.js +75 -0
- package/dist/figure-drag.js.map +1 -0
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +19 -7
- package/dist/formats.js.map +1 -1
- package/dist/gapcursor.d.ts +14 -0
- package/dist/gapcursor.d.ts.map +1 -0
- package/dist/gapcursor.js +17 -0
- package/dist/gapcursor.js.map +1 -0
- package/dist/html.d.ts +32 -2
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +185 -31
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -6
- package/dist/index.js.map +1 -1
- package/dist/insert-commands.d.ts +172 -0
- package/dist/insert-commands.d.ts.map +1 -0
- package/dist/insert-commands.js +560 -0
- package/dist/insert-commands.js.map +1 -0
- package/dist/isolating-selection.d.ts +59 -0
- package/dist/isolating-selection.d.ts.map +1 -0
- package/dist/isolating-selection.js +251 -0
- package/dist/isolating-selection.js.map +1 -0
- package/dist/noneditable.d.ts.map +1 -1
- package/dist/noneditable.js +18 -4
- package/dist/noneditable.js.map +1 -1
- package/dist/plugins.d.ts +20 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +27 -2
- package/dist/plugins.js.map +1 -1
- package/dist/preserve.d.ts +48 -17
- package/dist/preserve.d.ts.map +1 -1
- package/dist/preserve.js +214 -43
- package/dist/preserve.js.map +1 -1
- package/dist/schema.d.ts +15 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +157 -28
- package/dist/schema.js.map +1 -1
- package/dist/structure.d.ts +32 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +53 -10
- package/dist/structure.js.map +1 -1
- package/dist/tables.d.ts +47 -1
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +417 -40
- package/dist/tables.js.map +1 -1
- package/dist/testing.d.ts +3 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +54 -6
- package/dist/tokens.js.map +1 -1
- package/dist/url.d.ts +1 -25
- package/dist/url.d.ts.map +1 -1
- package/dist/url.js +1 -77
- package/dist/url.js.map +1 -1
- package/dist/visual-aids.d.ts +11 -1
- package/dist/visual-aids.d.ts.map +1 -1
- package/dist/visual-aids.js +124 -36
- package/dist/visual-aids.js.map +1 -1
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"elements.d.ts","sourceRoot":"","sources":["../src/elements.ts"],"names":[],"mappings":"AAAA,cAAc,0CAA0C,CAAA"}
|
package/dist/elements.js
ADDED
|
@@ -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
|
package/dist/embed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAA"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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"}
|
package/dist/extensions.d.ts
CHANGED
|
@@ -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
|
|
55
|
-
* the
|
|
56
|
-
* attribute on it; afterwards the spec keeps only what it declares.
|
|
57
|
-
* node modelling `class` silently drops `id` and `data-analytics`
|
|
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
|
package/dist/extensions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../src/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAGH,OAAO,
|
|
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"}
|