Mercurial Hosting > sceditor
diff src/formats/xhtml.js @ 4:b7725dab7482
move /development/* to /
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 04 Aug 2022 17:59:02 -0600 |
parents | src/development/formats/xhtml.js@4c4fc447baea |
children | 0cb206904499 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/formats/xhtml.js Thu Aug 04 17:59:02 2022 -0600 @@ -0,0 +1,1269 @@ +/** + * SCEditor XHTML Plugin + * http://www.sceditor.com/ + * + * Copyright (C) 2017, Sam Clarke (samclarke.com) + * + * SCEditor is licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + * @author Sam Clarke + */ +(function (sceditor) { + 'use strict'; + + var dom = sceditor.dom; + var utils = sceditor.utils; + + var css = dom.css; + var attr = dom.attr; + var is = dom.is; + var removeAttr = dom.removeAttr; + var convertElement = dom.convertElement; + var extend = utils.extend; + var each = utils.each; + var isEmptyObject = utils.isEmptyObject; + + var getEditorCommand = sceditor.command.get; + + var defaultCommandsOverrides = { + bold: { + txtExec: ['<strong>', '</strong>'] + }, + italic: { + txtExec: ['<em>', '</em>'] + }, + underline: { + txtExec: ['<span style="text-decoration:underline;">', '</span>'] + }, + strike: { + txtExec: ['<span style="text-decoration:line-through;">', '</span>'] + }, + subscript: { + txtExec: ['<sub>', '</sub>'] + }, + superscript: { + txtExec: ['<sup>', '</sup>'] + }, + left: { + txtExec: ['<div style="text-align:left;">', '</div>'] + }, + center: { + txtExec: ['<div style="text-align:center;">', '</div>'] + }, + right: { + txtExec: ['<div style="text-align:right;">', '</div>'] + }, + justify: { + txtExec: ['<div style="text-align:justify;">', '</div>'] + }, + font: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('font')._dropDown( + editor, + caller, + function (font) { + editor.insertText('<span style="font-family:' + + font + ';">', '</span>'); + } + ); + } + }, + size: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('size')._dropDown( + editor, + caller, + function (size) { + editor.insertText('<span style="font-size:' + + size + ';">', '</span>'); + } + ); + } + }, + color: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('color')._dropDown( + editor, + caller, + function (color) { + editor.insertText('<span style="color:' + + color + ';">', '</span>'); + } + ); + } + }, + bulletlist: { + txtExec: ['<ul><li>', '</li></ul>'] + }, + orderedlist: { + txtExec: ['<ol><li>', '</li></ol>'] + }, + table: { + txtExec: ['<table><tr><td>', '</td></tr></table>'] + }, + horizontalrule: { + txtExec: ['<hr />'] + }, + code: { + txtExec: ['<code>', '</code>'] + }, + image: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('image')._dropDown( + editor, + caller, + selected, + function (url, width, height) { + var attrs = ''; + + if (width) { + attrs += ' width="' + width + '"'; + } + + if (height) { + attrs += ' height="' + height + '"'; + } + + editor.insertText( + '<img' + attrs + ' src="' + url + '" />' + ); + } + ); + } + }, + email: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('email')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '<a href="mailto:' + url + '">' + + (text || selected || url) + + '</a>' + ); + } + ); + } + }, + link: { + txtExec: function (caller, selected) { + var editor = this; + + getEditorCommand('link')._dropDown( + editor, + caller, + function (url, text) { + editor.insertText( + '<a href="' + url + '">' + + (text || selected || url) + + '</a>' + ); + } + ); + } + }, + quote: { + txtExec: ['<blockquote>', '</blockquote>'] + }, + youtube: { + txtExec: function (caller) { + var editor = this; + + getEditorCommand('youtube')._dropDown( + editor, + caller, + function (id, time) { + editor.insertText( + '<iframe width="560" height="315" ' + + 'src="https://www.youtube.com/embed/{id}?' + + 'wmode=opaque&start=' + time + '" ' + + 'data-youtube-id="' + id + '" ' + + 'frameborder="0" allowfullscreen></iframe>' + ); + } + ); + } + }, + rtl: { + txtExec: ['<div stlye="direction:rtl;">', '</div>'] + }, + ltr: { + txtExec: ['<div stlye="direction:ltr;">', '</div>'] + } + }; + + /** + * XHTMLSerializer part of the XHTML plugin. + * + * @class XHTMLSerializer + * @name jQuery.sceditor.XHTMLSerializer + * @since v1.4.1 + */ + sceditor.XHTMLSerializer = function () { + var base = this; + + var opts = { + indentStr: '\t' + }; + + /** + * Array containing the output, used as it's faster + * than string concatenation in slow browsers. + * @type {Array} + * @private + */ + var outputStringBuilder = []; + + /** + * Current indention level + * @type {number} + * @private + */ + var currentIndent = 0; + + // TODO: use escape.entities + /** + * Escapes XHTML entities + * + * @param {string} str + * @return {string} + * @private + */ + function escapeEntities(str) { + var entities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\xa0': ' ' + }; + + return !str ? '' : str.replace(/[&<>"\xa0]/g, function (entity) { + return entities[entity] || entity; + }); + }; + + /** + * Replace spaces including newlines with a single + * space except for non-breaking spaces + * + * @param {string} str + * @return {string} + * @private + */ + function trim(str) { + return str.replace(/[^\S\u00A0]+/g, ' '); + }; + + /** + * Serializes a node to XHTML + * + * @param {Node} node Node to serialize + * @param {boolean} onlyChildren If to only serialize the nodes + * children and not the node + * itself + * @return {string} The serialized node + * @name serialize + * @memberOf jQuery.sceditor.XHTMLSerializer.prototype + * @since v1.4.1 + */ + base.serialize = function (node, onlyChildren) { + outputStringBuilder = []; + + if (onlyChildren) { + node = node.firstChild; + + while (node) { + serializeNode(node); + node = node.nextSibling; + } + } else { + serializeNode(node); + } + + return outputStringBuilder.join(''); + }; + + /** + * Serializes a node to the outputStringBuilder + * + * @param {Node} node + * @return {void} + * @private + */ + function serializeNode(node, parentIsPre) { + switch (node.nodeType) { + case 1: // element + handleElement(node, parentIsPre); + break; + + case 3: // text + handleText(node, parentIsPre); + break; + + case 4: // cdata section + handleCdata(node); + break; + + case 8: // comment + handleComment(node); + break; + + case 9: // document + case 11: // document fragment + handleDoc(node); + break; + + // Ignored types + case 2: // attribute + case 5: // entity ref + case 6: // entity + case 7: // processing instruction + case 10: // document type + case 12: // notation + break; + } + }; + + /** + * Handles doc node + * @param {Node} node + * @return {void} + * @private + */ + function handleDoc(node) { + var child = node.firstChild; + + while (child) { + serializeNode(child); + child = child.nextSibling; + } + }; + + /** + * Handles element nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleElement(node, parentIsPre) { + var child, attr, attrValue, + tagName = node.nodeName.toLowerCase(), + isIframe = tagName === 'iframe', + attrIdx = node.attributes.length, + firstChild = node.firstChild, + // pre || pre-wrap with any vendor prefix + isPre = parentIsPre || + /pre(?:\-wrap)?$/i.test(css(node, 'whiteSpace')), + selfClosing = !node.firstChild && !dom.canHaveChildren(node) && + !isIframe; + + if (is(node, '.sceditor-ignore')) { + return; + } + + output('<' + tagName, !parentIsPre && canIndent(node)); + while (attrIdx--) { + attr = node.attributes[attrIdx]; + + attrValue = attr.value; + + output(' ' + attr.name.toLowerCase() + '="' + + escapeEntities(attrValue) + '"', false); + } + output(selfClosing ? ' />' : '>', false); + + if (!isIframe) { + child = firstChild; + } + + while (child) { + currentIndent++; + + serializeNode(child, isPre); + child = child.nextSibling; + + currentIndent--; + } + + if (!selfClosing) { + output( + '</' + tagName + '>', + !isPre && !isIframe && canIndent(node) && + firstChild && canIndent(firstChild) + ); + } + }; + + /** + * Handles CDATA nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleCdata(node) { + output('<![CDATA[' + escapeEntities(node.nodeValue) + ']]>'); + }; + + /** + * Handles comment nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleComment(node) { + output('<!-- ' + escapeEntities(node.nodeValue) + ' -->'); + }; + + /** + * Handles text nodes + * @param {Node} node + * @return {void} + * @private + */ + function handleText(node, parentIsPre) { + var text = node.nodeValue; + + if (!parentIsPre) { + text = trim(text); + } + + if (text) { + output(escapeEntities(text), !parentIsPre && canIndent(node)); + } + }; + + /** + * Adds a string to the outputStringBuilder. + * + * The string will be indented unless indent is set to boolean false. + * @param {string} str + * @param {boolean} indent + * @return {void} + * @private + */ + function output(str, indent) { + var i = currentIndent; + + if (indent !== false) { + // Don't add a new line if it's the first element + if (outputStringBuilder.length) { + outputStringBuilder.push('\n'); + } + + while (i--) { + outputStringBuilder.push(opts.indentStr); + } + } + + outputStringBuilder.push(str); + }; + + /** + * Checks if should indent the node or not + * @param {Node} node + * @return {boolean} + * @private + */ + function canIndent(node) { + var prev = node.previousSibling; + + if (node.nodeType !== 1 && prev) { + return !dom.isInline(prev); + } + + // first child of a block element + if (!prev && !dom.isInline(node.parentNode)) { + return true; + } + + return !dom.isInline(node); + }; + }; + + /** + * SCEditor XHTML plugin + * @class xhtml + * @name jQuery.sceditor.plugins.xhtml + * @since v1.4.1 + */ + function xhtmlFormat() { + var base = this; + + /** + * Tag converters cache + * @type {Object} + * @private + */ + var tagConvertersCache = {}; + + /** + * Attributes filter cache + * @type {Object} + * @private + */ + var attrsCache = {}; + + /** + * Init + * @return {void} + */ + base.init = function () { + if (!isEmptyObject(xhtmlFormat.converters || {})) { + each( + xhtmlFormat.converters, + function (idx, converter) { + each(converter.tags, function (tagname) { + if (!tagConvertersCache[tagname]) { + tagConvertersCache[tagname] = []; + } + + tagConvertersCache[tagname].push(converter); + }); + } + ); + } + + this.commands = extend(true, + {}, defaultCommandsOverrides, this.commands); + }; + + /** + * Converts the WYSIWYG content to XHTML + * + * @param {boolean} isFragment + * @param {string} html + * @param {Document} context + * @param {HTMLElement} [parent] + * @return {string} + * @memberOf jQuery.sceditor.plugins.xhtml.prototype + */ + function toSource(isFragment, html, context) { + var xhtml, + container = context.createElement('div'); + container.innerHTML = html; + + css(container, 'visibility', 'hidden'); + context.body.appendChild(container); + + convertTags(container); + removeTags(container); + removeAttribs(container); + + if (!isFragment) { + wrapInlines(container); + } + + xhtml = (new sceditor.XHTMLSerializer()).serialize(container, true); + + context.body.removeChild(container); + + return xhtml; + }; + + base.toSource = toSource.bind(null, false); + + base.fragmentToSource = toSource.bind(null, true);; + + /** + * Runs all converters for the specified tagName + * against the DOM node. + * @param {string} tagName + * @return {Node} node + * @private + */ + function convertNode(tagName, node) { + if (!tagConvertersCache[tagName]) { + return; + } + + tagConvertersCache[tagName].forEach(function (converter) { + if (converter.tags[tagName]) { + each(converter.tags[tagName], function (attr, values) { + if (!node.getAttributeNode) { + return; + } + + attr = node.getAttributeNode(attr); + + if (!attr || values && values.indexOf(attr.value) < 0) { + return; + } + + converter.conv.call(base, node); + }); + } else if (converter.conv) { + converter.conv.call(base, node); + } + }); + }; + + /** + * Converts any tags/attributes to their XHTML equivalents + * @param {Node} node + * @return {void} + * @private + */ + function convertTags(node) { + dom.traverse(node, function (node) { + var tagName = node.nodeName.toLowerCase(); + + convertNode('*', node); + convertNode(tagName, node); + }, true); + }; + + /** + * Tests if a node is empty and can be removed. + * + * @param {Node} node + * @return {boolean} + * @private + */ + function isEmpty(node, excludeBr) { + var rect, + childNodes = node.childNodes, + tagName = node.nodeName.toLowerCase(), + nodeValue = node.nodeValue, + childrenLength = childNodes.length, + allowedEmpty = xhtmlFormat.allowedEmptyTags || []; + + if (excludeBr && tagName === 'br') { + return true; + } + + if (is(node, '.sceditor-ignore')) { + return true; + } + + if (allowedEmpty.indexOf(tagName) > -1 || tagName === 'td' || + !dom.canHaveChildren(node)) { + + return false; + } + + // \S|\u00A0 = any non space char + if (nodeValue && /\S|\u00A0/.test(nodeValue)) { + return false; + } + + while (childrenLength--) { + if (!isEmpty(childNodes[childrenLength], + excludeBr && !node.previousSibling && !node.nextSibling)) { + return false; + } + } + + // Treat tags with a width and height from CSS as not empty + if (node.getBoundingClientRect && + (node.className || node.hasAttributes('style'))) { + rect = node.getBoundingClientRect(); + return !rect.width || !rect.height; + } + + return true; + }; + + /** + * Removes any tags that are not white listed or if no + * tags are white listed it will remove any tags that + * are black listed. + * + * @param {Node} rootNode + * @return {void} + * @private + */ + function removeTags(rootNode) { + dom.traverse(rootNode, function (node) { + var remove, + tagName = node.nodeName.toLowerCase(), + parentNode = node.parentNode, + nodeType = node.nodeType, + isBlock = !dom.isInline(node), + previousSibling = node.previousSibling, + nextSibling = node.nextSibling, + isTopLevel = parentNode === rootNode, + noSiblings = !previousSibling && !nextSibling, + empty = tagName !== 'iframe' && isEmpty(node, + isTopLevel && noSiblings && tagName !== 'br'), + document = node.ownerDocument, + allowedTags = xhtmlFormat.allowedTags, + firstChild = node.firstChild, + disallowedTags = xhtmlFormat.disallowedTags; + + // 3 = text node + if (nodeType === 3) { + return; + } + + if (nodeType === 4) { + tagName = '!cdata'; + } else if (tagName === '!' || nodeType === 8) { + tagName = '!comment'; + } + + if (nodeType === 1) { + // skip empty nlf elements (new lines automatically + // added after block level elements like quotes) + if (is(node, '.sceditor-nlf')) { + if (!firstChild || (node.childNodes.length === 1 && + /br/i.test(firstChild.nodeName))) { + // Mark as empty,it will be removed by the next code + empty = true; + } else { + node.classList.remove('sceditor-nlf'); + + if (!node.className) { + removeAttr(node, 'class'); + } + } + } + } + + if (empty) { + remove = true; + // 3 is text node which do not get filtered + } else if (allowedTags && allowedTags.length) { + remove = (allowedTags.indexOf(tagName) < 0); + } else if (disallowedTags && disallowedTags.length) { + remove = (disallowedTags.indexOf(tagName) > -1); + } + + if (remove) { + if (!empty) { + if (isBlock && previousSibling && + dom.isInline(previousSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), node); + } + + // Insert all the childen after node + while (node.firstChild) { + parentNode.insertBefore(node.firstChild, + nextSibling); + } + + if (isBlock && nextSibling && + dom.isInline(nextSibling)) { + parentNode.insertBefore( + document.createTextNode(' '), nextSibling); + } + } + + parentNode.removeChild(node); + } + }, true); + }; + + /** + * Merges two sets of attribute filters into one + * + * @param {Object} filtersA + * @param {Object} filtersB + * @return {Object} + * @private + */ + function mergeAttribsFilters(filtersA, filtersB) { + var ret = {}; + + if (filtersA) { + ret = extend({}, ret, filtersA); + } + + if (!filtersB) { + return ret; + } + + each(filtersB, function (attrName, values) { + if (Array.isArray(values)) { + ret[attrName] = (ret[attrName] || []).concat(values); + } else if (!ret[attrName]) { + ret[attrName] = null; + } + }); + + return ret; + }; + + /** + * Wraps adjacent inline child nodes of root + * in paragraphs. + * + * @param {Node} root + * @private + */ + function wrapInlines(root) { + // Strip empty text nodes so they don't get wrapped. + dom.removeWhiteSpace(root); + + var wrapper; + var node = root.firstChild; + var next; + while (node) { + next = node.nextSibling; + + if (dom.isInline(node) && !is(node, '.sceditor-ignore')) { + if (!wrapper) { + wrapper = root.ownerDocument.createElement('p'); + node.parentNode.insertBefore(wrapper, node); + } + + wrapper.appendChild(node); + } else { + wrapper = null; + } + + node = next; + } + }; + + /** + * Removes any attributes that are not white listed or + * if no attributes are white listed it will remove + * any attributes that are black listed. + * @param {Node} node + * @return {void} + * @private + */ + function removeAttribs(node) { + var tagName, attr, attrName, attrsLength, validValues, remove, + allowedAttribs = xhtmlFormat.allowedAttribs, + isAllowed = allowedAttribs && + !isEmptyObject(allowedAttribs), + disallowedAttribs = xhtmlFormat.disallowedAttribs, + isDisallowed = disallowedAttribs && + !isEmptyObject(disallowedAttribs); + + attrsCache = {}; + + dom.traverse(node, function (node) { + if (!node.attributes) { + return; + } + + tagName = node.nodeName.toLowerCase(); + attrsLength = node.attributes.length; + + if (attrsLength) { + if (!attrsCache[tagName]) { + if (isAllowed) { + attrsCache[tagName] = mergeAttribsFilters( + allowedAttribs['*'], + allowedAttribs[tagName] + ); + } else { + attrsCache[tagName] = mergeAttribsFilters( + disallowedAttribs['*'], + disallowedAttribs[tagName] + ); + } + } + + while (attrsLength--) { + attr = node.attributes[attrsLength]; + attrName = attr.name; + validValues = attrsCache[tagName][attrName]; + remove = false; + + if (isAllowed) { + remove = validValues !== null && + (!Array.isArray(validValues) || + validValues.indexOf(attr.value) < 0); + } else if (isDisallowed) { + remove = validValues === null || + (Array.isArray(validValues) && + validValues.indexOf(attr.value) > -1); + } + + if (remove) { + node.removeAttribute(attrName); + } + } + } + }); + }; + }; + + /** + * Tag conveters, a converter is applied to all + * tags that match the criteria. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.converters + * @since v1.4.1 + */ + xhtmlFormat.converters = [ + { + tags: { + '*': { + width: null + } + }, + conv: function (node) { + css(node, 'width', attr(node, 'width')); + removeAttr(node, 'width'); + } + }, + { + tags: { + '*': { + height: null + } + }, + conv: function (node) { + css(node, 'height', attr(node, 'height')); + removeAttr(node, 'height'); + } + }, + { + tags: { + 'li': { + value: null + } + }, + conv: function (node) { + removeAttr(node, 'value'); + } + }, + { + tags: { + '*': { + text: null + } + }, + conv: function (node) { + css(node, 'color', attr(node, 'text')); + removeAttr(node, 'text'); + } + }, + { + tags: { + '*': { + color: null + } + }, + conv: function (node) { + css(node, 'color', attr(node, 'color')); + removeAttr(node, 'color'); + } + }, + { + tags: { + '*': { + face: null + } + }, + conv: function (node) { + css(node, 'fontFamily', attr(node, 'face')); + removeAttr(node, 'face'); + } + }, + { + tags: { + '*': { + align: null + } + }, + conv: function (node) { + css(node, 'textAlign', attr(node, 'align')); + removeAttr(node, 'align'); + } + }, + { + tags: { + '*': { + border: null + } + }, + conv: function (node) { + css(node, 'borderWidth', attr(node, 'border')); + removeAttr(node, 'border'); + } + }, + { + tags: { + applet: { + name: null + }, + img: { + name: null + }, + layer: { + name: null + }, + map: { + name: null + }, + object: { + name: null + }, + param: { + name: null + } + }, + conv: function (node) { + if (!attr(node, 'id')) { + attr(node, 'id', attr(node, 'name')); + } + + removeAttr(node, 'name'); + } + }, + { + tags: { + '*': { + vspace: null + } + }, + conv: function (node) { + css(node, 'marginTop', attr(node, 'vspace') - 0); + css(node, 'marginBottom', attr(node, 'vspace') - 0); + removeAttr(node, 'vspace'); + } + }, + { + tags: { + '*': { + hspace: null + } + }, + conv: function (node) { + css(node, 'marginLeft', attr(node, 'hspace') - 0); + css(node, 'marginRight', attr(node, 'hspace') - 0); + removeAttr(node, 'hspace'); + } + }, + { + tags: { + 'hr': { + noshade: null + } + }, + conv: function (node) { + css(node, 'borderStyle', 'solid'); + removeAttr(node, 'noshade'); + } + }, + { + tags: { + '*': { + nowrap: null + } + }, + conv: function (node) { + css(node, 'whiteSpace', 'nowrap'); + removeAttr(node, 'nowrap'); + } + }, + { + tags: { + big: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'fontSize', 'larger'); + } + }, + { + tags: { + small: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'fontSize', 'smaller'); + } + }, + { + tags: { + b: null + }, + conv: function (node) { + convertElement(node, 'strong'); + } + }, + { + tags: { + u: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'textDecoration', + 'underline'); + } + }, + { + tags: { + s: null, + strike: null + }, + conv: function (node) { + css(convertElement(node, 'span'), 'textDecoration', + 'line-through'); + } + }, + { + tags: { + dir: null + }, + conv: function (node) { + convertElement(node, 'ul'); + } + }, + { + tags: { + center: null + }, + conv: function (node) { + css(convertElement(node, 'div'), 'textAlign', 'center'); + } + }, + { + tags: { + font: { + size: null + } + }, + conv: function (node) { + css(node, 'fontSize', css(node, 'fontSize')); + removeAttr(node, 'size'); + } + }, + { + tags: { + font: null + }, + conv: function (node) { + // All it's attributes will be converted + // by the attribute converters + convertElement(node, 'span'); + } + }, + { + tags: { + '*': { + type: ['_moz'] + } + }, + conv: function (node) { + removeAttr(node, 'type'); + } + }, + { + tags: { + '*': { + '_moz_dirty': null + } + }, + conv: function (node) { + removeAttr(node, '_moz_dirty'); + } + }, + { + tags: { + '*': { + '_moz_editor_bogus_node': null + } + }, + conv: function (node) { + node.parentNode.removeChild(node); + } + }, + { + tags: { + '*': { + 'data-sce-target': null + } + }, + conv: function (node) { + var rel = attr(node, 'rel') || ''; + var target = attr(node, 'data-sce-target'); + + // Only allow the value _blank and only on links + if (target === '_blank' && is(node, 'a')) { + if (!/(^|\s)noopener(\s|$)/.test(rel)) { + attr(node, 'rel', 'noopener' + (rel ? ' ' + rel : '')); + } + + attr(node, 'target', target); + } + + + removeAttr(node, 'data-sce-target'); + } + }, + { + tags: { + code: null + }, + conv: function (node) { + var node, nodes = node.getElementsByTagName('div'); + while ((node = nodes[0])) { + node.style.display = 'block'; + convertElement(node, 'span'); + } + } + } + ]; + + /** + * Allowed attributes map. + * + * To allow an attribute for all tags use * as the tag name. + * + * Leave empty or null to allow all attributes. (the disallow + * list will be used to filter them instead) + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.allowedAttribs + * @since v1.4.1 + */ + xhtmlFormat.allowedAttribs = {}; + + /** + * Attributes that are not allowed. + * + * Only used if allowed attributes is null or empty. + * @type {Object} + * @name jQuery.sceditor.plugins.xhtml.disallowedAttribs + * @since v1.4.1 + */ + xhtmlFormat.disallowedAttribs = {}; + + /** + * Array containing all the allowed tags. + * + * If null or empty all tags will be allowed. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedTags + * @since v1.4.1 + */ + xhtmlFormat.allowedTags = []; + + /** + * Array containing all the disallowed tags. + * + * Only used if allowed tags is null or empty. + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.disallowedTags + * @since v1.4.1 + */ + xhtmlFormat.disallowedTags = []; + + /** + * Array containing tags which should not be removed when empty. + * + * @type {Array} + * @name jQuery.sceditor.plugins.xhtml.allowedEmptyTags + * @since v2.0.0 + */ + xhtmlFormat.allowedEmptyTags = []; + + sceditor.formats.xhtml = xhtmlFormat; +}(sceditor));