suneditor 3.1.3 → 3.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.1.3",
3
+ "version": "3.1.4",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -77,18 +77,10 @@ class HTML {
77
77
  }
78
78
 
79
79
  const stylesMap = new Map();
80
- const stylesObj = {
81
- ...splitTagStyles,
82
- line: options.get('_lineStylesRegExp'),
83
- };
84
- this.#textStyleTags.forEach((v) => {
85
- stylesObj[v] = options.get('_textStylesRegExp');
86
- });
87
-
88
- for (const key in stylesObj) {
89
- stylesMap.set(new RegExp(`^(${key})$`), stylesObj[key]);
80
+ for (const key in splitTagStyles) {
81
+ stylesMap.set(new RegExp(`^(${key})$`), splitTagStyles[key]);
90
82
  }
91
- this.#cleanStyleTagKeyRegExp = new RegExp(`^(${Object.keys(stylesObj).join('|')})$`, 'i');
83
+ this.#cleanStyleTagKeyRegExp = new RegExp(`^(${Object.keys(splitTagStyles).join('|')})$`, 'i');
92
84
  this.#cleanStyleRegExpMap = stylesMap;
93
85
 
94
86
  // font size unit
@@ -1966,12 +1958,12 @@ class HTML {
1966
1958
  v.push(sv[0]);
1967
1959
  }
1968
1960
  } else if (!v || !_RE_STYLE_EQ.test(v.toString())) {
1969
- if (this.#textStyleTags.includes(tagName)) {
1961
+ if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
1970
1962
  v = this.#cleanStyle(m, v, tagName);
1971
1963
  } else if (this.#$.format.isLine(tagName)) {
1972
- v = this.#cleanStyle(m, v, 'line');
1973
- } else if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
1974
- v = this.#cleanStyle(m, v, tagName);
1964
+ v = this.#cleanStyle(m, v, '@line');
1965
+ } else if (this.#textStyleTags.includes(tagName)) {
1966
+ v = this.#cleanStyle(m, v, '@text');
1975
1967
  }
1976
1968
  }
1977
1969
 
@@ -67,6 +67,8 @@ export const DEFAULTS = {
67
67
  'vertical-align|visibility|' +
68
68
  'white-space|word-break|word-wrap',
69
69
  TAG_STYLES: {
70
+ '@text': 'font-family|font-size|color|background-color|width|height',
71
+ '@line': 'text-align|margin|margin-left|margin-right|line-height',
70
72
  'table|th|td': 'border|border-[a-z]+|color|background-color|text-align|float|font-weight|text-decoration|font-style|vertical-align',
71
73
  'table|td': 'width',
72
74
  tr: 'height',
@@ -78,8 +80,6 @@ export const DEFAULTS = {
78
80
  'img|video|iframe': 'transform|transform-origin|width|min-width|max-width|height|min-height|max-height|float|margin|margin-top',
79
81
  hr: '',
80
82
  },
81
- SPAN_STYLES: 'font-family|font-size|color|background-color|width|height',
82
- LINE_STYLES: 'text-align|margin|margin-left|margin-right|line-height',
83
83
 
84
84
  RETAIN_STYLE_MODE: ['repeat', 'always', 'none'],
85
85
  };
@@ -317,7 +317,7 @@ export const DEFAULTS = {
317
317
  * - `classFilter`: Filters disallowed CSS class names (`allowedClassName`)
318
318
  * - `textStyleTagFilter`: Filters text style tags (b, i, u, span, etc.)
319
319
  * - `attrFilter`: Filters disallowed HTML attributes (`attributeWhitelist`/`attributeBlacklist`)
320
- * - `styleFilter`: Filters disallowed inline styles (`spanStyles`/`lineStyles`/`allUsedStyles`)
320
+ * - `styleFilter`: Filters disallowed inline styles (per-tag from `tagStyles`)
321
321
  * ```js
322
322
  * // disable only attribute and style filters
323
323
  * {
@@ -393,19 +393,23 @@ export const DEFAULTS = {
393
393
  * ```js
394
394
  * { allUsedStyles: 'color|background-color|text-shadow' }
395
395
  * ```
396
- * @property {Object<string, string>} [tagStyles={}] - Specifies allowed styles for HTML tags. Key is tag name(s), value is pipe-delimited allowed styles.
396
+ * @property {Object<string, string>} [tagStyles={}] - Specifies allowed CSS styles per HTML tag.
397
+ * - Key is a tag name, multiple tags joined with `|`, or a category sentinel (`@text`, `@line`).
398
+ * - Value is a pipe-delimited list of allowed style names.
399
+ * - Resolution order when filtering an element: explicit tag entry → `@line` (for formatLine elements) → `@text` (for textStyleTags).
400
+ * - An explicit tag entry **replaces** the category default — include category styles in the value if you want both.
401
+ * - Merged with {@link DEFAULTS.TAG_STYLES}; user-supplied keys win.
397
402
  * ```js
398
403
  * {
399
404
  * tagStyles: {
400
- * 'table|td': 'border|color|background-color',
401
- * hr: 'border-top'
405
+ * '@text': 'color|font-size|background-color', // default for span, b, i, em, ...
406
+ * '@line': 'text-align|margin|line-height', // default for p, h1-h6, div, li, ...
407
+ * 'table|td': 'border|color|background-color', // per-tag whitelist
408
+ * div: 'color', // explicit override; ignores `@line` default
409
+ * hr: 'border-top',
402
410
  * }
403
411
  * }
404
412
  * ```
405
- * @property {string} [spanStyles=CONSTANTS.SPAN_STYLES] - Specifies allowed styles for the `span` tag.
406
- * - The default follows {@link DEFAULTS.SPAN_STYLES}
407
- * @property {string} [lineStyles=CONSTANTS.LINE_STYLES] - Specifies allowed styles for the `line` element (p..).
408
- * - The default follows {@link DEFAULTS.LINE_STYLES}
409
413
  * @property {Array<string>} [fontSizeUnits=CONSTANTS.SIZE_UNITS] - Allowed font size units.
410
414
  * - The default follows {@link DEFAULTS.SIZE_UNITS}
411
415
  * @property {"repeat"|"always"|"none"} [retainStyleMode="repeat"] - Determines how inline elements (e.g. `<span>`, `<strong>`) are handled when deleting text.
@@ -625,8 +629,6 @@ export const DEFAULTS = {
625
629
  * @property {string[]} [_reverseCommandArray] - Internal key shortcut matcher for reverse commands.
626
630
  * @property {string} [_subMode] - Sub toolbar mode (e.g., `balloon`).
627
631
  * @property {string[]} [_textStyleTags] - Tag names used for text styling, plus span/li.
628
- * @property {RegExp} [_textStylesRegExp] - Regex to match inline styles (e.g., fontSize, color).
629
- * @property {RegExp} [_lineStylesRegExp] - Regex to match line styles (e.g., text-align, padding).
630
632
  * @property {Object<string, string>} [_defaultStyleTagMap] - Mapping HTML tag => standard tag.
631
633
  * @property {Object<string, string>} [_styleCommandMap] - Mapping HTML tag => command (e.g., bold, underline).
632
634
  * @property {Object<string, string>} [_defaultTagCommand] - Mapping command => preferred tag.
@@ -723,8 +725,6 @@ export const OPTION_FIXED_FLAG = {
723
725
  convertTextTags: 'fixed',
724
726
  __tagStyles: 'fixed',
725
727
  tagStyles: 'fixed',
726
- spanStyles: 'fixed',
727
- lineStyles: 'fixed',
728
728
  textDirection: true,
729
729
  reverseButtons: 'fixed',
730
730
  historyStackDelayTime: true,
@@ -788,7 +788,7 @@ export const OPTION_FIXED_FLAG = {
788
788
  * @property {boolean} classFilter - Filters disallowed CSS class names (`allowedClassName`)
789
789
  * @property {boolean} textStyleTagFilter - Filters text style tags (b, i, u, span, etc.)
790
790
  * @property {boolean} attrFilter - Filters disallowed HTML attributes (`attributeWhitelist`/`attributeBlacklist`)
791
- * @property {boolean} styleFilter - Filters disallowed inline styles (`spanStyles`/`lineStyles`/`allUsedStyles`)
791
+ * @property {boolean} styleFilter - Filters disallowed inline styles (per-tag from `tagStyles`)
792
792
  */
793
793
 
794
794
  /**
@@ -516,8 +516,6 @@ export function InitOptions(options, editorTargets, plugins) {
516
516
  return _default;
517
517
  }, {}),
518
518
  );
519
- o.set('_textStylesRegExp', new RegExp(`\\s*[^-a-zA-Z](${DEFAULTS.SPAN_STYLES}${options.spanStyles ? '|' + options.spanStyles : ''})\\s*:[^;]+(?!;)*`, 'gi'));
520
- o.set('_lineStylesRegExp', new RegExp(`\\s*[^-a-zA-Z](${DEFAULTS.LINE_STYLES}${options.lineStyles ? '|' + options.lineStyles : ''})\\s*:[^;]+(?!;)*`, 'gi'));
521
519
  o.set('_defaultStyleTagMap', {
522
520
  strong: textTags.bold,
523
521
  b: textTags.bold,
@@ -760,21 +758,13 @@ export function InitOptions(options, editorTargets, plugins) {
760
758
 
761
759
  /** Create all used styles */
762
760
  const allUsedStyles = new Set(DEFAULTS.CONTENT_STYLES.split('|'));
763
- const _ss = options.spanStyles?.split('|') || [];
764
761
  const _ls = o.get('__listCommonStyle');
765
- const _dts = DEFAULTS.SPAN_STYLES.split('|');
766
- for (let i = 0, len = _dts.length; i < len; i++) {
767
- allUsedStyles.add(_dts[i]);
768
- }
769
762
  for (const _ts of Object.values(o.get('tagStyles'))) {
770
763
  const _tss = _ts.split('|');
771
764
  for (let i = 0, len = _tss.length; i < len; i++) {
772
765
  allUsedStyles.add(_tss[i]);
773
766
  }
774
767
  }
775
- for (let i = 0, len = _ss.length; i < len; i++) {
776
- allUsedStyles.add(_ss[i]);
777
- }
778
768
  for (let i = 0, len = _ls.length; i < len; i++) {
779
769
  allUsedStyles.add(_ls[i]);
780
770
  }
@@ -40,7 +40,7 @@ import ApiManager from '../manager/ApiManager';
40
40
  * ]
41
41
  * }
42
42
  * ```
43
- * @property {Object<string, string>} [searchUrlHeader] - File server search http header. Optional. Can be overridden in browser.
43
+ * @property {Object<string, string>} [searchHeaders] - File server search http header. Optional. Can be overridden in browser.
44
44
  * @property {string} [listClass] - Class name of list div. Required. Can be overridden in browser.
45
45
  * @property {(item: BrowserFile) => string} [drawItemHandler] - Function that returns HTML string for rendering each file item. Required. Can be overridden in browser.
46
46
  * ```js
@@ -114,9 +114,9 @@ class Browser {
114
114
  this.listClass = params.listClass || 'se-preview-list';
115
115
  this.directData = params.data;
116
116
  this.url = params.url;
117
- this.urlHeader = params.headers;
117
+ this.headers = params.headers;
118
118
  this.searchUrl = params.searchUrl;
119
- this.searchUrlHeader = params.searchUrlHeader;
119
+ this.searchHeaders = params.searchHeaders;
120
120
  this.drawItemHandler = (params.drawItemHandler || DrawItems).bind({ thumbnail: params.thumbnail, props: params.props || [] });
121
121
  this.selectorHandler = params.selectorHandler;
122
122
  this.columnSize = params.columnSize || 4;
@@ -176,7 +176,7 @@ class Browser {
176
176
  * @param {string} [params.listClass] - Class name of list div. If not, use `this.listClass`.
177
177
  * @param {string} [params.title] - File browser window title. If not, use `this.title`.
178
178
  * @param {string} [params.url] - File server url. If not, use `this.url`.
179
- * @param {Object<string, string>} [params.urlHeader] - File server http header. If not, use `this.urlHeader`.
179
+ * @param {Object<string, string>} [params.headers] - File server http header. If not, use `this.headers`.
180
180
  * @example
181
181
  * // Open with default settings (configured at construction):
182
182
  * this.browser.open();
@@ -185,7 +185,7 @@ class Browser {
185
185
  * this.browser.open({
186
186
  * title: 'Select a video',
187
187
  * url: '/api/videos',
188
- * urlHeader: { Authorization: 'Bearer token' },
188
+ * headers: { Authorization: 'Bearer token' },
189
189
  * });
190
190
  */
191
191
  open(params = {}) {
@@ -205,7 +205,7 @@ class Browser {
205
205
  if (this.directData) {
206
206
  this.#drowItems(this.directData);
207
207
  } else {
208
- this.#drawFileList(params.url || this.url, params.urlHeader || this.urlHeader, false);
208
+ this.#drawFileList(params.url || this.url, params.headers || this.headers, false);
209
209
  }
210
210
 
211
211
  this.body.style.maxHeight = dom.utils.getClientSize().h - (this.#$.offset.getGlobal(this.body).top - _w.scrollY) - 20 + 'px';
@@ -246,7 +246,8 @@ class Browser {
246
246
  search(keyword) {
247
247
  if (this.searchUrl) {
248
248
  this.keyword = keyword;
249
- this.#drawFileList(this.searchUrl + '?keyword=' + keyword, this.searchUrlHeader, false);
249
+ const sep = this.searchUrl.includes('?') ? '&' : '?';
250
+ this.#drawFileList(this.searchUrl + sep + 'keyword=' + _w.encodeURIComponent(keyword), this.searchHeaders, false);
250
251
  } else {
251
252
  this.keyword = keyword.toLowerCase();
252
253
  this.#drawListItem(this.#allItems.length > 0 ? this.#allItems : this.items, false);
@@ -302,11 +303,11 @@ class Browser {
302
303
  /**
303
304
  * @description Fetches the file list from the server.
304
305
  * @param {string} url - The file server URL.
305
- * @param {Object<string, string>} urlHeader - The HTTP headers for the request.
306
+ * @param {Object<string, string>} headers - The HTTP headers for the request.
306
307
  * @param {boolean} pageLoading - Indicates if this is a paginated request.
307
308
  */
308
- #drawFileList(url, urlHeader, pageLoading) {
309
- this.apiManager.call({ method: 'GET', url, headers: urlHeader, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) });
309
+ #drawFileList(url, headers, pageLoading) {
310
+ this.apiManager.call({ method: 'GET', url, headers, callBack: this.#CallBackGet.bind(this), errorCallBack: this.#CallBackError.bind(this) });
310
311
  if (!pageLoading) {
311
312
  this.sideOpenBtn.style.display = 'none';
312
313
  this.showBrowserLoading();
@@ -601,7 +602,7 @@ class Browser {
601
602
  this.selectedTags = [];
602
603
 
603
604
  if (typeof data === 'string') {
604
- this.#drawFileList(data, this.urlHeader, true);
605
+ this.#drawFileList(data, this.headers, true);
605
606
  } else {
606
607
  this.#drawListItem(data, true);
607
608
  }
@@ -1553,6 +1553,7 @@ class Figure {
1553
1553
  this.setAlign(this._element, value);
1554
1554
  this.selectMenu_align.close();
1555
1555
  this.#$.component.select(this._element, this.kind);
1556
+ this.#$.history.push(false);
1556
1557
  }
1557
1558
 
1558
1559
  /**
@@ -1561,6 +1562,7 @@ class Figure {
1561
1562
  #SetMenuAs(value) {
1562
1563
  this.convertAsFormat(this._element, value);
1563
1564
  this.selectMenu_as.close();
1565
+ this.#$.history.push(false);
1564
1566
  }
1565
1567
 
1566
1568
  /**
@@ -1583,6 +1585,7 @@ class Figure {
1583
1585
 
1584
1586
  this.selectMenu_resize.close();
1585
1587
  this.#$.component.select(this._element, this.kind);
1588
+ this.#$.history.push(false);
1586
1589
  }
1587
1590
 
1588
1591
  #OffFigureContainer() {
@@ -10,6 +10,9 @@ const DIRECTION_CURSOR_MAP = { w: 'ns-resize', h: 'ew-resize', c: 'nwse-resize',
10
10
  class Modal {
11
11
  #$;
12
12
 
13
+ /** @type {Node} */
14
+ #targetElement;
15
+
13
16
  /** @type {HTMLElement} */
14
17
  #modalArea;
15
18
  /** @type {HTMLElement} */
@@ -175,6 +178,8 @@ class Modal {
175
178
  }
176
179
 
177
180
  if (this.focusElement) this.focusElement.focus();
181
+
182
+ this.#targetElement = this.#$.component.currentTarget;
178
183
  }
179
184
 
180
185
  /**
@@ -205,7 +210,13 @@ class Modal {
205
210
  this.inst.modalInit?.();
206
211
  this.inst.modalOff?.(this.isUpdate);
207
212
 
208
- if (!this.isUpdate) this.#$.focusManager.focus();
213
+ if (!this.isUpdate) {
214
+ this.#$.focusManager.focus();
215
+ } else if (this.#targetElement) {
216
+ this.inst.componentSelect?.(this.#targetElement);
217
+ }
218
+
219
+ this.#targetElement = null;
209
220
  }
210
221
 
211
222
  /**
@@ -209,6 +209,8 @@ class ModalAnchorEditor {
209
209
  /**
210
210
  * @description Creates an anchor (`<a>`) element with the specified attributes.
211
211
  * @param {boolean} notText - If `true`, the anchor will not contain text content.
212
+ * @param {?string} [urlOverride] - Fallback URL used when the modal's `linkValue` is empty
213
+ * - (e.g., when called outside the modal lifecycle such as from `retainFormat`)
212
214
  * @returns {HTMLElement|null} - The newly created anchor element, or `null` if the URL is empty.
213
215
  * @example
214
216
  * // In a link plugin — create anchor with text content:
@@ -221,11 +223,14 @@ class ModalAnchorEditor {
221
223
  * if (anchor) {
222
224
  * anchor.appendChild(imgElement);
223
225
  * }
226
+ *
227
+ * // Preserve an existing parent anchor's href when rebuilding outside the modal:
228
+ * const anchor = this.anchor.create(true, parentAnchor.href);
224
229
  */
225
- create(notText) {
226
- if (this.linkValue.length === 0) return null;
230
+ create(notText, urlOverride) {
231
+ const url = this.linkValue || urlOverride || '';
232
+ if (url.length === 0) return null;
227
233
 
228
- const url = this.linkValue;
229
234
  const displayText = this.displayInput.value.length === 0 ? url : this.displayInput.value;
230
235
 
231
236
  const oA = /** @type {HTMLAnchorElement} */ (this.currentTarget || dom.utils.createElement('A'));
@@ -19,6 +19,10 @@ import { Browser } from '../../modules/contract';
19
19
  * }
20
20
  * ```
21
21
  * @property {Object<string, string>} [headers] - Server request headers
22
+ * @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
23
+ * as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
24
+ * already-loaded items locally.
25
+ * @property {Object<string, string>} [searchHeaders] - Server-side search request headers
22
26
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
23
27
  */
24
28
 
@@ -51,6 +55,8 @@ class AudioGallery extends PluginBrowser {
51
55
  data: pluginOptions.data,
52
56
  url: pluginOptions.url,
53
57
  headers: pluginOptions.headers,
58
+ searchUrl: pluginOptions.searchUrl,
59
+ searchHeaders: pluginOptions.searchHeaders,
54
60
  selectorHandler: this.#SetItem.bind(this),
55
61
  columnSize: 4,
56
62
  className: 'se-audio-gallery',
@@ -26,6 +26,10 @@ import { Browser } from '../../modules/contract';
26
26
  * }
27
27
  * ```
28
28
  * @property {Object<string, string>} [headers] - Server request headers
29
+ * @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
30
+ * as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
31
+ * already-loaded items locally.
32
+ * @property {Object<string, string>} [searchHeaders] - Server-side search request headers
29
33
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail URL or a function that returns a thumbnail URL per item.
30
34
  * @property {number} [expand=1] - Initial folder expand depth. `1` expands the first level, `Infinity` expands all. Default: `1`.
31
35
  * @property {Array<string>} [props] - Additional tag names
@@ -64,6 +68,8 @@ class FileBrowser extends PluginBrowser {
64
68
  data: pluginOptions.data,
65
69
  url: pluginOptions.url,
66
70
  headers: pluginOptions.headers,
71
+ searchUrl: pluginOptions.searchUrl,
72
+ searchHeaders: pluginOptions.searchHeaders,
67
73
  selectorHandler: this.#SetItem.bind(this),
68
74
  columnSize: 4,
69
75
  className: 'se-file-browser',
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
20
20
  * }
21
21
  * ```
22
22
  * @property {Object<string, string>} [headers] - Server request headers
23
+ * @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
24
+ * as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
25
+ * already-loaded items locally.
26
+ * @property {Object<string, string>} [searchHeaders] - Server-side search request headers
23
27
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
24
28
  */
25
29
 
@@ -52,6 +56,8 @@ class FileGallery extends PluginBrowser {
52
56
  data: pluginOptions.data,
53
57
  url: pluginOptions.url,
54
58
  headers: pluginOptions.headers,
59
+ searchUrl: pluginOptions.searchUrl,
60
+ searchHeaders: pluginOptions.searchHeaders,
55
61
  selectorHandler: this.#SetItem.bind(this),
56
62
  columnSize: 4,
57
63
  className: 'se-file-gallery',
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
20
20
  * }
21
21
  * ```
22
22
  * @property {Object<string, string>} [headers] - Server request headers
23
+ * @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
24
+ * as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
25
+ * already-loaded items locally.
26
+ * @property {Object<string, string>} [searchHeaders] - Server-side search request headers
23
27
  */
24
28
 
25
29
  /**
@@ -50,6 +54,8 @@ class ImageGallery extends PluginBrowser {
50
54
  data: pluginOptions.data,
51
55
  url: pluginOptions.url,
52
56
  headers: pluginOptions.headers,
57
+ searchUrl: pluginOptions.searchUrl,
58
+ searchHeaders: pluginOptions.searchHeaders,
53
59
  selectorHandler: this.#SetItem.bind(this),
54
60
  columnSize: 4,
55
61
  className: 'se-image-gallery',
@@ -20,6 +20,10 @@ import { Browser } from '../../modules/contract';
20
20
  * }
21
21
  * ```
22
22
  * @property {Object<string, string>} [headers] - Server request headers
23
+ * @property {string} [searchUrl] - Server-side search URL. When set, the keyword is sent to this URL
24
+ * as `?keyword=<value>` and the server response replaces the list. When not set, search filters the
25
+ * already-loaded items locally.
26
+ * @property {Object<string, string>} [searchHeaders] - Server-side search request headers
23
27
  * @property {string|((item: SunEditor.Module.Browser.File) => string)} [thumbnail] - Default thumbnail
24
28
  */
25
29
 
@@ -52,6 +56,8 @@ class VideoGallery extends PluginBrowser {
52
56
  data: pluginOptions.data,
53
57
  url: pluginOptions.url,
54
58
  headers: pluginOptions.headers,
59
+ searchUrl: pluginOptions.searchUrl,
60
+ searchHeaders: pluginOptions.searchHeaders,
55
61
  selectorHandler: this.#SetItem.bind(this),
56
62
  columnSize: 4,
57
63
  className: 'se-video-gallery',
@@ -39,6 +39,17 @@ const { _w, NO_EVENT } = env;
39
39
  * { query_vimeo: 'autoplay=1' }
40
40
  * ```
41
41
  * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns to recognize as embeddable content.
42
+ * @property {Array<RegExp|string>} [scriptSrcWhitelist] - Allowed `<script src=...>` patterns for raw embed HTML
43
+ * - (e.g. Twitter blockquote + `widgets.js`). Each entry is a `RegExp` (tested against the full src) or a `string` (matched via `startsWith`).
44
+ * - Defaults to `[]` — all script tags are rejected.** Inline scripts (no `src`) are always rejected.
45
+ * ```js
46
+ * {
47
+ * scriptSrcWhitelist: [
48
+ * /^https:\/\/platform\.x\.com\/widgets\.js$/,
49
+ * /^https:\/\/www\.instagram\.com\/embed\.js$/,
50
+ * ]
51
+ * }
52
+ * ```
42
53
  * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom embed service definitions.
43
54
  * Each key is a service name, with `pattern` to match the URL, `action` to transform it into an embed URL, and `tag` for the output element.
44
55
  * ```js
@@ -125,6 +136,18 @@ class Embed extends PluginModal {
125
136
  return false;
126
137
  }
127
138
 
139
+ /**
140
+ * @description Validate a `<script src>` against the configured whitelist for raw embed HTML.
141
+ * Inline scripts (no `src`) are always rejected.
142
+ * @param {string} src
143
+ * @param {Array<RegExp|string>} whitelist
144
+ * @returns {boolean}
145
+ */
146
+ static #isAllowedScriptSrc(src, whitelist) {
147
+ if (!src) return false;
148
+ return whitelist.some((p) => (p instanceof RegExp ? p.test(src) : src.startsWith(p)));
149
+ }
150
+
128
151
  /** @type {Array<RegExp>} */
129
152
  static #urlPatterns = null;
130
153
 
@@ -168,6 +191,7 @@ class Embed extends PluginModal {
168
191
  iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
169
192
  query_youtube: pluginOptions.query_youtube || '',
170
193
  query_vimeo: pluginOptions.query_vimeo || '',
194
+ scriptSrcWhitelist: Array.isArray(pluginOptions.scriptSrcWhitelist) ? pluginOptions.scriptSrcWhitelist : [],
171
195
  insertBehavior: pluginOptions.insertBehavior,
172
196
  };
173
197
 
@@ -446,6 +470,19 @@ class Embed extends PluginModal {
446
470
  if (/^<iframe\s|^<blockquote\s/i.test(src)) {
447
471
  const embedDOM = new DOMParser().parseFromString(src, 'text/html').body.children;
448
472
  if (embedDOM.length === 0) return false;
473
+
474
+ // Validate every iframe in the raw HTML against the URL whitelist.
475
+ for (let i = 0; i < embedDOM.length; i++) {
476
+ const node = /** @type {Element} */ (embedDOM[i]);
477
+ if (/^iframe$/i.test(node.nodeName) && !Embed.#checkContentType(node.getAttribute('src') || '')) return false;
478
+ const nested = node.querySelectorAll?.('iframe');
479
+ if (nested) {
480
+ for (let j = 0; j < nested.length; j++) {
481
+ if (!Embed.#checkContentType(nested[j].getAttribute('src') || '')) return false;
482
+ }
483
+ }
484
+ }
485
+
449
486
  embedInfo = { children: embedDOM, ...this.#getInfo(), process: null };
450
487
  } else {
451
488
  const processUrl = this.findProcessUrl(src);
@@ -604,9 +641,12 @@ class Embed extends PluginModal {
604
641
  container = figure.container;
605
642
 
606
643
  const childNodes = Array.from(children);
644
+ const scriptWhitelist = this.pluginOptions.scriptSrcWhitelist;
607
645
  for (const chd of childNodes) {
608
646
  if (/^script$/i.test(chd.nodeName)) {
609
- scriptTag = dom.utils.createElement('script', { src: /** @type {Element} */ (chd).getAttribute('src'), async: 'true' }, null);
647
+ const scriptSrc = /** @type {Element} */ (chd).getAttribute('src') || '';
648
+ if (!Embed.#isAllowedScriptSrc(scriptSrc, scriptWhitelist)) continue;
649
+ scriptTag = dom.utils.createElement('script', { src: scriptSrc, async: 'true' }, null);
610
650
  continue;
611
651
  }
612
652
  cover.appendChild(chd);
@@ -774,7 +774,7 @@ class Image_ extends PluginModal {
774
774
 
775
775
  // link
776
776
  let isNewAnchor = null;
777
- const anchor = this.anchor.create(true);
777
+ const anchor = this.anchor.create(true, dom.check.isAnchor(this.#element.parentElement) ? this.#element.parentElement.href : null);
778
778
  if (anchor) {
779
779
  if (this.#linkElement !== anchor || (isNewContainer && !container.contains(anchor))) {
780
780
  this.#linkElement = anchor.cloneNode(false);
@@ -839,6 +839,7 @@ class Image_ extends PluginModal {
839
839
  */
840
840
  #setAnchor(imgTag, anchor) {
841
841
  if (anchor) {
842
+ /** @type {HTMLAnchorElement} */ (anchor).setAttribute('data-se-non-link', 'true');
842
843
  anchor.appendChild(imgTag);
843
844
  return anchor;
844
845
  }