@aquera/nile-elements 1.9.7 → 1.9.9
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 +6 -0
- package/dist/{index-249de90a.esm.js → index-289706c7.esm.js} +1 -1
- package/dist/{index-df196058.cjs.js → index-d7e6f1c6.cjs.js} +2 -2
- package/dist/index-d7e6f1c6.cjs.js.map +1 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1587 -1276
- package/dist/katex-44476ab4.esm.js +1 -0
- package/dist/katex-f4186f87.cjs.js +2 -0
- package/dist/katex-f4186f87.cjs.js.map +1 -0
- package/dist/nile-color-picker/index.cjs.js +1 -1
- package/dist/nile-color-picker/index.esm.js +1 -1
- package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
- package/dist/nile-color-picker/nile-color-picker.cjs.js.map +1 -1
- package/dist/nile-color-picker/nile-color-picker.esm.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
- package/dist/nile-select/virtual-scroll-helper.esm.js +52 -44
- package/dist/nile-virtual-select/renderer.cjs.js +1 -1
- package/dist/nile-virtual-select/renderer.cjs.js.map +1 -1
- package/dist/nile-virtual-select/renderer.esm.js +23 -22
- package/dist/nile-wysiwyg-editor/engine/commands.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/commands.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/commands.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/create-state.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/create-state.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/index.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/index.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/inputrules.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/keymap.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/code-highlight.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embed-view.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/embeds.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/math-view.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/style-block-view.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/tables.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/sanitize.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/sanitize.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/schema.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/schema.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/schema.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/selection-state.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/selection-state.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/index.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/index.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/math/katex-renderer.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/math/katex-renderer.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.css.esm.js +22 -3
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +7 -6
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.esm.js +33 -54
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.esm.js +83 -95
- package/dist/nile-wysiwyg-editor/types.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/types.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/types.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-item.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-item.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/bubble-menu.esm.js +23 -15
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/code-block-menu.esm.js +3 -3
- package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/embed-panel.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/embed-panel.esm.js +2 -2
- package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/math-panel.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/math-panel.esm.js +0 -3
- package/dist/nile-wysiwyg-editor/ui/preview.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/ui/preview.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/ui/preview.esm.js +8 -0
- package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/source-view.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/source-view.esm.js +225 -24
- package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/suggest-list.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/suggest-list.esm.js +9 -9
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/table-grid-picker.esm.js +115 -13
- package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/table-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/table-menu.esm.js +13 -12
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/nile-color-picker/nile-color-picker.d.ts +1 -0
- package/dist/src/nile-color-picker/nile-color-picker.js +1 -0
- package/dist/src/nile-color-picker/nile-color-picker.js.map +1 -1
- package/dist/src/nile-select/virtual-scroll-helper.js +9 -1
- package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
- package/dist/src/nile-virtual-select/renderer.js +1 -0
- package/dist/src/nile-virtual-select/renderer.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.js +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js +3 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embed-view.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.d.ts +19 -4
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js +23 -18
- package/dist/src/nile-wysiwyg-editor/engine/plugins/embeds.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.d.ts +8 -3
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js +41 -4
- package/dist/src/nile-wysiwyg-editor/engine/plugins/math-view.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.d.ts +17 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js +174 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/style-block-view.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +33 -2
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +49 -8
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +12 -0
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +76 -8
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +2 -0
- package/dist/src/nile-wysiwyg-editor/engine/schema.js +154 -2
- package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/index.d.ts +5 -0
- package/dist/src/nile-wysiwyg-editor/index.js +3 -0
- package/dist/src/nile-wysiwyg-editor/index.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/math/katex-renderer.d.ts +38 -0
- package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js +40 -0
- package/dist/src/nile-wysiwyg-editor/math/katex-renderer.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +22 -3
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.d.ts +36 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +125 -8
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +5 -2
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.d.ts +2 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.d.ts +55 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js +79 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js +33 -54
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.d.ts +50 -2
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js +158 -102
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/types.d.ts +4 -2
- package/dist/src/nile-wysiwyg-editor/types.js +2 -2
- package/dist/src/nile-wysiwyg-editor/types.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/bubble-item.d.ts +48 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js +69 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-item.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.d.ts +17 -2
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +128 -35
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.d.ts +2 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +4 -3
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +0 -3
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/preview.d.ts +20 -0
- package/dist/src/nile-wysiwyg-editor/ui/preview.js +44 -0
- package/dist/src/nile-wysiwyg-editor/ui/preview.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/source-view.d.ts +35 -3
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js +337 -24
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +9 -0
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.d.ts +13 -2
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +166 -10
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +10 -8
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/xml-25c4108a.esm.js +1 -0
- package/dist/xml-90fd974e.cjs.js +2 -0
- package/dist/xml-90fd974e.cjs.js.map +1 -0
- package/package.json +2 -1
- package/rollup.config.js +1 -0
- package/src/index.ts +8 -0
- package/src/nile-color-picker/nile-color-picker.ts +1 -0
- package/src/nile-select/virtual-scroll-helper.ts +9 -1
- package/src/nile-virtual-select/renderer.ts +1 -1
- package/src/nile-wysiwyg-editor/engine/index.ts +1 -0
- package/src/nile-wysiwyg-editor/engine/plugins/embed-view.ts +3 -0
- package/src/nile-wysiwyg-editor/engine/plugins/embeds.ts +26 -20
- package/src/nile-wysiwyg-editor/engine/plugins/math-view.ts +45 -4
- package/src/nile-wysiwyg-editor/engine/plugins/style-block-view.ts +178 -0
- package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +34 -2
- package/src/nile-wysiwyg-editor/engine/pm.test.ts +58 -8
- package/src/nile-wysiwyg-editor/engine/sanitize.ts +71 -8
- package/src/nile-wysiwyg-editor/engine/schema.ts +158 -7
- package/src/nile-wysiwyg-editor/index.ts +5 -0
- package/src/nile-wysiwyg-editor/math/katex-renderer.ts +59 -0
- package/src/nile-wysiwyg-editor/math/katex.d.ts +17 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +22 -3
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +5 -2
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +142 -7
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.ts +2 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar-item.ts +68 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.css.ts +33 -54
- package/src/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.ts +202 -108
- package/src/nile-wysiwyg-editor/types.ts +6 -4
- package/src/nile-wysiwyg-editor/ui/bubble-item.ts +60 -0
- package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +147 -40
- package/src/nile-wysiwyg-editor/ui/embed-panel.ts +4 -3
- package/src/nile-wysiwyg-editor/ui/math-panel.ts +0 -3
- package/src/nile-wysiwyg-editor/ui/preview.ts +42 -0
- package/src/nile-wysiwyg-editor/ui/source-view.ts +339 -31
- package/src/nile-wysiwyg-editor/ui/suggest-list.ts +10 -0
- package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +160 -10
- package/src/nile-wysiwyg-editor/ui/table-menu.ts +10 -8
- package/vscode-html-custom-data.json +75 -6
- package/dist/index-df196058.cjs.js.map +0 -1
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* string ever touches a live DOM, and strips style declarations down to the
|
|
6
6
|
* set the schema understands.
|
|
7
7
|
*/
|
|
8
|
-
import { isAllowedEmbedSrc } from './plugins/embeds';
|
|
8
|
+
import { isAllowedEmbedSrc, EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';
|
|
9
9
|
/** Elements removed together with their content. */
|
|
10
10
|
const DROP_WITH_CONTENT = new Set([
|
|
11
|
-
'script', '
|
|
11
|
+
'script', 'frame', 'frameset', 'object', 'embed',
|
|
12
12
|
'applet', 'form', 'input', 'textarea', 'select', 'button', 'meta', 'link',
|
|
13
13
|
'base', 'title', 'noscript', 'template', 'svg', 'math', 'canvas', 'audio',
|
|
14
14
|
'video', 'dialog', 'slot',
|
|
@@ -39,9 +39,21 @@ const TAG_ATTRS = {
|
|
|
39
39
|
th: new Set(['colspan', 'rowspan', 'data-colwidth']),
|
|
40
40
|
col: new Set(['span', 'width']),
|
|
41
41
|
div: new Set(['data-embed-provider', 'data-width', 'data-align']),
|
|
42
|
-
iframe: new Set([
|
|
42
|
+
iframe: new Set([
|
|
43
|
+
'src', 'title', 'frameborder', 'allowfullscreen', 'allow', 'sandbox', 'referrerpolicy',
|
|
44
|
+
]),
|
|
43
45
|
};
|
|
44
|
-
const GLOBAL_ATTRS = new Set(['style', 'align']);
|
|
46
|
+
const GLOBAL_ATTRS = new Set(['style', 'align', 'class', 'id']);
|
|
47
|
+
/** Author class/id names starting with these are the editor's own; drop them so
|
|
48
|
+
* content can't impersonate or restyle the chrome. */
|
|
49
|
+
const RESERVED_NAME = /^(?:nile-wysiwyg|ProseMirror)/;
|
|
50
|
+
/** Filtered `class`: reserved-prefixed tokens removed. Empty → drop attribute. */
|
|
51
|
+
function cleanClassAttr(value) {
|
|
52
|
+
return value
|
|
53
|
+
.split(/\s+/)
|
|
54
|
+
.filter(token => token && !RESERVED_NAME.test(token))
|
|
55
|
+
.join(' ');
|
|
56
|
+
}
|
|
45
57
|
/** Style declarations the schema can represent. Everything else is noise. */
|
|
46
58
|
const ALLOWED_STYLES = new Set([
|
|
47
59
|
'color', 'background-color', 'font-family', 'font-size', 'font-style',
|
|
@@ -89,6 +101,34 @@ function filterStyle(value, tag) {
|
|
|
89
101
|
})
|
|
90
102
|
.join('; ');
|
|
91
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Sanitizes the CSS inside a <style> block. Neutralizes the vectors that make
|
|
106
|
+
* a raw stylesheet dangerous while leaving ordinary rules intact:
|
|
107
|
+
* - `@import` (external fetch / data exfiltration) — removed,
|
|
108
|
+
* - `expression()`, `behavior:`, `-moz-binding:` (script execution) — broken,
|
|
109
|
+
* - `url(...)` pointing anywhere other than a safe data: image or a local
|
|
110
|
+
* `#fragment` — replaced with `url(#)`,
|
|
111
|
+
* - any `</style` sequence — stripped so the text can't break out of the
|
|
112
|
+
* element when the document is re-serialized.
|
|
113
|
+
* CSS comments are dropped first so none of the above can hide inside one.
|
|
114
|
+
*/
|
|
115
|
+
export function sanitizeCss(css) {
|
|
116
|
+
return css
|
|
117
|
+
.replace(/<\/style/gi, '')
|
|
118
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
119
|
+
.replace(/@import[^;]*;?/gi, '')
|
|
120
|
+
.replace(/expression\s*\(/gi, 'blocked(')
|
|
121
|
+
.replace(/(behavior|-moz-binding)(\s*):/gi, 'blocked-prop$2:')
|
|
122
|
+
.replace(/url\(\s*(['"]?)([^)]*?)\1\s*\)/gi, (full, _quote, raw) => {
|
|
123
|
+
const url = String(raw).trim();
|
|
124
|
+
if (/^#/.test(url))
|
|
125
|
+
return full;
|
|
126
|
+
if (/^data:image\/(png|gif|jpe?g|webp|avif|bmp);/i.test(url))
|
|
127
|
+
return full;
|
|
128
|
+
return 'url(#)';
|
|
129
|
+
})
|
|
130
|
+
.trim();
|
|
131
|
+
}
|
|
92
132
|
function sanitizeElement(el) {
|
|
93
133
|
const tag = el.tagName.toLowerCase();
|
|
94
134
|
const allowed = TAG_ATTRS[tag];
|
|
@@ -107,6 +147,17 @@ function sanitizeElement(el) {
|
|
|
107
147
|
else
|
|
108
148
|
el.removeAttribute('style');
|
|
109
149
|
}
|
|
150
|
+
else if (name === 'class') {
|
|
151
|
+
const cleaned = cleanClassAttr(attr.value);
|
|
152
|
+
if (cleaned)
|
|
153
|
+
el.setAttribute('class', cleaned);
|
|
154
|
+
else
|
|
155
|
+
el.removeAttribute('class');
|
|
156
|
+
}
|
|
157
|
+
else if (name === 'id') {
|
|
158
|
+
if (RESERVED_NAME.test(attr.value))
|
|
159
|
+
el.removeAttribute('id');
|
|
160
|
+
}
|
|
110
161
|
else if (name === 'href' || name === 'src') {
|
|
111
162
|
// Raster data: URLs are allowed on images only.
|
|
112
163
|
if (!isSafeUrl(attr.value, tag === 'img'))
|
|
@@ -127,9 +178,23 @@ function walk(node) {
|
|
|
127
178
|
child.remove();
|
|
128
179
|
continue;
|
|
129
180
|
}
|
|
130
|
-
// iframes are allowed
|
|
131
|
-
|
|
132
|
-
|
|
181
|
+
// iframes are allowed for any https src, but always force-sandboxed so an
|
|
182
|
+
// untrusted embed can never navigate the top window or run unrestricted.
|
|
183
|
+
if (tag === 'iframe') {
|
|
184
|
+
if (!isAllowedEmbedSrc(child.getAttribute('src') ?? '')) {
|
|
185
|
+
child.remove();
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
child.setAttribute('sandbox', EMBED_SANDBOX);
|
|
189
|
+
child.setAttribute('referrerpolicy', EMBED_REFERRER_POLICY);
|
|
190
|
+
}
|
|
191
|
+
// <style> blocks are kept, but their CSS is sanitized and any attributes
|
|
192
|
+
// (media, nonce, …) dropped. Handled before the unwrap branch below so the
|
|
193
|
+
// raw CSS is never spilled into the document as visible text.
|
|
194
|
+
if (tag === 'style') {
|
|
195
|
+
child.textContent = sanitizeCss(child.textContent ?? '');
|
|
196
|
+
for (const attr of Array.from(child.attributes))
|
|
197
|
+
child.removeAttribute(attr.name);
|
|
133
198
|
continue;
|
|
134
199
|
}
|
|
135
200
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
@@ -148,7 +213,10 @@ function walk(node) {
|
|
|
148
213
|
* DOMParser, so embedded scripts never execute and images never load.
|
|
149
214
|
*/
|
|
150
215
|
export function sanitizeHtml(html) {
|
|
151
|
-
|
|
216
|
+
// Wrap in an explicit <body> so metadata-like tags (notably <style>) stay in
|
|
217
|
+
// the body instead of being hoisted into <head> by the HTML parser — which
|
|
218
|
+
// would drop them before walk() ever sees them. Mirrors htmlToDoc's parse.
|
|
219
|
+
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
|
|
152
220
|
walk(doc.body);
|
|
153
221
|
return doc.body.innerHTML;
|
|
154
222
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../../../src/nile-wysiwyg-editor/engine/sanitize.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * HTML sanitization pre-pass. The ProseMirror schema is the real whitelist —\n * anything it can't parse is dropped — but this pass removes actively\n * dangerous content (scripts, event handlers, javascript: URLs) before the\n * string ever touches a live DOM, and strips style declarations down to the\n * set the schema understands.\n */\n\nimport { isAllowedEmbedSrc } from './plugins/embeds';\n\n/** Elements removed together with their content. */\nconst DROP_WITH_CONTENT = new Set([\n 'script', 'style', 'frame', 'frameset', 'object', 'embed',\n 'applet', 'form', 'input', 'textarea', 'select', 'button', 'meta', 'link',\n 'base', 'title', 'noscript', 'template', 'svg', 'math', 'canvas', 'audio',\n 'video', 'dialog', 'slot',\n]);\n\n/** Elements kept as-is (with filtered attributes). Extended in later phases. */\nconst ALLOWED_TAGS = new Set([\n 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'li',\n 'br', 'hr', 'a', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del',\n 'span', 'code', 'sub', 'sup', 'div', 'font',\n 'pre', 'figure', 'figcaption', 'img', 'iframe',\n 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'colgroup', 'col',\n]);\n\n/** Attributes allowed per tag (on top of GLOBAL_ATTRS). */\nconst TAG_ATTRS: Record<string, Set<string>> = {\n a: new Set(['href', 'title', 'target', 'rel', 'id', 'name', 'data-bookmark']),\n table: new Set(['role', 'data-layout']),\n ol: new Set(['start', 'type', 'data-list-style']),\n ul: new Set(['data-task-list']),\n figure: new Set(['data-align']),\n li: new Set(['data-task-item', 'data-checked']),\n span: new Set([\n 'data-mention-trigger', 'data-mention-key', 'data-mention-label',\n 'data-merge-field', 'data-merge-label', 'data-math', 'data-math-display',\n ]),\n pre: new Set(['data-language']),\n img: new Set(['src', 'alt', 'title', 'width', 'height']),\n td: new Set(['colspan', 'rowspan', 'data-colwidth']),\n th: new Set(['colspan', 'rowspan', 'data-colwidth']),\n col: new Set(['span', 'width']),\n div: new Set(['data-embed-provider', 'data-width', 'data-align']),\n iframe: new Set(['src', 'title', 'frameborder', 'allowfullscreen', 'allow']),\n};\n\nconst GLOBAL_ATTRS = new Set(['style', 'align']);\n\n/** Style declarations the schema can represent. Everything else is noise. */\nconst ALLOWED_STYLES = new Set([\n 'color', 'background-color', 'font-family', 'font-size', 'font-style',\n 'font-weight', 'text-align', 'text-decoration', 'text-decoration-line',\n 'vertical-align', 'margin-left', 'margin-inline-start', 'list-style-type',\n 'line-height',\n]);\n\nconst SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:', 'ftp:'];\n\nexport function isSafeUrl(url: string, allowDataImage = false): boolean {\n const trimmed = url.trim();\n if (trimmed === '') return false;\n // Relative URLs and fragments have no protocol — safe.\n if (/^(#|\\/|\\.\\/|\\.\\.\\/)/.test(trimmed)) return true;\n const match = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);\n if (!match) return true;\n const protocol = `${match[1].toLowerCase()}:`;\n if (SAFE_URL_PROTOCOLS.includes(protocol)) return true;\n if (allowDataImage && protocol === 'data:') {\n // Raster images only — data:image/svg+xml can carry scripts.\n return /^data:image\\/(png|gif|jpe?g|webp|avif|bmp);/i.test(trimmed);\n }\n return false;\n}\n\n/** Size styles allowed only where they are part of the document model. */\nconst SIZED_TAGS: Record<string, Set<string>> = {\n img: new Set(['width', 'height']),\n figure: new Set(['width', 'margin-right']),\n div: new Set(['width', 'max-width']),\n};\n\nfunction filterStyle(value: string, tag: string): string {\n const extra = SIZED_TAGS[tag];\n return value\n .split(';')\n .map(decl => decl.trim())\n .filter(decl => {\n const name = decl.slice(0, decl.indexOf(':')).trim().toLowerCase();\n if (!ALLOWED_STYLES.has(name) && !(extra && extra.has(name))) return false;\n // Defense in depth: CSS url()/expression() smuggling.\n return !/url\\s*\\(|expression\\s*\\(/i.test(decl);\n })\n .join('; ');\n}\n\nfunction sanitizeElement(el: Element): void {\n const tag = el.tagName.toLowerCase();\n const allowed = TAG_ATTRS[tag];\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n const keep =\n (GLOBAL_ATTRS.has(name) || (allowed !== undefined && allowed.has(name))) &&\n !name.startsWith('on');\n if (!keep) {\n el.removeAttribute(attr.name);\n continue;\n }\n if (name === 'style') {\n const filtered = filterStyle(attr.value, tag);\n if (filtered) el.setAttribute('style', filtered);\n else el.removeAttribute('style');\n } else if (name === 'href' || name === 'src') {\n // Raster data: URLs are allowed on images only.\n if (!isSafeUrl(attr.value, tag === 'img')) el.removeAttribute(attr.name);\n } else if (name === 'target') {\n if (attr.value === '_blank') el.setAttribute('rel', 'noopener noreferrer');\n else el.removeAttribute('target');\n }\n }\n}\n\nfunction walk(node: Element): void {\n for (const child of Array.from(node.children)) {\n const tag = child.tagName.toLowerCase();\n if (DROP_WITH_CONTENT.has(tag)) {\n child.remove();\n continue;\n }\n // iframes are allowed ONLY for allowlisted embed providers.\n if (tag === 'iframe' && !isAllowedEmbedSrc(child.getAttribute('src') ?? '')) {\n child.remove();\n continue;\n }\n if (!ALLOWED_TAGS.has(tag)) {\n // Unknown wrapper (article, section, table for now, …): keep its\n // content, drop the element itself.\n walk(child);\n child.replaceWith(...Array.from(child.childNodes));\n continue;\n }\n sanitizeElement(child);\n walk(child);\n }\n}\n\n/**\n * Sanitizes an HTML string. Parsing happens in an inert document created by\n * DOMParser, so embedded scripts never execute and images never load.\n */\nexport function sanitizeHtml(html: string): string {\n const doc = new DOMParser().parseFromString(html, 'text/html');\n walk(doc.body);\n return doc.body.innerHTML;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../../../../src/nile-wysiwyg-editor/engine/sanitize.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * HTML sanitization pre-pass. The ProseMirror schema is the real whitelist —\n * anything it can't parse is dropped — but this pass removes actively\n * dangerous content (scripts, event handlers, javascript: URLs) before the\n * string ever touches a live DOM, and strips style declarations down to the\n * set the schema understands.\n */\n\nimport { isAllowedEmbedSrc, EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';\n\n/** Elements removed together with their content. */\nconst DROP_WITH_CONTENT = new Set([\n 'script', 'frame', 'frameset', 'object', 'embed',\n 'applet', 'form', 'input', 'textarea', 'select', 'button', 'meta', 'link',\n 'base', 'title', 'noscript', 'template', 'svg', 'math', 'canvas', 'audio',\n 'video', 'dialog', 'slot',\n]);\n\n/** Elements kept as-is (with filtered attributes). Extended in later phases. */\nconst ALLOWED_TAGS = new Set([\n 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'li',\n 'br', 'hr', 'a', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del',\n 'span', 'code', 'sub', 'sup', 'div', 'font',\n 'pre', 'figure', 'figcaption', 'img', 'iframe',\n 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'colgroup', 'col',\n]);\n\n/** Attributes allowed per tag (on top of GLOBAL_ATTRS). */\nconst TAG_ATTRS: Record<string, Set<string>> = {\n a: new Set(['href', 'title', 'target', 'rel', 'id', 'name', 'data-bookmark']),\n table: new Set(['role', 'data-layout']),\n ol: new Set(['start', 'type', 'data-list-style']),\n ul: new Set(['data-task-list']),\n figure: new Set(['data-align']),\n li: new Set(['data-task-item', 'data-checked']),\n span: new Set([\n 'data-mention-trigger', 'data-mention-key', 'data-mention-label',\n 'data-merge-field', 'data-merge-label', 'data-math', 'data-math-display',\n ]),\n pre: new Set(['data-language']),\n img: new Set(['src', 'alt', 'title', 'width', 'height']),\n td: new Set(['colspan', 'rowspan', 'data-colwidth']),\n th: new Set(['colspan', 'rowspan', 'data-colwidth']),\n col: new Set(['span', 'width']),\n div: new Set(['data-embed-provider', 'data-width', 'data-align']),\n iframe: new Set([\n 'src', 'title', 'frameborder', 'allowfullscreen', 'allow', 'sandbox', 'referrerpolicy',\n ]),\n};\n\nconst GLOBAL_ATTRS = new Set(['style', 'align', 'class', 'id']);\n\n/** Author class/id names starting with these are the editor's own; drop them so\n * content can't impersonate or restyle the chrome. */\nconst RESERVED_NAME = /^(?:nile-wysiwyg|ProseMirror)/;\n\n/** Filtered `class`: reserved-prefixed tokens removed. Empty → drop attribute. */\nfunction cleanClassAttr(value: string): string {\n return value\n .split(/\\s+/)\n .filter(token => token && !RESERVED_NAME.test(token))\n .join(' ');\n}\n\n/** Style declarations the schema can represent. Everything else is noise. */\nconst ALLOWED_STYLES = new Set([\n 'color', 'background-color', 'font-family', 'font-size', 'font-style',\n 'font-weight', 'text-align', 'text-decoration', 'text-decoration-line',\n 'vertical-align', 'margin-left', 'margin-inline-start', 'list-style-type',\n 'line-height',\n]);\n\nconst SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:', 'ftp:'];\n\nexport function isSafeUrl(url: string, allowDataImage = false): boolean {\n const trimmed = url.trim();\n if (trimmed === '') return false;\n // Relative URLs and fragments have no protocol — safe.\n if (/^(#|\\/|\\.\\/|\\.\\.\\/)/.test(trimmed)) return true;\n const match = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed);\n if (!match) return true;\n const protocol = `${match[1].toLowerCase()}:`;\n if (SAFE_URL_PROTOCOLS.includes(protocol)) return true;\n if (allowDataImage && protocol === 'data:') {\n // Raster images only — data:image/svg+xml can carry scripts.\n return /^data:image\\/(png|gif|jpe?g|webp|avif|bmp);/i.test(trimmed);\n }\n return false;\n}\n\n/** Size styles allowed only where they are part of the document model. */\nconst SIZED_TAGS: Record<string, Set<string>> = {\n img: new Set(['width', 'height']),\n figure: new Set(['width', 'margin-right']),\n div: new Set(['width', 'max-width']),\n};\n\nfunction filterStyle(value: string, tag: string): string {\n const extra = SIZED_TAGS[tag];\n return value\n .split(';')\n .map(decl => decl.trim())\n .filter(decl => {\n const name = decl.slice(0, decl.indexOf(':')).trim().toLowerCase();\n if (!ALLOWED_STYLES.has(name) && !(extra && extra.has(name))) return false;\n // Defense in depth: CSS url()/expression() smuggling.\n return !/url\\s*\\(|expression\\s*\\(/i.test(decl);\n })\n .join('; ');\n}\n\n/**\n * Sanitizes the CSS inside a <style> block. Neutralizes the vectors that make\n * a raw stylesheet dangerous while leaving ordinary rules intact:\n * - `@import` (external fetch / data exfiltration) — removed,\n * - `expression()`, `behavior:`, `-moz-binding:` (script execution) — broken,\n * - `url(...)` pointing anywhere other than a safe data: image or a local\n * `#fragment` — replaced with `url(#)`,\n * - any `</style` sequence — stripped so the text can't break out of the\n * element when the document is re-serialized.\n * CSS comments are dropped first so none of the above can hide inside one.\n */\nexport function sanitizeCss(css: string): string {\n return css\n .replace(/<\\/style/gi, '')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/@import[^;]*;?/gi, '')\n .replace(/expression\\s*\\(/gi, 'blocked(')\n .replace(/(behavior|-moz-binding)(\\s*):/gi, 'blocked-prop$2:')\n .replace(/url\\(\\s*(['\"]?)([^)]*?)\\1\\s*\\)/gi, (full, _quote, raw) => {\n const url = String(raw).trim();\n if (/^#/.test(url)) return full;\n if (/^data:image\\/(png|gif|jpe?g|webp|avif|bmp);/i.test(url)) return full;\n return 'url(#)';\n })\n .trim();\n}\n\nfunction sanitizeElement(el: Element): void {\n const tag = el.tagName.toLowerCase();\n const allowed = TAG_ATTRS[tag];\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n const keep =\n (GLOBAL_ATTRS.has(name) || (allowed !== undefined && allowed.has(name))) &&\n !name.startsWith('on');\n if (!keep) {\n el.removeAttribute(attr.name);\n continue;\n }\n if (name === 'style') {\n const filtered = filterStyle(attr.value, tag);\n if (filtered) el.setAttribute('style', filtered);\n else el.removeAttribute('style');\n } else if (name === 'class') {\n const cleaned = cleanClassAttr(attr.value);\n if (cleaned) el.setAttribute('class', cleaned);\n else el.removeAttribute('class');\n } else if (name === 'id') {\n if (RESERVED_NAME.test(attr.value)) el.removeAttribute('id');\n } else if (name === 'href' || name === 'src') {\n // Raster data: URLs are allowed on images only.\n if (!isSafeUrl(attr.value, tag === 'img')) el.removeAttribute(attr.name);\n } else if (name === 'target') {\n if (attr.value === '_blank') el.setAttribute('rel', 'noopener noreferrer');\n else el.removeAttribute('target');\n }\n }\n}\n\nfunction walk(node: Element): void {\n for (const child of Array.from(node.children)) {\n const tag = child.tagName.toLowerCase();\n if (DROP_WITH_CONTENT.has(tag)) {\n child.remove();\n continue;\n }\n // iframes are allowed for any https src, but always force-sandboxed so an\n // untrusted embed can never navigate the top window or run unrestricted.\n if (tag === 'iframe') {\n if (!isAllowedEmbedSrc(child.getAttribute('src') ?? '')) {\n child.remove();\n continue;\n }\n child.setAttribute('sandbox', EMBED_SANDBOX);\n child.setAttribute('referrerpolicy', EMBED_REFERRER_POLICY);\n }\n // <style> blocks are kept, but their CSS is sanitized and any attributes\n // (media, nonce, …) dropped. Handled before the unwrap branch below so the\n // raw CSS is never spilled into the document as visible text.\n if (tag === 'style') {\n child.textContent = sanitizeCss(child.textContent ?? '');\n for (const attr of Array.from(child.attributes)) child.removeAttribute(attr.name);\n continue;\n }\n if (!ALLOWED_TAGS.has(tag)) {\n // Unknown wrapper (article, section, table for now, …): keep its\n // content, drop the element itself.\n walk(child);\n child.replaceWith(...Array.from(child.childNodes));\n continue;\n }\n sanitizeElement(child);\n walk(child);\n }\n}\n\n/**\n * Sanitizes an HTML string. Parsing happens in an inert document created by\n * DOMParser, so embedded scripts never execute and images never load.\n */\nexport function sanitizeHtml(html: string): string {\n // Wrap in an explicit <body> so metadata-like tags (notably <style>) stay in\n // the body instead of being hoisted into <head> by the HTML parser — which\n // would drop them before walk() ever sees them. Mirrors htmlToDoc's parse.\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');\n walk(doc.body);\n return doc.body.innerHTML;\n}\n"]}
|
|
@@ -3,6 +3,8 @@ export type Alignment = 'left' | 'center' | 'right' | 'justify';
|
|
|
3
3
|
/** Indent depth in 40px steps, like CKEditor/Google Docs. */
|
|
4
4
|
export declare const INDENT_STEP_PX = 40;
|
|
5
5
|
export declare const MAX_INDENT = 8;
|
|
6
|
+
export declare function cleanClass(value: string | null): string | null;
|
|
7
|
+
export declare function cleanId(value: string | null): string | null;
|
|
6
8
|
/** Bullet markers CSS understands; anything else falls back to default. */
|
|
7
9
|
export declare const BULLET_STYLES: readonly ["disc", "circle", "square"];
|
|
8
10
|
/** Numbering styles, keyed by the HTML `type` attribute letter. */
|
|
@@ -2,6 +2,7 @@ import { Schema } from 'prosemirror-model';
|
|
|
2
2
|
import { addListNodes } from 'prosemirror-schema-list';
|
|
3
3
|
import { tableNodes } from 'prosemirror-tables';
|
|
4
4
|
import OrderedMap from 'orderedmap';
|
|
5
|
+
import { EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';
|
|
5
6
|
const ALIGN_VALUES = ['left', 'center', 'right', 'justify'];
|
|
6
7
|
function getAlignFromDom(dom) {
|
|
7
8
|
const value = dom.style.textAlign || dom.getAttribute('align') || '';
|
|
@@ -47,6 +48,95 @@ function blockStyle(attrs) {
|
|
|
47
48
|
parts.push(`line-height: ${attrs.lineHeight}`);
|
|
48
49
|
return parts.length ? parts.join('; ') : null;
|
|
49
50
|
}
|
|
51
|
+
/* -------------------------------------------------------------------------
|
|
52
|
+
* Author class / id passthrough
|
|
53
|
+
*
|
|
54
|
+
* Content elements keep the `class` and `id` an author gave them, so class/id
|
|
55
|
+
* CSS (e.g. a scoped author <style> block) can target the document. Values that
|
|
56
|
+
* collide with the editor's own internal names are dropped so content can never
|
|
57
|
+
* impersonate or restyle the chrome; the editor's own classes (emitted in
|
|
58
|
+
* toDOM) are always preserved and merged with the author's.
|
|
59
|
+
* ------------------------------------------------------------------------- */
|
|
60
|
+
const RESERVED_NAME = /^(?:nile-wysiwyg|ProseMirror)/;
|
|
61
|
+
export function cleanClass(value) {
|
|
62
|
+
if (!value)
|
|
63
|
+
return null;
|
|
64
|
+
const kept = value.split(/\s+/).filter(token => token && !RESERVED_NAME.test(token));
|
|
65
|
+
return kept.length ? kept.join(' ') : null;
|
|
66
|
+
}
|
|
67
|
+
export function cleanId(value) {
|
|
68
|
+
return value && !RESERVED_NAME.test(value) ? value : null;
|
|
69
|
+
}
|
|
70
|
+
const HTML_ATTR_SPEC = { htmlClass: { default: null }, htmlId: { default: null } };
|
|
71
|
+
function readHtmlAttrs(dom) {
|
|
72
|
+
return {
|
|
73
|
+
htmlClass: cleanClass(dom.getAttribute('class')),
|
|
74
|
+
htmlId: cleanId(dom.getAttribute('id')),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Wraps a tag-based parse rule so it also captures author class/id. Rules
|
|
78
|
+
* keyed by `style` (whose getAttrs receives a value string, not an element)
|
|
79
|
+
* pass through untouched. */
|
|
80
|
+
function augmentParseRule(rule) {
|
|
81
|
+
if (!('tag' in rule) || 'style' in rule)
|
|
82
|
+
return rule;
|
|
83
|
+
const inner = rule.getAttrs;
|
|
84
|
+
return {
|
|
85
|
+
...rule,
|
|
86
|
+
getAttrs(dom) {
|
|
87
|
+
const base = inner ? inner(dom) : null;
|
|
88
|
+
if (base === false)
|
|
89
|
+
return false;
|
|
90
|
+
return { ...(base ?? {}), ...readHtmlAttrs(dom) };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Wraps a toDOM so stored class/id are written back onto the element, merged
|
|
95
|
+
* with any class the spec emits itself. Handles the `[tag]`, `[tag, attrs]`,
|
|
96
|
+
* `[tag, 0]`, and `[tag, attrs, …children]` output shapes. */
|
|
97
|
+
function augmentToDOM(toDOM) {
|
|
98
|
+
if (!toDOM)
|
|
99
|
+
return toDOM;
|
|
100
|
+
return (node, ...rest) => {
|
|
101
|
+
const out = toDOM(node, ...rest);
|
|
102
|
+
const cls = node.attrs.htmlClass;
|
|
103
|
+
const id = node.attrs.htmlId;
|
|
104
|
+
if (!Array.isArray(out) || (!cls && !id))
|
|
105
|
+
return out;
|
|
106
|
+
const [tag, ...tail] = out;
|
|
107
|
+
let attrs = {};
|
|
108
|
+
let children = tail;
|
|
109
|
+
if (tail.length && tail[0] && typeof tail[0] === 'object' && !Array.isArray(tail[0])) {
|
|
110
|
+
attrs = { ...tail[0] };
|
|
111
|
+
children = tail.slice(1);
|
|
112
|
+
}
|
|
113
|
+
if (cls)
|
|
114
|
+
attrs.class = attrs.class ? `${attrs.class} ${cls}` : cls;
|
|
115
|
+
if (id)
|
|
116
|
+
attrs.id = id;
|
|
117
|
+
return [tag, attrs, ...children];
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Adds `htmlClass`/`htmlId` attrs plus parse/serialize support to a spec. */
|
|
121
|
+
function withHtmlAttrs(spec) {
|
|
122
|
+
return {
|
|
123
|
+
...spec,
|
|
124
|
+
attrs: { ...(spec.attrs ?? {}), ...HTML_ATTR_SPEC },
|
|
125
|
+
parseDOM: spec.parseDOM?.map(augmentParseRule),
|
|
126
|
+
toDOM: augmentToDOM(spec.toDOM),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Content nodes/marks that carry author class/id. Excludes structural leaves
|
|
130
|
+
* (doc, text), atoms with bespoke DOM (image, embeds, mentions, math, …) and
|
|
131
|
+
* style-only marks (their spans are covered by `html_span`). */
|
|
132
|
+
const HTML_ATTR_NODES = [
|
|
133
|
+
'paragraph', 'heading', 'blockquote', 'code_block',
|
|
134
|
+
'bullet_list', 'ordered_list', 'list_item',
|
|
135
|
+
'table', 'table_row', 'table_cell', 'table_header',
|
|
136
|
+
];
|
|
137
|
+
const HTML_ATTR_MARKS = [
|
|
138
|
+
'link', 'bold', 'italic', 'underline', 'strike', 'code', 'subscript', 'superscript',
|
|
139
|
+
];
|
|
50
140
|
/**
|
|
51
141
|
* Block nodes. `align` is modeled as an attribute on textblocks rather than a
|
|
52
142
|
* wrapper node so alignment survives block-type changes (heading <-> paragraph).
|
|
@@ -360,6 +450,8 @@ const nodes = {
|
|
|
360
450
|
frameborder: '0',
|
|
361
451
|
allowfullscreen: 'true',
|
|
362
452
|
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
|
453
|
+
sandbox: EMBED_SANDBOX,
|
|
454
|
+
referrerpolicy: EMBED_REFERRER_POLICY,
|
|
363
455
|
};
|
|
364
456
|
if (title)
|
|
365
457
|
iframeAttrs.title = title;
|
|
@@ -384,6 +476,28 @@ const nodes = {
|
|
|
384
476
|
return ['div', wrapperAttrs, ['iframe', iframeAttrs]];
|
|
385
477
|
},
|
|
386
478
|
},
|
|
479
|
+
/**
|
|
480
|
+
* Author-supplied `<style>` block, entered via the HTML source view. The CSS
|
|
481
|
+
* is sanitized on the way in (see sanitizeCss) — `@import`, `expression()`
|
|
482
|
+
* and external `url()` are neutralized — then stored verbatim and serialized
|
|
483
|
+
* straight back to a `<style>` element. A node view renders it live in the
|
|
484
|
+
* editor with a small chip so the otherwise-invisible block stays selectable.
|
|
485
|
+
*/
|
|
486
|
+
styleBlock: {
|
|
487
|
+
group: 'block',
|
|
488
|
+
atom: true,
|
|
489
|
+
selectable: true,
|
|
490
|
+
attrs: { css: { default: '' } },
|
|
491
|
+
parseDOM: [
|
|
492
|
+
{
|
|
493
|
+
tag: 'style',
|
|
494
|
+
getAttrs: (dom) => ({ css: dom.textContent ?? '' }),
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
toDOM(node) {
|
|
498
|
+
return ['style', node.attrs.css];
|
|
499
|
+
},
|
|
500
|
+
},
|
|
387
501
|
/**
|
|
388
502
|
* Bookmark: a named in-text anchor (`<a id="…">`) for intra-document
|
|
389
503
|
* navigation. Invisible in serialized output; the editor draws a flag
|
|
@@ -685,6 +799,31 @@ const marks = {
|
|
|
685
799
|
return ['code', { class: 'nile-wysiwyg__inline-code' }, 0];
|
|
686
800
|
},
|
|
687
801
|
},
|
|
802
|
+
/** Carries an author `class`/`id` on an inline `<span>` that has no other
|
|
803
|
+
* meaning, so class/id CSS can target runs of text. Low priority so styled
|
|
804
|
+
* spans (color/font marks) and atom spans (mention/merge/math) win. */
|
|
805
|
+
html_span: {
|
|
806
|
+
attrs: { htmlClass: { default: null }, htmlId: { default: null } },
|
|
807
|
+
parseDOM: [
|
|
808
|
+
{
|
|
809
|
+
tag: 'span',
|
|
810
|
+
priority: 5,
|
|
811
|
+
getAttrs: (dom) => {
|
|
812
|
+
const htmlClass = cleanClass(dom.getAttribute('class'));
|
|
813
|
+
const htmlId = cleanId(dom.getAttribute('id'));
|
|
814
|
+
return htmlClass || htmlId ? { htmlClass, htmlId } : false;
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
toDOM(mark) {
|
|
819
|
+
const attrs = {};
|
|
820
|
+
if (mark.attrs.htmlClass)
|
|
821
|
+
attrs.class = mark.attrs.htmlClass;
|
|
822
|
+
if (mark.attrs.htmlId)
|
|
823
|
+
attrs.id = mark.attrs.htmlId;
|
|
824
|
+
return ['span', attrs, 0];
|
|
825
|
+
},
|
|
826
|
+
},
|
|
688
827
|
text_color: {
|
|
689
828
|
attrs: { color: {} },
|
|
690
829
|
parseDOM: [
|
|
@@ -811,12 +950,25 @@ const orderedListSpec = {
|
|
|
811
950
|
};
|
|
812
951
|
/** Builds the editor schema. List nodes are added via prosemirror-schema-list. */
|
|
813
952
|
export function createSchema() {
|
|
814
|
-
|
|
953
|
+
let nodeMap = addListNodes(OrderedMap.from(nodes), 'paragraph block*', 'block')
|
|
815
954
|
.update('bullet_list', bulletListSpec)
|
|
816
955
|
.update('ordered_list', orderedListSpec)
|
|
817
956
|
.append(tableNodeSpecs)
|
|
818
957
|
.update('table', tableSpec);
|
|
819
|
-
|
|
958
|
+
// Thread author class/id through the content nodes and marks (applied last so
|
|
959
|
+
// it also wraps the list/table specs pulled in from external packages).
|
|
960
|
+
for (const name of HTML_ATTR_NODES) {
|
|
961
|
+
const spec = nodeMap.get(name);
|
|
962
|
+
if (spec)
|
|
963
|
+
nodeMap = nodeMap.update(name, withHtmlAttrs(spec));
|
|
964
|
+
}
|
|
965
|
+
let markMap = OrderedMap.from(marks);
|
|
966
|
+
for (const name of HTML_ATTR_MARKS) {
|
|
967
|
+
const spec = markMap.get(name);
|
|
968
|
+
if (spec)
|
|
969
|
+
markMap = markMap.update(name, withHtmlAttrs(spec));
|
|
970
|
+
}
|
|
971
|
+
return new Schema({ nodes: nodeMap, marks: markMap });
|
|
820
972
|
}
|
|
821
973
|
/** A shared default schema instance. */
|
|
822
974
|
export const defaultSchema = createSchema();
|