diff src/development/formats/xhtml.js @ 0:4c4fc447baea

start with sceditor-3.1.1
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 04 Aug 2022 15:21:29 -0600
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/development/formats/xhtml.js	Thu Aug 04 15:21:29 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 = {
+				'&': '&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));