view src/formats/xhtml.js @ 8:292d40f68d50

minor
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 05 Aug 2022 12:56:50 -0600
parents b7725dab7482
children 0cb206904499
line wrap: on
line source

/**
 * 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 = {
				'&': '&amp;',
				'<': '&lt;',
				'>': '&gt;',
				'"': '&quot;',
				'\xa0': '&nbsp;'
			};

			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));