@oix1987/yjd 1.0.3 → 2.1.0

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.
Files changed (73) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -142
  3. package/core.js +82 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +230 -103
  11. package/index.js +297 -0
  12. package/lib/core/editor.js +1885 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +341 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/mention.js +200 -0
  44. package/lib/modules/resize-handles.js +701 -0
  45. package/lib/modules/slash-menu.js +183 -0
  46. package/lib/modules/table-toolbar.js +635 -0
  47. package/lib/modules/toolbar.js +607 -0
  48. package/lib/serialize.js +241 -0
  49. package/lib/static.js +28 -0
  50. package/lib/styles-loader.js +142 -0
  51. package/{dist → lib}/styles.css +1392 -35
  52. package/lib/styles.css.js +2 -0
  53. package/lib/styles.min.css +1 -0
  54. package/lib/ui/color-picker.js +296 -0
  55. package/lib/ui/customselect.js +351 -0
  56. package/lib/ui/emoji-picker.js +196 -0
  57. package/lib/ui/icons.js +145 -0
  58. package/lib/ui/image-popup.js +435 -0
  59. package/lib/ui/import-popup.js +288 -0
  60. package/lib/ui/link-popup.js +139 -0
  61. package/lib/ui/list-picker.js +307 -0
  62. package/lib/ui/select-button.js +68 -0
  63. package/lib/ui/table-popup.js +171 -0
  64. package/lib/ui/tag-popup.js +249 -0
  65. package/lib/ui/text-align-picker.js +278 -0
  66. package/lib/ui/video-popup.js +413 -0
  67. package/lib/utils/exec-command.js +72 -0
  68. package/lib/utils/history-helper.js +50 -0
  69. package/lib/utils/popup-helper.js +219 -0
  70. package/lib/utils/popup-positioning.js +234 -0
  71. package/lib/utils/sanitize.js +164 -0
  72. package/package.json +51 -32
  73. package/umd-entry.js +19 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Popup Helper Utility
3
+ * Helps popups append to the yjd-rich-editor instead of document.body
4
+ * Now supports multiple editor instances with separate popup containers
5
+ */
6
+ import Editor from '../core/editor.js';
7
+
8
+ /**
9
+ * Get the appropriate container for popups
10
+ * @param {string} editorId - Optional editor instance ID
11
+ * @returns {HTMLElement} Container element for popups
12
+ */
13
+ export function getPopupContainer(editorId = null) {
14
+ let editor;
15
+
16
+ if (editorId) {
17
+ // Get specific editor instance
18
+ editor = Editor.getInstanceById(editorId);
19
+ } else {
20
+ // Try to get current editor instance
21
+ editor = Editor.getCurrentInstance();
22
+ }
23
+
24
+ if (editor) {
25
+ return editor.getPopupContainer();
26
+ }
27
+
28
+ // Fallback to document.body if no editor instance
29
+ return document.body;
30
+ }
31
+
32
+ /**
33
+ * Append popup to the appropriate container
34
+ * @param {HTMLElement} popup - Popup element to append
35
+ * @param {string} editorId - Optional editor instance ID
36
+ */
37
+ export function appendPopup(popup, editorId = null) {
38
+ const container = getPopupContainer(editorId);
39
+
40
+ // Remove from current parent if exists
41
+ if (popup.parentNode) {
42
+ popup.parentNode.removeChild(popup);
43
+ }
44
+
45
+ container.appendChild(popup);
46
+
47
+ // Note: pointer-events are now controlled by CSS rules
48
+ // Popup containers have pointer-events: none by default
49
+ // Interactive elements inside popups have pointer-events: auto
50
+ }
51
+
52
+ /**
53
+ * Get popup dimensions by temporarily showing it if needed
54
+ * @param {HTMLElement} popup - Popup element
55
+ * @returns {Object} Object with width and height
56
+ */
57
+ function getPopupDimensions(popup) {
58
+ if (!popup) return { width: 300, height: 200 };
59
+
60
+ // Try getBoundingClientRect first
61
+ const rect = popup.getBoundingClientRect();
62
+ if (rect.width > 0 && rect.height > 0) {
63
+ return { width: rect.width, height: rect.height };
64
+ }
65
+
66
+ // Try offsetWidth/offsetHeight
67
+ if (popup.offsetWidth > 0 && popup.offsetHeight > 0) {
68
+ return { width: popup.offsetWidth, height: popup.offsetHeight };
69
+ }
70
+
71
+ // Check if popup is hidden
72
+ const computedStyle = window.getComputedStyle(popup);
73
+ const isHidden = computedStyle.display === 'none' || computedStyle.visibility === 'hidden';
74
+
75
+ if (isHidden) {
76
+ // Temporarily show popup to get dimensions
77
+ const originalDisplay = popup.style.display;
78
+ const originalVisibility = popup.style.visibility;
79
+ const originalPosition = popup.style.position;
80
+ const originalTop = popup.style.top;
81
+ const originalLeft = popup.style.left;
82
+ const originalZIndex = popup.style.zIndex;
83
+
84
+ // Make popup visible but off-screen
85
+ popup.style.display = 'block';
86
+ popup.style.visibility = 'visible';
87
+ popup.style.position = 'absolute';
88
+ popup.style.top = '-9999px';
89
+ popup.style.left = '-9999px';
90
+ popup.style.zIndex = '-1';
91
+
92
+ // Force reflow
93
+ popup.offsetHeight;
94
+
95
+ // Get dimensions
96
+ const tempRect = popup.getBoundingClientRect();
97
+ const width = tempRect.width > 0 ? tempRect.width : 300;
98
+ const height = tempRect.height > 0 ? tempRect.height : 200;
99
+
100
+ // Restore original styles
101
+ popup.style.display = originalDisplay;
102
+ popup.style.visibility = originalVisibility;
103
+ popup.style.position = originalPosition;
104
+ popup.style.top = originalTop;
105
+ popup.style.left = originalLeft;
106
+ popup.style.zIndex = originalZIndex;
107
+
108
+ return { width, height };
109
+ }
110
+
111
+ // Last resort: try computed styles
112
+ const computedWidth = parseInt(computedStyle.width);
113
+ const computedHeight = parseInt(computedStyle.height);
114
+
115
+ return {
116
+ width: computedWidth > 0 ? computedWidth : 300,
117
+ height: computedHeight > 0 ? computedHeight : 200
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Calculate position for popup relative to anchor element
123
+ * @param {HTMLElement} anchor - Anchor element
124
+ * @param {HTMLElement} popup - Popup element
125
+ * @param {Object} options - Positioning options
126
+ * @returns {Object} Position object with top and left values
127
+ */
128
+ export function calculatePopupPosition(anchor, popup, options = {}) {
129
+ const {
130
+ offsetX = 0,
131
+ offsetY = 5,
132
+ preferTop = false,
133
+ preferLeft = false
134
+ } = options;
135
+
136
+ const anchorRect = anchor.getBoundingClientRect();
137
+ const container = getPopupContainer();
138
+ const isInWrapper = container.classList.contains('rich-editor-popup-container');
139
+
140
+ let top, left;
141
+
142
+ if (isInWrapper) {
143
+ // Position relative to wrapper
144
+ const wrapperRect = container.getBoundingClientRect();
145
+
146
+ // Calculate position relative to wrapper
147
+ top = anchorRect.top - wrapperRect.top + anchorRect.height + offsetY;
148
+ left = anchorRect.left - wrapperRect.left + offsetX;
149
+
150
+ // Get popup dimensions using the helper function
151
+ const { width: popupWidth, height: popupHeight } = getPopupDimensions(popup);
152
+
153
+
154
+ // Check if popup would overflow bottom of wrapper
155
+ if (top + popupHeight > wrapperRect.height && !preferTop) {
156
+ // Try to position above the anchor
157
+ const topPosition = anchorRect.top - wrapperRect.top - popupHeight - offsetY;
158
+ if (topPosition >= 0) {
159
+ top = topPosition;
160
+ } else {
161
+ // If still doesn't fit, try to center it vertically within the wrapper
162
+ top = Math.max(offsetY, (wrapperRect.height - popupHeight) / 2);
163
+ }
164
+ }
165
+
166
+ // Check if popup would overflow right of wrapper
167
+ if (left + popupWidth + 5 > wrapperRect.width && !preferLeft) {
168
+ left = wrapperRect.width - popupWidth - offsetX -15;
169
+ }
170
+
171
+ // Ensure popup doesn't go off-screen
172
+ if (left < 0) left = offsetX;
173
+ if (top < 0) top = offsetY;
174
+
175
+ } else {
176
+ // Fallback to document.body positioning
177
+ top = anchorRect.bottom + window.scrollY + offsetY;
178
+ left = anchorRect.left + window.scrollX + offsetX;
179
+
180
+
181
+ // Get popup dimensions using the helper function
182
+ const { width: popupWidth, height: popupHeight } = getPopupDimensions(popup);
183
+
184
+ // Check if popup would overflow right edge
185
+ if (left + popupWidth > window.innerWidth && !preferLeft) {
186
+ left = window.innerWidth - popupWidth - offsetX;
187
+ }
188
+
189
+ // Check if popup would overflow bottom edge
190
+ if (top + popupHeight > window.innerHeight + window.scrollY && !preferTop) {
191
+ // Try to position above the anchor
192
+ const topPosition = anchorRect.top + window.scrollY - popupHeight - offsetY;
193
+ if (topPosition >= window.scrollY) {
194
+ top = topPosition;
195
+ } else {
196
+ // If still doesn't fit, try to center it vertically within the viewport
197
+ top = Math.max(window.scrollY + offsetY, window.scrollY + (window.innerHeight - popupHeight) / 2);
198
+ }
199
+ }
200
+
201
+ // Ensure popup doesn't go off-screen
202
+ if (left < 0) left = offsetX;
203
+ if (top < 0) top = offsetY;
204
+ }
205
+
206
+ return { top, left };
207
+ }
208
+
209
+ /**
210
+ * Set popup position
211
+ * @param {HTMLElement} popup - Popup element
212
+ * @param {Object} position - Position object with top and left values
213
+ */
214
+ export function setPopupPosition(popup, position) {
215
+ popup.style.position = 'absolute';
216
+ popup.style.top = `${position.top}px`;
217
+ popup.style.left = `${position.left}px`;
218
+ popup.style.zIndex = '1000';
219
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Responsive Popup Positioning Utility
3
+ * Handles positioning of popups to ensure they stay within viewport on mobile devices
4
+ */
5
+ export class PopupPositioning {
6
+ /**
7
+ * Calculate optimal position for popup to stay within viewport
8
+ * @param {HTMLElement} anchor - The anchor element
9
+ * @param {HTMLElement} popup - The popup element
10
+ * @param {Object} options - Positioning options
11
+ * @returns {Object} - Calculated position {top, left, transform}
12
+ */
13
+ static calculatePosition(anchor, popup, options = {}) {
14
+ const {
15
+ offsetX = 0,
16
+ offsetY = 5,
17
+ preferredPosition = 'bottom-right', // 'bottom-right', 'bottom-left', 'top-right', 'top-left'
18
+ maxWidth = null,
19
+ maxHeight = null
20
+ } = options;
21
+
22
+ // Get viewport dimensions
23
+ const viewportWidth = window.innerWidth;
24
+ const viewportHeight = window.innerHeight;
25
+ const scrollX = window.scrollX;
26
+ const scrollY = window.scrollY;
27
+
28
+ // Get anchor dimensions
29
+ const anchorRect = anchor.getBoundingClientRect();
30
+
31
+ // Get popup dimensions (measure if not already rendered)
32
+ let popupWidth = popup.offsetWidth;
33
+ let popupHeight = popup.offsetHeight;
34
+
35
+ // If popup is not yet visible, measure it off-screen WITHOUT adding the
36
+ // `.visible` class — adding that class would trigger the entrance animation
37
+ // during measurement, making the real show flicker / feel laggy.
38
+ let wasVisible = popup.classList.contains('visible');
39
+ let prevDisplay = '';
40
+ let prevVisibility = '';
41
+ if (!wasVisible) {
42
+ prevDisplay = popup.style.display;
43
+ prevVisibility = popup.style.visibility;
44
+ popup.style.visibility = 'hidden';
45
+ popup.style.display = 'block';
46
+ }
47
+
48
+ // Measure popup dimensions
49
+ popupWidth = popup.offsetWidth;
50
+ popupHeight = popup.offsetHeight;
51
+
52
+ // Apply max constraints
53
+ if (maxWidth && popupWidth > maxWidth) {
54
+ popupWidth = maxWidth;
55
+ }
56
+ if (maxHeight && popupHeight > maxHeight) {
57
+ popupHeight = maxHeight;
58
+ }
59
+
60
+ // Restore hidden state
61
+ if (!wasVisible) {
62
+ popup.style.display = prevDisplay;
63
+ popup.style.visibility = prevVisibility;
64
+ }
65
+
66
+ // Calculate base positions for different orientations
67
+ const positions = {
68
+ 'bottom-right': {
69
+ top: anchorRect.bottom + scrollY + offsetY,
70
+ left: anchorRect.left + scrollX + offsetX
71
+ },
72
+ 'bottom-left': {
73
+ top: anchorRect.bottom + scrollY + offsetY,
74
+ left: anchorRect.right + scrollX - popupWidth - offsetX
75
+ },
76
+ 'top-right': {
77
+ top: anchorRect.top + scrollY - popupHeight - offsetY,
78
+ left: anchorRect.left + scrollX + offsetX
79
+ },
80
+ 'top-left': {
81
+ top: anchorRect.top + scrollY - popupHeight - offsetY,
82
+ left: anchorRect.right + scrollX - popupWidth - offsetX
83
+ }
84
+ };
85
+
86
+ // Start with preferred position
87
+ let position = positions[preferredPosition];
88
+ let transform = '';
89
+
90
+ // Check if popup fits in preferred position
91
+ const fitsInPreferred = this.checkFitsInViewport(position, popupWidth, popupHeight, viewportWidth, viewportHeight, scrollX, scrollY);
92
+
93
+ if (!fitsInPreferred) {
94
+ // Try alternative positions
95
+ const alternativePositions = this.getAlternativePositions(preferredPosition);
96
+
97
+ for (const altPosition of alternativePositions) {
98
+ const testPosition = positions[altPosition];
99
+ if (this.checkFitsInViewport(testPosition, popupWidth, popupHeight, viewportWidth, viewportHeight, scrollX, scrollY)) {
100
+ position = testPosition;
101
+ break;
102
+ }
103
+ }
104
+
105
+ // If no position fits, use the best available with adjustments
106
+ if (!this.checkFitsInViewport(position, popupWidth, popupHeight, viewportWidth, viewportHeight, scrollX, scrollY)) {
107
+ position = this.adjustToFitViewport(position, popupWidth, popupHeight, viewportWidth, viewportHeight, scrollX, scrollY);
108
+ }
109
+ }
110
+
111
+ // For mobile devices, add additional constraints
112
+ if (viewportWidth <= 768) {
113
+ position = this.applyMobileConstraints(position, popupWidth, popupHeight, viewportWidth, viewportHeight, scrollX, scrollY);
114
+ }
115
+
116
+ return {
117
+ top: position.top,
118
+ left: position.left,
119
+ transform: transform
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Check if position fits within viewport
125
+ */
126
+ static checkFitsInViewport(position, width, height, viewportWidth, viewportHeight, scrollX, scrollY) {
127
+ const right = position.left + width;
128
+ const bottom = position.top + height;
129
+
130
+ return position.left >= scrollX &&
131
+ position.top >= scrollY &&
132
+ right <= scrollX + viewportWidth &&
133
+ bottom <= scrollY + viewportHeight;
134
+ }
135
+
136
+ /**
137
+ * Get alternative positions to try
138
+ */
139
+ static getAlternativePositions(preferredPosition) {
140
+ const alternatives = {
141
+ 'bottom-right': ['bottom-left', 'top-right', 'top-left'],
142
+ 'bottom-left': ['bottom-right', 'top-left', 'top-right'],
143
+ 'top-right': ['top-left', 'bottom-right', 'bottom-left'],
144
+ 'top-left': ['top-right', 'bottom-left', 'bottom-right']
145
+ };
146
+
147
+ return alternatives[preferredPosition] || ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
148
+ }
149
+
150
+ /**
151
+ * Adjust position to fit within viewport
152
+ */
153
+ static adjustToFitViewport(position, width, height, viewportWidth, viewportHeight, scrollX, scrollY) {
154
+ let { top, left } = position;
155
+
156
+ // Adjust horizontal position
157
+ if (left < scrollX) {
158
+ left = scrollX + 10;
159
+ } else if (left + width > scrollX + viewportWidth) {
160
+ left = scrollX + viewportWidth - width - 10;
161
+ }
162
+
163
+ // Adjust vertical position
164
+ if (top < scrollY) {
165
+ top = scrollY + 10;
166
+ } else if (top + height > scrollY + viewportHeight) {
167
+ top = scrollY + viewportHeight - height - 10;
168
+ }
169
+
170
+ return { top, left };
171
+ }
172
+
173
+ /**
174
+ * Apply mobile-specific constraints
175
+ */
176
+ static applyMobileConstraints(position, width, height, viewportWidth, viewportHeight, scrollX, scrollY) {
177
+ let { top, left } = position;
178
+
179
+ // On mobile, prefer center positioning if popup is too large
180
+ if (width > viewportWidth * 0.9) {
181
+ left = scrollX + (viewportWidth - width) / 2;
182
+ }
183
+
184
+ if (height > viewportHeight * 0.8) {
185
+ top = scrollY + (viewportHeight - height) / 2;
186
+ }
187
+
188
+ // Ensure minimum margins
189
+ const minMargin = 10;
190
+ if (left < scrollX + minMargin) left = scrollX + minMargin;
191
+ if (top < scrollY + minMargin) top = scrollY + minMargin;
192
+ if (left + width > scrollX + viewportWidth - minMargin) {
193
+ left = scrollX + viewportWidth - width - minMargin;
194
+ }
195
+ if (top + height > scrollY + viewportHeight - minMargin) {
196
+ top = scrollY + viewportHeight - height - minMargin;
197
+ }
198
+
199
+ return { top, left };
200
+ }
201
+
202
+ /**
203
+ * Apply calculated position to popup element
204
+ */
205
+ static applyPosition(popup, position) {
206
+ popup.style.position = 'absolute';
207
+ popup.style.top = `${position.top}px`;
208
+ popup.style.left = `${position.left}px`;
209
+
210
+ if (position.transform) {
211
+ popup.style.transform = position.transform;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Check if device is mobile
217
+ */
218
+ static isMobile() {
219
+ return window.innerWidth <= 768;
220
+ }
221
+
222
+ /**
223
+ * Get recommended max dimensions for mobile
224
+ */
225
+ static getMobileMaxDimensions() {
226
+ const viewportWidth = window.innerWidth;
227
+ const viewportHeight = window.innerHeight;
228
+
229
+ return {
230
+ maxWidth: Math.min(viewportWidth * 0.95, 350),
231
+ maxHeight: Math.min(viewportHeight * 0.8, 400)
232
+ };
233
+ }
234
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Sanitize utilities - dependency-free XSS protection helpers.
3
+ *
4
+ * These functions provide a defense-in-depth layer for the few places where
5
+ * the editor turns user-supplied strings into live DOM (links, images, video,
6
+ * HTML import, code view). They are intentionally conservative: anything that
7
+ * is not provably safe is rejected.
8
+ */
9
+
10
+ // URL schemes that are safe to use as navigable links / resource sources.
11
+ const SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:', 'tel:', 'ftp:'];
12
+
13
+ // Trusted iframe embed prefixes (used by the video feature).
14
+ const TRUSTED_IFRAME_PREFIXES = [
15
+ 'https://www.youtube.com/embed/',
16
+ 'https://www.youtube-nocookie.com/embed/',
17
+ 'https://player.vimeo.com/video/'
18
+ ];
19
+
20
+ /**
21
+ * Determine whether a URL is safe to assign to href/src.
22
+ *
23
+ * Relative URLs, anchors and path-only references (no scheme) are considered
24
+ * safe. URLs with an explicit scheme are only allowed if the scheme is in the
25
+ * whitelist. `javascript:`, `vbscript:`, `data:text/html`, etc. are rejected.
26
+ *
27
+ * @param {string} url - URL to validate
28
+ * @param {object} [options]
29
+ * @param {boolean} [options.allowDataImage] - allow `data:image/*` (except SVG)
30
+ * @returns {boolean}
31
+ */
32
+ export function isSafeUrl(url, { allowDataImage = false } = {}) {
33
+ if (typeof url !== 'string') return false;
34
+
35
+ const trimmed = url.trim();
36
+ if (trimmed === '') return false;
37
+
38
+ // Strip control/whitespace characters that are commonly used to smuggle
39
+ // schemes past naive validators (e.g. "java\tscript:alert(1)").
40
+ const stripped = trimmed.replace(/[\u0000-\u0020\u007F-\u009F]/g, '');
41
+
42
+ // Detect a leading scheme. If there is none it is a relative URL → safe.
43
+ const schemeMatch = stripped.match(/^([a-z][a-z0-9+.-]*):/i);
44
+ if (!schemeMatch) {
45
+ return true;
46
+ }
47
+
48
+ const scheme = schemeMatch[1].toLowerCase() + ':';
49
+
50
+ if (scheme === 'data:') {
51
+ if (!allowDataImage) return false;
52
+ // Allow raster image data URIs only. SVG can carry script, so it is denied.
53
+ return /^data:image\//i.test(stripped) && !/^data:image\/svg/i.test(stripped);
54
+ }
55
+
56
+ return SAFE_URL_SCHEMES.includes(scheme);
57
+ }
58
+
59
+ /**
60
+ * Return the URL if it is safe, otherwise an empty string.
61
+ * @param {string} url
62
+ * @param {object} [options] - same options as isSafeUrl
63
+ * @returns {string}
64
+ */
65
+ export function sanitizeUrl(url, options) {
66
+ return isSafeUrl(url, options) ? url.trim() : '';
67
+ }
68
+
69
+ // Tags that are never allowed in sanitized HTML.
70
+ const FORBIDDEN_TAGS = new Set([
71
+ 'SCRIPT', 'STYLE', 'OBJECT', 'EMBED', 'LINK', 'META', 'BASE',
72
+ 'FORM', 'INPUT', 'BUTTON', 'TEXTAREA', 'SELECT', 'OPTION', 'NOSCRIPT'
73
+ ]);
74
+
75
+ /**
76
+ * Clean a single element node in place: drop event-handler attributes,
77
+ * unsafe href/src URLs and dangerous inline styles.
78
+ * @param {Element} el
79
+ */
80
+ function cleanElement(el) {
81
+ const attrs = Array.from(el.attributes);
82
+ for (const attr of attrs) {
83
+ const name = attr.name.toLowerCase();
84
+ const value = attr.value;
85
+
86
+ // Strip all inline event handlers (onclick, onerror, onload, ...).
87
+ if (name.startsWith('on')) {
88
+ el.removeAttribute(attr.name);
89
+ continue;
90
+ }
91
+
92
+ // Validate URL-bearing attributes.
93
+ if (name === 'href' || name === 'src' || name === 'xlink:href') {
94
+ const allowDataImage = el.tagName === 'IMG';
95
+ if (!isSafeUrl(value, { allowDataImage })) {
96
+ el.removeAttribute(attr.name);
97
+ }
98
+ continue;
99
+ }
100
+
101
+ // Reject styles that can execute script (legacy IE expression / url(javascript:)).
102
+ if (name === 'style' && /expression\s*\(|javascript:/i.test(value)) {
103
+ el.removeAttribute(attr.name);
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Sanitize an HTML string and return safe HTML.
110
+ *
111
+ * Parsing is done with DOMParser so that no scripts execute and no network
112
+ * resources load during sanitization (the parsed document is inert).
113
+ *
114
+ * @param {string} html - untrusted HTML
115
+ * @returns {string} sanitized HTML
116
+ */
117
+ export function sanitizeHtml(html) {
118
+ if (typeof html !== 'string' || html === '') return '';
119
+
120
+ const doc = new DOMParser().parseFromString(html, 'text/html');
121
+ const elements = Array.from(doc.body.querySelectorAll('*'));
122
+
123
+ for (const el of elements) {
124
+ const tag = el.tagName;
125
+
126
+ if (FORBIDDEN_TAGS.has(tag)) {
127
+ el.remove();
128
+ continue;
129
+ }
130
+
131
+ // Iframes are only allowed when pointing at a trusted embed host.
132
+ if (tag === 'IFRAME') {
133
+ const src = el.getAttribute('src') || '';
134
+ const trusted = TRUSTED_IFRAME_PREFIXES.some(prefix => src.startsWith(prefix));
135
+ if (!trusted) {
136
+ el.remove();
137
+ continue;
138
+ }
139
+ }
140
+
141
+ cleanElement(el);
142
+ }
143
+
144
+ return doc.body.innerHTML;
145
+ }
146
+
147
+ /**
148
+ * Sanitize an already-parsed DOM subtree in place (event handlers + unsafe URLs).
149
+ * Use when content is built via the DOM rather than from an HTML string.
150
+ * @param {Element} root
151
+ */
152
+ export function sanitizeNode(root) {
153
+ if (!root) return;
154
+ const elements = Array.from(root.querySelectorAll('*'));
155
+ for (const el of elements) {
156
+ if (FORBIDDEN_TAGS.has(el.tagName)) {
157
+ el.remove();
158
+ continue;
159
+ }
160
+ cleanElement(el);
161
+ }
162
+ }
163
+
164
+ export default { isSafeUrl, sanitizeUrl, sanitizeHtml, sanitizeNode };
package/package.json CHANGED
@@ -1,32 +1,51 @@
1
- {
2
- "name": "@oix1987/yjd",
3
- "version": "1.0.3",
4
- "description": "A powerful rich text editor with real-time content change tracking for web applications using base javascript",
5
- "license": "ISC",
6
- "author": "Oix1987",
7
- "type": "module",
8
- "main": "dist/rich-editor.min.js",
9
- "module": "dist/rich-editor.esm.js",
10
- "exports": {
11
- ".": {
12
- "import": "./dist/rich-editor.esm.js",
13
- "require": "./dist/rich-editor.min.js"
14
- },
15
- "./styles.css": "./dist/styles.css"
16
- },
17
- "types": "index.d.ts",
18
- "files": [
19
- "dist",
20
- "index.d.ts",
21
- "README.md"
22
- ],
23
- "scripts": {
24
- "test": "echo \"Error: no test specified\" && exit 1",
25
- "build": "rollup -c",
26
- "prepublishOnly": "npm run build"
27
- },
28
- "devDependencies": {
29
- "@rollup/plugin-terser": "^0.4.4",
30
- "rollup": "^4.46.3"
31
- }
32
- }
1
+ {
2
+ "name": "@oix1987/yjd",
3
+ "version": "2.1.0",
4
+ "description": "A powerful rich text editor with real-time content change tracking for web applications using base javascript",
5
+ "license": "ISC",
6
+ "author": "Oix1987",
7
+ "type": "module",
8
+ "main": "dist/rich-editor.min.js",
9
+ "module": "dist/rich-editor.esm.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/rich-editor.esm.js",
13
+ "require": "./dist/rich-editor.min.js"
14
+ },
15
+ "./core": "./core.js",
16
+ "./styles.css": "./lib/styles.min.css",
17
+ "./lib/*": "./lib/*",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "types": "index.d.ts",
21
+ "files": [
22
+ "dist",
23
+ "lib",
24
+ "core.js",
25
+ "index.js",
26
+ "umd-entry.js",
27
+ "index.d.ts",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "test": "node --test test/*.test.js",
33
+ "generate:css": "node scripts/generate-css.js",
34
+ "prebuild": "npm run generate:css",
35
+ "build": "rollup -c",
36
+ "prepublishOnly": "npm run build",
37
+ "build:demos": "rollup -c demos/rollup.demos.config.js",
38
+ "build:site": "node scripts/build-site.js",
39
+ "build:pages": "npm run build && node scripts/build-site.js"
40
+ },
41
+ "devDependencies": {
42
+ "@rollup/plugin-terser": "^0.4.4",
43
+ "csso": "^5.0.5",
44
+ "jsdom": "^29.1.1",
45
+ "rollup": "^4.46.3"
46
+ },
47
+ "sideEffects": [
48
+ "./index.js",
49
+ "./umd-entry.js"
50
+ ]
51
+ }