@ctzhian/tiptap 2.11.3 → 2.11.5

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.
@@ -8,35 +8,30 @@ import { EditLineIcon } from "../../../component/Icons";
8
8
  import { Box, Stack, Tooltip } from "@mui/material";
9
9
  import { MarkViewContent } from "@tiptap/react";
10
10
  import React, { useEffect, useMemo, useRef, useState } from "react";
11
+ import sanitizeHtml from 'sanitize-html';
11
12
  import EditPopover from "./EditPopover";
12
-
13
- // 简单的 HTML 清理函数,用于 SSR 环境(不依赖 jsdom)
14
- // 确保 SSR 和客户端首次渲染一致,避免 hydration mismatch
15
- var simpleSanitize = function simpleSanitize(html) {
16
- if (!html) return '';
17
- return html
18
- // 移除 script 标签
19
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
20
- // 移除 style 标签(防止 CSS 注入)
21
- .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
22
- // 移除 iframe 标签
23
- .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
24
- // 移除 object、embed、link 等危险标签
25
- .replace(/<(object|embed|link|meta|base)\b[^<]*(?:(?!<\/\1>)<[^<]*)*<\/\1>/gi, '')
26
- // 移除所有事件处理器(onclick, onerror 等)
27
- .replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '')
28
- // 移除 javascript: 协议
29
- .replace(/javascript:/gi, '')
30
- // 移除 data: URL(防止 base64 注入)
31
- .replace(/data:\s*[^;]*;base64[^"'\s]*/gi, '').trim();
32
- };
33
-
34
- // DOMPurify 配置(客户端使用)
35
- var DOMPURIFY_CONFIG = {
36
- // 允许基本的格式化标签
37
- ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'b', 'i', 'u', 's', 'strike', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'span', 'div', 'blockquote', 'code', 'pre'],
38
- ALLOWED_ATTR: ['href', 'title', 'target', 'style', 'class'],
39
- ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
13
+ var SANITIZE_CONFIG = {
14
+ allowedTags: ['p', 'br', 'strong', 'em', 'b', 'i', 'u', 's', 'strike', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'span', 'div', 'blockquote', 'code', 'pre'],
15
+ allowedAttributes: {
16
+ a: ['href', 'title', 'target'],
17
+ span: ['style', 'class'],
18
+ div: ['style', 'class'],
19
+ p: ['style', 'class'],
20
+ '*': ['class']
21
+ },
22
+ allowedStyles: {
23
+ '*': {
24
+ color: [/^#[0-9A-Fa-f]{3,6}$/, /^rgb/, /^rgba/],
25
+ 'text-align': [/^left$/, /^right$/, /^center$/, /^justify$/],
26
+ 'font-size': [/^\d+(?:px|em|rem|%)$/],
27
+ 'font-weight': [/^\d+$/, /^normal$/, /^bold$/]
28
+ }
29
+ },
30
+ disallowedTagsMode: 'discard',
31
+ allowedSchemes: ['http', 'https', 'mailto'],
32
+ allowedSchemesByTag: {
33
+ a: ['http', 'https', 'mailto']
34
+ }
40
35
  };
41
36
  var TooltipView = function TooltipView(_ref) {
42
37
  var mark = _ref.mark,
@@ -55,72 +50,10 @@ var TooltipView = function TooltipView(_ref) {
55
50
  _useState6 = _slicedToArray(_useState5, 2),
56
51
  isMobile = _useState6[0],
57
52
  setIsMobile = _useState6[1];
58
- var _useState7 = useState(false),
59
- _useState8 = _slicedToArray(_useState7, 2),
60
- isHydrated = _useState8[0],
61
- setIsHydrated = _useState8[1];
62
53
  var anchorRef = useRef(null);
63
-
64
- // SSR 和客户端首次渲染时使用 simpleSanitize,确保一致性
65
- var initialSanitized = useMemo(function () {
66
- return simpleSanitize(rawTooltip);
54
+ var tooltip = useMemo(function () {
55
+ return sanitizeHtml(rawTooltip, SANITIZE_CONFIG).trim();
67
56
  }, [rawTooltip]);
68
- var _useState9 = useState(initialSanitized),
69
- _useState10 = _slicedToArray(_useState9, 2),
70
- tooltip = _useState10[0],
71
- setTooltip = _useState10[1];
72
-
73
- // 检测是否在客户端环境(hydration 后)
74
- useEffect(function () {
75
- setIsHydrated(true);
76
- }, []);
77
-
78
- // 延迟导入 DOMPurify,避免在 SSR 环境下加载 jsdom
79
- // 只在客户端环境下动态导入,确保 SSR 时不会触发 jsdom 初始化
80
- useEffect(function () {
81
- // SSR 环境下直接返回,使用 simpleSanitize 的结果
82
- if (typeof window === 'undefined' || !isHydrated) {
83
- return;
84
- }
85
-
86
- // 如果内容为空,不需要 sanitize
87
- if (!rawTooltip.trim()) {
88
- return;
89
- }
90
-
91
- // 客户端环境下异步导入 isomorphic-dompurify
92
- // 使用动态 import() 确保在 SSR 构建时不会被打包进去
93
- var cancelled = false;
94
- import('isomorphic-dompurify').then(function (DOMPurify) {
95
- // 检查组件是否已卸载
96
- if (cancelled) return;
97
-
98
- // 使用 DOMPurify 进行完整的 HTML sanitize
99
- var sanitized = DOMPurify.default.sanitize(rawTooltip, DOMPURIFY_CONFIG).trim();
100
-
101
- // 更新 tooltip(使用函数式更新避免依赖 tooltip 状态)
102
- setTooltip(function (prevTooltip) {
103
- // 只在内容发生变化时更新,避免不必要的重渲染
104
- return sanitized !== prevTooltip ? sanitized : prevTooltip;
105
- });
106
- }).catch(function (error) {
107
- // 如果导入失败(例如在某些 SSR 环境下),使用简单清理
108
- if (!cancelled) {
109
- console.warn('Failed to load DOMPurify, using simple sanitize:', error);
110
- // 使用函数式更新
111
- setTooltip(function (prevTooltip) {
112
- var fallbackSanitized = simpleSanitize(rawTooltip);
113
- return fallbackSanitized !== prevTooltip ? fallbackSanitized : prevTooltip;
114
- });
115
- }
116
- });
117
-
118
- // 清理函数:组件卸载时取消异步操作
119
- return function () {
120
- cancelled = true;
121
- };
122
- }, [rawTooltip, isHydrated]); // 依赖 rawTooltip 和 isHydrated
123
-
124
57
  var handleEditClick = function handleEditClick(e) {
125
58
  e.stopPropagation();
126
59
  setOpen(true);
@@ -156,7 +89,7 @@ var TooltipView = function TooltipView(_ref) {
156
89
  if (isEditable && tooltip === '' && !isSelectionEmpty && anchorRef.current) {
157
90
  setOpen(true);
158
91
  }
159
- }, [tooltip, isEditable, anchorRef.current]);
92
+ }, [tooltip, isEditable]);
160
93
  var isMobileReadonly = !isEditable && isMobile;
161
94
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Tooltip, {
162
95
  arrow: true,
@@ -175,9 +175,19 @@ export var InlineLinkExtension = Node.create({
175
175
  return [{
176
176
  tag: 'a',
177
177
  getAttrs: function getAttrs(dom) {
178
- var href = dom.getAttribute('href');
179
- var dataType = dom.getAttribute('type');
180
- var download = dom.getAttribute('download');
178
+ var element = dom;
179
+ var href = element.getAttribute('href');
180
+ var dataType = element.getAttribute('type');
181
+ var download = element.getAttribute('download');
182
+
183
+ // 如果 <a> 标签内部包含图片,则视为"图片链接",不解析为 inlineLink,
184
+ // 让内部的 <img> 节点按普通图片逻辑处理,避免丢失图片和文本。
185
+ var hasImageChild = Array.from(element.childNodes).some(function (node) {
186
+ return node.nodeType === 1 && node.tagName.toLowerCase() === 'img';
187
+ });
188
+ if (hasImageChild) {
189
+ return false;
190
+ }
181
191
 
182
192
  // 如果存在 download 属性,认为是附件,不解析为链接
183
193
  if (download !== null) {
@@ -200,12 +210,12 @@ export var InlineLinkExtension = Node.create({
200
210
  }
201
211
  return {
202
212
  href: href,
203
- target: dom.getAttribute('target') || _this.options.HTMLAttributes.target,
204
- class: dom.getAttribute('class') || _this.options.HTMLAttributes.class,
205
- rel: dom.getAttribute('rel'),
206
- title: dom.textContent || dom.getAttribute('title'),
207
- type: dom.getAttribute('type') || 'icon',
208
- download: dom.getAttribute('download')
213
+ target: element.getAttribute('target') || _this.options.HTMLAttributes.target,
214
+ class: element.getAttribute('class') || _this.options.HTMLAttributes.class,
215
+ rel: element.getAttribute('rel'),
216
+ title: element.textContent || element.getAttribute('title'),
217
+ type: element.getAttribute('type') || 'icon',
218
+ download: element.getAttribute('download')
209
219
  };
210
220
  }
211
221
  }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctzhian/tiptap",
3
- "version": "2.11.3",
3
+ "version": "2.11.5",
4
4
  "description": "基于 Tiptap 二次开发的编辑器组件",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -66,6 +66,7 @@
66
66
  "@types/diff-match-patch": "^1.0.36",
67
67
  "@types/react": "^18.0.0",
68
68
  "@types/react-dom": "^18.0.0",
69
+ "@types/sanitize-html": "^2.16.0",
69
70
  "@umijs/lint": "^4.0.0",
70
71
  "dumi": "^2.4.21",
71
72
  "eslint": "^8.23.0",
@@ -118,7 +119,6 @@
118
119
  "core-js": "^3.46.0",
119
120
  "diff-match-patch": "^1.0.5",
120
121
  "highlight.js": "^11.11.1",
121
- "isomorphic-dompurify": "^2.34.0",
122
122
  "jszip": "^3.10.1",
123
123
  "katex": "^0.16.22",
124
124
  "linkifyjs": "^4.3.2",
@@ -128,6 +128,7 @@
128
128
  "react-colorful": "^5.6.1",
129
129
  "react-image-crop": "^11.0.10",
130
130
  "react-photo-view": "^1.2.7",
131
+ "sanitize-html": "^2.17.0",
131
132
  "uuid": "^11.1.0"
132
133
  },
133
134
  "scripts": {