@ctzhian/tiptap 2.11.1 → 2.11.3

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.
@@ -33,12 +33,12 @@ var ExcalidrawEditorWrapper = function ExcalidrawEditorWrapper(_ref) {
33
33
  setIsLoaded = _useState2[1];
34
34
  var handleSave = /*#__PURE__*/function () {
35
35
  var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
36
- var elements, appState, files, excalidrawModule, exportToBlob, blob, _file;
36
+ var elements, appState, files, excalidrawModule, exportToSvg, svgElement, svgString, blob, _file;
37
37
  return _regeneratorRuntime().wrap(function _callee$(_context) {
38
38
  while (1) switch (_context.prev = _context.next) {
39
39
  case 0:
40
40
  if (!excalidrawAPIRef.current) {
41
- _context.next = 23;
41
+ _context.next = 25;
42
42
  break;
43
43
  }
44
44
  _context.prev = 1;
@@ -55,39 +55,42 @@ var ExcalidrawEditorWrapper = function ExcalidrawEditorWrapper(_ref) {
55
55
  return import('@excalidraw/excalidraw');
56
56
  case 9:
57
57
  excalidrawModule = _context.sent;
58
- exportToBlob = excalidrawModule.exportToBlob;
58
+ exportToSvg = excalidrawModule.exportToSvg;
59
59
  _context.next = 13;
60
- return exportToBlob({
60
+ return exportToSvg({
61
61
  elements: elements,
62
62
  appState: _objectSpread(_objectSpread({}, appState), {}, {
63
63
  exportBackground: true,
64
64
  exportScale: 2
65
65
  }),
66
- files: files,
67
- mimeType: 'image/svg+xml'
66
+ files: files
68
67
  });
69
68
  case 13:
70
- blob = _context.sent;
69
+ svgElement = _context.sent;
70
+ svgString = new XMLSerializer().serializeToString(svgElement);
71
+ blob = new Blob([svgString], {
72
+ type: 'image/svg+xml'
73
+ });
71
74
  _file = new File([blob], "excalidraw-".concat(Date.now(), ".svg"), {
72
75
  type: 'image/svg+xml'
73
76
  });
74
77
  onSave(_file);
75
- _context.next = 21;
78
+ _context.next = 23;
76
79
  break;
77
- case 18:
78
- _context.prev = 18;
80
+ case 20:
81
+ _context.prev = 20;
79
82
  _context.t0 = _context["catch"](1);
80
- console.error('Failed to export Excalidraw as image', _context.t0);
81
- case 21:
82
- _context.next = 24;
83
- break;
83
+ console.error('Failed to export Excalidraw as SVG', _context.t0);
84
84
  case 23:
85
+ _context.next = 26;
86
+ break;
87
+ case 25:
85
88
  console.warn('Excalidraw API not available');
86
- case 24:
89
+ case 26:
87
90
  case "end":
88
91
  return _context.stop();
89
92
  }
90
- }, _callee, null, [[1, 18]]);
93
+ }, _callee, null, [[1, 20]]);
91
94
  }));
92
95
  return function handleSave() {
93
96
  return _ref2.apply(this, arguments);
@@ -7,13 +7,41 @@ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
7
7
  import { EditLineIcon } from "../../../component/Icons";
8
8
  import { Box, Stack, Tooltip } from "@mui/material";
9
9
  import { MarkViewContent } from "@tiptap/react";
10
- import DOMPurify from 'isomorphic-dompurify';
11
- import React, { useEffect, useRef, useState } from "react";
10
+ import React, { useEffect, useMemo, useRef, useState } from "react";
12
11
  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
40
+ };
13
41
  var TooltipView = function TooltipView(_ref) {
14
42
  var mark = _ref.mark,
15
43
  editor = _ref.editor;
16
- var tooltip = DOMPurify.sanitize(mark.attrs.tooltip || '').trim();
44
+ var rawTooltip = mark.attrs.tooltip || '';
17
45
  var isEditable = editor.isEditable;
18
46
  var _useState = useState(false),
19
47
  _useState2 = _slicedToArray(_useState, 2),
@@ -27,7 +55,72 @@ var TooltipView = function TooltipView(_ref) {
27
55
  _useState6 = _slicedToArray(_useState5, 2),
28
56
  isMobile = _useState6[0],
29
57
  setIsMobile = _useState6[1];
58
+ var _useState7 = useState(false),
59
+ _useState8 = _slicedToArray(_useState7, 2),
60
+ isHydrated = _useState8[0],
61
+ setIsHydrated = _useState8[1];
30
62
  var anchorRef = useRef(null);
63
+
64
+ // SSR 和客户端首次渲染时使用 simpleSanitize,确保一致性
65
+ var initialSanitized = useMemo(function () {
66
+ return simpleSanitize(rawTooltip);
67
+ }, [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
+
31
124
  var handleEditClick = function handleEditClick(e) {
32
125
  e.stopPropagation();
33
126
  setOpen(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctzhian/tiptap",
3
- "version": "2.11.1",
3
+ "version": "2.11.3",
4
4
  "description": "基于 Tiptap 二次开发的编辑器组件",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",