@edgex-fe/editor-preview 0.0.1
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 +98 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/sanitizer-runtime.browser.d.ts +6 -0
- package/dist/internal/sanitizer-runtime.browser.js +205 -0
- package/dist/internal/sanitizer-runtime.browser.js.map +1 -0
- package/dist/internal/sanitizer-runtime.node.d.ts +6 -0
- package/dist/internal/sanitizer-runtime.node.js +228 -0
- package/dist/internal/sanitizer-runtime.node.js.map +1 -0
- package/dist/styles/editor-preview.css +778 -0
- package/dist/types-S5GYGk8H.d.ts +14 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @edgex-fe/editor-preview
|
|
2
|
+
|
|
3
|
+
`@edgex-fe/editor-preview` 是基于 EdgeX UI 规范的富文本预览组件,用于渲染富文本编辑器返回的 HTML 内容。组件内置统一的排版样式和 HTML 清理逻辑,可在 React、Vue 或原生 Web 项目中使用。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @edgex-fe/editor-preview
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
在应用入口引入样式:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import "@edgex-fe/editor-preview/styles.css";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
在 React 中可以基于包提供的样式和 HTML 清理函数封装组件:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {
|
|
23
|
+
getEditorClassName,
|
|
24
|
+
sanitizeEditorHtml,
|
|
25
|
+
type EditorHtml,
|
|
26
|
+
} from "@edgex-fe/editor-preview";
|
|
27
|
+
import "@edgex-fe/editor-preview/styles.css";
|
|
28
|
+
|
|
29
|
+
interface EditorPreviewProps {
|
|
30
|
+
html: EditorHtml;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function EditorPreview({ html, className }: EditorPreviewProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={getEditorClassName(className)}
|
|
38
|
+
dangerouslySetInnerHTML={{ __html: sanitizeEditorHtml(html) }}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
使用组件:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<EditorPreview html={editorHtml} />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Vue
|
|
51
|
+
|
|
52
|
+
Vue 项目可以直接使用包内置的 Custom Element。在应用入口注册一次:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { defineEditorElement } from "@edgex-fe/editor-preview";
|
|
56
|
+
import "@edgex-fe/editor-preview/styles.css";
|
|
57
|
+
|
|
58
|
+
defineEditorElement();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
在组件中传入编辑器返回的 HTML:
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
defineProps<{
|
|
66
|
+
html: string;
|
|
67
|
+
}>();
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<edgex-editor-preview :html="html" />
|
|
72
|
+
</template>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
如果项目使用 Vite,需要让 Vue 编译器将该标签识别为 Custom Element:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import vue from "@vitejs/plugin-vue";
|
|
79
|
+
import { defineConfig } from "vite";
|
|
80
|
+
|
|
81
|
+
export default defineConfig({
|
|
82
|
+
plugins: [
|
|
83
|
+
vue({
|
|
84
|
+
template: {
|
|
85
|
+
compilerOptions: {
|
|
86
|
+
isCustomElement: (tag) => tag === "edgex-editor-preview",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## HTML 安全
|
|
95
|
+
|
|
96
|
+
组件默认会清理 HTML 中的脚本、事件属性和危险链接。React 示例显式调用了 `sanitizeEditorHtml`,Vue 的 Custom Element 会在渲染时自动执行相同的清理逻辑。
|
|
97
|
+
|
|
98
|
+
只有在 HTML 来源完全可信时,才应通过 `renderEditor` 的 `sanitize: false` 选项跳过清理。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { E as EditorElement, a as EditorClassName, b as EditorHtml, c as EditorRenderOptions } from './types-S5GYGk8H.js';
|
|
2
|
+
export { d as EditorSanitizer } from './types-S5GYGk8H.js';
|
|
3
|
+
export { releaseEditorSanitizer, sanitizeEditorHtml } from '@edgex-fe/editor-preview/internal/sanitizer-runtime';
|
|
4
|
+
|
|
5
|
+
declare const EDITOR_CLASS_NAME = "edgex-editor-preview";
|
|
6
|
+
declare const EDITOR_LEGACY_CLASS_NAME = "rich-content";
|
|
7
|
+
declare const EDITOR_ELEMENT_NAME = "edgex-editor-preview";
|
|
8
|
+
|
|
9
|
+
type EditorElementConstructor = CustomElementConstructor & {
|
|
10
|
+
prototype: EditorElement;
|
|
11
|
+
};
|
|
12
|
+
declare function defineEditorElement(): EditorElementConstructor | undefined;
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap {
|
|
15
|
+
"edgex-editor-preview": EditorElement;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare function getEditorClassName(...classNames: EditorClassName[]): string;
|
|
20
|
+
declare function renderEditor(target: HTMLElement, html: EditorHtml, options?: EditorRenderOptions): HTMLElement;
|
|
21
|
+
|
|
22
|
+
export { EDITOR_CLASS_NAME, EDITOR_ELEMENT_NAME, EDITOR_LEGACY_CLASS_NAME, EditorClassName, EditorElement, EditorHtml, EditorRenderOptions, defineEditorElement, getEditorClassName, renderEditor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var EDITOR_CLASS_NAME = "edgex-editor-preview";
|
|
7
|
+
var EDITOR_LEGACY_CLASS_NAME = "rich-content";
|
|
8
|
+
var EDITOR_ELEMENT_NAME = "edgex-editor-preview";
|
|
9
|
+
|
|
10
|
+
// src/sanitizer.ts
|
|
11
|
+
import {
|
|
12
|
+
releaseEditorSanitizer,
|
|
13
|
+
sanitizeEditorHtml
|
|
14
|
+
} from "@edgex-fe/editor-preview/internal/sanitizer-runtime";
|
|
15
|
+
|
|
16
|
+
// src/renderer.ts
|
|
17
|
+
var toClassNames = (className) => typeof className === "string" ? className.split(/\s+/).filter(Boolean) : [];
|
|
18
|
+
var resolveEditorHtml = (html, sanitizer) => {
|
|
19
|
+
if (sanitizer === false) return html;
|
|
20
|
+
if (sanitizer) return sanitizer(html);
|
|
21
|
+
return sanitizeEditorHtml(html);
|
|
22
|
+
};
|
|
23
|
+
function getEditorClassName(...classNames) {
|
|
24
|
+
const resolvedClassNames = [EDITOR_CLASS_NAME];
|
|
25
|
+
for (const className of classNames) {
|
|
26
|
+
resolvedClassNames.push(...toClassNames(className));
|
|
27
|
+
}
|
|
28
|
+
return resolvedClassNames.join(" ");
|
|
29
|
+
}
|
|
30
|
+
function renderEditor(target, html, options = {}) {
|
|
31
|
+
target.classList.add(EDITOR_CLASS_NAME);
|
|
32
|
+
for (const className of toClassNames(options.className)) {
|
|
33
|
+
target.classList.add(className);
|
|
34
|
+
}
|
|
35
|
+
target.innerHTML = resolveEditorHtml(html, options.sanitize);
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/customElement.ts
|
|
40
|
+
var editorElementConstructor;
|
|
41
|
+
var createEditorElementConstructor = () => {
|
|
42
|
+
if (typeof HTMLElement === "undefined") {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
if (editorElementConstructor) {
|
|
46
|
+
return editorElementConstructor;
|
|
47
|
+
}
|
|
48
|
+
class CustomEditorElement extends HTMLElement {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(...arguments);
|
|
51
|
+
__publicField(this, "editorHtml", "");
|
|
52
|
+
__publicField(this, "hasEditorHtml", false);
|
|
53
|
+
}
|
|
54
|
+
connectedCallback() {
|
|
55
|
+
this.classList.add(EDITOR_CLASS_NAME);
|
|
56
|
+
if (this.hasEditorHtml) {
|
|
57
|
+
this.render();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
61
|
+
if (name !== "html" || oldValue === newValue) return;
|
|
62
|
+
this.html = newValue != null ? newValue : "";
|
|
63
|
+
}
|
|
64
|
+
get html() {
|
|
65
|
+
return this.editorHtml;
|
|
66
|
+
}
|
|
67
|
+
set html(value) {
|
|
68
|
+
this.editorHtml = value;
|
|
69
|
+
this.hasEditorHtml = true;
|
|
70
|
+
if (this.isConnected) {
|
|
71
|
+
this.render();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
render(html = this.editorHtml) {
|
|
75
|
+
this.editorHtml = html;
|
|
76
|
+
this.hasEditorHtml = true;
|
|
77
|
+
renderEditor(this, this.editorHtml);
|
|
78
|
+
}
|
|
79
|
+
clear() {
|
|
80
|
+
this.removeAttribute("html");
|
|
81
|
+
this.editorHtml = "";
|
|
82
|
+
this.hasEditorHtml = false;
|
|
83
|
+
this.innerHTML = "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
__publicField(CustomEditorElement, "observedAttributes", ["html"]);
|
|
87
|
+
editorElementConstructor = CustomEditorElement;
|
|
88
|
+
return editorElementConstructor;
|
|
89
|
+
};
|
|
90
|
+
function defineEditorElement() {
|
|
91
|
+
if (typeof customElements === "undefined") {
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
const existing = customElements.get(EDITOR_ELEMENT_NAME);
|
|
95
|
+
if (existing) {
|
|
96
|
+
return existing;
|
|
97
|
+
}
|
|
98
|
+
const elementConstructor = createEditorElementConstructor();
|
|
99
|
+
if (!elementConstructor) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
customElements.define(EDITOR_ELEMENT_NAME, elementConstructor);
|
|
103
|
+
return elementConstructor;
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
EDITOR_CLASS_NAME,
|
|
107
|
+
EDITOR_ELEMENT_NAME,
|
|
108
|
+
EDITOR_LEGACY_CLASS_NAME,
|
|
109
|
+
defineEditorElement,
|
|
110
|
+
getEditorClassName,
|
|
111
|
+
releaseEditorSanitizer,
|
|
112
|
+
renderEditor,
|
|
113
|
+
sanitizeEditorHtml
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/sanitizer.ts","../src/renderer.ts","../src/customElement.ts"],"sourcesContent":["export const EDITOR_CLASS_NAME = \"edgex-editor-preview\";\nexport const EDITOR_LEGACY_CLASS_NAME = \"rich-content\";\nexport const EDITOR_ELEMENT_NAME = \"edgex-editor-preview\";\n","export {\n releaseEditorSanitizer,\n sanitizeEditorHtml,\n} from \"@edgex-fe/editor-preview/internal/sanitizer-runtime\";\n","import { EDITOR_CLASS_NAME } from \"./constants\";\nimport { sanitizeEditorHtml } from \"./sanitizer\";\nimport type { EditorClassName, EditorHtml, EditorRenderOptions } from \"./types\";\n\nconst toClassNames = (className: EditorClassName) =>\n typeof className === \"string\" ? className.split(/\\s+/).filter(Boolean) : [];\n\nconst resolveEditorHtml = (html: EditorHtml, sanitizer: EditorRenderOptions[\"sanitize\"]) => {\n if (sanitizer === false) return html;\n if (sanitizer) return sanitizer(html);\n\n return sanitizeEditorHtml(html);\n};\n\nexport function getEditorClassName(...classNames: EditorClassName[]) {\n const resolvedClassNames = [EDITOR_CLASS_NAME];\n\n for (const className of classNames) {\n resolvedClassNames.push(...toClassNames(className));\n }\n\n return resolvedClassNames.join(\" \");\n}\n\nexport function renderEditor(\n target: HTMLElement,\n html: EditorHtml,\n options: EditorRenderOptions = {},\n) {\n target.classList.add(EDITOR_CLASS_NAME);\n\n for (const className of toClassNames(options.className)) {\n target.classList.add(className);\n }\n\n target.innerHTML = resolveEditorHtml(html, options.sanitize);\n return target;\n}\n","import { EDITOR_CLASS_NAME, EDITOR_ELEMENT_NAME } from \"./constants\";\nimport { renderEditor } from \"./renderer\";\nimport type { EditorElement, EditorHtml } from \"./types\";\n\ntype EditorElementConstructor = CustomElementConstructor & {\n prototype: EditorElement;\n};\n\nlet editorElementConstructor: EditorElementConstructor | undefined;\n\nconst createEditorElementConstructor = () => {\n if (typeof HTMLElement === \"undefined\") {\n return undefined;\n }\n\n if (editorElementConstructor) {\n return editorElementConstructor;\n }\n\n class CustomEditorElement extends HTMLElement implements EditorElement {\n static observedAttributes = [\"html\"];\n\n private editorHtml: EditorHtml = \"\";\n private hasEditorHtml = false;\n\n connectedCallback() {\n this.classList.add(EDITOR_CLASS_NAME);\n\n if (this.hasEditorHtml) {\n this.render();\n }\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {\n if (name !== \"html\" || oldValue === newValue) return;\n\n this.html = newValue ?? \"\";\n }\n\n get html() {\n return this.editorHtml;\n }\n\n set html(value: EditorHtml) {\n this.editorHtml = value;\n this.hasEditorHtml = true;\n\n if (this.isConnected) {\n this.render();\n }\n }\n\n render(html = this.editorHtml) {\n this.editorHtml = html;\n this.hasEditorHtml = true;\n renderEditor(this, this.editorHtml);\n }\n\n clear() {\n this.removeAttribute(\"html\");\n this.editorHtml = \"\";\n this.hasEditorHtml = false;\n this.innerHTML = \"\";\n }\n }\n\n editorElementConstructor = CustomEditorElement as EditorElementConstructor;\n\n return editorElementConstructor;\n};\n\nexport function defineEditorElement() {\n if (typeof customElements === \"undefined\") {\n return undefined;\n }\n\n const existing = customElements.get(EDITOR_ELEMENT_NAME);\n if (existing) {\n return existing as EditorElementConstructor;\n }\n\n const elementConstructor = createEditorElementConstructor();\n if (!elementConstructor) {\n return undefined;\n }\n\n customElements.define(EDITOR_ELEMENT_NAME, elementConstructor);\n return elementConstructor;\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"edgex-editor-preview\": EditorElement;\n }\n}\n"],"mappings":";;;;;AAAO,IAAM,oBAAoB;AAC1B,IAAM,2BAA2B;AACjC,IAAM,sBAAsB;;;ACFnC;AAAA,EACE;AAAA,EACA;AAAA,OACK;;;ACCP,IAAM,eAAe,CAAC,cACpB,OAAO,cAAc,WAAW,UAAU,MAAM,KAAK,EAAE,OAAO,OAAO,IAAI,CAAC;AAE5E,IAAM,oBAAoB,CAAC,MAAkB,cAA+C;AAC1F,MAAI,cAAc,MAAO,QAAO;AAChC,MAAI,UAAW,QAAO,UAAU,IAAI;AAEpC,SAAO,mBAAmB,IAAI;AAChC;AAEO,SAAS,sBAAsB,YAA+B;AACnE,QAAM,qBAAqB,CAAC,iBAAiB;AAE7C,aAAW,aAAa,YAAY;AAClC,uBAAmB,KAAK,GAAG,aAAa,SAAS,CAAC;AAAA,EACpD;AAEA,SAAO,mBAAmB,KAAK,GAAG;AACpC;AAEO,SAAS,aACd,QACA,MACA,UAA+B,CAAC,GAChC;AACA,SAAO,UAAU,IAAI,iBAAiB;AAEtC,aAAW,aAAa,aAAa,QAAQ,SAAS,GAAG;AACvD,WAAO,UAAU,IAAI,SAAS;AAAA,EAChC;AAEA,SAAO,YAAY,kBAAkB,MAAM,QAAQ,QAAQ;AAC3D,SAAO;AACT;;;AC7BA,IAAI;AAEJ,IAAM,iCAAiC,MAAM;AAC3C,MAAI,OAAO,gBAAgB,aAAa;AACtC,WAAO;AAAA,EACT;AAEA,MAAI,0BAA0B;AAC5B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,4BAA4B,YAAqC;AAAA,IAAvE;AAAA;AAGE,0BAAQ,cAAyB;AACjC,0BAAQ,iBAAgB;AAAA;AAAA,IAExB,oBAAoB;AAClB,WAAK,UAAU,IAAI,iBAAiB;AAEpC,UAAI,KAAK,eAAe;AACtB,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,IAEA,yBAAyB,MAAc,UAAyB,UAAyB;AACvF,UAAI,SAAS,UAAU,aAAa,SAAU;AAE9C,WAAK,OAAO,8BAAY;AAAA,IAC1B;AAAA,IAEA,IAAI,OAAO;AACT,aAAO,KAAK;AAAA,IACd;AAAA,IAEA,IAAI,KAAK,OAAmB;AAC1B,WAAK,aAAa;AAClB,WAAK,gBAAgB;AAErB,UAAI,KAAK,aAAa;AACpB,aAAK,OAAO;AAAA,MACd;AAAA,IACF;AAAA,IAEA,OAAO,OAAO,KAAK,YAAY;AAC7B,WAAK,aAAa;AAClB,WAAK,gBAAgB;AACrB,mBAAa,MAAM,KAAK,UAAU;AAAA,IACpC;AAAA,IAEA,QAAQ;AACN,WAAK,gBAAgB,MAAM;AAC3B,WAAK,aAAa;AAClB,WAAK,gBAAgB;AACrB,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AA5CE,gBADI,qBACG,sBAAqB,CAAC,MAAM;AA8CrC,6BAA2B;AAE3B,SAAO;AACT;AAEO,SAAS,sBAAsB;AACpC,MAAI,OAAO,mBAAmB,aAAa;AACzC,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,eAAe,IAAI,mBAAmB;AACvD,MAAI,UAAU;AACZ,WAAO;AAAA,EACT;AAEA,QAAM,qBAAqB,+BAA+B;AAC1D,MAAI,CAAC,oBAAoB;AACvB,WAAO;AAAA,EACT;AAEA,iBAAe,OAAO,qBAAqB,kBAAkB;AAC7D,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/sanitizerRuntime.browser.ts
|
|
2
|
+
import DOMPurifyFactory from "dompurify";
|
|
3
|
+
|
|
4
|
+
// src/sanitizerConfig.ts
|
|
5
|
+
var EDITOR_ALLOWED_TAGS = [
|
|
6
|
+
"a",
|
|
7
|
+
"abbr",
|
|
8
|
+
"b",
|
|
9
|
+
"blockquote",
|
|
10
|
+
"br",
|
|
11
|
+
"caption",
|
|
12
|
+
"code",
|
|
13
|
+
"col",
|
|
14
|
+
"colgroup",
|
|
15
|
+
"dd",
|
|
16
|
+
"del",
|
|
17
|
+
"details",
|
|
18
|
+
"div",
|
|
19
|
+
"dl",
|
|
20
|
+
"dt",
|
|
21
|
+
"em",
|
|
22
|
+
"figcaption",
|
|
23
|
+
"figure",
|
|
24
|
+
"footer",
|
|
25
|
+
"h1",
|
|
26
|
+
"h2",
|
|
27
|
+
"h3",
|
|
28
|
+
"h4",
|
|
29
|
+
"h5",
|
|
30
|
+
"h6",
|
|
31
|
+
"hr",
|
|
32
|
+
"i",
|
|
33
|
+
"img",
|
|
34
|
+
"kbd",
|
|
35
|
+
"li",
|
|
36
|
+
"mark",
|
|
37
|
+
"ol",
|
|
38
|
+
"p",
|
|
39
|
+
"pre",
|
|
40
|
+
"s",
|
|
41
|
+
"section",
|
|
42
|
+
"small",
|
|
43
|
+
"source",
|
|
44
|
+
"span",
|
|
45
|
+
"strong",
|
|
46
|
+
"sub",
|
|
47
|
+
"summary",
|
|
48
|
+
"sup",
|
|
49
|
+
"table",
|
|
50
|
+
"tbody",
|
|
51
|
+
"td",
|
|
52
|
+
"tfoot",
|
|
53
|
+
"th",
|
|
54
|
+
"thead",
|
|
55
|
+
"tr",
|
|
56
|
+
"u",
|
|
57
|
+
"ul",
|
|
58
|
+
"video"
|
|
59
|
+
];
|
|
60
|
+
var EDITOR_FORBIDDEN_CONTENT_TAGS = [
|
|
61
|
+
"base",
|
|
62
|
+
"button",
|
|
63
|
+
"embed",
|
|
64
|
+
"form",
|
|
65
|
+
"iframe",
|
|
66
|
+
"input",
|
|
67
|
+
"link",
|
|
68
|
+
"math",
|
|
69
|
+
"meta",
|
|
70
|
+
"object",
|
|
71
|
+
"script",
|
|
72
|
+
"select",
|
|
73
|
+
"style",
|
|
74
|
+
"svg",
|
|
75
|
+
"textarea"
|
|
76
|
+
];
|
|
77
|
+
var EDITOR_ALLOWED_ATTRIBUTES = [
|
|
78
|
+
"abbr",
|
|
79
|
+
"alt",
|
|
80
|
+
"aria-label",
|
|
81
|
+
"aria-labelledby",
|
|
82
|
+
"autoplay",
|
|
83
|
+
"class",
|
|
84
|
+
"colspan",
|
|
85
|
+
"controls",
|
|
86
|
+
"data-align",
|
|
87
|
+
"decoding",
|
|
88
|
+
"dir",
|
|
89
|
+
"headers",
|
|
90
|
+
"height",
|
|
91
|
+
"href",
|
|
92
|
+
"lang",
|
|
93
|
+
"loading",
|
|
94
|
+
"loop",
|
|
95
|
+
"muted",
|
|
96
|
+
"open",
|
|
97
|
+
"playsinline",
|
|
98
|
+
"poster",
|
|
99
|
+
"preload",
|
|
100
|
+
"rel",
|
|
101
|
+
"reversed",
|
|
102
|
+
"role",
|
|
103
|
+
"rowspan",
|
|
104
|
+
"scope",
|
|
105
|
+
"span",
|
|
106
|
+
"src",
|
|
107
|
+
"start",
|
|
108
|
+
"style",
|
|
109
|
+
"summary",
|
|
110
|
+
"target",
|
|
111
|
+
"title",
|
|
112
|
+
"type",
|
|
113
|
+
"value",
|
|
114
|
+
"width"
|
|
115
|
+
];
|
|
116
|
+
var EDITOR_SAFE_URI_PATTERN = /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i;
|
|
117
|
+
var EDITOR_SANITIZE_CONFIG = {
|
|
118
|
+
ADD_DATA_URI_TAGS: ["img"],
|
|
119
|
+
ALLOW_ARIA_ATTR: true,
|
|
120
|
+
ALLOW_DATA_ATTR: true,
|
|
121
|
+
ALLOWED_ATTR: EDITOR_ALLOWED_ATTRIBUTES,
|
|
122
|
+
ALLOWED_TAGS: EDITOR_ALLOWED_TAGS,
|
|
123
|
+
ALLOWED_URI_REGEXP: EDITOR_SAFE_URI_PATTERN,
|
|
124
|
+
FORBID_CONTENTS: EDITOR_FORBIDDEN_CONTENT_TAGS,
|
|
125
|
+
FORBID_TAGS: EDITOR_FORBIDDEN_CONTENT_TAGS,
|
|
126
|
+
KEEP_CONTENT: true
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/sanitizerHook.ts
|
|
130
|
+
var ALLOWED_LINK_TARGETS = /* @__PURE__ */ new Set(["_blank", "_parent", "_self", "_top"]);
|
|
131
|
+
var SAFE_STYLE_PROPERTIES = /* @__PURE__ */ new Set(["background", "background-color", "color", "text-align"]);
|
|
132
|
+
var DATA_IMAGE_URI_PATTERN = /^data:image\/(?:gif|jpeg|jpg|png|svg\+xml|webp);/i;
|
|
133
|
+
var TABLE_CONTAINER_CLASS_NAME = "wysiwyg-table";
|
|
134
|
+
var sanitizeEditorStyle = (style) => {
|
|
135
|
+
const declarations = [];
|
|
136
|
+
for (const rawDeclaration of style.split(";")) {
|
|
137
|
+
const declaration = rawDeclaration.trim();
|
|
138
|
+
const separatorIndex = declaration.indexOf(":");
|
|
139
|
+
if (separatorIndex === -1) continue;
|
|
140
|
+
const property = declaration.slice(0, separatorIndex).trim().toLowerCase();
|
|
141
|
+
const value = declaration.slice(separatorIndex + 1).trim();
|
|
142
|
+
if (!SAFE_STYLE_PROPERTIES.has(property)) continue;
|
|
143
|
+
if (!value || /(?:expression|url)\s*\(|@import|behavior\s*:/i.test(value)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (property === "text-align" && !/^(?:center|end|justify|left|right|start)$/i.test(value)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
declarations.push(`${property}: ${value}`);
|
|
150
|
+
}
|
|
151
|
+
return declarations.join("; ");
|
|
152
|
+
};
|
|
153
|
+
var isAllowedEditorLinkTarget = (target) => ALLOWED_LINK_TARGETS.has(target);
|
|
154
|
+
var isAllowedEditorDataImageUri = (src) => !/^data:/i.test(src.trim()) || DATA_IMAGE_URI_PATTERN.test(src.trim());
|
|
155
|
+
var sanitizeEditorAttributes = (element) => {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
const safeStyle = sanitizeEditorStyle((_a = element.getAttribute("style")) != null ? _a : "");
|
|
158
|
+
if (safeStyle) {
|
|
159
|
+
element.setAttribute("style", safeStyle);
|
|
160
|
+
} else {
|
|
161
|
+
element.removeAttribute("style");
|
|
162
|
+
}
|
|
163
|
+
const tagName = element.tagName.toLowerCase();
|
|
164
|
+
if (tagName === "a") {
|
|
165
|
+
const target = element.getAttribute("target");
|
|
166
|
+
if (target && !isAllowedEditorLinkTarget(target)) {
|
|
167
|
+
element.removeAttribute("target");
|
|
168
|
+
}
|
|
169
|
+
if (element.getAttribute("target") === "_blank") {
|
|
170
|
+
element.setAttribute("rel", "noopener noreferrer");
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const src = (_b = element.getAttribute("src")) != null ? _b : "";
|
|
175
|
+
if (/^data:/i.test(src.trim()) && (tagName !== "img" || !isAllowedEditorDataImageUri(src))) {
|
|
176
|
+
element.removeAttribute("src");
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var wrapEditorTable = (node) => {
|
|
180
|
+
if (node.nodeType !== node.ELEMENT_NODE) return;
|
|
181
|
+
const table = node;
|
|
182
|
+
if (table.tagName.toLowerCase() !== "table") return;
|
|
183
|
+
const parent = table.parentElement;
|
|
184
|
+
if (!parent || parent.closest(`.${TABLE_CONTAINER_CLASS_NAME}`)) return;
|
|
185
|
+
const container = table.ownerDocument.createElement("div");
|
|
186
|
+
container.className = TABLE_CONTAINER_CLASS_NAME;
|
|
187
|
+
parent.insertBefore(container, table);
|
|
188
|
+
container.append(table);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/sanitizerRuntime.browser.ts
|
|
192
|
+
var sanitizer = DOMPurifyFactory(window);
|
|
193
|
+
sanitizer.addHook("afterSanitizeAttributes", sanitizeEditorAttributes);
|
|
194
|
+
sanitizer.addHook("afterSanitizeElements", wrapEditorTable);
|
|
195
|
+
function sanitizeEditorHtml(html) {
|
|
196
|
+
return sanitizer.sanitize(html, EDITOR_SANITIZE_CONFIG);
|
|
197
|
+
}
|
|
198
|
+
function releaseEditorSanitizer() {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
releaseEditorSanitizer,
|
|
203
|
+
sanitizeEditorHtml
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=sanitizer-runtime.browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sanitizerRuntime.browser.ts","../../src/sanitizerConfig.ts","../../src/sanitizerHook.ts"],"sourcesContent":["import DOMPurifyFactory from \"dompurify\";\nimport { EDITOR_SANITIZE_CONFIG } from \"./sanitizerConfig\";\nimport { sanitizeEditorAttributes, wrapEditorTable } from \"./sanitizerHook\";\nimport type { EditorHtml } from \"./types\";\n\nconst sanitizer = DOMPurifyFactory(window);\nsanitizer.addHook(\"afterSanitizeAttributes\", sanitizeEditorAttributes);\nsanitizer.addHook(\"afterSanitizeElements\", wrapEditorTable);\n\nexport function sanitizeEditorHtml(html: EditorHtml) {\n return sanitizer.sanitize(html, EDITOR_SANITIZE_CONFIG);\n}\n\nexport function releaseEditorSanitizer(): void {\n return;\n}\n","import type { Config } from \"dompurify\";\n\nexport const EDITOR_ALLOWED_TAGS = [\n \"a\",\n \"abbr\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"caption\",\n \"code\",\n \"col\",\n \"colgroup\",\n \"dd\",\n \"del\",\n \"details\",\n \"div\",\n \"dl\",\n \"dt\",\n \"em\",\n \"figcaption\",\n \"figure\",\n \"footer\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"hr\",\n \"i\",\n \"img\",\n \"kbd\",\n \"li\",\n \"mark\",\n \"ol\",\n \"p\",\n \"pre\",\n \"s\",\n \"section\",\n \"small\",\n \"source\",\n \"span\",\n \"strong\",\n \"sub\",\n \"summary\",\n \"sup\",\n \"table\",\n \"tbody\",\n \"td\",\n \"tfoot\",\n \"th\",\n \"thead\",\n \"tr\",\n \"u\",\n \"ul\",\n \"video\",\n];\n\nexport const EDITOR_FORBIDDEN_CONTENT_TAGS = [\n \"base\",\n \"button\",\n \"embed\",\n \"form\",\n \"iframe\",\n \"input\",\n \"link\",\n \"math\",\n \"meta\",\n \"object\",\n \"script\",\n \"select\",\n \"style\",\n \"svg\",\n \"textarea\",\n];\n\nexport const EDITOR_ALLOWED_ATTRIBUTES = [\n \"abbr\",\n \"alt\",\n \"aria-label\",\n \"aria-labelledby\",\n \"autoplay\",\n \"class\",\n \"colspan\",\n \"controls\",\n \"data-align\",\n \"decoding\",\n \"dir\",\n \"headers\",\n \"height\",\n \"href\",\n \"lang\",\n \"loading\",\n \"loop\",\n \"muted\",\n \"open\",\n \"playsinline\",\n \"poster\",\n \"preload\",\n \"rel\",\n \"reversed\",\n \"role\",\n \"rowspan\",\n \"scope\",\n \"span\",\n \"src\",\n \"start\",\n \"style\",\n \"summary\",\n \"target\",\n \"title\",\n \"type\",\n \"value\",\n \"width\",\n];\n\nexport const EDITOR_SAFE_URI_PATTERN =\n /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i;\n\nexport const EDITOR_SANITIZE_CONFIG = {\n ADD_DATA_URI_TAGS: [\"img\"],\n ALLOW_ARIA_ATTR: true,\n ALLOW_DATA_ATTR: true,\n ALLOWED_ATTR: EDITOR_ALLOWED_ATTRIBUTES,\n ALLOWED_TAGS: EDITOR_ALLOWED_TAGS,\n ALLOWED_URI_REGEXP: EDITOR_SAFE_URI_PATTERN,\n FORBID_CONTENTS: EDITOR_FORBIDDEN_CONTENT_TAGS,\n FORBID_TAGS: EDITOR_FORBIDDEN_CONTENT_TAGS,\n KEEP_CONTENT: true,\n} satisfies Config;\n","import type { ElementHook, NodeHook } from \"dompurify\";\n\nconst ALLOWED_LINK_TARGETS = new Set([\"_blank\", \"_parent\", \"_self\", \"_top\"]);\nconst SAFE_STYLE_PROPERTIES = new Set([\"background\", \"background-color\", \"color\", \"text-align\"]);\nconst DATA_IMAGE_URI_PATTERN = /^data:image\\/(?:gif|jpeg|jpg|png|svg\\+xml|webp);/i;\nconst TABLE_CONTAINER_CLASS_NAME = \"wysiwyg-table\";\n\nexport const sanitizeEditorStyle = (style: string) => {\n const declarations: string[] = [];\n\n for (const rawDeclaration of style.split(\";\")) {\n const declaration = rawDeclaration.trim();\n const separatorIndex = declaration.indexOf(\":\");\n if (separatorIndex === -1) continue;\n\n const property = declaration.slice(0, separatorIndex).trim().toLowerCase();\n const value = declaration.slice(separatorIndex + 1).trim();\n\n if (!SAFE_STYLE_PROPERTIES.has(property)) continue;\n if (!value || /(?:expression|url)\\s*\\(|@import|behavior\\s*:/i.test(value)) {\n continue;\n }\n if (property === \"text-align\" && !/^(?:center|end|justify|left|right|start)$/i.test(value)) {\n continue;\n }\n\n declarations.push(`${property}: ${value}`);\n }\n\n return declarations.join(\"; \");\n};\n\nexport const isAllowedEditorLinkTarget = (target: string) =>\n ALLOWED_LINK_TARGETS.has(target);\n\nexport const isAllowedEditorDataImageUri = (src: string) =>\n !/^data:/i.test(src.trim()) || DATA_IMAGE_URI_PATTERN.test(src.trim());\n\nexport const sanitizeEditorAttributes: ElementHook = (element) => {\n const safeStyle = sanitizeEditorStyle(element.getAttribute(\"style\") ?? \"\");\n if (safeStyle) {\n element.setAttribute(\"style\", safeStyle);\n } else {\n element.removeAttribute(\"style\");\n }\n\n const tagName = element.tagName.toLowerCase();\n\n if (tagName === \"a\") {\n const target = element.getAttribute(\"target\");\n if (target && !isAllowedEditorLinkTarget(target)) {\n element.removeAttribute(\"target\");\n }\n\n if (element.getAttribute(\"target\") === \"_blank\") {\n element.setAttribute(\"rel\", \"noopener noreferrer\");\n }\n return;\n }\n\n const src = element.getAttribute(\"src\") ?? \"\";\n if (\n /^data:/i.test(src.trim()) &&\n (tagName !== \"img\" || !isAllowedEditorDataImageUri(src))\n ) {\n element.removeAttribute(\"src\");\n }\n};\n\nexport const wrapEditorTable: NodeHook = (node) => {\n if (node.nodeType !== node.ELEMENT_NODE) return;\n\n const table = node as Element;\n if (table.tagName.toLowerCase() !== \"table\") return;\n\n const parent = table.parentElement;\n if (!parent || parent.closest(`.${TABLE_CONTAINER_CLASS_NAME}`)) return;\n\n const container = table.ownerDocument.createElement(\"div\");\n container.className = TABLE_CONTAINER_CLASS_NAME;\n parent.insertBefore(container, table);\n container.append(table);\n};\n"],"mappings":"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","names":[]}
|