suneditor
Version:
Vanilla JavaScript based WYSIWYG web editor
1,350 lines (1,195 loc) • 76.5 kB
JavaScript
import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../../../helper';
const { _d } = env;
const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
const V2_MIG_DATA_ATTRS = '|data-index|data-file-size|data-file-name|data-exp|data-font-size';
/**
* @description All HTML related classes involved in the editing area
*/
class HTML {
#$;
#store;
#frameContext;
#frameOptions;
#options;
#instanceCheck;
#fontSizeUnitRegExp;
#isAllowedClassName;
#allowHTMLComment;
#disallowedStyleNodesRegExp;
#htmlCheckWhitelistRegExp;
#htmlCheckBlacklistRegExp;
#elementWhitelistRegExp;
#elementBlacklistRegExp;
#attributeWhitelistRegExp;
#attributeBlacklistRegExp;
#cleanStyleTagKeyRegExp;
#cleanStyleRegExpMap;
#textStyleTags;
#disallowedTagsRegExp;
#disallowedTagNameRegExp;
#allowedTagNameRegExp;
/** @type {Object<string, RegExp>} */
#attributeWhitelist;
/** @type {Object<string, RegExp>} */
#attributeBlacklist;
/** @type {Object<string, *>} */
#autoStyleify;
/**
* @constructor
* @param {SunEditor.Kernel} kernel
*/
constructor(kernel) {
this.#$ = kernel.$;
this.#store = kernel.store;
const options = (this.#options = this.#$.options);
this.#frameOptions = this.#$.frameOptions;
this.#frameContext = this.#$.frameContext;
this.#instanceCheck = this.#$.instanceCheck;
// members
this.#isAllowedClassName = function (v) {
return this.test(v) ? v : '';
}.bind(options.get('allowedClassName'));
this.#textStyleTags = options.get('_textStyleTags');
// clean styles
const tagStyles = options.get('tagStyles');
const splitTagStyles = {};
for (const k in tagStyles) {
const s = k.split('|');
for (let i = 0, len = s.length, n; i < len; i++) {
n = s[i];
if (!splitTagStyles[n]) splitTagStyles[n] = '';
else splitTagStyles[n] += '|';
splitTagStyles[n] += tagStyles[k];
}
}
for (const k in splitTagStyles) {
splitTagStyles[k] = new RegExp(`\\s*[^-a-zA-Z](${splitTagStyles[k]})\\s*:[^;]+(?!;)*`, 'gi');
}
const stylesMap = new Map();
const stylesObj = {
...splitTagStyles,
line: options.get('_lineStylesRegExp'),
};
this.#textStyleTags.forEach((v) => {
stylesObj[v] = options.get('_textStylesRegExp');
});
for (const key in stylesObj) {
stylesMap.set(new RegExp(`^(${key})$`), stylesObj[key]);
}
this.#cleanStyleTagKeyRegExp = new RegExp(`^(${Object.keys(stylesObj).join('|')})$`, 'i');
this.#cleanStyleRegExpMap = stylesMap;
// font size unit
this.#fontSizeUnitRegExp = new RegExp('\\d+(' + options.get('fontSizeUnits').join('|') + ')$', 'i');
// extra tags
const allowedExtraTags = options.get('_allowedExtraTag');
const disallowedExtraTags = options.get('_disallowedExtraTag');
this.#disallowedTagsRegExp = new RegExp(`<(${disallowedExtraTags})[^>]*>([\\s\\S]*?)<\\/\\1>|<(${disallowedExtraTags})[^>]*\\/?>`, 'gi');
this.#disallowedTagNameRegExp = new RegExp(`^(${disallowedExtraTags})$`, 'i');
this.#allowedTagNameRegExp = new RegExp(`^(${allowedExtraTags})$`, 'i');
// set disallow text nodes
const disallowStyleNodes = Object.keys(options.get('_defaultStyleTagMap'));
const allowStyleNodes = !options.get('elementWhitelist')
? []
: options
.get('elementWhitelist')
.split('|')
.filter((v) => /b|i|ins|s|strike/i.test(v));
for (let i = 0; i < allowStyleNodes.length; i++) {
disallowStyleNodes.splice(disallowStyleNodes.indexOf(allowStyleNodes[i].toLowerCase()), 1);
}
this.#disallowedStyleNodesRegExp = disallowStyleNodes.length === 0 ? null : new RegExp('(<\\/?)(' + disallowStyleNodes.join('|') + ')\\b\\s*([^>^<]+)?\\s*(?=>)', 'gi');
// whitelist
// tags
const defaultAttr = options.get('__defaultAttributeWhitelist');
this.#allowHTMLComment = options.get('_editorElementWhitelist').includes('//') || options.get('_editorElementWhitelist') === '*';
// html check
this.#htmlCheckWhitelistRegExp = new RegExp('^(' + GetRegList(options.get('_editorElementWhitelist').replace('|//', ''), '') + ')$', 'i');
this.#htmlCheckBlacklistRegExp = new RegExp('^(' + (options.get('elementBlacklist') || '^') + ')$', 'i');
// elements
this.#elementWhitelistRegExp = converter.createElementWhitelist(GetRegList(options.get('_editorElementWhitelist').replace('|//', '|<!--|-->'), ''));
this.#elementBlacklistRegExp = converter.createElementBlacklist(options.get('elementBlacklist').replace('|//', '|<!--|-->'));
// attributes
const regEndStr = '\\s*=\\s*(")[^"]*\\1';
const _wAttr = options.get('attributeWhitelist');
/** @type {Object<string, RegExp>} */
let tagsAttr = {};
let allAttr = '';
if (_wAttr) {
for (const k in _wAttr) {
if (/^on[a-z]+$/i.test(_wAttr[k])) continue;
if (k === '*') {
allAttr = GetRegList(_wAttr[k], defaultAttr);
} else {
tagsAttr[k] = new RegExp('\\s(?:' + GetRegList(_wAttr[k], defaultAttr) + ')' + regEndStr, 'ig');
}
}
}
this.#attributeWhitelistRegExp = new RegExp('\\s(?:' + (allAttr || defaultAttr) + '|' + REQUIRED_DATA_ATTRS + (options.get('v2Migration') ? V2_MIG_DATA_ATTRS : '') + ')' + regEndStr, 'ig');
this.#attributeWhitelist = tagsAttr;
// blacklist
const _bAttr = options.get('attributeBlacklist');
tagsAttr = {};
allAttr = '';
if (_bAttr) {
for (const k in _bAttr) {
if (k === '*') {
allAttr = GetRegList(_bAttr[k], '');
} else {
tagsAttr[k] = new RegExp('\\s(?:' + GetRegList(_bAttr[k], '') + ')' + regEndStr, 'ig');
}
}
}
this.#attributeBlacklistRegExp = new RegExp('\\s(?:' + (allAttr || '^') + ')' + regEndStr, 'ig');
this.#attributeBlacklist = tagsAttr;
// autoStyleify
this.__resetAutoStyleify(options.get('autoStyleify'));
}
/**
* @description Filters an HTML string based on allowed and disallowed tags, with optional custom validation.
* - Removes blacklisted tags and keeps only whitelisted tags.
* - Allows custom validation functions to replace, modify, or remove elements.
* @param {string} html - The HTML string to be filtered.
* @param {Object} params - Filtering parameters.
* @param {string} [params.tagWhitelist] - Allowed tags, specified as a string with tags separated by `'|'`. (e.g. `"div|p|span"`).
* @param {string} [params.tagBlacklist] - Disallowed tags, specified as a string with tags separated by `'|'`. (e.g. `"script|iframe"`).
* @param {(node: Node) => Node | string | null} [params.validate] - Function to validate and modify individual nodes.
* - Return `null` to remove the node.
* - Return a `Node` to replace the current node.
* - Return a `string` to replace the node's `outerHTML`.
* @param {boolean} [params.validateAll] - Whether to apply validation to all nodes.
* @returns {string} - The filtered HTML string.
* @example
* // Remove script and iframe tags using blacklist
* const filtered = editor.html.filter('<div>Content<script>alert("xss")</script></div>', {
* tagBlacklist: 'script|iframe'
* });
*
* // Keep only specific tags using whitelist
* const filtered = editor.html.filter('<div><span>Text</span><img src="x"></div>', {
* tagWhitelist: 'div|span'
* });
*
* // Custom validation to modify nodes
* const filtered = editor.html.filter('<div class="test"><a href="#">Link</a></div>', {
* validate: (node) => {
* if (node.tagName === 'A') {
* node.setAttribute('target', '_blank');
* return node;
* }
* }
* });
*/
filter(html, { tagWhitelist, tagBlacklist, validate, validateAll }) {
if (tagWhitelist) {
html = html.replace(converter.createElementWhitelist(tagWhitelist), '');
}
if (tagBlacklist) {
html = html.replace(converter.createElementBlacklist(tagBlacklist), '');
}
if (validate) {
const parseDocument = new DOMParser().parseFromString(html, 'text/html');
parseDocument.body.querySelectorAll('*').forEach((node) => {
if (validateAll || (!node.closest('.se-component') && !node.closest('.se-flex-component'))) {
const result = validate(node);
if (result === null) {
node.remove();
} else if (this.#instanceCheck.isNode(result)) {
node.replaceWith(result);
} else if (typeof result === 'string') {
node.outerHTML = result;
}
}
});
html = parseDocument.body.innerHTML;
}
return html;
}
/**
* @description Cleans and compresses HTML code to suit the editor format.
* @param {string} html HTML string to clean and compress
* @param {Object} [options] Cleaning options
* @param {boolean} [options.forceFormat=false] If `true`, wraps text nodes without a format node in the format tag.
* @param {?(string|RegExp)} [options.whitelist] Regular expression of allowed tags.
* Create RegExp object using `helper.converter.createElementWhitelist` method.
* @param {?(string|RegExp)} [options.blacklist] Regular expression of disallowed tags.
* Create RegExp object using `helper.converter.createElementBlacklist` method.
* @param {boolean} [options._freeCodeViewMode=false] If `true`, the free code view mode is enabled.
* @returns {string} Cleaned and compressed HTML string
* @example
* // Basic cleaning
* const cleaned = editor.html.clean('<div> <p>Hello</p> </div>');
*
* // Clean with format wrapping
* const cleaned = editor.html.clean('Plain text content', { forceFormat: true });
*
* // Clean with blacklist to remove specific tags
* const cleaned = editor.html.clean('<div><script>alert(1)</script>Content</div>', {
* blacklist: 'script|style'
* });
*/
clean(html, { forceFormat, whitelist, blacklist, _freeCodeViewMode } = {}) {
const { tagFilter, formatFilter, classFilter, textStyleTagFilter, attrFilter, styleFilter } = this.#options.get('strictMode');
let cleanData = '';
html = this.compress(html);
if (tagFilter) {
html = html.replace(this.#disallowedTagsRegExp, '');
html = this.#deleteDisallowedTags(html, this.#elementWhitelistRegExp, this.#elementBlacklistRegExp).replace(/<br\/?>$/i, '');
}
if (this.#autoStyleify) {
const domParser = new DOMParser().parseFromString(html, 'text/html');
dom.query.getListChildNodes(domParser.body, converter.spanToStyleNode.bind(null, this.#autoStyleify), null);
html = domParser.body.innerHTML;
}
if (attrFilter || styleFilter) {
html = html.replace(/(<[a-zA-Z0-9-]+)[^>]*(?=>)/g, this.#CleanElements.bind(this, attrFilter, styleFilter));
}
// get dom tree
const domParser = _d.createRange().createContextualFragment(html);
if (tagFilter) {
try {
this.#consistencyCheckOfHTML(domParser, this.#htmlCheckWhitelistRegExp, this.#htmlCheckBlacklistRegExp, tagFilter, formatFilter, classFilter, _freeCodeViewMode);
} catch (error) {
console.warn('[SUNEDITOR.html.clean.fail]', error.message);
}
}
// iframe placeholder parsing
const iframePlaceholders = domParser.querySelectorAll('[data-se-iframe-holder]');
for (let i = 0, len = iframePlaceholders.length; i < len; i++) {
/** @type {HTMLIFrameElement} */
const iframe = dom.utils.createElement('iframe');
const attrs = JSON.parse(iframePlaceholders[i].getAttribute('data-se-iframe-holder-attrs'));
for (const [key, value] of Object.entries(attrs)) {
// Block event handler attributes and validate src protocol
if (/^on/i.test(key)) continue;
if (key === 'src' && !_isSafeURL(String(value))) continue;
iframe.setAttribute(key, value);
}
iframePlaceholders[i].replaceWith(iframe);
}
this.#$.pluginManager.applyRetainFormat(domParser);
if (formatFilter) {
let domTree = domParser.childNodes;
forceFormat ||= this.#isFormatData(domTree);
if (forceFormat) domTree = this.#editFormat(domParser).childNodes;
for (let i = 0, len = domTree.length, t; i < len; i++) {
t = domTree[i];
if (this.#allowedTagNameRegExp.test(t.nodeName)) {
cleanData += /** @type {HTMLElement} */ (t).outerHTML;
continue;
}
cleanData += this.#makeLine(t, forceFormat);
}
}
// set clean data
cleanData ||= html;
// whitelist, blacklist
if (tagFilter) {
if (whitelist) cleanData = cleanData.replace(typeof whitelist === 'string' ? converter.createElementWhitelist(whitelist) : whitelist, '');
if (blacklist) cleanData = cleanData.replace(typeof blacklist === 'string' ? converter.createElementBlacklist(blacklist) : blacklist, '');
}
if (textStyleTagFilter) {
cleanData = this.#styleNodeConvertor(cleanData);
}
return cleanData;
}
/**
* @description Inserts an (HTML element / HTML string / plain string) at the selection range.
* - If `frameOptions.get('charCounter_max')` is exceeded when `html` is added, `null` is returned without addition.
* @param {Node|string} html HTML Element or HTML string or plain string
* @param {Object} [options] Options
* @param {boolean} [options.selectInserted=false] If `true`, selects the range of the inserted node.
* @param {boolean} [options.skipCharCount=false] If `true`, inserts even if `frameOptions.get('charCounter_max')` is exceeded.
* @param {boolean} [options.skipCleaning=false] If `true`, inserts the HTML string without refining it with `html.clean`.
* @returns {HTMLElement|null} The inserted element or `null` if insertion failed
* @example
* // Insert HTML string at cursor
* editor.html.insert('<strong>Bold text</strong>');
*
* // Insert and select the inserted content
* editor.html.insert('<p>New paragraph</p>', { selectInserted: true });
*
* // Insert raw HTML without cleaning
* editor.html.insert('<div class="custom">Content</div>', { skipCleaning: true });
*/
insert(html, { selectInserted, skipCharCount, skipCleaning } = {}) {
if (!this.#frameContext.get('wysiwyg').contains(this.#$.selection.get().focusNode)) this.#$.focusManager.focus();
this.remove();
this.#$.focusManager.focus();
let focusNode = null;
if (typeof html === 'string') {
if (!skipCleaning) html = this.clean(html, { forceFormat: false, whitelist: null, blacklist: null });
try {
if (dom.check.isListCell(this.#$.format.getLine(this.#$.selection.getNode(), null))) {
const domParser = _d.createRange().createContextualFragment(html);
const domTree = domParser.childNodes;
if (this.#isFormatData(domTree)) html = this.#convertListCell(domTree);
}
const domParser = _d.createRange().createContextualFragment(html);
const domTree = domParser.childNodes;
if (!skipCharCount) {
const type = this.#frameOptions.get('charCounter_type') === 'byte-html' ? 'outerHTML' : 'textContent';
let checkHTML = '';
for (let i = 0, len = domTree.length; i < len; i++) {
checkHTML += domTree[i][type];
}
if (!this.#$.char.check(checkHTML)) return;
}
let c, a, t, prev, firstCon;
while ((c = domTree[0])) {
if (prev?.nodeType === 3 && a?.nodeType === 1 && dom.check.isBreak(c)) {
prev = c;
dom.utils.removeItem(c);
continue;
}
t = this.insertNode(c, { afterNode: a, skipCharCount: true });
a = t.container || t;
firstCon ||= t;
prev = c;
}
if (prev?.nodeType === 3 && a?.nodeType === 1) a = prev;
const offset = a.nodeType === 3 ? t.endOffset || a.textContent.length : a.childNodes.length;
focusNode = a;
if (selectInserted) {
this.#$.selection.setRange(firstCon.container || firstCon, firstCon.startOffset || 0, a, offset);
} else if (!this.#$.component.is(a)) {
this.#$.selection.setRange(a, offset, a, offset);
}
} catch (error) {
if (this.#frameContext.get('isReadOnly') || this.#frameContext.get('isDisabled')) return;
throw Error(`[SUNEDITOR.html.insert.error] ${error.message}`);
}
} else {
if (this.#$.component.is(html)) {
this.#$.component.insert(html, { skipCharCount, insertBehavior: 'none' });
} else {
let afterNode = null;
if (this.#$.format.isLine(html) || dom.check.isMedia(html)) {
afterNode = this.#$.format.getLine(this.#$.selection.getNode(), null);
}
this.insertNode(html, { afterNode, skipCharCount });
}
}
// focus
this.#store.set('_lastSelectionNode', null);
if (focusNode) {
const children = dom.query.getListChildNodes(focusNode, null, null);
if (children.length > 0) {
focusNode = children.at(-1);
const offset = focusNode?.nodeType === 3 ? focusNode.textContent.length : 1;
this.#$.selection.setRange(focusNode, offset, focusNode, offset);
} else {
this.#$.focusManager.focus();
}
} else {
this.#$.focusManager.focus();
}
this.#$.history.push(false);
}
/**
* @description Delete selected node and insert argument value node and return.
* - If the `afterNode` exists, it is inserted after the `afterNode`
* - Inserting a text node merges with both text nodes on both sides and returns a new `{ container, startOffset, endOffset }`.
* @param {Node} oNode Node to be inserted
* @param {Object} [options] Options
* @param {Node} [options.afterNode=null] If the node exists, it is inserted after the node
* @param {boolean} [options.skipCharCount=null] If `true`, it will be inserted even if `frameOptions.get('charCounter_max')` is exceeded.
* @returns {Object|Node|null}
* @example
* // Insert node at current selection
* const strongNode = document.createElement('strong');
* strongNode.textContent = 'Bold';
* editor.html.insertNode(strongNode);
*
* // Insert node after a specific element
* const paragraph = editor.html.getNode();
* const newSpan = document.createElement('span');
* editor.html.insertNode(newSpan, { afterNode: paragraph });
*
* // Insert bypassing character count limit
* editor.html.insertNode(largeContentNode, { skipCharCount: true });
*/
insertNode(oNode, { afterNode, skipCharCount } = {}) {
let result = null;
if (this.#frameContext.get('isReadOnly') || (!skipCharCount && !this.#$.char.check(oNode))) {
return result;
}
let fNode = null;
let range = null;
if (afterNode) {
const afterNewLine = this.#$.format.isLine(afterNode) || this.#$.format.isBlock(afterNode) || this.#$.component.is(afterNode) ? this.#$.format.addLine(afterNode, null) : afterNode;
range = this.#$.selection.setRange(afterNewLine, 1, afterNewLine, 1);
} else {
range = this.#$.selection.getRange();
}
let line = dom.check.isListCell(range.commonAncestorContainer) ? range.commonAncestorContainer : this.#$.format.getLine(this.#$.selection.getNode(), null);
let insertListCell = dom.check.isListCell(line) && (dom.check.isListCell(oNode) || dom.check.isList(oNode));
let parentNode,
originAfter,
tempAfterNode,
tempParentNode = null;
const freeFormat = this.#$.format.isBrLine(line);
const isFormats = this.#$.format.isLine(oNode) || this.#$.format.isBlock(oNode) || this.#$.component.isBasic(oNode);
if (insertListCell) {
tempAfterNode = afterNode || dom.check.isList(oNode) ? line.lastChild : line.nextElementSibling;
tempParentNode = dom.check.isList(oNode) ? line : (tempAfterNode || line).parentNode;
}
if (!afterNode && (isFormats || this.#$.component.isBasic(oNode) || dom.check.isMedia(oNode))) {
const isEdge = dom.check.isEdgePoint(range.endContainer, range.endOffset, 'end');
const r = this.remove();
const container = r.container;
const prevContainer = container === r.prevContainer && range.collapsed ? null : r.prevContainer;
if (insertListCell && prevContainer) {
tempParentNode = prevContainer.nodeType === 3 ? prevContainer.parentNode : prevContainer;
if (tempParentNode.contains(container)) {
let sameParent = true;
tempAfterNode = container;
while (tempAfterNode.parentNode && tempAfterNode.parentNode !== tempParentNode) {
tempAfterNode = tempAfterNode.parentNode;
sameParent = false;
}
if (sameParent && container === prevContainer) tempAfterNode = tempAfterNode.nextSibling;
} else {
tempAfterNode = null;
}
} else if (insertListCell && dom.check.isListCell(container) && !line.parentElement) {
line = dom.utils.createElement('LI');
tempParentNode.appendChild(line);
container.appendChild(tempParentNode);
tempAfterNode = null;
} else if (container.nodeType === 3 || dom.check.isBreak(container) || insertListCell) {
const depthFormat = dom.query.getParentElement(container, (current) => {
return this.#$.format.isBlock(current) || dom.check.isListCell(current);
});
afterNode = this.#$.nodeTransform.split(container, r.offset, !depthFormat ? 0 : dom.query.getNodeDepth(depthFormat) + 1);
if (!afterNode) {
if (!dom.check.isListCell(line)) {
tempAfterNode = afterNode = line;
}
} else if (insertListCell) {
if (line.contains(container)) {
const subList = dom.check.isList(line.lastElementChild);
let newCell = null;
if (!isEdge) {
newCell = line.cloneNode(false);
newCell.appendChild(afterNode.textContent.trim() ? afterNode : dom.utils.createTextNode(unicode.zeroWidthSpace));
}
if (subList) {
if (!newCell) {
newCell = line.cloneNode(false);
newCell.appendChild(dom.utils.createTextNode(unicode.zeroWidthSpace));
}
newCell.appendChild(line.lastElementChild);
}
if (newCell) {
line.parentNode.insertBefore(newCell, line.nextElementSibling);
tempAfterNode = afterNode = newCell;
}
}
} else {
afterNode = afterNode.previousSibling;
}
}
}
range = !afterNode && !isFormats ? this.#$.selection.getRangeAndAddLine(this.#$.selection.getRange(), null) : this.#$.selection.getRange();
const commonCon = range.commonAncestorContainer;
const startOff = range.startOffset;
const endOff = range.endOffset;
const formatRange = range.startContainer === commonCon && this.#$.format.isLine(commonCon);
const startCon = formatRange ? commonCon.childNodes[startOff] || commonCon.childNodes[0] || range.startContainer : range.startContainer;
const endCon = formatRange ? commonCon.childNodes[endOff] || commonCon.childNodes[commonCon.childNodes.length - 1] || range.endContainer : range.endContainer;
if (!insertListCell) {
if (!afterNode) {
parentNode = startCon;
if (startCon.nodeType === 3) {
parentNode = startCon.parentNode;
}
/** No Select range node */
if (range.collapsed) {
if (commonCon.nodeType === 3) {
if (commonCon.textContent.length > endOff) afterNode = /** @type {Text} */ (commonCon).splitText(endOff);
else afterNode = commonCon.nextSibling;
} else {
if (!dom.check.isBreak(parentNode)) {
const c = parentNode.childNodes[startOff];
const focusNode = c?.nodeType === 3 && dom.check.isZeroWidth(c) && dom.check.isBreak(c.nextSibling) ? c.nextSibling : c;
if (focusNode) {
if (!focusNode.nextSibling && dom.check.isBreak(focusNode)) {
parentNode.removeChild(focusNode);
afterNode = null;
} else {
afterNode = dom.check.isBreak(focusNode) && !dom.check.isBreak(oNode) ? focusNode : focusNode.nextSibling;
}
} else {
afterNode = null;
}
} else {
afterNode = parentNode;
parentNode = parentNode.parentNode;
}
}
} else {
/** Select range nodes */
const isSameContainer = startCon === endCon;
if (isSameContainer) {
if (dom.check.isEdgePoint(endCon, endOff)) afterNode = endCon.nextSibling;
else afterNode = /** @type {Text} */ (endCon).splitText(endOff);
let removeNode = startCon;
if (!dom.check.isEdgePoint(startCon, startOff)) removeNode = /** @type {Text} */ (startCon).splitText(startOff);
parentNode.removeChild(removeNode);
if (parentNode.childNodes.length === 0 && isFormats) {
/** @type {HTMLElement} */ (parentNode).innerHTML = '<br>';
}
} else {
const removedTag = this.remove();
const container = removedTag.container;
const prevContainer = removedTag.prevContainer;
if (container?.childNodes.length === 0 && isFormats) {
if (this.#$.format.isLine(container)) {
container.innerHTML = '<br>';
} else if (this.#$.format.isBlock(container)) {
container.innerHTML = '<' + this.#options.get('defaultLine') + '><br></' + this.#options.get('defaultLine') + '>';
}
}
if (dom.check.isListCell(container) && oNode.nodeType === 3) {
parentNode = container;
afterNode = null;
} else if (!isFormats && prevContainer) {
parentNode = prevContainer.nodeType === 3 ? prevContainer.parentNode : prevContainer;
if (parentNode.contains(container)) {
let sameParent = true;
afterNode = container;
while (afterNode.parentNode && afterNode.parentNode !== parentNode) {
afterNode = afterNode.parentNode;
sameParent = false;
}
if (sameParent && container === prevContainer) afterNode = afterNode.nextSibling;
} else {
afterNode = null;
}
} else if (dom.check.isWysiwygFrame(container) && !this.#$.format.isLine(oNode)) {
parentNode = container.appendChild(dom.utils.createElement(this.#options.get('defaultLine')));
afterNode = null;
} else {
afterNode = isFormats ? endCon : container === prevContainer ? container.nextSibling : container;
parentNode = !afterNode || !afterNode.parentNode ? commonCon : afterNode.parentNode;
}
while (afterNode && !this.#$.format.isLine(afterNode) && afterNode.parentNode !== commonCon) {
afterNode = afterNode.parentNode;
}
}
}
} else {
// has afterNode
parentNode = afterNode.parentNode;
afterNode = afterNode.nextSibling;
originAfter = true;
}
}
try {
// set node
const wysiwyg = this.#frameContext.get('wysiwyg');
if (!insertListCell) {
if (dom.check.isWysiwygFrame(afterNode) || parentNode === wysiwyg.parentNode) {
parentNode = wysiwyg;
afterNode = null;
}
if (this.#$.format.isLine(oNode) || this.#$.format.isBlock(oNode) || (!dom.check.isListCell(parentNode) && this.#$.component.isBasic(oNode))) {
const oldParent = parentNode;
if (dom.check.isListCell(afterNode)) {
parentNode = afterNode.previousElementSibling || afterNode;
} else if (!originAfter && !afterNode) {
const r = this.remove();
const container = r.container.nodeType === 3 ? (dom.check.isListCell(this.#$.format.getLine(r.container, null)) ? r.container : this.#$.format.getLine(r.container, null) || r.container.parentNode) : r.container;
const rangeCon = dom.check.isWysiwygFrame(container) || this.#$.format.isBlock(container);
parentNode = rangeCon ? container : container.parentNode;
afterNode = rangeCon ? null : container.nextSibling;
}
if (oldParent.childNodes.length === 0 && parentNode !== oldParent) dom.utils.removeItem(oldParent);
}
if (isFormats && !freeFormat && !this.#$.format.isBlock(parentNode) && !dom.check.isListCell(parentNode) && !dom.check.isWysiwygFrame(parentNode)) {
afterNode = /** @type {HTMLElement} */ (parentNode).nextElementSibling;
parentNode = parentNode.parentNode;
}
if (dom.check.isWysiwygFrame(parentNode) && (oNode.nodeType === 3 || dom.check.isBreak(oNode))) {
const formatNode = dom.utils.createElement(this.#options.get('defaultLine'), null, oNode);
fNode = oNode;
oNode = formatNode;
}
}
// insert--
if (insertListCell) {
if (!tempParentNode.parentNode) {
parentNode = wysiwyg;
afterNode = null;
} else {
parentNode = tempParentNode;
afterNode = tempAfterNode;
}
} else {
afterNode = parentNode === afterNode ? parentNode.lastChild : afterNode;
}
if (dom.check.isListCell(oNode) && !dom.check.isList(parentNode)) {
if (dom.check.isListCell(parentNode)) {
afterNode = parentNode.nextElementSibling;
parentNode = parentNode.parentNode;
} else {
const ul = dom.utils.createElement('ol');
parentNode.insertBefore(ul, afterNode);
parentNode = ul;
afterNode = null;
}
insertListCell = true;
}
this.#checkDuplicateNode(oNode, parentNode);
parentNode.insertBefore(oNode, afterNode);
if (insertListCell) {
if (dom.check.isZeroWidth(line.textContent.trim())) {
dom.utils.removeItem(line);
oNode = oNode.lastChild;
} else {
const chList = dom.utils.arrayFind(line.children, dom.check.isList);
if (chList) {
if (oNode !== chList) {
oNode.appendChild(chList);
oNode = chList.previousSibling;
} else {
parentNode.appendChild(oNode);
oNode = parentNode;
}
if (dom.check.isZeroWidth(line.textContent.trim())) {
dom.utils.removeItem(line);
}
}
}
}
} catch (error) {
parentNode.appendChild(oNode);
console.warn('[SUNEDITOR.html.insertNode.warn]', error);
} finally {
if (fNode) oNode = fNode;
const dupleNodes = /** @type {HTMLElement} */ (parentNode).querySelectorAll('[data-duple]');
if (dupleNodes.length > 0) {
for (let i = 0, len = dupleNodes.length, d, c, ch, parent; i < len; i++) {
d = dupleNodes[i];
ch = d.childNodes;
parent = d.parentNode;
while (ch[0]) {
c = ch[0];
parent.insertBefore(c, d);
}
if (d === oNode) oNode = c;
dom.utils.removeItem(d);
}
}
if ((this.#$.format.isLine(oNode) || this.#$.component.isBasic(oNode)) && startCon === endCon) {
const cItem = this.#$.format.getLine(commonCon, null);
if (cItem?.nodeType === 1 && dom.check.isEmptyLine(cItem)) {
dom.utils.removeItem(cItem);
}
}
if (freeFormat && !dom.check.isList(oNode) && (this.#$.format.isLine(oNode) || this.#$.format.isBlock(oNode))) {
oNode = this.#setIntoFreeFormat(oNode);
}
if (!this.#$.component.isBasic(oNode)) {
let offset = 1;
if (oNode.nodeType === 3) {
offset = oNode.textContent.length;
this.#$.selection.setRange(oNode, offset, oNode, offset);
} else if (!dom.check.isBreak(oNode) && !dom.check.isListCell(oNode) && this.#$.format.isLine(parentNode)) {
let zeroWidth = null;
if (!oNode.previousSibling || dom.check.isBreak(oNode.previousSibling)) {
zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
oNode.parentNode.insertBefore(zeroWidth, oNode);
}
if (!oNode.nextSibling || dom.check.isBreak(oNode.nextSibling)) {
zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
oNode.parentNode.insertBefore(zeroWidth, oNode.nextSibling);
}
if (this.#$.inline._isIgnoreNodeChange(oNode)) {
oNode = oNode.nextSibling;
offset = 0;
}
this.#$.selection.setRange(oNode, offset, oNode, offset);
}
}
result = oNode;
}
return result;
}
/**
* @description Delete the selected range.
* @returns {{container: Node, offset: number, commonCon?: ?Node, prevContainer?: ?Node}}
* - `container`: the last element after deletion
* - `offset`: offset
* - `commonCon`: `commonAncestorContainer`
* - `prevContainer`: `previousElementSibling` of the deleted area
*/
remove() {
this.#$.selection.resetRangeToTextNode();
const range = this.#$.selection.getRange();
const isStartEdge = range.startOffset === 0;
const isEndEdge = dom.check.isEdgePoint(range.endContainer, range.endOffset, 'end');
let prevContainer = null;
let startPrevEl = null;
let endNextEl = null;
if (isStartEdge) {
startPrevEl = this.#$.format.getLine(range.startContainer);
prevContainer = startPrevEl ? startPrevEl.previousElementSibling : null;
startPrevEl = startPrevEl ? prevContainer : startPrevEl;
}
if (isEndEdge) {
endNextEl = this.#$.format.getLine(range.endContainer);
endNextEl = endNextEl ? endNextEl.nextElementSibling : endNextEl;
}
let container,
offset = 0;
let startCon = range.startContainer;
let endCon = range.endContainer;
let startOff = range.startOffset;
let endOff = range.endOffset;
const commonCon = /** @type {HTMLElement} */ (range.commonAncestorContainer.nodeType === 3 && range.commonAncestorContainer.parentNode === startCon.parentNode ? startCon.parentNode : range.commonAncestorContainer);
if (dom.check.isWysiwygFrame(startCon) && dom.check.isWysiwygFrame(endCon)) {
this.set('');
const newInitBR = this.#$.selection.getNode();
return {
container: newInitBR,
offset: 0,
commonCon,
};
}
if (commonCon === startCon && commonCon === endCon) {
if (this.#$.component.isBasic(commonCon)) {
const compInfo = this.#$.component.get(commonCon);
const compContainer = compInfo.container;
const parent = compContainer.parentElement;
const next = compContainer.nextSibling || compContainer.previousSibling;
const nextOffset = next === compContainer.previousSibling ? next?.textContent?.length || 1 : 0;
const parentNext = parent.nextElementSibling || parent.previousElementSibling;
const parentNextOffset = parentNext === parent.previousElementSibling ? parentNext?.textContent?.length || 1 : 0;
dom.utils.removeItem(compContainer);
if (this.#$.format.isLine(parent)) {
if (parent.childNodes.length === 0) {
dom.utils.removeItem(parent);
return {
container: parentNext,
offset: parentNextOffset,
commonCon,
};
} else {
return {
container: next,
offset: nextOffset,
commonCon,
};
}
} else {
return {
container: parentNext,
offset: parentNextOffset,
commonCon,
};
}
} else {
if ((commonCon.nodeType === 1 && startOff === 0 && endOff === 1) || (commonCon.nodeType === 3 && startOff === 0 && endOff === commonCon.textContent.length)) {
const nextEl = dom.query.getNextDeepestNode(commonCon, this.#frameContext.get('wysiwyg'));
const prevEl = dom.query.getPreviousDeepestNode(commonCon, this.#frameContext.get('wysiwyg'));
const line = this.#$.format.getLine(commonCon);
dom.utils.removeItem(commonCon);
let rEl = nextEl || prevEl;
let rOffset = nextEl ? 0 : rEl?.nodeType === 3 ? rEl.textContent.length : 1;
const npEl = this.#$.format.getLine(rEl) || this.#$.component.get(rEl);
if (line !== npEl) {
rEl = /** @type {Node} */ (npEl);
rOffset = rOffset === 0 ? 0 : 1;
}
if (dom.check.isZeroWidth(line) && !line.contains(rEl)) {
dom.utils.removeItem(line);
}
return {
container: rEl,
offset: rOffset,
commonCon,
};
}
startCon = commonCon.children[startOff];
endCon = commonCon.children[endOff];
startOff = endOff = 0;
}
}
if (!startCon || !endCon)
return {
container: commonCon,
offset: 0,
commonCon,
};
if (startCon === endCon && range.collapsed) {
if (dom.check.isZeroWidth(startCon.textContent?.substring(startOff))) {
return {
container: startCon,
offset: startOff,
prevContainer: startCon && startCon.parentNode ? startCon : null,
commonCon,
};
}
}
let beforeNode = null;
let afterNode = null;
const childNodes = dom.query.getListChildNodes(commonCon, null, null);
let startIndex = dom.utils.getArrayIndex(childNodes, startCon);
let endIndex = dom.utils.getArrayIndex(childNodes, endCon);
if (childNodes.length > 0 && startIndex > -1 && endIndex > -1) {
for (let i = startIndex + 1, startNode = startCon; i >= 0; i--) {
if (childNodes[i] === startNode.parentNode && childNodes[i].firstChild === startNode && startOff === 0) {
startIndex = i;
startNode = startNode.parentNode;
}
}
for (let i = endIndex - 1, endNode = endCon; i > startIndex; i--) {
if (childNodes[i] === endNode.parentNode && childNodes[i].nodeType === 1) {
childNodes.splice(i, 1);
endNode = endNode.parentNode;
--endIndex;
}
}
} else {
if (childNodes.length === 0) {
if (this.#$.format.isLine(commonCon) || this.#$.format.isBlock(commonCon) || dom.check.isWysiwygFrame(commonCon) || dom.check.isBreak(commonCon) || dom.check.isMedia(commonCon)) {
return {
container: commonCon,
offset: 0,
commonCon,
};
} else if (dom.check.isText(commonCon)) {
return {
container: commonCon,
offset: endOff,
commonCon,
};
}
childNodes.push(commonCon);
startCon = endCon = commonCon;
} else {
startCon = endCon = childNodes[0];
if (dom.check.isBreak(startCon) || dom.check.isZeroWidth(startCon)) {
return {
container: dom.check.isMedia(commonCon) ? commonCon : startCon,
offset: 0,
commonCon,
};
}
}
startIndex = endIndex = 0;
}
const _isText = dom.check.isText;
const _isElement = dom.check.isElement;
const _isSingleItem = startIndex === endIndex;
let nextFocusNodes = null;
for (let i = startIndex; i <= endIndex; i++) {
const item = /** @type {Text} */ (childNodes[i]);
if (_isText(item) && (item.data === undefined || item.length === 0)) {
nextFocusNodes = this.#nodeRemoveListItem(item, _isSingleItem);
continue;
}
if (item === startCon) {
if (_isElement(startCon)) {
if (this.#$.component.is(startCon)) continue;
else beforeNode = dom.utils.createTextNode(startCon.textContent);
} else {
const sc = /** @type {Text} */ (startCon);
const ec = /** @type {Text} */ (endCon);
if (item === endCon) {
beforeNode = dom.utils.createTextNode(sc.substringData(0, startOff) + ec.substringData(endOff, ec.length - endOff));
offset = startOff;
} else {
beforeNode = dom.utils.createTextNode(sc.substringData(0, startOff));
}
}
if (beforeNode.length > 0) {
/** @type {Text} */ (startCon).data = beforeNode.data;
} else {
nextFocusNodes = this.#nodeRemoveListItem(startCon, _isSingleItem);
}
if (item === endCon) break;
continue;
}
if (item === endCon) {
if (_isText(endCon)) {
afterNode = dom.utils.createTextNode(endCon.substringData(endOff, endCon.length - endOff));
} else {
if (this.#$.component.is(endCon)) continue;
else afterNode = dom.utils.createTextNode(endCon.textContent);
}
if (afterNode.length > 0) {
/** @type {Text} */ (endCon).data = afterNode.data;
} else {
nextFocusNodes = this.#nodeRemoveListItem(endCon, _isSingleItem);
}
continue;
}
nextFocusNodes = this.#nodeRemoveListItem(item, _isSingleItem);
}
const endUl = dom.query.getParentElement(endCon, 'ul');
const startLi = dom.query.getParentElement(startCon, 'li');
if (endUl && startLi && startLi.contains(endUl)) {
container = endUl.previousSibling;
offset = container.textContent.length;
} else {
container = endCon && endCon.parentNode ? endCon : startCon && startCon.parentNode ? startCon : range.endContainer || range.startContainer;
if (isStartEdge || isEndEdge) {
if (isEndEdge) {
if (container.nodeType === 1 && container.childNodes.length === 0) {
container.appendChild(dom.utils.createElement('BR'));
offset = 1;
} else {
offset = container.textContent.length;
}
} else {
offset = 0;
}
}
}
if (!this.#$.format.getLine(container) && !(startCon && startCon.parentNode)) {
if (endNextEl) {
container = endNextEl;
offset = 0;
} else if (startPrevEl) {
container = startPrevEl;
offset = 1;
}
}
if (!dom.check.isWysiwygFrame(container) && container.childNodes.length === 0) {
const rc = this.#$.nodeTransform.removeAllParents(container, null, null);
if (rc) container = rc.sc || rc.ec || this.#frameContext.get('wysiwyg');
}
if (!container || (container.nodeType === 1 && !this.#$.format.isLine(container) && !dom.check.isBreak(container))) {
container = nextFocusNodes?.sc || nextFocusNodes?.ec;
offset = container?.nodeType === 3 ? container.textContent.length : 1;
}
// set range
this.#$.selection.setRange(container, offset, container, offset);
return {
container,
offset,
prevContainer,
commonCon: commonCon?.parentElement ? commonCon : null,
};
}
/**
* @description Gets the current content
* @param {Object} [options] Options
* @param {boolean} [options.withFrame=false] Gets the current content with containing parent `div.sun-editor-editable` (`<div class="sun-editor-editable">{content}</div>`).
* Ignored for `targetOptions.get('iframe_fullPage')` is `true`.
* @param {boolean} [options.includeFullPage=false] Return only the content of the body without headers when the `iframe_fullPage` option is `true`
* @param {number|Array<number>} [options.rootKey=null] Root index
* @returns {string|Object<*, string>}
* @example
* const html = editor.$.html.get();
* const htmlWithFrame = editor.$.html.get({ withFrame: true });
*/
get({ withFrame, includeFullPage, rootKey } = {}) {
if (!rootKey) rootKey = [this.#store.get('rootKey')];
else if (!Array.isArray(rootKey)) rootKey = [rootKey];
const prevrootKey = this.#store.get('rootKey');
const resultValue = {};
for (let i = 0, len = rootKey.length, r; i < len; i++) {
this.#$.facade.changeFrameContext(rootKey[i]);
const renderHTML = dom.utils.createElement('DIV', null, this._convertToCode(this.#frameContext.get('wysiwyg'), true));
const isTableCell = dom.check.isTableCell;
const isEmptyLine = dom.check.isEmptyLine;
const editableEls = [];
const emptyCells = [];
dom.query.getListChildren(
renderHTML,
(current) => {
if (current.hasAttribute('contenteditable')) {
editableEls.push(current);
}
const parent = current.parentElement;
if (isTableCell(parent) && parent.children.length <= 1 && isEmptyLine(current)) {
emptyCells.push(parent);
}
return false;
},
null,
);
for (let j = 0, jlen = editableEls.length; j < jlen; j++) {
editableEls[j].removeAttribute('contenteditable');
}
for (let j = 0, jlen = emptyCells.length; j < jlen; j++) {
emptyCells[j].innerHTML = '<br>';
}
// output: wrap code blocks <pre class="language-xxx"> → <pre><code class="language-xxx">
this.#wrapPreCode(renderHTML);
const content = renderHTML.innerHTML;
if (this.#frameOptions.get('iframe_fullPage')) {
if (includeFullPage) {
const attrs = dom.utils.getAttributesToString(this.#frameContext.get('_wd').body, ['contenteditable']);
r = `<!DOCTYPE html><html>${this.#frameContext.get('_wd').head.outerHTML}<body ${attrs}>${content}</body></html>`;
} else {
r = content;
}
} else {
r = withFrame ? `<div class="${this.#options.get('_editableClass') + '' + (this.#options.get('_rtl') ? ' se-rtl' : '')}">${content}</div>` : renderHTML.innerHTML;
}
resultValue[rootKey[i]] = r;
}
this.#$.facade.changeFrameContext(prevrootKey);
return rootKey.length > 1 ? resultValue : resultValue[rootKey[0]];
}
/**
* @description Sets the HTML string to the editor content
* @param {string} html HTML string
* @param {Object} [options] Options
* @param {number|Array<number>} [options.rootKey=null] Root index
* @example
* editor.$.html.set('<p>New content</p>');
* editor.$.html.set(html, { rootKey: 'header' });
*/
set(html, { rootKey } = {}) {
this.#$.ui.offCurrentController();
this.#$.selection.removeRange();
const convertValue = html === null || html === undefined ? '' : this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
if (!rootKey) rootKey = [this.#store.get('rootKey')];
else if (!Array.isArray(rootKey)) rootKey = [rootKey];
for (let i = 0; i < rootKey.length; i++) {
this.#$.facade.changeFrameContext(rootKey[i]);
if (this.#frameContext.get('isMarkdownView')) {
const json = converter.htmlToJson(convertValue);
this.#frameContext.get('markdown').value = markdown.jsonToMarkdown(json);
} else if (!this.#frameContext.get('isCodeView')) {
this.#frameContext.get('wysiwyg').innerHTML = convertValue;
this.#$.pluginManager.resetFileInfo();
this.#$.history.push(false, rootKey[i]);
} else {
const value = this._convertToCode(convertValue, false);
this.#$.viewer._setCodeView(value);
}
}
}
/**
* @description Add content to the end of content.
* @param {string} html Content to Input
* @param {Object} [options] Options
* @param {number|Array<number>} [options.rootKey=null] Root index
*/
add(html, { rootKey } = {}) {
this.#$.ui.offCurrentController();
if (!rootKey) rootKey = [this.#store.get('rootKey')];
else if (!Array.isArray(rootKey)) rootKey = [rootKey];
for (let i = 0; i < rootKey.length; i++) {
this.#$.facade.changeFrameContext(rootKey[i]);
const convertValue = this.clean(html, { forceFormat: true, whitelist: null, blacklist: null });
if (this.#frameContext.get('isMarkdownView')) {
const json = converter.htmlToJson(convertValue);
this.#frameContext.get('markdown').value += '\n' + markdown.jsonToMarkdown(json);
} else if (!this.#frameContext.get('isCodeView')) {
const temp = dom.utils.createElement('DIV', null, convertValue);
const children = Array.from(temp.children);
for (let j = 0, jLen = children.length; j < jLen; j++) {
this.#frameContext.get('wysiwyg').appendChild(children[j]);
}
this.#$.history.push(false, rootKey[i]);
this.#$.selection.scrollTo(children.at(-1));
} else {
this.#$.viewer._setCodeView(this.#$.viewer._getCodeView() + '\n' + this._convertToCode(convertValue, false));
}
}
}
/**
* @description Gets the current content to JSON data
* @param {Object} [options] Options
* @param {boolean} [options.withFrame=false] Gets the current content with containing parent `div.sun-editor-editable` (`<div class="sun-editor-editable">{content}</div>`).
* @param {number|Array<number>} [options.rootKey=null] Root index
* @returns {Object<string, *>} JSON data
* @example
* const json = editor.$.html.getJson();
* const jsonWithFrame = editor.$.html.getJson({ withFrame: true });
*/
getJson({ withFrame, rootKey } = {}) {
return converter.htmlToJson(this.get({ withFrame, rootKey }));
}
/**
* @description Sets the JSON data to the editor content
* (see @link converter.jsonToHtml)
* @param {Object<string, *>} jsdonData HTML string
* @param {Object} [options] Options
* @param {number|Array<number>} [options.rootKey=null] Root index
* @example
* const html = editor.$.html.setJson({
* type: 'element', tag: 'p', attributes: { class: 'txt' },
* children: [{ type: 'text', content: 'Hello' }],
* });
* // '<p class="txt">Hello</p>'
*/
setJson(jsdonData, { rootKey } = {}) {
this.set(converter.jsonToHtml(jsdonData), { rootKey });
}
/**
* @description Call `clipboard.write` to copy the contents and display a success/failure toast message.
* @param {Node|Element|Text|string} content Content to be copied to the clipboard
* @returns {Promise<boolean>} Success or failure
*/
async copy(content) {
try {
if (typeof content !== 'string' && !dom.check.isElement(content) && !dom.check.isText(content)) return false;
if ((await clipboard.write(content)) === false) {
this.#$.ui.showToast(this.#$.lang.message_copy_fail, this.#options.get('toastMessageTime').copy, 'error');
return false;
}
this.#$.ui.showToast(this.#$.lang.message_copy_success, this.#options.get('toastMessageTime').copy);
return true;
} catch (err) {
console.error('[SUNEDITOR.html.copy.fail] :', err);
this.#$.ui.showToast(this.#$.lang.message_copy_fail, this.#options.get('toastMessageTime').copy, 'error');
return false;
}
}
/**
* @description Sets the content of the iframe's head tag and body tag when using the `iframe` or `iframe_fullPage` option.
* @param {{head: string, body: string}} ctx { head: HTML string, body: HTML string}
* @param {Object} [options] Options
* @param {number|Array<number>} [options.rootKey=null] Root index
*/
setFullPage(ctx, { rootKey } = {}) {
if (!this.#frameOptions.get('iframe')) return false;
if (!rootKey) rootKey = [this.#store.get('rootKey')];
else if (!Array.isArray(rootKey)) rootKey = [rootKey];
for (let i = 0; i < rootKey.length; i++) {
this.#$.facade.changeFrameContext(rootKey[i]);
if (ctx.head) this.#frameContext.get('_wd').head.innerHTML = ctx.head.replace(this.#disallowedTagsRegExp, '');
if (ctx.body) this.#frameContext.get('_wd').body.innerHTML = this.clean(ctx.body, { forceFormat: true, whitelist: null, blacklist: null });
this.#$.pluginManager.resetFileInfo();
}
}
/**
* @description HTML code compression
* @param {string} html HTML string
* @returns {string} HTML string
*/
compress(html) {
return html.replace(/>\s+</g, '> <').replace(/\n/g, '').trim();
}
/**
* @internal
* @description construct wysiwyg area element to html string
* @param {Node|string} html WYSIWYG element (this.#frameContext.get('wysiwyg')) or HTML string.
* @param {boolean} comp If `true`, does not line break and indentation of tags.
* @returns {string}
*/
_convertToCode(html, comp) {
let returnHTML = '';
const wRegExp = RegExp;
const brReg = new wRegExp('^(BLOCKQUOTE|PRE|TABLE|THEAD|TBODY|TR|TH|TD|OL|UL|IMG|IFRAME|VIDEO|AUDIO|FIGURE|FIGCAPTION|HR|BR|CANVAS|SELECT)$', 'i');
const wDoc = typeof html === 'string' ? _d.createRange().createContextualFragment(html) : html;
const isFormat = (current) => {
return this.#$.format.isLine(current) || this.#$.component.is(current);
};
const brChar = comp ? '' : '\n';
const codeSize = comp ? 0 : this.#store.get('codeIndentSize') * 1;
const indentSize = codeSize > 0 ? new Array(codeSize + 1).join(' ') : '';
(function recursionFunc(element, indent) {
const children = element?.childNodes;
if (!children) return;
const elementRegTest = brReg.test(element.nodeName);
const elementIndent = elementRegTest ? indent : '';
for (let i = 0, len = children.length, node, br, lineBR, nodeRegTest, tag, tagI