3
0
Fork 0
This repository has been archived on 2024-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
ThemeParkPlus-Panel/public/assets/development/formats/xhtml.js
2021-06-27 21:01:43 +02:00

1248 lines
25 KiB
JavaScript
Vendored

/**
* 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 IE_VER = sceditor.ie;
// In IE < 11 a BR at the end of a block level element
// causes a double line break.
var IE_BR_FIX = IE_VER && IE_VER < 11;
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;
});
};
/**
* @param {string} str
* @return {string}
* @private
*/
function trim(str) {
return str
// New lines will be shown as spaces so just convert to spaces.
.replace(/[\r\n]/, ' ')
.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
var tagName = node.nodeName.toLowerCase();
// IE comment
if (tagName === '!') {
handleComment(node);
} else {
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 || (!IE_BR_FIX &&
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) {
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);
}
}
];
/**
* 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));