suneditor 3.0.0-rc.5 → 3.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.
Files changed (118) hide show
  1. package/README.md +3 -2
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +1 -1
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +3 -3
  6. package/src/assets/design/color.css +14 -2
  7. package/src/assets/design/typography.css +5 -0
  8. package/src/assets/icons/defaultIcons.js +22 -4
  9. package/src/assets/suneditor-contents.css +1 -1
  10. package/src/assets/suneditor.css +312 -18
  11. package/src/core/config/eventManager.js +6 -9
  12. package/src/core/editor.js +1 -1
  13. package/src/core/event/actions/index.js +5 -0
  14. package/src/core/event/effects/keydown.registry.js +25 -0
  15. package/src/core/event/eventOrchestrator.js +69 -2
  16. package/src/core/event/handlers/handler_ww_mouse.js +1 -0
  17. package/src/core/event/rules/keydown.rule.backspace.js +9 -1
  18. package/src/core/kernel/coreKernel.js +4 -0
  19. package/src/core/kernel/store.js +2 -0
  20. package/src/core/logic/dom/html.js +110 -11
  21. package/src/core/logic/dom/offset.js +89 -35
  22. package/src/core/logic/dom/selection.js +46 -19
  23. package/src/core/logic/panel/finder.js +982 -0
  24. package/src/core/logic/panel/menu.js +8 -6
  25. package/src/core/logic/panel/toolbar.js +112 -19
  26. package/src/core/logic/panel/viewer.js +214 -43
  27. package/src/core/logic/shell/_commandExecutor.js +7 -1
  28. package/src/core/logic/shell/commandDispatcher.js +1 -1
  29. package/src/core/logic/shell/component.js +5 -7
  30. package/src/core/logic/shell/history.js +24 -0
  31. package/src/core/logic/shell/shortcuts.js +5 -3
  32. package/src/core/logic/shell/ui.js +25 -26
  33. package/src/core/schema/frameContext.js +15 -1
  34. package/src/core/schema/options.js +75 -16
  35. package/src/core/section/constructor.js +62 -21
  36. package/src/core/section/documentType.js +1 -1
  37. package/src/events.js +12 -0
  38. package/src/helper/clipboard.js +1 -1
  39. package/src/helper/dom/domUtils.js +5 -14
  40. package/src/helper/index.js +3 -0
  41. package/src/helper/markdown.js +876 -0
  42. package/src/langs/ckb.js +9 -0
  43. package/src/langs/cs.js +9 -0
  44. package/src/langs/da.js +9 -0
  45. package/src/langs/de.js +9 -0
  46. package/src/langs/en.js +9 -0
  47. package/src/langs/es.js +9 -0
  48. package/src/langs/fa.js +9 -0
  49. package/src/langs/fr.js +9 -0
  50. package/src/langs/he.js +9 -0
  51. package/src/langs/hu.js +9 -0
  52. package/src/langs/it.js +9 -0
  53. package/src/langs/ja.js +9 -0
  54. package/src/langs/km.js +9 -0
  55. package/src/langs/ko.js +9 -0
  56. package/src/langs/lv.js +9 -0
  57. package/src/langs/nl.js +9 -0
  58. package/src/langs/pl.js +9 -0
  59. package/src/langs/pt_br.js +9 -0
  60. package/src/langs/ro.js +9 -0
  61. package/src/langs/ru.js +9 -0
  62. package/src/langs/se.js +9 -0
  63. package/src/langs/tr.js +9 -0
  64. package/src/langs/uk.js +9 -0
  65. package/src/langs/ur.js +9 -0
  66. package/src/langs/zh_cn.js +9 -0
  67. package/src/modules/contract/Controller.js +50 -39
  68. package/src/modules/manager/ApiManager.js +27 -4
  69. package/src/modules/manager/FileManager.js +1 -1
  70. package/src/modules/ui/SelectMenu.js +22 -11
  71. package/src/plugins/command/codeBlock.js +324 -0
  72. package/src/plugins/command/exportPDF.js +15 -3
  73. package/src/plugins/dropdown/blockStyle.js +1 -1
  74. package/src/plugins/dropdown/paragraphStyle.js +1 -2
  75. package/src/plugins/dropdown/table/render/table.html.js +1 -1
  76. package/src/plugins/dropdown/table/services/table.grid.js +16 -8
  77. package/src/plugins/dropdown/table/services/table.style.js +5 -9
  78. package/src/plugins/index.js +3 -0
  79. package/src/plugins/input/fontSize.js +4 -2
  80. package/src/plugins/modal/audio.js +2 -1
  81. package/src/plugins/modal/image/index.js +2 -1
  82. package/src/plugins/modal/math.js +2 -1
  83. package/src/plugins/modal/video/index.js +2 -1
  84. package/src/themes/cobalt.css +13 -4
  85. package/src/themes/cream.css +44 -35
  86. package/src/themes/dark.css +13 -4
  87. package/src/themes/midnight.css +13 -4
  88. package/src/typedef.js +4 -4
  89. package/types/assets/icons/defaultIcons.d.ts +12 -1
  90. package/types/core/config/eventManager.d.ts +6 -8
  91. package/types/core/event/actions/index.d.ts +1 -0
  92. package/types/core/event/effects/keydown.registry.d.ts +2 -0
  93. package/types/core/event/eventOrchestrator.d.ts +2 -1
  94. package/types/core/kernel/coreKernel.d.ts +5 -0
  95. package/types/core/kernel/store.d.ts +5 -0
  96. package/types/core/logic/dom/offset.d.ts +16 -3
  97. package/types/core/logic/dom/selection.d.ts +3 -3
  98. package/types/core/logic/panel/finder.d.ts +83 -0
  99. package/types/core/logic/panel/toolbar.d.ts +14 -1
  100. package/types/core/logic/panel/viewer.d.ts +22 -2
  101. package/types/core/logic/shell/shortcuts.d.ts +1 -1
  102. package/types/core/schema/frameContext.d.ts +22 -0
  103. package/types/core/schema/options.d.ts +153 -31
  104. package/types/events.d.ts +11 -0
  105. package/types/helper/dom/domUtils.d.ts +2 -2
  106. package/types/helper/index.d.ts +5 -0
  107. package/types/helper/markdown.d.ts +27 -0
  108. package/types/langs/_Lang.d.ts +9 -0
  109. package/types/modules/contract/Controller.d.ts +8 -1
  110. package/types/modules/ui/SelectMenu.d.ts +12 -0
  111. package/types/plugins/command/codeBlock.d.ts +53 -0
  112. package/types/plugins/index.d.ts +3 -0
  113. package/types/plugins/input/fontSize.d.ts +6 -2
  114. package/types/plugins/modal/audio.d.ts +4 -2
  115. package/types/plugins/modal/image/index.d.ts +3 -1
  116. package/types/plugins/modal/math.d.ts +3 -1
  117. package/types/plugins/modal/video/index.d.ts +3 -1
  118. package/types/typedef.d.ts +5 -2
@@ -1,4 +1,4 @@
1
- import { dom, numbers, env } from '../../helper';
1
+ import { dom, env } from '../../helper';
2
2
  import { _DragHandle } from '../../modules/ui';
3
3
 
4
4
  const { _w, _d, NO_EVENT } = env;
@@ -54,9 +54,6 @@ class EventManager {
54
54
 
55
55
  return NO_EVENT;
56
56
  };
57
-
58
- /** @type {HTMLInputElement} */
59
- this.__focusTemp = contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
60
57
  }
61
58
 
62
59
  /**
@@ -64,7 +61,7 @@ class EventManager {
64
61
  * - Only events registered with this method are unregistered or re-registered when methods such as 'setOptions', 'destroy' are called.
65
62
  * @param {*} target Target element
66
63
  * @param {string} type Event type
67
- * @param {EventListenerOrEventListenerObject} listener Event handler
64
+ * @param {*} listener Event handler
68
65
  * @param {boolean|AddEventListenerOptions} [useCapture] Event useCapture option
69
66
  * @return {?SunEditor.Event.Info} Registered event information
70
67
  */
@@ -85,7 +82,7 @@ class EventManager {
85
82
  }
86
83
 
87
84
  return {
88
- target: len > 1 ? target : target[0],
85
+ target,
89
86
  type,
90
87
  listener,
91
88
  useCapture,
@@ -106,7 +103,7 @@ class EventManager {
106
103
  const useCapture = params.useCapture;
107
104
 
108
105
  if (!target) return;
109
- if (!numbers.is(target.length) || target.nodeName || (!Array.isArray(target) && target.length < 1)) target = /** @type {Array<Element>} */ ([target]);
106
+ if (target === _w || target === _d || typeof target.length !== 'number' || target.nodeType || (!Array.isArray(target) && target.length < 1)) target = [target];
110
107
  if (target.length === 0) return;
111
108
 
112
109
  for (let i = 0, len = target.length; i < len; i++) {
@@ -120,7 +117,7 @@ class EventManager {
120
117
  * @description Add an event to document.
121
118
  * - When created as an Iframe, the same event is added to the document in the Iframe.
122
119
  * @param {string} type Event type
123
- * @param {(...args: *) => *} listener Event listener
120
+ * @param {*} listener Event listener
124
121
  * @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
125
122
  * @return {SunEditor.Event.GlobalInfo} Registered event information
126
123
  */
@@ -140,7 +137,7 @@ class EventManager {
140
137
  * @description Remove events from document.
141
138
  * - When created as an Iframe, the event of the document inside the Iframe is also removed.
142
139
  * @param {string|SunEditor.Event.GlobalInfo} type Event type or (Event info = this.addGlobalEvent())
143
- * @param {(...args: *) => *} [listener] Event listener
140
+ * @param {*} [listener] Event listener
144
141
  * @param {boolean|AddEventListenerOptions} [useCapture] Use event capture
145
142
  * @returns {undefined|null} Success: `null`, Not found: `undefined`
146
143
  */
@@ -224,7 +224,7 @@ class Editor {
224
224
  */
225
225
  #init(options) {
226
226
  this.$.pluginManager.init(options);
227
- this.$.shortcuts._registerCustomShortcuts();
227
+ this.$.shortcuts._registerShortcuts();
228
228
  this.$.commandDispatcher._initCommandButtons();
229
229
  this.$.ui.init();
230
230
  }
@@ -91,6 +91,11 @@ export const A = {
91
91
  * @returns {Action}
92
92
  */
93
93
  backspaceFormatMaintain: (formatEl) => ({ t: 'backspace.format.maintain', p: { formatEl } }),
94
+ /**
95
+ * @param {Element} formatEl - brLine element (e.g. PRE) to strip
96
+ * @returns {Action}
97
+ */
98
+ backspaceBrLineStrip: (formatEl) => ({ t: 'backspace.brline.strip', p: { formatEl } }),
94
99
  /**
95
100
  * @param {Node} selectionNode
96
101
  * @param {Range} range
@@ -30,6 +30,31 @@ export default {
30
30
 
31
31
  /** [backspace] */
32
32
 
33
+ /** @action backspaceBrLineStrip — extract first line from brLine (PRE) */
34
+ 'backspace.brline.strip': ({ ctx, ports }, { formatEl }) => {
35
+ const defaultTag = ctx.options.get('defaultLine');
36
+ const parent = formatEl.parentNode;
37
+ const newLine = dom.utils.createElement(defaultTag);
38
+
39
+ while (formatEl.firstChild && !dom.check.isBreak(formatEl.firstChild)) {
40
+ newLine.appendChild(formatEl.firstChild);
41
+ }
42
+
43
+ if (formatEl.firstChild && dom.check.isBreak(formatEl.firstChild)) {
44
+ formatEl.removeChild(formatEl.firstChild);
45
+ }
46
+
47
+ if (!newLine.firstChild) newLine.innerHTML = '<br>';
48
+ parent.insertBefore(newLine, formatEl);
49
+
50
+ if (!formatEl.firstChild || (!formatEl.textContent.trim() && !formatEl.querySelector('br'))) {
51
+ parent.removeChild(formatEl);
52
+ }
53
+
54
+ const focusNode = newLine.firstChild;
55
+ ports.selection.setRange(focusNode, 0, focusNode, 0);
56
+ },
57
+
33
58
  /** @action backspaceFormatMaintain */
34
59
  'backspace.format.maintain': ({ ctx }, { formatEl }) => {
35
60
  if (formatEl.nodeName.toUpperCase() === ctx.options.get('defaultLine').toUpperCase()) {
@@ -106,6 +106,9 @@ class EventOrchestrator extends KernelInjector {
106
106
  this.__eventDoc = null;
107
107
  /** @type {string} */
108
108
  this.__secopy = null;
109
+
110
+ /** @type {HTMLInputElement} */
111
+ this.__focusTemp = this.#contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
109
112
  }
110
113
 
111
114
  /**
@@ -343,6 +346,9 @@ class EventOrchestrator extends KernelInjector {
343
346
  entries.forEach((e) => {
344
347
  this.#ui._emitResizeEvent(this.$.frameRoots.get(e.target.getAttribute('data-root-key')), -1, e);
345
348
  });
349
+ if (this.#store.mode.isInline && this.#store.mode.isBottom) {
350
+ this.#toolbar._resetSticky();
351
+ }
346
352
  }, 0);
347
353
  });
348
354
  }
@@ -443,10 +449,35 @@ class EventOrchestrator extends KernelInjector {
443
449
  this.#eventManager.addEvent(codeArea, 'keyup', cvAuthHeight, false);
444
450
  this.#eventManager.addEvent(codeArea, 'paste', cvAuthHeight, false);
445
451
 
452
+ /** code view tab key */
453
+ if (!this.#options.get('tabDisable')) {
454
+ this.#eventManager.addEvent(codeArea, 'keydown', InsertTab, false);
455
+ }
456
+
446
457
  /** code view numbers */
447
458
  if (codeNumbers) this.#eventManager.addEvent(codeArea, 'scroll', this.$.viewer._scrollLineNumbers.bind(codeArea, codeNumbers), false);
448
459
  }
449
460
 
461
+ /** markdown view area */
462
+ const markdownArea = fc.get('markdown');
463
+ if (markdownArea) {
464
+ this.#eventManager.addEvent(markdownArea, 'mousedown', this.#OnFocus_markdown.bind(this, fc), false);
465
+
466
+ const mdNumbers = fc.get('markdownNumbers');
467
+ const mdAutoHeight = this.$.viewer._markdownViewAutoHeight.bind(this.$.viewer, markdownArea, mdNumbers, this.$.frameOptions.get('height') === 'auto');
468
+
469
+ this.#eventManager.addEvent(markdownArea, 'keydown', mdAutoHeight, false);
470
+ this.#eventManager.addEvent(markdownArea, 'keyup', mdAutoHeight, false);
471
+ this.#eventManager.addEvent(markdownArea, 'paste', mdAutoHeight, false);
472
+
473
+ /** markdown view tab key */
474
+ if (!this.#options.get('tabDisable')) {
475
+ this.#eventManager.addEvent(markdownArea, 'keydown', InsertTab, false);
476
+ }
477
+
478
+ if (mdNumbers) this.#eventManager.addEvent(markdownArea, 'scroll', this.$.viewer._scrollMarkdownLineNumbers.bind(markdownArea, mdNumbers), false);
479
+ }
480
+
450
481
  if (fc.has('statusbar')) this.__addStatusbarEvent(fc, fc.get('options'));
451
482
 
452
483
  const OnScrollAbs = this.#OnScroll_Abs.bind(this);
@@ -636,7 +667,16 @@ class EventOrchestrator extends KernelInjector {
636
667
  * @param {FocusEvent} event - Focus event object
637
668
  */
638
669
  __postBlurEvent(frameContext, event) {
639
- if (this.#store.mode.isInline || this.#store.mode.isBalloon) this._hideToolbar();
670
+ if (this.#store.mode.isInline) {
671
+ // Defer hide — prevents race with deferred focus show (setTimeout in #OnFocus_wysiwyg)
672
+ _w.setTimeout(() => {
673
+ if (!this.#store.get('hasFocus')) {
674
+ this._hideToolbar();
675
+ }
676
+ }, 0);
677
+ } else if (this.#store.mode.isBalloon) {
678
+ this._hideToolbar();
679
+ }
640
680
  if (this.#store.mode.isSubBalloon) this._hideToolbar_sub();
641
681
 
642
682
  // user event
@@ -699,7 +739,9 @@ class EventOrchestrator extends KernelInjector {
699
739
  this.#ui._iframeAutoHeight(fc);
700
740
 
701
741
  if (this.#toolbar.isSticky) {
702
- this.#context.get('toolbar_main').style.width = fc.get('topArea').offsetWidth - 2 + 'px';
742
+ if (!this.#toolbar.isCSSSticky) {
743
+ this.#context.get('toolbar_main').style.width = fc.get('topArea').offsetWidth - 2 + 'px';
744
+ }
703
745
  this.#toolbar._resetSticky();
704
746
  }
705
747
  }
@@ -939,6 +981,31 @@ class EventOrchestrator extends KernelInjector {
939
981
  dom.utils.addClass(this.$.commandDispatcher.targets.get('codeView'), 'active');
940
982
  this.#ui._toggleCodeViewButtons(true);
941
983
  }
984
+
985
+ /**
986
+ * @param {SunEditor.FrameContext} frameContext - frame context object
987
+ */
988
+ #OnFocus_markdown(frameContext) {
989
+ this.$.facade.changeFrameContext(frameContext.get('key'));
990
+ dom.utils.addClass(this.$.commandDispatcher.targets.get('markdownView'), 'active');
991
+ this.#ui._toggleCodeViewButtons(true);
992
+ }
993
+ }
994
+
995
+ /**
996
+ * @description Inserts a tab character at the cursor position in a textarea
997
+ * @param {KeyboardEvent} e
998
+ */
999
+ function InsertTab(e) {
1000
+ if (e.key !== 'Tab') return;
1001
+ e.preventDefault();
1002
+
1003
+ const textarea = /** @type {HTMLTextAreaElement} */ (e.target);
1004
+ const start = textarea.selectionStart;
1005
+ const end = textarea.selectionEnd;
1006
+
1007
+ textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
1008
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
942
1009
  }
943
1010
 
944
1011
  export default EventOrchestrator;
@@ -89,6 +89,7 @@ export async function OnClick_wysiwyg(fc, e) {
89
89
  // plugin event
90
90
  if ((await this._callPluginEventAsync('onClick', { frameContext: fc, event: e })) === false) return;
91
91
 
92
+ // component
92
93
  const componentInfo = this.$.component.get(eventTarget);
93
94
  if (componentInfo) {
94
95
  e.preventDefault();
@@ -59,7 +59,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
59
59
  !selectionNode.previousSibling &&
60
60
  !dom.check.isListCell(formatEl) &&
61
61
  format.isLine(formatEl) &&
62
- (!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl))
62
+ (!format.isBrLine(formatEl) || format.isClosureBrLine(formatEl) || dom.check.isWysiwygFrame(formatEl.parentNode))
63
63
  ) {
64
64
  // closure range
65
65
  if (format.isClosureBlock(formatEl.parentNode)) {
@@ -67,6 +67,14 @@ export function reduceBackspaceDown(actions, ports, ctx) {
67
67
  return false;
68
68
  }
69
69
 
70
+ // brLine (pre): strip tag to default line(s)
71
+ if (format.isBrLine(formatEl) && dom.check.isWysiwygFrame(formatEl.parentNode)) {
72
+ actions.push(A.preventStop());
73
+ actions.push(A.backspaceBrLineStrip(formatEl));
74
+ actions.push(A.historyPush(true));
75
+ return false;
76
+ }
77
+
70
78
  // maintain default format
71
79
  if (dom.check.isWysiwygFrame(formatEl.parentNode) && formatEl.childNodes.length <= 1 && (!formatEl.firstChild || dom.check.isZeroWidth(formatEl.textContent))) {
72
80
  actions.push(A.preventStop());
@@ -30,6 +30,7 @@ import History from '../logic/shell/history';
30
30
  import Toolbar from '../logic/panel/toolbar';
31
31
  import Menu from '../logic/panel/menu';
32
32
  import Viewer from '../logic/panel/viewer';
33
+ import Finder from '../logic/panel/finder';
33
34
 
34
35
  // L4: event
35
36
  import EventOrchestrator from '../event/eventOrchestrator';
@@ -78,6 +79,7 @@ import EventOrchestrator from '../event/eventOrchestrator';
78
79
  * @property {import('../logic/panel/toolbar').default} subToolbar - L3: Sub-toolbar renderer
79
80
  * @property {import('../logic/panel/menu').default} menu - L3: Menu renderer
80
81
  * @property {import('../logic/panel/viewer').default} viewer - L3: View mode handler
82
+ * @property {import('../logic/panel/finder').default} finder - L3: Finder handler
81
83
  */
82
84
 
83
85
  /**
@@ -218,6 +220,7 @@ class CoreKernel {
218
220
  }
219
221
  this.#logic.set('menu', new Menu(this));
220
222
  this.#logic.set('viewer', new Viewer(this));
223
+ this.#logic.set('finder', new Finder(this));
221
224
 
222
225
  // history (last — closure captures all L3 modules above)
223
226
  this.#logic.set('history', History(this));
@@ -268,6 +271,7 @@ class CoreKernel {
268
271
  subToolbar: this.#logic.get('subToolbar'),
269
272
  menu: this.#logic.get('menu'),
270
273
  viewer: this.#logic.get('viewer'),
274
+ finder: this.#logic.get('finder'),
271
275
  });
272
276
  }
273
277
 
@@ -28,6 +28,7 @@ import { numbers } from '../../helper';
28
28
  * @property {boolean} isBalloonAlways - Whether the toolbar is in `balloon-always` mode (always visible as floating).
29
29
  * @property {boolean} isSubBalloon - Whether the sub-toolbar is in `balloon` mode.
30
30
  * @property {boolean} isSubBalloonAlways - Whether the sub-toolbar is in `balloon-always` mode.
31
+ * @property {boolean} isBottom - Whether the toolbar is placed at the bottom of the editor (`classic:bottom`, `inline:bottom`).
31
32
  */
32
33
 
33
34
  /**
@@ -64,6 +65,7 @@ class Store {
64
65
  isBalloonAlways: /balloon-always/i.test(mode),
65
66
  isSubBalloon: /balloon/i.test(subMode),
66
67
  isSubBalloonAlways: /balloon-always/i.test(subMode),
68
+ isBottom: !!options.get('_toolbar_bottom'),
67
69
  };
68
70
 
69
71
  this.#state = {
@@ -1,4 +1,4 @@
1
- import { dom, converter, numbers, unicode, clipboard, env } from '../../../helper';
1
+ import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../../../helper';
2
2
 
3
3
  const { _d } = env;
4
4
  const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
@@ -293,7 +293,7 @@ class HTML {
293
293
  for (const [key, value] of Object.entries(attrs)) {
294
294
  // Block event handler attributes and validate src protocol
295
295
  if (/^on/i.test(key)) continue;
296
- if (key === 'src' && !_SAFE_URL_PROTOCOL.test(String(value).replace(/[\s\r\n\t]+/g, ''))) continue;
296
+ if (key === 'src' && !_isSafeURL(String(value))) continue;
297
297
  iframe.setAttribute(key, value);
298
298
  }
299
299
 
@@ -1149,6 +1149,9 @@ class HTML {
1149
1149
  emptyCells[j].innerHTML = '<br>';
1150
1150
  }
1151
1151
 
1152
+ // output: wrap code blocks <pre class="language-xxx"> → <pre><code class="language-xxx">
1153
+ this.#wrapPreCode(renderHTML);
1154
+
1152
1155
  const content = renderHTML.innerHTML;
1153
1156
  if (this.#frameOptions.get('iframe_fullPage')) {
1154
1157
  if (includeFullPage) {
@@ -1188,7 +1191,10 @@ class HTML {
1188
1191
  for (let i = 0; i < rootKey.length; i++) {
1189
1192
  this.#$.facade.changeFrameContext(rootKey[i]);
1190
1193
 
1191
- if (!this.#frameContext.get('isCodeView')) {
1194
+ if (this.#frameContext.get('isMarkdownView')) {
1195
+ const json = converter.htmlToJson(convertValue);
1196
+ this.#frameContext.get('markdown').value = markdown.jsonToMarkdown(json);
1197
+ } else if (!this.#frameContext.get('isCodeView')) {
1192
1198
  this.#frameContext.get('wysiwyg').innerHTML = convertValue;
1193
1199
  this.#$.pluginManager.resetFileInfo();
1194
1200
  this.#$.history.push(false, rootKey[i]);
@@ -1215,7 +1221,10 @@ class HTML {
1215
1221
  this.#$.facade.changeFrameContext(rootKey[i]);
1216
1222
  const convertValue = this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
1217
1223
 
1218
- if (!this.#frameContext.get('isCodeView')) {
1224
+ if (this.#frameContext.get('isMarkdownView')) {
1225
+ const json = converter.htmlToJson(convertValue);
1226
+ this.#frameContext.get('markdown').value += '\n' + markdown.jsonToMarkdown(json);
1227
+ } else if (!this.#frameContext.get('isCodeView')) {
1219
1228
  const temp = dom.utils.createElement('DIV', null, convertValue);
1220
1229
  const children = Array.from(temp.children);
1221
1230
  for (let j = 0, jLen = children.length; j < jLen; j++) {
@@ -1521,6 +1530,9 @@ class HTML {
1521
1530
  }
1522
1531
  }
1523
1532
 
1533
+ // code block: unwrap <pre><code class="language-xxx"> → <pre class="language-xxx">
1534
+ if (current.nodeName === 'PRE') this.#unwrapPreCode(current);
1535
+
1524
1536
  const nrtag = !dom.query.getParentElement(current, dom.check.isExcludeFormat);
1525
1537
 
1526
1538
  // formatFilter
@@ -2000,13 +2012,58 @@ class HTML {
2000
2012
  this.#cleanStyleRegExpMap.clear();
2001
2013
  }
2002
2014
  }
2015
+
2016
+ /**
2017
+ * @description Input: unwrap `<pre><code class="language-xxx">` → `<pre class="language-xxx">`
2018
+ * @param {HTMLElement} pre
2019
+ */
2020
+ #unwrapPreCode(pre) {
2021
+ if (pre.children.length !== 1 || pre.firstElementChild?.nodeName !== 'CODE') return;
2022
+
2023
+ const codeEl = pre.firstElementChild;
2024
+ const langMatch = (codeEl.className || '').match(/language-(\S+)/);
2025
+
2026
+ if (langMatch) {
2027
+ dom.utils.addClass(pre, 'language-' + langMatch[1]);
2028
+ pre.setAttribute('data-se-lang', langMatch[1]);
2029
+ }
2030
+
2031
+ while (codeEl.firstChild) {
2032
+ pre.insertBefore(codeEl.firstChild, codeEl);
2033
+ }
2034
+
2035
+ pre.removeChild(codeEl);
2036
+ }
2037
+
2038
+ /**
2039
+ * @description Output: wrap `<pre class="language-xxx">` → `<pre><code class="language-xxx">`
2040
+ * @param {HTMLElement} container
2041
+ */
2042
+ #wrapPreCode(container) {
2043
+ const preEls = container.querySelectorAll('pre[class*="language-"]');
2044
+
2045
+ for (let j = 0, jlen = preEls.length, pre, lang, codeEl; j < jlen; j++) {
2046
+ pre = preEls[j];
2047
+ lang = (pre.className.match(/language-(\S+)/) || [])[1];
2048
+ if (!lang) continue;
2049
+
2050
+ codeEl = dom.utils.createElement('CODE', { class: 'language-' + lang });
2051
+ while (pre.firstChild) {
2052
+ codeEl.appendChild(pre.firstChild);
2053
+ }
2054
+
2055
+ pre.appendChild(codeEl);
2056
+ pre.className = pre.className.replace(/\s*language-\S+/g, '').trim();
2057
+ pre.removeAttribute('data-se-lang');
2058
+ }
2059
+ }
2003
2060
  }
2004
2061
 
2005
2062
  /** Module-level regex constants */
2006
2063
  // #CleanElements
2007
2064
  const _RE_XML_NS_TAG = /^<[a-z0-9]+:[a-z0-9]+/i;
2008
2065
  const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
2009
- const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(")[^"]*\1/gi;
2066
+ const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(?:(["'])[^"']*\1|\S+)/gi;
2010
2067
  const _RE_STYLE_EQ = /style=/i;
2011
2068
  const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
2012
2069
  const _RE_LEADING_SPACE = /^\s/;
@@ -2029,20 +2086,62 @@ const _RE_SPAN = /^span$/i;
2029
2086
 
2030
2087
  // _isSafeAttribute
2031
2088
  const _SAFE_URL_PROTOCOL = /^(?:https?|ftps?|mailto|tel|blob|sms|geo|webcal|callto):|^[#/]|^data:image\//i;
2032
- const _URL_ATTR_PATTERN = /^(?:href|src)\s*=\s*(?:'|"|\s)*/i;
2033
- const _RE_ATTR_VALUE = /=\s*(?:"|'|)\s*([^"'\s>]*)/;
2034
- const _RE_WHITESPACE = /[\s\r\n\t]+/g;
2035
- const _RE_HTML_ENTITY = /&(#x?[0-9a-f]+|[a-z]+);/gi;
2089
+ const _URL_ATTR_PATTERN = /^(?:href|src)\s*=/i;
2090
+ const _RE_ATTR_VALUE = /=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/;
2036
2091
  const _RE_COLON = /:/i;
2037
2092
 
2093
+ /**
2094
+ * @description Normalize a URL by decoding all HTML entities, URL-encoded characters,
2095
+ * and stripping whitespace/control characters. Used to detect obfuscated dangerous protocols.
2096
+ * e.g. `java&#x09;script:`, `java%09script:`, `&#106;avascript:`
2097
+ * @param {string} url Raw URL string
2098
+ * @returns {string} Normalized URL
2099
+ */
2100
+ function _normalizeURL(url) {
2101
+ // Decode HTML entities (&#x09; &#9; etc.) — repeat to handle nested encoding (e.g. &#38;#x6a; → &#x6a; → j)
2102
+ let prev,
2103
+ limit = 5;
2104
+ do {
2105
+ prev = url;
2106
+ url = url.replace(/&(#x([0-9a-f]+)|#([0-9]+)|([a-z]+));/gi, (_, __, hex, dec) => {
2107
+ if (hex) return String.fromCharCode(parseInt(hex, 16));
2108
+ if (dec) return String.fromCharCode(parseInt(dec, 10));
2109
+ return '';
2110
+ });
2111
+ } while (url !== prev && --limit);
2112
+
2113
+ // Decode URL-encoded characters (%09, %0a, etc.)
2114
+ try {
2115
+ url = decodeURIComponent(url);
2116
+ } catch {
2117
+ // malformed URI — keep as is
2118
+ }
2119
+
2120
+ // Strip all whitespace and control characters (U+0000–U+0020)
2121
+ // eslint-disable-next-line no-control-regex -- intentional: strip control chars used to bypass protocol detection
2122
+ url = url.replace(/[\u0000-\u0020]+/g, '');
2123
+
2124
+ return url;
2125
+ }
2126
+
2127
+ /**
2128
+ * @description Check if a URL is safe (matches the allowed protocol whitelist).
2129
+ * @param {string} url Raw URL string
2130
+ * @returns {boolean}
2131
+ */
2132
+ function _isSafeURL(url) {
2133
+ const normalized = _normalizeURL(url);
2134
+ return _SAFE_URL_PROTOCOL.test(normalized) || !_RE_COLON.test(normalized);
2135
+ }
2136
+
2038
2137
  function _isSafeAttribute(attr) {
2039
2138
  if (!_URL_ATTR_PATTERN.test(attr)) return true;
2040
2139
 
2041
2140
  const urlMatch = attr.match(_RE_ATTR_VALUE);
2042
2141
  if (!urlMatch) return true;
2043
2142
 
2044
- const url = urlMatch[1].replace(_RE_WHITESPACE, '').replace(_RE_HTML_ENTITY, '');
2045
- return _SAFE_URL_PROTOCOL.test(url) || !_RE_COLON.test(url);
2143
+ const url = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3] ?? '';
2144
+ return _isSafeURL(url);
2046
2145
  }
2047
2146
 
2048
2147
  /**