@aquera/nile-elements 2.0.15 → 2.0.17
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/demo/nxtgen-classes.css +5 -5
- package/demo/nxtgen-dark.css +16 -16
- package/demo/nxtgen-properties.css +2 -2
- package/demo/nxtgen-utilities.css +5 -5
- package/demo/nxtgen.css +5 -5
- package/demo/utilities.css +2 -2
- package/demo/variables.css +2 -2
- package/dist/{index-f6944844.cjs.js → index-0c0c869d.cjs.js} +2 -2
- package/dist/{index-f6944844.cjs.js.map → index-0c0c869d.cjs.js.map} +1 -1
- package/dist/{index-403841c7.cjs.js → index-129ef6fb.cjs.js} +3 -3
- package/dist/{index-403841c7.cjs.js.map → index-129ef6fb.cjs.js.map} +1 -1
- package/dist/{index-1faa963c.esm.js → index-36f2a6b5.esm.js} +1 -1
- package/dist/index-4df50481.esm.js +1 -0
- package/dist/index-5d34cff1.esm.js +1 -0
- package/dist/index-7ae95edd.cjs.js +2 -0
- package/dist/{index-3ca67627.cjs.js.map → index-7ae95edd.cjs.js.map} +1 -1
- package/dist/index-7ca72b78.esm.js +1 -0
- package/dist/index-bbe23ad8.cjs.js +2 -0
- package/dist/index-bbe23ad8.cjs.js.map +1 -0
- package/dist/index-ccc6a799.cjs.js +5 -0
- package/dist/index-ccc6a799.cjs.js.map +1 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +124 -72
- package/dist/nile-auto-complete/index.cjs.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.cjs.js +1 -1
- package/dist/nile-auto-complete/portal-manager.cjs.js +1 -1
- package/dist/nile-auto-complete/portal-utils.cjs.js +1 -1
- package/dist/nile-code-editor/extensionSetup.cjs.js +2 -2
- package/dist/nile-code-editor/index.cjs.js +1 -1
- package/dist/nile-code-editor/nile-code-editor.cjs.js +1 -1
- package/dist/nile-code-editor/theme.cjs.js +1 -1
- package/dist/nile-color-picker/index.cjs.js +1 -1
- package/dist/nile-color-picker/nile-color-picker.cjs.js +1 -1
- package/dist/nile-combobox/index.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
- package/dist/nile-combobox/portal-manager.cjs.js +1 -1
- package/dist/nile-context-menu/index.cjs.js +1 -1
- package/dist/nile-context-menu/nile-context-menu.cjs.js +1 -1
- package/dist/nile-context-submenu/index.cjs.js +1 -1
- package/dist/nile-context-submenu/nile-context-submenu.cjs.js +1 -1
- package/dist/nile-detail/index.cjs.js +1 -1
- package/dist/nile-detail/nile-detail.cjs.js +1 -1
- package/dist/nile-dropdown/index.cjs.js +1 -1
- package/dist/nile-dropdown/nile-dropdown.cjs.js +1 -1
- package/dist/nile-dropdown/portal-manager.cjs.js +1 -1
- package/dist/nile-file-upload/index.cjs.js +1 -1
- package/dist/nile-file-upload/nile-file-upload.cjs.js +1 -1
- package/dist/nile-file-upload/nile-file-upload.template.cjs.js +1 -1
- package/dist/nile-floating-panel/index.cjs.js +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.cjs.js +1 -1
- package/dist/nile-lite-tooltip/index.cjs.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.css.cjs.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.css.cjs.js.map +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.css.esm.js +1 -1
- package/dist/nile-markdown-editor/index.cjs.js +1 -1
- package/dist/nile-markdown-editor/nile-markdown-editor.cjs.js +1 -1
- package/dist/nile-popover/index.cjs.js +1 -1
- package/dist/nile-popover/nile-popover.cjs.js +1 -1
- package/dist/nile-popover/portal-manager.cjs.js +1 -1
- package/dist/nile-popup/index.cjs.js +1 -1
- package/dist/nile-popup/nile-popup.cjs.js +1 -1
- package/dist/nile-segmented-progress-bar/index.cjs.js +1 -1
- package/dist/nile-segmented-progress-bar/nile-segmented-progress-bar.cjs.js +1 -1
- package/dist/nile-select/index.cjs.js +1 -1
- package/dist/nile-select/nile-select.cjs.js +1 -1
- package/dist/nile-select/portal-manager.cjs.js +1 -1
- package/dist/nile-side-bar-action/index.cjs.js +1 -1
- package/dist/nile-side-bar-action/nile-side-bar-action.cjs.js +1 -1
- package/dist/nile-side-bar-action/portal-manager.cjs.js +1 -1
- package/dist/nile-tooltip/index.cjs.js +1 -1
- package/dist/nile-tooltip/nile-tooltip.cjs.js +1 -1
- package/dist/nile-tooltip/nile-tooltip.css.cjs.js +1 -1
- package/dist/nile-tooltip/nile-tooltip.css.cjs.js.map +1 -1
- package/dist/nile-tooltip/nile-tooltip.css.esm.js +1 -1
- package/dist/nile-virtual-select/index.cjs.js +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +1 -1
- package/dist/nile-virtual-select/portal-manager.cjs.js +1 -1
- package/dist/nile-virtual-select/renderer.cjs.js +1 -1
- 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/email-css.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/email-css.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/email-css.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/email-render.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/email-render.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/email-render.esm.js +1 -0
- package/dist/nile-wysiwyg-editor/engine/html-detect.cjs.js +2 -0
- package/dist/nile-wysiwyg-editor/engine/html-detect.cjs.js.map +1 -0
- package/dist/nile-wysiwyg-editor/engine/html-detect.esm.js +1 -0
- 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/inputrules.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/inputrules.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/keymap.cjs.js.map +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/find-replace.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/find-replace.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/format-painter.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image-view.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/image.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/link.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/link.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/max-length.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/paste.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/placeholder.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/single-line.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/plugins/suggestions.esm.js +1 -1
- 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 +2 -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/engine/serializer.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/engine/serializer.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/engine/serializer.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/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 +45 -4
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-editor.esm.js +2 -2
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/index.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/nile-wysiwyg-toolbar/nile-wysiwyg-toolbar.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/bookmark-panel.esm.js +2 -2
- 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 +5 -5
- 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 +1 -1
- package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/embed-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/embed-menu.esm.js +1 -1
- 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/emoji-picker.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/emoji-picker.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/emoji-picker.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/find-replace.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/find-replace.esm.js +3 -3
- package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/image-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/image-menu.esm.js +3 -3
- package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/image-panel.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/image-panel.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/insert-menu.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/insert-menu.esm.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/link-editor.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/link-editor.esm.js +9 -9
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/markdown-panel.esm.js +1 -1
- 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 +1 -1
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/shortcut-help.esm.js +1 -1
- 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 +4 -4
- package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js +1 -1
- package/dist/nile-wysiwyg-editor/ui/special-chars.cjs.js.map +1 -1
- package/dist/nile-wysiwyg-editor/ui/special-chars.esm.js +1 -1
- 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 +23 -12
- 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 +7 -7
- 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 +1 -1
- package/dist/src/nile-lite-tooltip/nile-lite-tooltip.css.js +1 -1
- package/dist/src/nile-lite-tooltip/nile-lite-tooltip.css.js.map +1 -1
- package/dist/src/nile-tooltip/nile-tooltip.css.js +1 -1
- package/dist/src/nile-tooltip/nile-tooltip.css.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/create-state.d.ts +5 -0
- package/dist/src/nile-wysiwyg-editor/engine/create-state.js +2 -0
- package/dist/src/nile-wysiwyg-editor/engine/create-state.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/email-css.d.ts +14 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-css.js +140 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-css.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-fidelity.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-fidelity.test.js +1045 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-fidelity.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-render.d.ts +11 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-render.js +93 -0
- package/dist/src/nile-wysiwyg-editor/engine/email-render.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/html-detect.d.ts +2 -0
- package/dist/src/nile-wysiwyg-editor/engine/html-detect.js +24 -0
- package/dist/src/nile-wysiwyg-editor/engine/html-detect.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/engine/index.d.ts +4 -1
- package/dist/src/nile-wysiwyg-editor/engine/index.js +3 -1
- package/dist/src/nile-wysiwyg-editor/engine/index.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.d.ts +0 -5
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.js +19 -0
- package/dist/src/nile-wysiwyg-editor/engine/inputrules.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js +3 -2
- package/dist/src/nile-wysiwyg-editor/engine/plugins/image-view.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.d.ts +4 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js +56 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/link.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.d.ts +5 -0
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js +27 -6
- package/dist/src/nile-wysiwyg-editor/engine/plugins/paste.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js +30 -7
- package/dist/src/nile-wysiwyg-editor/engine/plugins/tables.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js +46 -0
- package/dist/src/nile-wysiwyg-editor/engine/pm.test.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.d.ts +9 -1
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js +730 -7
- package/dist/src/nile-wysiwyg-editor/engine/sanitize.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/schema.d.ts +3 -0
- package/dist/src/nile-wysiwyg-editor/engine/schema.js +215 -34
- package/dist/src/nile-wysiwyg-editor/engine/schema.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/selection-state.js +3 -1
- package/dist/src/nile-wysiwyg-editor/engine/selection-state.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/engine/serializer.d.ts +12 -4
- package/dist/src/nile-wysiwyg-editor/engine/serializer.js +18 -8
- package/dist/src/nile-wysiwyg-editor/engine/serializer.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor-email-fidelity.test.d.ts +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor-email-fidelity.test.js +287 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor-email-fidelity.test.js.map +1 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.js +45 -4
- 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 +23 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js +113 -15
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js +213 -0
- package/dist/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js +2 -2
- package/dist/src/nile-wysiwyg-editor/ui/bookmark-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/bubble-menu.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-menu.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/embed-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/emoji-picker.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/find-replace.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/find-replace.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/image-menu.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/image-menu.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/image-panel.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/image-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/insert-menu.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/link-editor.js +2 -2
- package/dist/src/nile-wysiwyg-editor/ui/link-editor.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/markdown-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/math-panel.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/shortcut-help.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js +2 -2
- package/dist/src/nile-wysiwyg-editor/ui/source-view.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/special-chars.js +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/special-chars.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js +14 -3
- package/dist/src/nile-wysiwyg-editor/ui/suggest-list.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js +2 -2
- package/dist/src/nile-wysiwyg-editor/ui/table-grid-picker.js.map +1 -1
- package/dist/src/nile-wysiwyg-editor/ui/table-menu.js +1 -1
- 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/version.cjs.js +1 -1
- package/dist/version.cjs.js.map +1 -1
- package/dist/version.esm.js +1 -1
- package/package.json +1 -1
- package/src/nile-lite-tooltip/nile-lite-tooltip.css.ts +1 -1
- package/src/nile-tooltip/nile-tooltip.css.ts +1 -1
- package/src/nile-wysiwyg-editor/engine/create-state.ts +7 -0
- package/src/nile-wysiwyg-editor/engine/email-css.ts +136 -0
- package/src/nile-wysiwyg-editor/engine/email-fidelity.test.ts +1347 -0
- package/src/nile-wysiwyg-editor/engine/email-render.ts +99 -0
- package/src/nile-wysiwyg-editor/engine/html-detect.ts +26 -0
- package/src/nile-wysiwyg-editor/engine/index.ts +10 -1
- package/src/nile-wysiwyg-editor/engine/inputrules.ts +18 -1
- package/src/nile-wysiwyg-editor/engine/plugins/image-view.ts +3 -2
- package/src/nile-wysiwyg-editor/engine/plugins/link.ts +58 -2
- package/src/nile-wysiwyg-editor/engine/plugins/paste.ts +31 -8
- package/src/nile-wysiwyg-editor/engine/plugins/tables.ts +29 -7
- package/src/nile-wysiwyg-editor/engine/pm.test.ts +73 -0
- package/src/nile-wysiwyg-editor/engine/sanitize.ts +729 -7
- package/src/nile-wysiwyg-editor/engine/schema.ts +206 -33
- package/src/nile-wysiwyg-editor/engine/selection-state.ts +3 -1
- package/src/nile-wysiwyg-editor/engine/serializer.ts +30 -8
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor-email-fidelity.test.ts +357 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.css.ts +45 -4
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.test.ts +258 -0
- package/src/nile-wysiwyg-editor/nile-wysiwyg-editor.ts +119 -12
- package/src/nile-wysiwyg-editor/ui/bookmark-panel.ts +2 -2
- package/src/nile-wysiwyg-editor/ui/bubble-menu.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/embed-menu.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/embed-panel.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/emoji-picker.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/find-replace.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/image-menu.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/image-panel.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/insert-menu.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/link-editor.ts +2 -2
- package/src/nile-wysiwyg-editor/ui/markdown-panel.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/math-panel.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/shortcut-help.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/source-view.ts +2 -2
- package/src/nile-wysiwyg-editor/ui/special-chars.ts +1 -1
- package/src/nile-wysiwyg-editor/ui/suggest-list.ts +14 -3
- package/src/nile-wysiwyg-editor/ui/table-grid-picker.ts +2 -2
- package/src/nile-wysiwyg-editor/ui/table-menu.ts +1 -1
- package/vscode-html-custom-data.json +15 -1
- package/web-test-runner.config.mjs +4 -0
- package/dist/index-3ca67627.cjs.js +0 -2
- package/dist/index-6f3fa4ab.esm.js +0 -1
- package/dist/index-7b1c9857.esm.js +0 -1
- package/dist/index-89c520e9.cjs.js +0 -5
- package/dist/index-89c520e9.cjs.js.map +0 -1
- package/dist/index-ba47645d.esm.js +0 -1
- package/dist/index-c3210a9c.cjs.js +0 -2
- package/dist/index-c3210a9c.cjs.js.map +0 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* set the schema understands.
|
|
7
7
|
*/
|
|
8
8
|
import { isAllowedEmbedSrc, EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds';
|
|
9
|
+
import { isExtensionValueValid } from './email-css';
|
|
9
10
|
/** Elements removed together with their content. */
|
|
10
11
|
const DROP_WITH_CONTENT = new Set([
|
|
11
12
|
'script', 'frame', 'frameset', 'object', 'embed',
|
|
@@ -101,6 +102,380 @@ function filterStyle(value, tag) {
|
|
|
101
102
|
})
|
|
102
103
|
.join('; ');
|
|
103
104
|
}
|
|
105
|
+
/* --- email-fidelity: container styles become node attrs, not per-run marks (flag-gated) --- */
|
|
106
|
+
/** Containers whose inline styles must not become per-run marks. */
|
|
107
|
+
const CONTAINER_TAGS = new Set([
|
|
108
|
+
'table', 'td', 'th', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
109
|
+
'div', 'li', 'blockquote', 'tr', 'tbody',
|
|
110
|
+
]);
|
|
111
|
+
/** Mark-triggering declarations dropped on any container. */
|
|
112
|
+
const CONTAINER_DROP = new Set([
|
|
113
|
+
'color', 'background-color', 'font-family', 'font-size', 'font-weight',
|
|
114
|
+
'font-style', 'text-decoration', 'text-decoration-line',
|
|
115
|
+
'text-decoration-style', 'text-decoration-color',
|
|
116
|
+
]);
|
|
117
|
+
/** Layout declarations allowed on cells with fidelity. */
|
|
118
|
+
const CELL_LAYOUT = new Set([
|
|
119
|
+
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
120
|
+
'width', 'vertical-align', 'border', 'border-top', 'border-right',
|
|
121
|
+
'border-bottom', 'border-left', 'border-radius',
|
|
122
|
+
]);
|
|
123
|
+
/** `border-width`, `border-top-color`, … — resolved per side through CSSOM instead. */
|
|
124
|
+
const CELL_BORDER_LONGHAND = /^border-(?:(?:top|right|bottom|left)-)?(?:width|style|color)$/;
|
|
125
|
+
/** Layout declarations allowed on tables with fidelity. */
|
|
126
|
+
const TABLE_LAYOUT = new Set(['width', 'max-width', 'border', 'margin-left', 'margin-right']);
|
|
127
|
+
/** Block margins allowed on paragraphs/headings with fidelity. */
|
|
128
|
+
const BLOCK_LAYOUT = new Set(['margin', 'margin-top', 'margin-bottom']);
|
|
129
|
+
const BORDER_STYLES = new Set(['solid', 'dashed', 'dotted', 'double', 'none']);
|
|
130
|
+
const VALIGN_KEYWORDS = new Set([
|
|
131
|
+
'top', 'middle', 'bottom', 'baseline', 'sub', 'super', 'text-top', 'text-bottom',
|
|
132
|
+
]);
|
|
133
|
+
const LENGTH_TOKEN = /^-?(?:\d+\.?\d*|\.\d+)(?:px|%|em|rem|pt)$/;
|
|
134
|
+
function isAscii(value) {
|
|
135
|
+
// eslint-disable-next-line no-control-regex
|
|
136
|
+
return !/[^\x00-\x7f]/.test(value);
|
|
137
|
+
}
|
|
138
|
+
/** Forbidden function-ish / escape tokens shared by the length & border grammars. */
|
|
139
|
+
function hasUnsafeToken(value) {
|
|
140
|
+
return /[\\/(]|url|var|calc|expression/i.test(value);
|
|
141
|
+
}
|
|
142
|
+
function isLengthToken(token) {
|
|
143
|
+
return token === '0' || token === 'auto' || LENGTH_TOKEN.test(token);
|
|
144
|
+
}
|
|
145
|
+
/** 1–4 whitespace-separated length tokens (padding/margin/width/radius). */
|
|
146
|
+
function isLengthList(value) {
|
|
147
|
+
if (!isAscii(value) || hasUnsafeToken(value))
|
|
148
|
+
return false;
|
|
149
|
+
const tokens = value.trim().split(/\s+/);
|
|
150
|
+
return tokens.length >= 1 && tokens.length <= 4 && tokens.every(isLengthToken);
|
|
151
|
+
}
|
|
152
|
+
function isColor(value) {
|
|
153
|
+
if (!isAscii(value))
|
|
154
|
+
return false;
|
|
155
|
+
const v = value.trim();
|
|
156
|
+
if (/^[a-z]+$/i.test(v))
|
|
157
|
+
return true;
|
|
158
|
+
if (/^#[0-9a-f]{3,8}$/i.test(v))
|
|
159
|
+
return true;
|
|
160
|
+
// rgb()/rgba() with numeric (optionally %) arguments only.
|
|
161
|
+
if (/^rgba?\(\s*[\d.]+%?(?:\s*,\s*[\d.]+%?){2,3}\s*\)$/i.test(v))
|
|
162
|
+
return true;
|
|
163
|
+
// hsl()/hsla() with numeric (optionally %) arguments only — numeric-only, so a
|
|
164
|
+
// smuggled `hsl(var(--x))` still fails (it has letters inside the parens).
|
|
165
|
+
return /^hsla?\(\s*[\d.]+%?(?:\s*,\s*[\d.]+%?){2,3}\s*\)$/i.test(v);
|
|
166
|
+
}
|
|
167
|
+
/** Line-height for tables: a unitless number or a single length token. */
|
|
168
|
+
function isLineHeight(value) {
|
|
169
|
+
if (!isAscii(value) || hasUnsafeToken(value))
|
|
170
|
+
return false;
|
|
171
|
+
const v = value.trim();
|
|
172
|
+
if (/^\d+(?:\.\d+)?$/.test(v))
|
|
173
|
+
return true;
|
|
174
|
+
return isLengthToken(v);
|
|
175
|
+
}
|
|
176
|
+
/** Cell `text-align` keyword set (also the legacy `align` conversion source). */
|
|
177
|
+
const TEXT_ALIGN_KEYWORDS = new Set(['left', 'center', 'right', 'justify', 'start', 'end']);
|
|
178
|
+
const LEGACY_ALIGN_KEYWORDS = new Set(['left', 'center', 'right', 'justify']);
|
|
179
|
+
function isBorder(value) {
|
|
180
|
+
if (!isAscii(value))
|
|
181
|
+
return false;
|
|
182
|
+
if (/[\\/]|url|var|calc|expression/i.test(value))
|
|
183
|
+
return false;
|
|
184
|
+
const tokens = value.trim().match(/rgba?\([^)]*\)|\S+/gi) ?? [];
|
|
185
|
+
if (!tokens.length)
|
|
186
|
+
return false;
|
|
187
|
+
let hasStyle = false;
|
|
188
|
+
for (const token of tokens) {
|
|
189
|
+
if (BORDER_STYLES.has(token.toLowerCase())) {
|
|
190
|
+
hasStyle = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (isLengthToken(token) || isColor(token))
|
|
194
|
+
continue;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return hasStyle;
|
|
198
|
+
}
|
|
199
|
+
function isValign(value) {
|
|
200
|
+
const v = value.trim().toLowerCase();
|
|
201
|
+
return VALIGN_KEYWORDS.has(v) || isLengthToken(v);
|
|
202
|
+
}
|
|
203
|
+
function isLayoutValid(name, value) {
|
|
204
|
+
if (name === 'vertical-align')
|
|
205
|
+
return isValign(value);
|
|
206
|
+
// `border-radius` is a length list (a corner rounding), not the width/style/
|
|
207
|
+
// colour grammar the `border*` shorthands use — validate it as such.
|
|
208
|
+
if (name === 'border-radius')
|
|
209
|
+
return isLengthList(value);
|
|
210
|
+
if (name.startsWith('border'))
|
|
211
|
+
return isBorder(value);
|
|
212
|
+
return isLengthList(value);
|
|
213
|
+
}
|
|
214
|
+
/** Validates a cell `color`/`font-*` declaration kept as part of `textStyle`. */
|
|
215
|
+
function isCellTextValid(name, value) {
|
|
216
|
+
if (!isAscii(value) || /url|expression|var|calc/i.test(value))
|
|
217
|
+
return false;
|
|
218
|
+
if (name === 'color')
|
|
219
|
+
return isColor(value);
|
|
220
|
+
if (name === 'font-size')
|
|
221
|
+
return isLengthToken(value);
|
|
222
|
+
if (name === 'font-weight') {
|
|
223
|
+
return /^(normal|bold|bolder|lighter|[1-9]00)$/i.test(value.trim());
|
|
224
|
+
}
|
|
225
|
+
// font-family: names, commas and quotes only — no function-ish characters.
|
|
226
|
+
return !/[(){}<>;\\]/.test(value);
|
|
227
|
+
}
|
|
228
|
+
/** A container's kept style under fidelity: validated layout, carried background/font, legacy attrs. */
|
|
229
|
+
function computeContainerStyle(el, tag) {
|
|
230
|
+
const isCell = tag === 'td' || tag === 'th';
|
|
231
|
+
const isTable = tag === 'table';
|
|
232
|
+
const isBlock = /^(?:p|h[1-6])$/.test(tag);
|
|
233
|
+
const extra = SIZED_TAGS[tag];
|
|
234
|
+
const kept = [];
|
|
235
|
+
for (const decl of (el.getAttribute('style') ?? '').split(';')) {
|
|
236
|
+
const idx = decl.indexOf(':');
|
|
237
|
+
if (idx === -1)
|
|
238
|
+
continue;
|
|
239
|
+
const name = decl.slice(0, idx).trim().toLowerCase();
|
|
240
|
+
const value = decl.slice(idx + 1).trim();
|
|
241
|
+
if (!value || /url\s*\(|expression\s*\(/i.test(decl))
|
|
242
|
+
continue;
|
|
243
|
+
if (name === 'background-color') {
|
|
244
|
+
if ((isCell || isTable) && isColor(value))
|
|
245
|
+
kept.push(`${name}: ${value}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (name === 'color' || name === 'font-family' || name === 'font-size' || name === 'font-weight') {
|
|
249
|
+
// Blocks and tables keep their own text style; cells inherit the table's.
|
|
250
|
+
if ((isCell || isBlock || isTable) && isCellTextValid(name, value)) {
|
|
251
|
+
kept.push(`${name}: ${value}`);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (name === 'text-align') {
|
|
256
|
+
// Cells keep a validated text-align (lifted to data-cell-align); blocks let
|
|
257
|
+
// it fall through to the align attr via the ALLOWED_STYLES branch below.
|
|
258
|
+
if (isCell && TEXT_ALIGN_KEYWORDS.has(value.toLowerCase())) {
|
|
259
|
+
kept.push(`text-align: ${value.toLowerCase()}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (name === 'line-height' && isTable) {
|
|
264
|
+
if (isLineHeight(value))
|
|
265
|
+
kept.push(`line-height: ${value}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (name === 'margin' && isTable) {
|
|
269
|
+
// `margin: 0 auto` centring: keep only the horizontal parts as longhands so
|
|
270
|
+
// the table parseDOM's `margin-left/right: auto` check recognises it.
|
|
271
|
+
if (isLengthList(value)) {
|
|
272
|
+
const tokens = value.trim().split(/\s+/);
|
|
273
|
+
const [, right] = tokens.length === 1
|
|
274
|
+
? [tokens[0], tokens[0]]
|
|
275
|
+
: [tokens[0], tokens[1]];
|
|
276
|
+
const left = tokens.length >= 4 ? tokens[3] : right;
|
|
277
|
+
kept.push(`margin-left: ${left}`, `margin-right: ${right}`);
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (CONTAINER_DROP.has(name))
|
|
282
|
+
continue;
|
|
283
|
+
// Border longhand groups (`border-width/-style/-color`, per side or not) are
|
|
284
|
+
// how the browser re-serializes four equal sides; recombined per side below.
|
|
285
|
+
if (isCell && CELL_BORDER_LONGHAND.test(name))
|
|
286
|
+
continue;
|
|
287
|
+
if (isCell && CELL_LAYOUT.has(name) && isLayoutValid(name, value)) {
|
|
288
|
+
kept.push(`${name}: ${value}`);
|
|
289
|
+
}
|
|
290
|
+
else if (isTable && TABLE_LAYOUT.has(name) && isLayoutValid(name, value)) {
|
|
291
|
+
kept.push(`${name}: ${value}`);
|
|
292
|
+
}
|
|
293
|
+
else if (isBlock && BLOCK_LAYOUT.has(name) && isLengthList(value)) {
|
|
294
|
+
kept.push(`${name}: ${value}`);
|
|
295
|
+
}
|
|
296
|
+
else if (ALLOWED_STYLES.has(name)) {
|
|
297
|
+
kept.push(`${name}: ${value}`);
|
|
298
|
+
}
|
|
299
|
+
else if (extra && extra.has(name)) {
|
|
300
|
+
kept.push(`${name}: ${value}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// A cell border written as longhand groups: read each side back through CSSOM,
|
|
304
|
+
// which resolves any mix of shorthands and longhands to `width style color`.
|
|
305
|
+
if (isCell && !kept.some(d => d.startsWith('border:'))) {
|
|
306
|
+
const style = el.style;
|
|
307
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
308
|
+
const prop = `border-${side}`;
|
|
309
|
+
if (kept.some(d => d.startsWith(`${prop}:`)))
|
|
310
|
+
continue;
|
|
311
|
+
const value = style?.getPropertyValue(prop);
|
|
312
|
+
if (value && isBorder(value))
|
|
313
|
+
kept.push(`${prop}: ${value}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Legacy presentational attributes → validated styles (raw attr removed by the
|
|
317
|
+
// standard attribute pass, which doesn't list them).
|
|
318
|
+
if (isCell || isTable) {
|
|
319
|
+
const bgcolor = el.getAttribute('bgcolor');
|
|
320
|
+
if (bgcolor && isColor(bgcolor) && !kept.some(d => d.startsWith('background-color'))) {
|
|
321
|
+
kept.push(`background-color: ${bgcolor}`);
|
|
322
|
+
}
|
|
323
|
+
const widthAttr = el.getAttribute('width');
|
|
324
|
+
if (widthAttr && !kept.some(d => d.startsWith('width'))) {
|
|
325
|
+
const width = /^\d+$/.test(widthAttr)
|
|
326
|
+
? `${widthAttr}px`
|
|
327
|
+
: /^\d+%$/.test(widthAttr)
|
|
328
|
+
? widthAttr
|
|
329
|
+
: null;
|
|
330
|
+
if (width)
|
|
331
|
+
kept.push(`width: ${width}`);
|
|
332
|
+
}
|
|
333
|
+
const valign = el.getAttribute('valign');
|
|
334
|
+
if (isCell && valign && isValign(valign) && !kept.some(d => d.startsWith('vertical-align'))) {
|
|
335
|
+
kept.push(`vertical-align: ${valign}`);
|
|
336
|
+
}
|
|
337
|
+
// Legacy cell align → text-align; the attr stays for table centring.
|
|
338
|
+
const alignAttr = el.getAttribute('align');
|
|
339
|
+
if (isCell &&
|
|
340
|
+
alignAttr &&
|
|
341
|
+
LEGACY_ALIGN_KEYWORDS.has(alignAttr.toLowerCase()) &&
|
|
342
|
+
!kept.some(d => d.startsWith('text-align'))) {
|
|
343
|
+
kept.push(`text-align: ${alignAttr.toLowerCase()}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return kept.join('; ');
|
|
347
|
+
}
|
|
348
|
+
/** Cell text declarations lifted onto `data-cell-text-style` before PM parse. */
|
|
349
|
+
const CELL_TEXT_PROPS = ['color', 'font-family', 'font-size', 'font-weight'];
|
|
350
|
+
/** Table text declarations lifted onto `data-table-text-style` before PM parse. */
|
|
351
|
+
const TABLE_TEXT_PROPS = ['color', 'font-family', 'font-size', 'font-weight', 'line-height'];
|
|
352
|
+
/** Moves carried background/font declarations onto data-attrs so they never become marks. */
|
|
353
|
+
function relocateElement(el, tag) {
|
|
354
|
+
if (tag === 'td' || tag === 'th') {
|
|
355
|
+
const bg = el.style.backgroundColor;
|
|
356
|
+
if (bg) {
|
|
357
|
+
el.setAttribute('data-cell-bg', bg);
|
|
358
|
+
el.style.removeProperty('background-color');
|
|
359
|
+
}
|
|
360
|
+
const parts = [];
|
|
361
|
+
for (const prop of CELL_TEXT_PROPS) {
|
|
362
|
+
const value = el.style.getPropertyValue(prop);
|
|
363
|
+
if (value) {
|
|
364
|
+
parts.push(`${prop}: ${value}`);
|
|
365
|
+
el.style.removeProperty(prop);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (parts.length)
|
|
369
|
+
el.setAttribute('data-cell-text-style', parts.join('; '));
|
|
370
|
+
// Cell text-align → data-cell-align (so it stays a node attr, never a mark).
|
|
371
|
+
const align = el.style.textAlign;
|
|
372
|
+
if (align) {
|
|
373
|
+
el.setAttribute('data-cell-align', align);
|
|
374
|
+
el.style.removeProperty('text-align');
|
|
375
|
+
}
|
|
376
|
+
if (!el.getAttribute('style'))
|
|
377
|
+
el.removeAttribute('style');
|
|
378
|
+
}
|
|
379
|
+
else if (tag === 'table') {
|
|
380
|
+
const bg = el.style.backgroundColor;
|
|
381
|
+
if (bg) {
|
|
382
|
+
el.setAttribute('data-table-bg', bg);
|
|
383
|
+
el.style.removeProperty('background-color');
|
|
384
|
+
}
|
|
385
|
+
const textParts = [];
|
|
386
|
+
for (const prop of TABLE_TEXT_PROPS) {
|
|
387
|
+
const value = el.style.getPropertyValue(prop);
|
|
388
|
+
if (value) {
|
|
389
|
+
textParts.push(`${prop}: ${value}`);
|
|
390
|
+
el.style.removeProperty(prop);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (textParts.length)
|
|
394
|
+
el.setAttribute('data-table-text-style', textParts.join('; '));
|
|
395
|
+
// Email containers are centred with `align="center"` on the table or on
|
|
396
|
+
// the cell that holds it.
|
|
397
|
+
const holder = el.parentElement?.closest('td, th');
|
|
398
|
+
if (el.getAttribute('align')?.toLowerCase() === 'center' ||
|
|
399
|
+
holder?.getAttribute('align')?.toLowerCase() === 'center') {
|
|
400
|
+
el.setAttribute('data-table-align', 'center');
|
|
401
|
+
}
|
|
402
|
+
if (!el.getAttribute('style'))
|
|
403
|
+
el.removeAttribute('style');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function relocateWalk(node) {
|
|
407
|
+
for (const child of Array.from(node.children)) {
|
|
408
|
+
relocateElement(child, child.tagName.toLowerCase());
|
|
409
|
+
relocateWalk(child);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/** A `<col>` width (style width or `width` attr) normalized to a px/% token. */
|
|
413
|
+
function colWidth(col) {
|
|
414
|
+
const styleWidth = /width\s*:\s*([^;]+)/i.exec(col.getAttribute('style') ?? '')?.[1]?.trim();
|
|
415
|
+
const v = (styleWidth || col.getAttribute('width') || '').trim();
|
|
416
|
+
if (!v)
|
|
417
|
+
return null;
|
|
418
|
+
if (/^\d+$/.test(v))
|
|
419
|
+
return `${v}px`;
|
|
420
|
+
if (LENGTH_TOKEN.test(v) && /(?:px|%)$/.test(v))
|
|
421
|
+
return v;
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
/** Per-column widths of a table's own colgroup, expanding `span`. */
|
|
425
|
+
function tableColWidths(table) {
|
|
426
|
+
const widths = [];
|
|
427
|
+
for (const col of Array.from(table.querySelectorAll(':scope > colgroup > col, :scope > col'))) {
|
|
428
|
+
const span = parseInt(col.getAttribute('span') ?? '1', 10);
|
|
429
|
+
const n = Number.isFinite(span) && span > 0 ? span : 1;
|
|
430
|
+
const w = colWidth(col);
|
|
431
|
+
for (let i = 0; i < n; i += 1)
|
|
432
|
+
widths.push(w);
|
|
433
|
+
}
|
|
434
|
+
return widths;
|
|
435
|
+
}
|
|
436
|
+
/** A table's own rows (not nested tables' rows). */
|
|
437
|
+
function directRows(table) {
|
|
438
|
+
const rows = [];
|
|
439
|
+
for (const child of Array.from(table.children)) {
|
|
440
|
+
const t = child.tagName.toLowerCase();
|
|
441
|
+
if (t === 'tr')
|
|
442
|
+
rows.push(child);
|
|
443
|
+
else if (t === 'thead' || t === 'tbody' || t === 'tfoot') {
|
|
444
|
+
for (const row of Array.from(child.children)) {
|
|
445
|
+
if (row.tagName.toLowerCase() === 'tr')
|
|
446
|
+
rows.push(row);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return rows;
|
|
451
|
+
}
|
|
452
|
+
/** Copies colgroup widths onto unsized cells (rows with a colspan are skipped). */
|
|
453
|
+
function applyColgroupWidths(table) {
|
|
454
|
+
const widths = tableColWidths(table);
|
|
455
|
+
if (!widths.some(Boolean))
|
|
456
|
+
return;
|
|
457
|
+
for (const row of directRows(table)) {
|
|
458
|
+
const cells = Array.from(row.children).filter(c => /^t[dh]$/.test(c.tagName.toLowerCase()));
|
|
459
|
+
if (cells.some(c => parseInt(c.getAttribute('colspan') ?? '1', 10) > 1))
|
|
460
|
+
continue;
|
|
461
|
+
cells.forEach((cell, idx) => {
|
|
462
|
+
const w = widths[idx];
|
|
463
|
+
if (!w)
|
|
464
|
+
return;
|
|
465
|
+
const hasWidth = /(?:^|;)\s*width\s*:/i.test(cell.getAttribute('style') ?? '') || cell.hasAttribute('width');
|
|
466
|
+
if (hasWidth)
|
|
467
|
+
return;
|
|
468
|
+
const style = cell.getAttribute('style') ?? '';
|
|
469
|
+
cell.setAttribute('style', style ? `${style}; width: ${w}` : `width: ${w}`);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Fidelity-only pass run right before parsing; see `relocateElement`. */
|
|
474
|
+
export function relocateEmailContainers(html) {
|
|
475
|
+
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
|
|
476
|
+
relocateWalk(doc.body);
|
|
477
|
+
return doc.body.innerHTML;
|
|
478
|
+
}
|
|
104
479
|
/**
|
|
105
480
|
* Sanitizes the CSS inside a <style> block. Neutralizes the vectors that make
|
|
106
481
|
* a raw stylesheet dangerous while leaving ordinary rules intact:
|
|
@@ -129,8 +504,55 @@ export function sanitizeCss(css) {
|
|
|
129
504
|
})
|
|
130
505
|
.trim();
|
|
131
506
|
}
|
|
132
|
-
|
|
507
|
+
/** Container tags an app may widen with `allowedStyles`. */
|
|
508
|
+
const EXTENSION_TAGS = new Set(['td', 'th', 'table', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
|
|
509
|
+
/** The allowlisted, grammar-valid extension declarations for a container, or ''. */
|
|
510
|
+
function computeExtraStyle(el, allowed) {
|
|
511
|
+
if (!allowed)
|
|
512
|
+
return '';
|
|
513
|
+
const kept = [];
|
|
514
|
+
for (const decl of (el.getAttribute('style') ?? '').split(';')) {
|
|
515
|
+
const idx = decl.indexOf(':');
|
|
516
|
+
if (idx === -1)
|
|
517
|
+
continue;
|
|
518
|
+
const name = decl.slice(0, idx).trim().toLowerCase();
|
|
519
|
+
const value = decl.slice(idx + 1).trim();
|
|
520
|
+
if (allowed.has(name) && isExtensionValueValid(value))
|
|
521
|
+
kept.push(`${name}: ${value}`);
|
|
522
|
+
}
|
|
523
|
+
return kept.join('; ');
|
|
524
|
+
}
|
|
525
|
+
/** Re-validates a stored cell/table text style on write-back. */
|
|
526
|
+
export function sanitizeTableTextStyleForWrite(s) {
|
|
527
|
+
const kept = [];
|
|
528
|
+
for (const decl of String(s ?? '').split(';')) {
|
|
529
|
+
const idx = decl.indexOf(':');
|
|
530
|
+
if (idx === -1)
|
|
531
|
+
continue;
|
|
532
|
+
const name = decl.slice(0, idx).trim().toLowerCase();
|
|
533
|
+
const value = decl.slice(idx + 1).trim();
|
|
534
|
+
if (!value)
|
|
535
|
+
continue;
|
|
536
|
+
const ok = name === 'line-height'
|
|
537
|
+
? isLineHeight(value)
|
|
538
|
+
: TABLE_TEXT_PROPS.includes(name) && isCellTextValid(name, value);
|
|
539
|
+
if (ok)
|
|
540
|
+
kept.push(`${name}: ${value}`);
|
|
541
|
+
}
|
|
542
|
+
return kept.join('; ');
|
|
543
|
+
}
|
|
544
|
+
function sanitizeElement(el, emailFidelity = false, allowedStyles) {
|
|
133
545
|
const tag = el.tagName.toLowerCase();
|
|
546
|
+
// With fidelity, a table pushes its colgroup column widths down onto its own
|
|
547
|
+
// cells before they are sanitized (the <col> widths are stripped otherwise).
|
|
548
|
+
if (emailFidelity && tag === 'table')
|
|
549
|
+
applyColgroupWidths(el);
|
|
550
|
+
// Fidelity recomputes the container style, written back after the attribute pass.
|
|
551
|
+
const email = emailFidelity && CONTAINER_TAGS.has(tag) ? computeContainerStyle(el, tag) : null;
|
|
552
|
+
// Extension declarations come from the original style and move to data-extra-style.
|
|
553
|
+
const extra = emailFidelity && EXTENSION_TAGS.has(tag)
|
|
554
|
+
? computeExtraStyle(el, allowedStyles?.get(tag))
|
|
555
|
+
: '';
|
|
134
556
|
const allowed = TAG_ATTRS[tag];
|
|
135
557
|
for (const attr of Array.from(el.attributes)) {
|
|
136
558
|
const name = attr.name.toLowerCase();
|
|
@@ -170,14 +592,31 @@ function sanitizeElement(el) {
|
|
|
170
592
|
el.removeAttribute('target');
|
|
171
593
|
}
|
|
172
594
|
}
|
|
595
|
+
if (email !== null) {
|
|
596
|
+
if (email)
|
|
597
|
+
el.setAttribute('style', email);
|
|
598
|
+
else
|
|
599
|
+
el.removeAttribute('style');
|
|
600
|
+
}
|
|
601
|
+
if (extra)
|
|
602
|
+
el.setAttribute('data-extra-style', extra);
|
|
173
603
|
}
|
|
174
|
-
function walk(node) {
|
|
604
|
+
function walk(node, emailFidelity = false, allowedStyles) {
|
|
175
605
|
for (const child of Array.from(node.children)) {
|
|
176
606
|
const tag = child.tagName.toLowerCase();
|
|
177
607
|
if (DROP_WITH_CONTENT.has(tag)) {
|
|
178
608
|
child.remove();
|
|
179
609
|
continue;
|
|
180
610
|
}
|
|
611
|
+
// Email preheaders: elements hidden with display:none / mso-hide are dropped
|
|
612
|
+
// together with their content so they don't surface as 1px text.
|
|
613
|
+
if (emailFidelity) {
|
|
614
|
+
const style = child.getAttribute('style') ?? '';
|
|
615
|
+
if (/display\s*:\s*none/i.test(style) || /mso-hide\s*:\s*all/i.test(style)) {
|
|
616
|
+
child.remove();
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
181
620
|
// iframes are allowed for any https src, but always force-sandboxed so an
|
|
182
621
|
// untrusted embed can never navigate the top window or run unrestricted.
|
|
183
622
|
if (tag === 'iframe') {
|
|
@@ -200,24 +639,308 @@ function walk(node) {
|
|
|
200
639
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
201
640
|
// Unknown wrapper (article, section, table for now, …): keep its
|
|
202
641
|
// content, drop the element itself.
|
|
203
|
-
walk(child);
|
|
642
|
+
walk(child, emailFidelity, allowedStyles);
|
|
204
643
|
child.replaceWith(...Array.from(child.childNodes));
|
|
205
644
|
continue;
|
|
206
645
|
}
|
|
207
|
-
sanitizeElement(child);
|
|
208
|
-
walk(child);
|
|
646
|
+
sanitizeElement(child, emailFidelity, allowedStyles);
|
|
647
|
+
walk(child, emailFidelity, allowedStyles);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/* --- email-fidelity: button links → one-cell table buttons (flag-gated) --- */
|
|
651
|
+
/** Box declarations an email "button" link carries; moved onto the new cell. */
|
|
652
|
+
const BUTTON_CELL_PROPS = new Set([
|
|
653
|
+
'background-color', 'padding', 'padding-top', 'padding-right',
|
|
654
|
+
'padding-bottom', 'padding-left', 'border-radius', 'width', 'border',
|
|
655
|
+
'border-top', 'border-right', 'border-bottom', 'border-left', 'text-align',
|
|
656
|
+
]);
|
|
657
|
+
/** Text-level declarations kept on the link (they become marks on its text). */
|
|
658
|
+
const BUTTON_LINK_PROPS = new Set([
|
|
659
|
+
'color', 'font-size', 'font-weight', 'font-family', 'font-style',
|
|
660
|
+
'text-decoration', 'text-decoration-line', 'line-height',
|
|
661
|
+
]);
|
|
662
|
+
/** Block parents whose sole-child button link may be lifted into a table. */
|
|
663
|
+
const BUTTON_PARENT_TAGS = new Set(['td', 'th', 'div', 'p']);
|
|
664
|
+
/** Splits an inline `style` into `{name, value}` pairs (names lowercased). */
|
|
665
|
+
function parseDecls(style) {
|
|
666
|
+
const out = [];
|
|
667
|
+
for (const decl of style.split(';')) {
|
|
668
|
+
const idx = decl.indexOf(':');
|
|
669
|
+
if (idx === -1)
|
|
670
|
+
continue;
|
|
671
|
+
const name = decl.slice(0, idx).trim().toLowerCase();
|
|
672
|
+
const value = decl.slice(idx + 1).trim();
|
|
673
|
+
if (name && value)
|
|
674
|
+
out.push({ name, value });
|
|
675
|
+
}
|
|
676
|
+
return out;
|
|
677
|
+
}
|
|
678
|
+
/** A button link: `display:inline-block|block` plus a background or padding. */
|
|
679
|
+
function isButtonLink(decls) {
|
|
680
|
+
const display = decls.find(d => d.name === 'display')?.value.toLowerCase();
|
|
681
|
+
if (display !== 'inline-block' && display !== 'block')
|
|
682
|
+
return false;
|
|
683
|
+
return decls.some(d => d.name === 'background-color' || d.name.startsWith('padding'));
|
|
684
|
+
}
|
|
685
|
+
/** True when the link is horizontally auto-centred (`margin: 0 auto`, etc.). */
|
|
686
|
+
function hasAutoHMargin(decls) {
|
|
687
|
+
const ml = decls.find(d => d.name === 'margin-left')?.value.toLowerCase();
|
|
688
|
+
const mr = decls.find(d => d.name === 'margin-right')?.value.toLowerCase();
|
|
689
|
+
if (ml === 'auto' || mr === 'auto')
|
|
690
|
+
return true;
|
|
691
|
+
const margin = decls.find(d => d.name === 'margin')?.value;
|
|
692
|
+
if (!margin)
|
|
693
|
+
return false;
|
|
694
|
+
const tokens = margin.trim().split(/\s+/);
|
|
695
|
+
const right = tokens.length >= 2 ? tokens[1] : tokens[0];
|
|
696
|
+
const left = tokens.length >= 4 ? tokens[3] : right;
|
|
697
|
+
return left.toLowerCase() === 'auto' || right.toLowerCase() === 'auto';
|
|
698
|
+
}
|
|
699
|
+
/** True when a block centres its content via text-align / legacy align. */
|
|
700
|
+
function blockCentered(el) {
|
|
701
|
+
if (/text-align\s*:\s*center/i.test(el.getAttribute('style') ?? ''))
|
|
702
|
+
return true;
|
|
703
|
+
return el.getAttribute('align')?.toLowerCase() === 'center';
|
|
704
|
+
}
|
|
705
|
+
/** The link is the only non-whitespace content of its parent block. */
|
|
706
|
+
function isSoleContent(parent, child) {
|
|
707
|
+
for (const node of Array.from(parent.childNodes)) {
|
|
708
|
+
if (node === child)
|
|
709
|
+
continue;
|
|
710
|
+
if (node.nodeType === 3) {
|
|
711
|
+
if ((node.textContent ?? '').trim() !== '')
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
/** Rebuilds each email button link as a one-cell layout table. */
|
|
721
|
+
function convertButtonLinks(body) {
|
|
722
|
+
const doc = body.ownerDocument;
|
|
723
|
+
for (const a of Array.from(body.querySelectorAll('a[href]'))) {
|
|
724
|
+
const decls = parseDecls(a.getAttribute('style') ?? '');
|
|
725
|
+
if (!isButtonLink(decls))
|
|
726
|
+
continue;
|
|
727
|
+
const parent = a.parentElement;
|
|
728
|
+
if (!parent)
|
|
729
|
+
continue;
|
|
730
|
+
if (!BUTTON_PARENT_TAGS.has(parent.tagName.toLowerCase()))
|
|
731
|
+
continue;
|
|
732
|
+
if (!isSoleContent(parent, a))
|
|
733
|
+
continue;
|
|
734
|
+
// Box declarations go to the cell; text declarations stay on the link;
|
|
735
|
+
// margins/display are dropped (margins only inform centring below).
|
|
736
|
+
const cell = decls.filter(d => BUTTON_CELL_PROPS.has(d.name));
|
|
737
|
+
if (!cell.some(d => d.name === 'text-align')) {
|
|
738
|
+
cell.push({ name: 'text-align', value: 'center' });
|
|
739
|
+
}
|
|
740
|
+
const linkStyle = decls
|
|
741
|
+
.filter(d => BUTTON_LINK_PROPS.has(d.name))
|
|
742
|
+
.map(d => `${d.name}: ${d.value}`)
|
|
743
|
+
.join('; ');
|
|
744
|
+
const centred = hasAutoHMargin(decls) || blockCentered(parent);
|
|
745
|
+
const table = doc.createElement('table');
|
|
746
|
+
table.setAttribute('role', 'presentation');
|
|
747
|
+
if (centred)
|
|
748
|
+
table.setAttribute('align', 'center');
|
|
749
|
+
const tbody = doc.createElement('tbody');
|
|
750
|
+
const tr = doc.createElement('tr');
|
|
751
|
+
const td = doc.createElement('td');
|
|
752
|
+
td.setAttribute('style', cell.map(d => `${d.name}: ${d.value}`).join('; '));
|
|
753
|
+
if (linkStyle)
|
|
754
|
+
a.setAttribute('style', linkStyle);
|
|
755
|
+
else
|
|
756
|
+
a.removeAttribute('style');
|
|
757
|
+
td.appendChild(a); // detaches the link from its original parent
|
|
758
|
+
tr.appendChild(td);
|
|
759
|
+
tbody.appendChild(tr);
|
|
760
|
+
table.appendChild(tbody);
|
|
761
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
762
|
+
if (parentTag === 'p' || parentTag === 'div')
|
|
763
|
+
parent.replaceWith(table);
|
|
764
|
+
else
|
|
765
|
+
parent.appendChild(table);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/* --- email-fidelity: push centring onto blocks that can carry it (flag-gated) --- */
|
|
769
|
+
/** Inline tags: loose runs of these (and text) inside a div/center are wrapped. */
|
|
770
|
+
const INLINE_TAGS = new Set([
|
|
771
|
+
'a', 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'span', 'code',
|
|
772
|
+
'sub', 'sup', 'font', 'br', 'img', 'small', 'big', 'mark', 'abbr', 'q', 'cite',
|
|
773
|
+
'label', 'bdi', 'bdo', 'wbr', 'time', 'kbd', 'samp', 'var',
|
|
774
|
+
]);
|
|
775
|
+
/** A node that may be lifted into a wrapping paragraph (inline element or text). */
|
|
776
|
+
function isLooseInline(node) {
|
|
777
|
+
if (node.nodeType === 3)
|
|
778
|
+
return true;
|
|
779
|
+
if (node.nodeType !== 1)
|
|
780
|
+
return false;
|
|
781
|
+
return INLINE_TAGS.has(node.tagName.toLowerCase());
|
|
782
|
+
}
|
|
783
|
+
/** True when `el` already declares a valid `text-align` or legacy `align`. */
|
|
784
|
+
function hasOwnTextAlign(el) {
|
|
785
|
+
const styleAlign = /text-align\s*:\s*([a-z-]+)/i.exec(el.getAttribute('style') ?? '')?.[1];
|
|
786
|
+
if (styleAlign && TEXT_ALIGN_KEYWORDS.has(styleAlign.toLowerCase()))
|
|
787
|
+
return true;
|
|
788
|
+
const attr = el.getAttribute('align');
|
|
789
|
+
return !!(attr && LEGACY_ALIGN_KEYWORDS.has(attr.toLowerCase()));
|
|
790
|
+
}
|
|
791
|
+
/** Appends `text-align: value` to an element's inline style. */
|
|
792
|
+
function addTextAlign(el, value) {
|
|
793
|
+
const style = (el.getAttribute('style') ?? '').trim();
|
|
794
|
+
el.setAttribute('style', style ? `${style}; text-align: ${value}` : `text-align: ${value}`);
|
|
795
|
+
}
|
|
796
|
+
/** Direct cells of a table (its own rows', not a nested table's). */
|
|
797
|
+
function directCells(table) {
|
|
798
|
+
const cells = [];
|
|
799
|
+
for (const row of directRows(table)) {
|
|
800
|
+
for (const child of Array.from(row.children)) {
|
|
801
|
+
if (/^t[dh]$/.test(child.tagName.toLowerCase()))
|
|
802
|
+
cells.push(child);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return cells;
|
|
806
|
+
}
|
|
807
|
+
/** True when `el` sits inside a table that is itself a descendant of `wrapper`. */
|
|
808
|
+
function insideNestedTable(el, wrapper) {
|
|
809
|
+
let p = el.parentElement;
|
|
810
|
+
while (p && p !== wrapper) {
|
|
811
|
+
if (p.tagName.toLowerCase() === 'table')
|
|
812
|
+
return true;
|
|
813
|
+
p = p.parentElement;
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
/** The block descendants (p/h1-6/li) of `wrapper` not inside a nested table. */
|
|
818
|
+
function alignableBlocks(wrapper) {
|
|
819
|
+
return Array.from(wrapper.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li')).filter(el => !insideNestedTable(el, wrapper));
|
|
820
|
+
}
|
|
821
|
+
/** Wraps each loose inline run directly inside `wrapper` in an aligned `<p>`. */
|
|
822
|
+
function wrapLooseInline(wrapper, value) {
|
|
823
|
+
const doc = wrapper.ownerDocument;
|
|
824
|
+
let group = [];
|
|
825
|
+
const flush = () => {
|
|
826
|
+
// Trim trailing whitespace-only text nodes so they stay outside the paragraph.
|
|
827
|
+
while (group.length && group[group.length - 1].nodeType === 3
|
|
828
|
+
&& (group[group.length - 1].textContent ?? '').trim() === '') {
|
|
829
|
+
group.pop();
|
|
830
|
+
}
|
|
831
|
+
if (!group.length)
|
|
832
|
+
return;
|
|
833
|
+
const p = doc.createElement('p');
|
|
834
|
+
p.setAttribute('style', `text-align: ${value}`);
|
|
835
|
+
group[0].before(p);
|
|
836
|
+
for (const node of group)
|
|
837
|
+
p.appendChild(node);
|
|
838
|
+
group = [];
|
|
839
|
+
};
|
|
840
|
+
for (const node of Array.from(wrapper.childNodes)) {
|
|
841
|
+
if (isLooseInline(node)) {
|
|
842
|
+
const blank = node.nodeType === 3 && (node.textContent ?? '').trim() === '';
|
|
843
|
+
// Leading whitespace with nothing accumulated is left where it is.
|
|
844
|
+
if (blank && !group.length)
|
|
845
|
+
continue;
|
|
846
|
+
group.push(node);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
flush();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
flush();
|
|
853
|
+
}
|
|
854
|
+
/** Pushes a `<center>`/aligned-`<div>` alignment onto its blocks (and tables, for legacy forms). */
|
|
855
|
+
function propagateBlockAlignment(wrapper, value, centreTables) {
|
|
856
|
+
wrapLooseInline(wrapper, value);
|
|
857
|
+
for (const block of alignableBlocks(wrapper)) {
|
|
858
|
+
if (!hasOwnTextAlign(block))
|
|
859
|
+
addTextAlign(block, value);
|
|
860
|
+
}
|
|
861
|
+
if (centreTables) {
|
|
862
|
+
for (const table of Array.from(wrapper.querySelectorAll('table'))) {
|
|
863
|
+
if (insideNestedTable(table, wrapper))
|
|
864
|
+
continue;
|
|
865
|
+
if (!table.getAttribute('align'))
|
|
866
|
+
table.setAttribute('align', value);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/** Pre-pass: resolve container-level alignment onto blocks/cells (see above). */
|
|
871
|
+
function propagateEmailAlignment(body) {
|
|
872
|
+
// Cases 2–4: <center>, div[align], div[style text-align].
|
|
873
|
+
for (const el of Array.from(body.querySelectorAll('center, div'))) {
|
|
874
|
+
const tag = el.tagName.toLowerCase();
|
|
875
|
+
if (tag === 'center') {
|
|
876
|
+
propagateBlockAlignment(el, 'center', true);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const attr = el.getAttribute('align')?.toLowerCase();
|
|
880
|
+
if (attr && LEGACY_ALIGN_KEYWORDS.has(attr)) {
|
|
881
|
+
propagateBlockAlignment(el, attr, true);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const styleAlign = /text-align\s*:\s*([a-z-]+)/i.exec(el.getAttribute('style') ?? '')?.[1]
|
|
885
|
+
?.toLowerCase();
|
|
886
|
+
if (styleAlign && TEXT_ALIGN_KEYWORDS.has(styleAlign)) {
|
|
887
|
+
propagateBlockAlignment(el, styleAlign, false);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Case 1: a cell's align must not leak into a table nested directly in it.
|
|
891
|
+
for (const cell of Array.from(body.querySelectorAll('td[align], th[align]'))) {
|
|
892
|
+
const attr = cell.getAttribute('align')?.toLowerCase();
|
|
893
|
+
if (!attr || !LEGACY_ALIGN_KEYWORDS.has(attr))
|
|
894
|
+
continue;
|
|
895
|
+
for (const table of Array.from(cell.querySelectorAll('table'))) {
|
|
896
|
+
if (table.closest('td, th') !== cell)
|
|
897
|
+
continue;
|
|
898
|
+
for (const nested of directCells(table)) {
|
|
899
|
+
if (!hasOwnTextAlign(nested))
|
|
900
|
+
addTextAlign(nested, 'left');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// Case 5: CSS text-align on a table → its direct cells that have none.
|
|
905
|
+
for (const table of Array.from(body.querySelectorAll('table'))) {
|
|
906
|
+
const styleAlign = /text-align\s*:\s*([a-z-]+)/i.exec(table.getAttribute('style') ?? '')?.[1]
|
|
907
|
+
?.toLowerCase();
|
|
908
|
+
if (!styleAlign || !TEXT_ALIGN_KEYWORDS.has(styleAlign))
|
|
909
|
+
continue;
|
|
910
|
+
for (const cell of directCells(table)) {
|
|
911
|
+
if (!hasOwnTextAlign(cell))
|
|
912
|
+
addTextAlign(cell, styleAlign);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// Case 6: tr[align] → its own cells that have none.
|
|
916
|
+
for (const tr of Array.from(body.querySelectorAll('tr[align]'))) {
|
|
917
|
+
const attr = tr.getAttribute('align')?.toLowerCase();
|
|
918
|
+
if (!attr || !LEGACY_ALIGN_KEYWORDS.has(attr))
|
|
919
|
+
continue;
|
|
920
|
+
for (const child of Array.from(tr.children)) {
|
|
921
|
+
if (!/^t[dh]$/.test(child.tagName.toLowerCase()))
|
|
922
|
+
continue;
|
|
923
|
+
if (!hasOwnTextAlign(child))
|
|
924
|
+
addTextAlign(child, attr);
|
|
925
|
+
}
|
|
209
926
|
}
|
|
210
927
|
}
|
|
211
928
|
/**
|
|
212
929
|
* Sanitizes an HTML string. Parsing happens in an inert document created by
|
|
213
930
|
* DOMParser, so embedded scripts never execute and images never load.
|
|
214
931
|
*/
|
|
215
|
-
export function sanitizeHtml(html) {
|
|
932
|
+
export function sanitizeHtml(html, options) {
|
|
216
933
|
// Wrap in an explicit <body> so metadata-like tags (notably <style>) stay in
|
|
217
934
|
// the body instead of being hoisted into <head> by the HTML parser — which
|
|
218
935
|
// would drop them before walk() ever sees them. Mirrors htmlToDoc's parse.
|
|
219
936
|
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html');
|
|
220
|
-
walk
|
|
937
|
+
// Button links are restructured into one-cell tables before the walk, so the
|
|
938
|
+
// cell/link styles they produce pass through the normal validators.
|
|
939
|
+
if (options?.emailFidelity) {
|
|
940
|
+
convertButtonLinks(doc.body);
|
|
941
|
+
propagateEmailAlignment(doc.body);
|
|
942
|
+
}
|
|
943
|
+
walk(doc.body, options?.emailFidelity ?? false, options?.allowedStyles);
|
|
221
944
|
return doc.body.innerHTML;
|
|
222
945
|
}
|
|
223
946
|
//# sourceMappingURL=sanitize.js.map
|