suneditor 3.0.0-rc.4 → 3.0.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.
- package/README.md +4 -3
- package/dist/suneditor-contents.min.css +1 -1
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +10 -6
- package/src/assets/design/color.css +14 -2
- package/src/assets/design/typography.css +5 -0
- package/src/assets/icons/defaultIcons.js +22 -4
- package/src/assets/suneditor-contents.css +1 -1
- package/src/assets/suneditor.css +312 -18
- package/src/core/config/eventManager.js +6 -9
- package/src/core/editor.js +1 -1
- package/src/core/event/actions/index.js +5 -0
- package/src/core/event/effects/keydown.registry.js +25 -0
- package/src/core/event/eventOrchestrator.js +69 -2
- package/src/core/event/handlers/handler_ww_mouse.js +1 -0
- package/src/core/event/rules/keydown.rule.backspace.js +9 -1
- package/src/core/kernel/coreKernel.js +4 -0
- package/src/core/kernel/store.js +2 -0
- package/src/core/logic/dom/char.js +11 -0
- package/src/core/logic/dom/format.js +22 -0
- package/src/core/logic/dom/html.js +126 -11
- package/src/core/logic/dom/nodeTransform.js +13 -0
- package/src/core/logic/dom/offset.js +100 -37
- package/src/core/logic/dom/selection.js +54 -22
- package/src/core/logic/panel/finder.js +982 -0
- package/src/core/logic/panel/menu.js +8 -6
- package/src/core/logic/panel/toolbar.js +112 -19
- package/src/core/logic/panel/viewer.js +214 -43
- package/src/core/logic/shell/_commandExecutor.js +7 -1
- package/src/core/logic/shell/commandDispatcher.js +1 -1
- package/src/core/logic/shell/component.js +5 -7
- package/src/core/logic/shell/history.js +24 -0
- package/src/core/logic/shell/shortcuts.js +3 -3
- package/src/core/logic/shell/ui.js +25 -26
- package/src/core/schema/frameContext.js +15 -1
- package/src/core/schema/options.js +180 -39
- package/src/core/section/constructor.js +61 -20
- package/src/core/section/documentType.js +2 -2
- package/src/events.js +12 -0
- package/src/helper/clipboard.js +1 -1
- package/src/helper/converter.js +15 -0
- package/src/helper/dom/domQuery.js +12 -0
- package/src/helper/dom/domUtils.js +26 -14
- package/src/helper/index.js +3 -0
- package/src/helper/markdown.js +876 -0
- package/src/interfaces/plugins.js +7 -5
- package/src/langs/ckb.js +9 -0
- package/src/langs/cs.js +9 -0
- package/src/langs/da.js +9 -0
- package/src/langs/de.js +9 -0
- package/src/langs/en.js +9 -0
- package/src/langs/es.js +9 -0
- package/src/langs/fa.js +9 -0
- package/src/langs/fr.js +9 -0
- package/src/langs/he.js +9 -0
- package/src/langs/hu.js +9 -0
- package/src/langs/it.js +9 -0
- package/src/langs/ja.js +9 -0
- package/src/langs/km.js +9 -0
- package/src/langs/ko.js +9 -0
- package/src/langs/lv.js +9 -0
- package/src/langs/nl.js +9 -0
- package/src/langs/pl.js +9 -0
- package/src/langs/pt_br.js +9 -0
- package/src/langs/ro.js +9 -0
- package/src/langs/ru.js +9 -0
- package/src/langs/se.js +9 -0
- package/src/langs/tr.js +9 -0
- package/src/langs/uk.js +9 -0
- package/src/langs/ur.js +9 -0
- package/src/langs/zh_cn.js +9 -0
- package/src/modules/contract/Browser.js +31 -1
- package/src/modules/contract/ColorPicker.js +6 -0
- package/src/modules/contract/Controller.js +77 -39
- package/src/modules/contract/Figure.js +57 -0
- package/src/modules/contract/Modal.js +6 -0
- package/src/modules/manager/ApiManager.js +53 -4
- package/src/modules/manager/FileManager.js +18 -1
- package/src/modules/ui/ModalAnchorEditor.js +35 -2
- package/src/modules/ui/SelectMenu.js +44 -12
- package/src/plugins/browser/fileBrowser.js +5 -2
- package/src/plugins/command/codeBlock.js +324 -0
- package/src/plugins/command/exportPDF.js +15 -3
- package/src/plugins/command/fileUpload.js +4 -1
- package/src/plugins/dropdown/backgroundColor.js +5 -1
- package/src/plugins/dropdown/blockStyle.js +8 -2
- package/src/plugins/dropdown/fontColor.js +5 -1
- package/src/plugins/dropdown/hr.js +6 -0
- package/src/plugins/dropdown/layout.js +4 -1
- package/src/plugins/dropdown/lineHeight.js +3 -0
- package/src/plugins/dropdown/paragraphStyle.js +5 -5
- package/src/plugins/dropdown/table/index.js +4 -1
- package/src/plugins/dropdown/table/render/table.html.js +1 -1
- package/src/plugins/dropdown/table/services/table.grid.js +16 -8
- package/src/plugins/dropdown/table/services/table.style.js +5 -9
- package/src/plugins/dropdown/template.js +3 -0
- package/src/plugins/dropdown/textStyle.js +5 -1
- package/src/plugins/field/mention.js +5 -1
- package/src/plugins/index.js +3 -0
- package/src/plugins/input/fontSize.js +10 -3
- package/src/plugins/modal/audio.js +7 -3
- package/src/plugins/modal/embed.js +23 -20
- package/src/plugins/modal/image/index.js +5 -1
- package/src/plugins/modal/math.js +7 -2
- package/src/plugins/modal/video/index.js +21 -4
- package/src/themes/cobalt.css +13 -4
- package/src/themes/cream.css +11 -2
- package/src/themes/dark.css +13 -4
- package/src/themes/midnight.css +13 -4
- package/src/typedef.js +4 -4
- package/types/assets/icons/defaultIcons.d.ts +12 -1
- package/types/assets/suneditor.css.d.ts +1 -1
- package/types/core/config/eventManager.d.ts +6 -8
- package/types/core/event/actions/index.d.ts +1 -0
- package/types/core/event/effects/keydown.registry.d.ts +2 -0
- package/types/core/event/eventOrchestrator.d.ts +2 -1
- package/types/core/kernel/coreKernel.d.ts +5 -0
- package/types/core/kernel/store.d.ts +5 -0
- package/types/core/logic/dom/char.d.ts +11 -0
- package/types/core/logic/dom/format.d.ts +22 -0
- package/types/core/logic/dom/html.d.ts +16 -0
- package/types/core/logic/dom/nodeTransform.d.ts +13 -0
- package/types/core/logic/dom/offset.d.ts +23 -2
- package/types/core/logic/dom/selection.d.ts +9 -3
- package/types/core/logic/panel/finder.d.ts +83 -0
- package/types/core/logic/panel/toolbar.d.ts +14 -1
- package/types/core/logic/panel/viewer.d.ts +22 -2
- package/types/core/logic/shell/shortcuts.d.ts +1 -1
- package/types/core/schema/frameContext.d.ts +22 -0
- package/types/core/schema/options.d.ts +362 -79
- package/types/events.d.ts +11 -0
- package/types/helper/converter.d.ts +15 -0
- package/types/helper/dom/domQuery.d.ts +12 -0
- package/types/helper/dom/domUtils.d.ts +23 -2
- package/types/helper/index.d.ts +5 -0
- package/types/helper/markdown.d.ts +27 -0
- package/types/interfaces/plugins.d.ts +7 -5
- package/types/langs/_Lang.d.ts +9 -0
- package/types/modules/contract/Browser.d.ts +36 -2
- package/types/modules/contract/ColorPicker.d.ts +6 -0
- package/types/modules/contract/Controller.d.ts +35 -1
- package/types/modules/contract/Figure.d.ts +57 -0
- package/types/modules/contract/Modal.d.ts +6 -0
- package/types/modules/manager/ApiManager.d.ts +26 -0
- package/types/modules/manager/FileManager.d.ts +17 -0
- package/types/modules/ui/ModalAnchorEditor.d.ts +41 -4
- package/types/modules/ui/SelectMenu.d.ts +40 -2
- package/types/plugins/browser/fileBrowser.d.ts +10 -4
- package/types/plugins/command/codeBlock.d.ts +53 -0
- package/types/plugins/command/fileUpload.d.ts +8 -2
- package/types/plugins/dropdown/backgroundColor.d.ts +10 -2
- package/types/plugins/dropdown/blockStyle.d.ts +14 -2
- package/types/plugins/dropdown/fontColor.d.ts +10 -2
- package/types/plugins/dropdown/hr.d.ts +12 -0
- package/types/plugins/dropdown/layout.d.ts +8 -2
- package/types/plugins/dropdown/lineHeight.d.ts +6 -0
- package/types/plugins/dropdown/paragraphStyle.d.ts +14 -3
- package/types/plugins/dropdown/table/index.d.ts +9 -3
- package/types/plugins/dropdown/template.d.ts +6 -0
- package/types/plugins/dropdown/textStyle.d.ts +10 -2
- package/types/plugins/field/mention.d.ts +10 -2
- package/types/plugins/index.d.ts +3 -0
- package/types/plugins/input/fontSize.d.ts +18 -4
- package/types/plugins/modal/audio.d.ts +14 -6
- package/types/plugins/modal/embed.d.ts +44 -38
- package/types/plugins/modal/image/index.d.ts +9 -1
- package/types/plugins/modal/link.d.ts +6 -2
- package/types/plugins/modal/math.d.ts +23 -5
- package/types/plugins/modal/video/index.d.ts +49 -9
- package/types/typedef.d.ts +5 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dom,
|
|
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 {
|
|
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
|
|
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 (
|
|
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 {
|
|
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 {
|
|
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
|
*/
|
package/src/core/editor.js
CHANGED
|
@@ -224,7 +224,7 @@ class Editor {
|
|
|
224
224
|
*/
|
|
225
225
|
#init(options) {
|
|
226
226
|
this.$.pluginManager.init(options);
|
|
227
|
-
this.$.shortcuts.
|
|
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
|
|
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.#
|
|
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
|
|
package/src/core/kernel/store.js
CHANGED
|
@@ -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 = {
|
|
@@ -27,6 +27,8 @@ class Char {
|
|
|
27
27
|
* @description Returns `false` if char count is greater than "frameOptions.get('charCounter_max')" when "html" is added to the current editor.
|
|
28
28
|
* @param {Node|string} html Element node or String.
|
|
29
29
|
* @returns {boolean}
|
|
30
|
+
* @example
|
|
31
|
+
* const canInsert = editor.$.char.check('<strong>new text</strong>');
|
|
30
32
|
*/
|
|
31
33
|
check(html) {
|
|
32
34
|
const maxCharCount = this.#frameOptions.get('charCounter_max');
|
|
@@ -45,6 +47,9 @@ class Char {
|
|
|
45
47
|
* - If [content] is `undefined`, get the current editor's number of characters or binary data size.
|
|
46
48
|
* @param {string} [content] Content to count. (defalut: this.#frameContext.get('wysiwyg'))
|
|
47
49
|
* @returns {number}
|
|
50
|
+
* @example
|
|
51
|
+
* const currentLength = editor.$.char.getLength();
|
|
52
|
+
* const textLength = editor.$.char.getLength('Hello World');
|
|
48
53
|
*/
|
|
49
54
|
getLength(content) {
|
|
50
55
|
if (typeof content !== 'string') {
|
|
@@ -57,6 +62,8 @@ class Char {
|
|
|
57
62
|
* @descriptionGets Get the length in bytes of a string.
|
|
58
63
|
* @param {string} text String text
|
|
59
64
|
* @returns {number}
|
|
65
|
+
* @example
|
|
66
|
+
* const bytes = editor.$.char.getByteLength('Hello 世界'); // 12
|
|
60
67
|
*/
|
|
61
68
|
getByteLength(text) {
|
|
62
69
|
if (!text || !text.toString) return 0;
|
|
@@ -105,6 +112,10 @@ class Char {
|
|
|
105
112
|
* @param {string} inputText Text added.
|
|
106
113
|
* @param {boolean} _fromInputEvent Whether the test is triggered from an input event.
|
|
107
114
|
* @returns {boolean}
|
|
115
|
+
* @example
|
|
116
|
+
* if (!editor.$.char.test(inputData, true)) {
|
|
117
|
+
* e.preventDefault();
|
|
118
|
+
* }
|
|
108
119
|
*/
|
|
109
120
|
test(inputText, _fromInputEvent) {
|
|
110
121
|
let nextCharCount = 0;
|
|
@@ -94,6 +94,9 @@ class Format {
|
|
|
94
94
|
* @param {Node} node Reference node.
|
|
95
95
|
* @param {?(current: Node) => boolean} [validation] Additional validation function.
|
|
96
96
|
* @returns {HTMLElement|null}
|
|
97
|
+
* @example
|
|
98
|
+
* const line = editor.$.format.getLine(editor.$.selection.getNode());
|
|
99
|
+
* const lineWithFilter = editor.$.format.getLine(node, (el) => el.nodeName !== 'LI');
|
|
97
100
|
*/
|
|
98
101
|
getLine(node, validation) {
|
|
99
102
|
if (!node) return null;
|
|
@@ -125,6 +128,8 @@ class Format {
|
|
|
125
128
|
/**
|
|
126
129
|
* @description Replace the br-line tag of the current selection.
|
|
127
130
|
* @param {Node} element BR-Line element (PRE..)
|
|
131
|
+
* @example
|
|
132
|
+
* editor.$.format.setBrLine(document.createElement('pre'));
|
|
128
133
|
*/
|
|
129
134
|
setBrLine(element) {
|
|
130
135
|
if (!this.isBrLine(element)) {
|
|
@@ -195,6 +200,8 @@ class Format {
|
|
|
195
200
|
* @param {Node} element Reference node.
|
|
196
201
|
* @param {?(current: Node) => boolean} [validation] Additional validation function.
|
|
197
202
|
* @returns {HTMLBRElement|null}
|
|
203
|
+
* @example
|
|
204
|
+
* const brLine = editor.$.format.getBrLine(editor.$.selection.getNode());
|
|
198
205
|
*/
|
|
199
206
|
getBrLine(element, validation) {
|
|
200
207
|
if (!element) return null;
|
|
@@ -245,6 +252,8 @@ class Format {
|
|
|
245
252
|
* @param {Node} element Reference node.
|
|
246
253
|
* @param {?(current: Node) => boolean} [validation] Additional validation function.
|
|
247
254
|
* @returns {HTMLElement|null}
|
|
255
|
+
* @example
|
|
256
|
+
* const block = editor.$.format.getBlock(editor.$.selection.getNode());
|
|
248
257
|
*/
|
|
249
258
|
getBlock(element, validation) {
|
|
250
259
|
if (!element) return null;
|
|
@@ -777,6 +786,9 @@ class Format {
|
|
|
777
786
|
* @description It is judged whether it is a node related to the text style.
|
|
778
787
|
* @param {Node|string} element The node to check
|
|
779
788
|
* @returns {element is HTMLElement}
|
|
789
|
+
* @example
|
|
790
|
+
* editor.$.format.isTextStyleNode('STRONG'); // true
|
|
791
|
+
* editor.$.format.isTextStyleNode('P'); // false
|
|
780
792
|
*/
|
|
781
793
|
isTextStyleNode(element) {
|
|
782
794
|
return typeof element === 'string' ? this.#textStyleTagsCheck.test(element) : element?.nodeType === 1 && this.#textStyleTagsCheck.test(element.nodeName);
|
|
@@ -788,6 +800,9 @@ class Format {
|
|
|
788
800
|
* - `line` element also contain `brLine` element
|
|
789
801
|
* @param {Node|string} element The node to check
|
|
790
802
|
* @returns {element is HTMLElement}
|
|
803
|
+
* @example
|
|
804
|
+
* editor.$.format.isLine(document.createElement('p')); // true
|
|
805
|
+
* editor.$.format.isLine('SPAN'); // false
|
|
791
806
|
*/
|
|
792
807
|
isLine(element) {
|
|
793
808
|
return typeof element === 'string'
|
|
@@ -812,6 +827,8 @@ class Format {
|
|
|
812
827
|
* ※ Entering the Enter key in the space on the last line ends `brLine` and appends `line`.
|
|
813
828
|
* @param {Node|string} element The node to check
|
|
814
829
|
* @returns {element is HTMLElement}
|
|
830
|
+
* @example
|
|
831
|
+
* editor.$.format.isBrLine(document.createElement('pre')); // true
|
|
815
832
|
*/
|
|
816
833
|
isBrLine(element) {
|
|
817
834
|
return (
|
|
@@ -828,6 +845,9 @@ class Format {
|
|
|
828
845
|
* - `block` is wrap the `line` and `component`
|
|
829
846
|
* @param {Node|string} element The node to check
|
|
830
847
|
* @returns {element is HTMLElement}
|
|
848
|
+
* @example
|
|
849
|
+
* editor.$.format.isBlock(document.createElement('blockquote')); // true
|
|
850
|
+
* editor.$.format.isBlock(document.createElement('ul')); // true
|
|
831
851
|
*/
|
|
832
852
|
isBlock(element) {
|
|
833
853
|
return typeof element === 'string'
|
|
@@ -871,6 +891,8 @@ class Format {
|
|
|
871
891
|
* @description Returns a `line` array from selected range.
|
|
872
892
|
* @param {?(current: Node) => boolean} [validation] The validation function. (Replaces the default validation `format.isLine(current)`)
|
|
873
893
|
* @returns {Array<HTMLElement>}
|
|
894
|
+
* @example
|
|
895
|
+
* const selectedLines = editor.$.format.getLines();
|
|
874
896
|
*/
|
|
875
897
|
getLines(validation) {
|
|
876
898
|
if (!this.#$.selection.resetRangeToTextNode()) return [];
|
|
@@ -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' && !
|
|
296
|
+
if (key === 'src' && !_isSafeURL(String(value))) continue;
|
|
297
297
|
iframe.setAttribute(key, value);
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -1107,6 +1107,9 @@ class HTML {
|
|
|
1107
1107
|
* @param {boolean} [options.includeFullPage=false] Return only the content of the body without headers when the `iframe_fullPage` option is `true`
|
|
1108
1108
|
* @param {number|Array<number>} [options.rootKey=null] Root index
|
|
1109
1109
|
* @returns {string|Object<*, string>}
|
|
1110
|
+
* @example
|
|
1111
|
+
* const html = editor.$.html.get();
|
|
1112
|
+
* const htmlWithFrame = editor.$.html.get({ withFrame: true });
|
|
1110
1113
|
*/
|
|
1111
1114
|
get({ withFrame, includeFullPage, rootKey } = {}) {
|
|
1112
1115
|
if (!rootKey) rootKey = [this.#store.get('rootKey')];
|
|
@@ -1146,6 +1149,9 @@ class HTML {
|
|
|
1146
1149
|
emptyCells[j].innerHTML = '<br>';
|
|
1147
1150
|
}
|
|
1148
1151
|
|
|
1152
|
+
// output: wrap code blocks <pre class="language-xxx"> → <pre><code class="language-xxx">
|
|
1153
|
+
this.#wrapPreCode(renderHTML);
|
|
1154
|
+
|
|
1149
1155
|
const content = renderHTML.innerHTML;
|
|
1150
1156
|
if (this.#frameOptions.get('iframe_fullPage')) {
|
|
1151
1157
|
if (includeFullPage) {
|
|
@@ -1170,6 +1176,9 @@ class HTML {
|
|
|
1170
1176
|
* @param {string} html HTML string
|
|
1171
1177
|
* @param {Object} [options] Options
|
|
1172
1178
|
* @param {number|Array<number>} [options.rootKey=null] Root index
|
|
1179
|
+
* @example
|
|
1180
|
+
* editor.$.html.set('<p>New content</p>');
|
|
1181
|
+
* editor.$.html.set(html, { rootKey: 'header' });
|
|
1173
1182
|
*/
|
|
1174
1183
|
set(html, { rootKey } = {}) {
|
|
1175
1184
|
this.#$.ui.offCurrentController();
|
|
@@ -1182,7 +1191,10 @@ class HTML {
|
|
|
1182
1191
|
for (let i = 0; i < rootKey.length; i++) {
|
|
1183
1192
|
this.#$.facade.changeFrameContext(rootKey[i]);
|
|
1184
1193
|
|
|
1185
|
-
if (
|
|
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')) {
|
|
1186
1198
|
this.#frameContext.get('wysiwyg').innerHTML = convertValue;
|
|
1187
1199
|
this.#$.pluginManager.resetFileInfo();
|
|
1188
1200
|
this.#$.history.push(false, rootKey[i]);
|
|
@@ -1209,7 +1221,10 @@ class HTML {
|
|
|
1209
1221
|
this.#$.facade.changeFrameContext(rootKey[i]);
|
|
1210
1222
|
const convertValue = this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
|
|
1211
1223
|
|
|
1212
|
-
if (
|
|
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')) {
|
|
1213
1228
|
const temp = dom.utils.createElement('DIV', null, convertValue);
|
|
1214
1229
|
const children = Array.from(temp.children);
|
|
1215
1230
|
for (let j = 0, jLen = children.length; j < jLen; j++) {
|
|
@@ -1229,6 +1244,9 @@ class HTML {
|
|
|
1229
1244
|
* @param {boolean} [options.withFrame=false] Gets the current content with containing parent `div.sun-editor-editable` (`<div class="sun-editor-editable">{content}</div>`).
|
|
1230
1245
|
* @param {number|Array<number>} [options.rootKey=null] Root index
|
|
1231
1246
|
* @returns {Object<string, *>} JSON data
|
|
1247
|
+
* @example
|
|
1248
|
+
* const json = editor.$.html.getJson();
|
|
1249
|
+
* const jsonWithFrame = editor.$.html.getJson({ withFrame: true });
|
|
1232
1250
|
*/
|
|
1233
1251
|
getJson({ withFrame, rootKey } = {}) {
|
|
1234
1252
|
return converter.htmlToJson(this.get({ withFrame, rootKey }));
|
|
@@ -1236,9 +1254,16 @@ class HTML {
|
|
|
1236
1254
|
|
|
1237
1255
|
/**
|
|
1238
1256
|
* @description Sets the JSON data to the editor content
|
|
1257
|
+
* (see @link converter.jsonToHtml)
|
|
1239
1258
|
* @param {Object<string, *>} jsdonData HTML string
|
|
1240
1259
|
* @param {Object} [options] Options
|
|
1241
1260
|
* @param {number|Array<number>} [options.rootKey=null] Root index
|
|
1261
|
+
* @example
|
|
1262
|
+
* const html = editor.$.html.setJson({
|
|
1263
|
+
* type: 'element', tag: 'p', attributes: { class: 'txt' },
|
|
1264
|
+
* children: [{ type: 'text', content: 'Hello' }],
|
|
1265
|
+
* });
|
|
1266
|
+
* // '<p class="txt">Hello</p>'
|
|
1242
1267
|
*/
|
|
1243
1268
|
setJson(jsdonData, { rootKey } = {}) {
|
|
1244
1269
|
this.set(converter.jsonToHtml(jsdonData), { rootKey });
|
|
@@ -1505,6 +1530,9 @@ class HTML {
|
|
|
1505
1530
|
}
|
|
1506
1531
|
}
|
|
1507
1532
|
|
|
1533
|
+
// code block: unwrap <pre><code class="language-xxx"> → <pre class="language-xxx">
|
|
1534
|
+
if (current.nodeName === 'PRE') this.#unwrapPreCode(current);
|
|
1535
|
+
|
|
1508
1536
|
const nrtag = !dom.query.getParentElement(current, dom.check.isExcludeFormat);
|
|
1509
1537
|
|
|
1510
1538
|
// formatFilter
|
|
@@ -1984,13 +2012,58 @@ class HTML {
|
|
|
1984
2012
|
this.#cleanStyleRegExpMap.clear();
|
|
1985
2013
|
}
|
|
1986
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
|
+
}
|
|
1987
2060
|
}
|
|
1988
2061
|
|
|
1989
2062
|
/** Module-level regex constants */
|
|
1990
2063
|
// #CleanElements
|
|
1991
2064
|
const _RE_XML_NS_TAG = /^<[a-z0-9]+:[a-z0-9]+/i;
|
|
1992
2065
|
const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
|
|
1993
|
-
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;
|
|
1994
2067
|
const _RE_STYLE_EQ = /style=/i;
|
|
1995
2068
|
const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
|
|
1996
2069
|
const _RE_LEADING_SPACE = /^\s/;
|
|
@@ -2013,20 +2086,62 @@ const _RE_SPAN = /^span$/i;
|
|
|
2013
2086
|
|
|
2014
2087
|
// _isSafeAttribute
|
|
2015
2088
|
const _SAFE_URL_PROTOCOL = /^(?:https?|ftps?|mailto|tel|blob|sms|geo|webcal|callto):|^[#/]|^data:image\//i;
|
|
2016
|
-
const _URL_ATTR_PATTERN = /^(?:href|src)\s
|
|
2017
|
-
const _RE_ATTR_VALUE = /=\s*(?:"|'
|
|
2018
|
-
const _RE_WHITESPACE = /[\s\r\n\t]+/g;
|
|
2019
|
-
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+))/;
|
|
2020
2091
|
const _RE_COLON = /:/i;
|
|
2021
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	script:`, `java%09script:`, `javascript:`
|
|
2097
|
+
* @param {string} url Raw URL string
|
|
2098
|
+
* @returns {string} Normalized URL
|
|
2099
|
+
*/
|
|
2100
|
+
function _normalizeURL(url) {
|
|
2101
|
+
// Decode HTML entities (	 	 etc.) — repeat to handle nested encoding (e.g. &#x6a; → j → 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
|
+
|
|
2022
2137
|
function _isSafeAttribute(attr) {
|
|
2023
2138
|
if (!_URL_ATTR_PATTERN.test(attr)) return true;
|
|
2024
2139
|
|
|
2025
2140
|
const urlMatch = attr.match(_RE_ATTR_VALUE);
|
|
2026
2141
|
if (!urlMatch) return true;
|
|
2027
2142
|
|
|
2028
|
-
const url = urlMatch[1]
|
|
2029
|
-
return
|
|
2143
|
+
const url = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3] ?? '';
|
|
2144
|
+
return _isSafeURL(url);
|
|
2030
2145
|
}
|
|
2031
2146
|
|
|
2032
2147
|
/**
|