init
This commit is contained in:
756
public/tinymce/classes/html/DomParser.js
Normal file
756
public/tinymce/classes/html/DomParser.js
Normal file
@ -0,0 +1,756 @@
|
||||
/**
|
||||
* DomParser.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
|
||||
* sure that the node tree is valid according to the specified schema.
|
||||
* So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
|
||||
*
|
||||
* @example
|
||||
* var parser = new tinymce.html.DomParser({validate: true}, schema);
|
||||
* var rootNode = parser.parse('<h1>content</h1>');
|
||||
*
|
||||
* @class tinymce.html.DomParser
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/DomParser", [
|
||||
"tinymce/html/Node",
|
||||
"tinymce/html/Schema",
|
||||
"tinymce/html/SaxParser",
|
||||
"tinymce/util/Tools"
|
||||
], function(Node, Schema, SaxParser, Tools) {
|
||||
var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
|
||||
|
||||
/**
|
||||
* Constructs a new DomParser instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method DomParser
|
||||
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
|
||||
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
|
||||
*/
|
||||
return function(settings, schema) {
|
||||
var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
|
||||
|
||||
settings = settings || {};
|
||||
settings.validate = "validate" in settings ? settings.validate : true;
|
||||
settings.root_name = settings.root_name || 'body';
|
||||
self.schema = schema = schema || new Schema();
|
||||
|
||||
function fixInvalidChildren(nodes) {
|
||||
var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
|
||||
var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode;
|
||||
|
||||
nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
|
||||
nonEmptyElements = schema.getNonEmptyElements();
|
||||
textBlockElements = schema.getTextBlockElements();
|
||||
|
||||
for (ni = 0; ni < nodes.length; ni++) {
|
||||
node = nodes[ni];
|
||||
|
||||
// Already removed or fixed
|
||||
if (!node.parent || node.fixed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the invalid element is a text block and the text block is within a parent LI element
|
||||
// Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
|
||||
if (textBlockElements[node.name] && node.parent.name == 'li') {
|
||||
// Move sibling text blocks after LI element
|
||||
sibling = node.next;
|
||||
while (sibling) {
|
||||
if (textBlockElements[sibling.name]) {
|
||||
sibling.name = 'li';
|
||||
sibling.fixed = true;
|
||||
node.parent.insert(sibling, node.parent);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
sibling = sibling.next;
|
||||
}
|
||||
|
||||
// Unwrap current text block
|
||||
node.unwrap(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get list of all parent nodes until we find a valid parent to stick the child into
|
||||
parents = [node];
|
||||
for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
|
||||
!nonSplitableElements[parent.name]; parent = parent.parent) {
|
||||
parents.push(parent);
|
||||
}
|
||||
|
||||
// Found a suitable parent
|
||||
if (parent && parents.length > 1) {
|
||||
// Reverse the array since it makes looping easier
|
||||
parents.reverse();
|
||||
|
||||
// Clone the related parent and insert that after the moved node
|
||||
newParent = currentNode = self.filterNode(parents[0].clone());
|
||||
|
||||
// Start cloning and moving children on the left side of the target node
|
||||
for (i = 0; i < parents.length - 1; i++) {
|
||||
if (schema.isValidChild(currentNode.name, parents[i].name)) {
|
||||
tempNode = self.filterNode(parents[i].clone());
|
||||
currentNode.append(tempNode);
|
||||
} else {
|
||||
tempNode = currentNode;
|
||||
}
|
||||
|
||||
for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
|
||||
nextNode = childNode.next;
|
||||
tempNode.append(childNode);
|
||||
childNode = nextNode;
|
||||
}
|
||||
|
||||
currentNode = tempNode;
|
||||
}
|
||||
|
||||
if (!newParent.isEmpty(nonEmptyElements)) {
|
||||
parent.insert(newParent, parents[0], true);
|
||||
parent.insert(node, newParent);
|
||||
} else {
|
||||
parent.insert(node, parents[0], true);
|
||||
}
|
||||
|
||||
// Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
|
||||
parent = parents[0];
|
||||
if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
|
||||
parent.empty().remove();
|
||||
}
|
||||
} else if (node.parent) {
|
||||
// If it's an LI try to find a UL/OL for it or wrap it
|
||||
if (node.name === 'li') {
|
||||
sibling = node.prev;
|
||||
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
|
||||
sibling.append(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
sibling = node.next;
|
||||
if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
|
||||
sibling.insert(node, sibling.firstChild, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
node.wrap(self.filterNode(new Node('ul', 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try wrapping the element in a DIV
|
||||
if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
|
||||
node.wrap(self.filterNode(new Node('div', 1)));
|
||||
} else {
|
||||
// We failed wrapping it, then remove or unwrap it
|
||||
if (node.name === 'style' || node.name === 'script') {
|
||||
node.empty().remove();
|
||||
} else {
|
||||
node.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified node though the element and attributes filters.
|
||||
*
|
||||
* @method filterNode
|
||||
* @param {tinymce.html.Node} Node the node to run filters on.
|
||||
* @return {tinymce.html.Node} The passed in node.
|
||||
*/
|
||||
self.filterNode = function(node) {
|
||||
var i, name, list;
|
||||
|
||||
// Run element filters
|
||||
if (name in nodeFilters) {
|
||||
list = matchedNodes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedNodes[name] = [node];
|
||||
}
|
||||
}
|
||||
|
||||
// Run attribute filters
|
||||
i = attributeFilters.length;
|
||||
while (i--) {
|
||||
name = attributeFilters[i].name;
|
||||
|
||||
if (name in node.attributes.map) {
|
||||
list = matchedAttributes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedAttributes[name] = [node];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a node filter function to the parser, the parser will collect the specified nodes by name
|
||||
* and then execute the callback ones it has finished parsing the document.
|
||||
*
|
||||
* @example
|
||||
* parser.addNodeFilter('p,h1', function(nodes, name) {
|
||||
* for (var i = 0; i < nodes.length; i++) {
|
||||
* console.log(nodes[i].name);
|
||||
* }
|
||||
* });
|
||||
* @method addNodeFilter
|
||||
* @method {String} name Comma separated list of nodes to collect.
|
||||
* @param {function} callback Callback function to execute once it has collected nodes.
|
||||
*/
|
||||
self.addNodeFilter = function(name, callback) {
|
||||
each(explode(name), function(name) {
|
||||
var list = nodeFilters[name];
|
||||
|
||||
if (!list) {
|
||||
nodeFilters[name] = list = [];
|
||||
}
|
||||
|
||||
list.push(callback);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
|
||||
* and then execute the callback ones it has finished parsing the document.
|
||||
*
|
||||
* @example
|
||||
* parser.addAttributeFilter('src,href', function(nodes, name) {
|
||||
* for (var i = 0; i < nodes.length; i++) {
|
||||
* console.log(nodes[i].name);
|
||||
* }
|
||||
* });
|
||||
* @method addAttributeFilter
|
||||
* @method {String} name Comma separated list of nodes to collect.
|
||||
* @param {function} callback Callback function to execute once it has collected nodes.
|
||||
*/
|
||||
self.addAttributeFilter = function(name, callback) {
|
||||
each(explode(name), function(name) {
|
||||
var i;
|
||||
|
||||
for (i = 0; i < attributeFilters.length; i++) {
|
||||
if (attributeFilters[i].name === name) {
|
||||
attributeFilters[i].callbacks.push(callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
attributeFilters.push({name: name, callbacks: [callback]});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the specified HTML string into a DOM like node tree and returns the result.
|
||||
*
|
||||
* @example
|
||||
* var rootNode = new DomParser({...}).parse('<b>text</b>');
|
||||
* @method parse
|
||||
* @param {String} html Html string to sax parse.
|
||||
* @param {Object} args Optional args object that gets passed to all filter functions.
|
||||
* @return {tinymce.html.Node} Root node containing the tree.
|
||||
*/
|
||||
self.parse = function(html, args) {
|
||||
var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
|
||||
var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
|
||||
var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
|
||||
var children, nonEmptyElements, rootBlockName;
|
||||
|
||||
args = args || {};
|
||||
matchedNodes = {};
|
||||
matchedAttributes = {};
|
||||
blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
|
||||
nonEmptyElements = schema.getNonEmptyElements();
|
||||
children = schema.children;
|
||||
validate = settings.validate;
|
||||
rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
|
||||
|
||||
whiteSpaceElements = schema.getWhiteSpaceElements();
|
||||
startWhiteSpaceRegExp = /^[ \t\r\n]+/;
|
||||
endWhiteSpaceRegExp = /[ \t\r\n]+$/;
|
||||
allWhiteSpaceRegExp = /[ \t\r\n]+/g;
|
||||
isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
|
||||
|
||||
function addRootBlocks() {
|
||||
var node = rootNode.firstChild, next, rootBlockNode;
|
||||
|
||||
// Removes whitespace at beginning and end of block so:
|
||||
// <p> x </p> -> <p>x</p>
|
||||
function trim(rootBlockNode) {
|
||||
if (rootBlockNode) {
|
||||
node = rootBlockNode.firstChild;
|
||||
if (node && node.type == 3) {
|
||||
node.value = node.value.replace(startWhiteSpaceRegExp, '');
|
||||
}
|
||||
|
||||
node = rootBlockNode.lastChild;
|
||||
if (node && node.type == 3) {
|
||||
node.value = node.value.replace(endWhiteSpaceRegExp, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
|
||||
if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node) {
|
||||
next = node.next;
|
||||
|
||||
if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
|
||||
!blockElements[node.name] && !node.attr('data-mce-type'))) {
|
||||
if (!rootBlockNode) {
|
||||
// Create a new root block element
|
||||
rootBlockNode = createNode(rootBlockName, 1);
|
||||
rootBlockNode.attr(settings.forced_root_block_attrs);
|
||||
rootNode.insert(rootBlockNode, node);
|
||||
rootBlockNode.append(node);
|
||||
} else {
|
||||
rootBlockNode.append(node);
|
||||
}
|
||||
} else {
|
||||
trim(rootBlockNode);
|
||||
rootBlockNode = null;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
trim(rootBlockNode);
|
||||
}
|
||||
|
||||
function createNode(name, type) {
|
||||
var node = new Node(name, type), list;
|
||||
|
||||
if (name in nodeFilters) {
|
||||
list = matchedNodes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedNodes[name] = [node];
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function removeWhitespaceBefore(node) {
|
||||
var textNode, textVal, sibling;
|
||||
|
||||
for (textNode = node.prev; textNode && textNode.type === 3; ) {
|
||||
textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
|
||||
|
||||
if (textVal.length > 0) {
|
||||
textNode.value = textVal;
|
||||
textNode = textNode.prev;
|
||||
} else {
|
||||
sibling = textNode.prev;
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cloneAndExcludeBlocks(input) {
|
||||
var name, output = {};
|
||||
|
||||
for (name in input) {
|
||||
if (name !== 'li' && name != 'p') {
|
||||
output[name] = input[name];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
parser = new SaxParser({
|
||||
validate: validate,
|
||||
allow_script_urls: settings.allow_script_urls,
|
||||
allow_conditional_comments: settings.allow_conditional_comments,
|
||||
|
||||
// Exclude P and LI from DOM parsing since it's treated better by the DOM parser
|
||||
self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
|
||||
|
||||
cdata: function(text) {
|
||||
node.append(createNode('#cdata', 4)).value = text;
|
||||
},
|
||||
|
||||
text: function(text, raw) {
|
||||
var textNode;
|
||||
|
||||
// Trim all redundant whitespace on non white space elements
|
||||
if (!isInWhiteSpacePreservedElement) {
|
||||
text = text.replace(allWhiteSpaceRegExp, ' ');
|
||||
|
||||
if (node.lastChild && blockElements[node.lastChild.name]) {
|
||||
text = text.replace(startWhiteSpaceRegExp, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to create the node
|
||||
if (text.length !== 0) {
|
||||
textNode = createNode('#text', 3);
|
||||
textNode.raw = !!raw;
|
||||
node.append(textNode).value = text;
|
||||
}
|
||||
},
|
||||
|
||||
comment: function(text) {
|
||||
node.append(createNode('#comment', 8)).value = text;
|
||||
},
|
||||
|
||||
pi: function(name, text) {
|
||||
node.append(createNode(name, 7)).value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
doctype: function(text) {
|
||||
var newNode;
|
||||
|
||||
newNode = node.append(createNode('#doctype', 10));
|
||||
newNode.value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
start: function(name, attrs, empty) {
|
||||
var newNode, attrFiltersLen, elementRule, attrName, parent;
|
||||
|
||||
elementRule = validate ? schema.getElementRule(name) : {};
|
||||
if (elementRule) {
|
||||
newNode = createNode(elementRule.outputName || name, 1);
|
||||
newNode.attributes = attrs;
|
||||
newNode.shortEnded = empty;
|
||||
|
||||
node.append(newNode);
|
||||
|
||||
// Check if node is valid child of the parent node is the child is
|
||||
// unknown we don't collect it since it's probably a custom element
|
||||
parent = children[node.name];
|
||||
if (parent && children[newNode.name] && !parent[newNode.name]) {
|
||||
invalidChildren.push(newNode);
|
||||
}
|
||||
|
||||
attrFiltersLen = attributeFilters.length;
|
||||
while (attrFiltersLen--) {
|
||||
attrName = attributeFilters[attrFiltersLen].name;
|
||||
|
||||
if (attrName in attrs.map) {
|
||||
list = matchedAttributes[attrName];
|
||||
|
||||
if (list) {
|
||||
list.push(newNode);
|
||||
} else {
|
||||
matchedAttributes[attrName] = [newNode];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim whitespace before block
|
||||
if (blockElements[name]) {
|
||||
removeWhitespaceBefore(newNode);
|
||||
}
|
||||
|
||||
// Change current node if the element wasn't empty i.e not <br /> or <img />
|
||||
if (!empty) {
|
||||
node = newNode;
|
||||
}
|
||||
|
||||
// Check if we are inside a whitespace preserved element
|
||||
if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
||||
isInWhiteSpacePreservedElement = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
end: function(name) {
|
||||
var textNode, elementRule, text, sibling, tempNode;
|
||||
|
||||
elementRule = validate ? schema.getElementRule(name) : {};
|
||||
if (elementRule) {
|
||||
if (blockElements[name]) {
|
||||
if (!isInWhiteSpacePreservedElement) {
|
||||
// Trim whitespace of the first node in a block
|
||||
textNode = node.firstChild;
|
||||
if (textNode && textNode.type === 3) {
|
||||
text = textNode.value.replace(startWhiteSpaceRegExp, '');
|
||||
|
||||
// Any characters left after trim or should we remove it
|
||||
if (text.length > 0) {
|
||||
textNode.value = text;
|
||||
textNode = textNode.next;
|
||||
} else {
|
||||
sibling = textNode.next;
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
|
||||
// Remove any pure whitespace siblings
|
||||
while (textNode && textNode.type === 3) {
|
||||
text = textNode.value;
|
||||
sibling = textNode.next;
|
||||
|
||||
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim whitespace of the last node in a block
|
||||
textNode = node.lastChild;
|
||||
if (textNode && textNode.type === 3) {
|
||||
text = textNode.value.replace(endWhiteSpaceRegExp, '');
|
||||
|
||||
// Any characters left after trim or should we remove it
|
||||
if (text.length > 0) {
|
||||
textNode.value = text;
|
||||
textNode = textNode.prev;
|
||||
} else {
|
||||
sibling = textNode.prev;
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
|
||||
// Remove any pure whitespace siblings
|
||||
while (textNode && textNode.type === 3) {
|
||||
text = textNode.value;
|
||||
sibling = textNode.prev;
|
||||
|
||||
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim start white space
|
||||
// Removed due to: #5424
|
||||
/*textNode = node.prev;
|
||||
if (textNode && textNode.type === 3) {
|
||||
text = textNode.value.replace(startWhiteSpaceRegExp, '');
|
||||
|
||||
if (text.length > 0)
|
||||
textNode.value = text;
|
||||
else
|
||||
textNode.remove();
|
||||
}*/
|
||||
}
|
||||
|
||||
// Check if we exited a whitespace preserved element
|
||||
if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
||||
isInWhiteSpacePreservedElement = false;
|
||||
}
|
||||
|
||||
// Handle empty nodes
|
||||
if (elementRule.removeEmpty || elementRule.paddEmpty) {
|
||||
if (node.isEmpty(nonEmptyElements)) {
|
||||
if (elementRule.paddEmpty) {
|
||||
node.empty().append(new Node('#text', '3')).value = '\u00a0';
|
||||
} else {
|
||||
// Leave nodes that have a name like <a name="name">
|
||||
if (!node.attributes.map.name && !node.attributes.map.id) {
|
||||
tempNode = node.parent;
|
||||
node.empty().remove();
|
||||
node = tempNode;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
}, schema);
|
||||
|
||||
rootNode = node = new Node(args.context || settings.root_name, 11);
|
||||
|
||||
parser.parse(html);
|
||||
|
||||
// Fix invalid children or report invalid children in a contextual parsing
|
||||
if (validate && invalidChildren.length) {
|
||||
if (!args.context) {
|
||||
fixInvalidChildren(invalidChildren);
|
||||
} else {
|
||||
args.invalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap nodes in the root into block elements if the root is body
|
||||
if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
|
||||
addRootBlocks();
|
||||
}
|
||||
|
||||
// Run filters only when the contents is valid
|
||||
if (!args.invalid) {
|
||||
// Run node filters
|
||||
for (name in matchedNodes) {
|
||||
list = nodeFilters[name];
|
||||
nodes = matchedNodes[name];
|
||||
|
||||
// Remove already removed children
|
||||
fi = nodes.length;
|
||||
while (fi--) {
|
||||
if (!nodes[fi].parent) {
|
||||
nodes.splice(fi, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, l = list.length; i < l; i++) {
|
||||
list[i](nodes, name, args);
|
||||
}
|
||||
}
|
||||
|
||||
// Run attribute filters
|
||||
for (i = 0, l = attributeFilters.length; i < l; i++) {
|
||||
list = attributeFilters[i];
|
||||
|
||||
if (list.name in matchedAttributes) {
|
||||
nodes = matchedAttributes[list.name];
|
||||
|
||||
// Remove already removed children
|
||||
fi = nodes.length;
|
||||
while (fi--) {
|
||||
if (!nodes[fi].parent) {
|
||||
nodes.splice(fi, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
|
||||
list.callbacks[fi](nodes, list.name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNode;
|
||||
};
|
||||
|
||||
// Remove <br> at end of block elements Gecko and WebKit injects BR elements to
|
||||
// make it possible to place the caret inside empty blocks. This logic tries to remove
|
||||
// these elements and keep br elements that where intended to be there intact
|
||||
if (settings.remove_trailing_brs) {
|
||||
self.addNodeFilter('br', function(nodes) {
|
||||
var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
|
||||
var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
|
||||
var elementRule, textNode;
|
||||
|
||||
// Remove brs from body element as well
|
||||
blockElements.body = 1;
|
||||
|
||||
// Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
|
||||
for (i = 0; i < l; i++) {
|
||||
node = nodes[i];
|
||||
parent = node.parent;
|
||||
|
||||
if (blockElements[node.parent.name] && node === parent.lastChild) {
|
||||
// Loop all nodes to the left of the current node and check for other BR elements
|
||||
// excluding bookmarks since they are invisible
|
||||
prev = node.prev;
|
||||
while (prev) {
|
||||
prevName = prev.name;
|
||||
|
||||
// Ignore bookmarks
|
||||
if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
|
||||
// Found a non BR element
|
||||
if (prevName !== "br") {
|
||||
break;
|
||||
}
|
||||
|
||||
// Found another br it's a <br><br> structure then don't remove anything
|
||||
if (prevName === 'br') {
|
||||
node = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prev = prev.prev;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
node.remove();
|
||||
|
||||
// Is the parent to be considered empty after we removed the BR
|
||||
if (parent.isEmpty(nonEmptyElements)) {
|
||||
elementRule = schema.getElementRule(parent.name);
|
||||
|
||||
// Remove or padd the element depending on schema rule
|
||||
if (elementRule) {
|
||||
if (elementRule.removeEmpty) {
|
||||
parent.remove();
|
||||
} else if (elementRule.paddEmpty) {
|
||||
parent.empty().append(new Node('#text', 3)).value = '\u00a0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
|
||||
// so they become <p><b><i> </i></b></p>
|
||||
lastParent = node;
|
||||
while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
|
||||
lastParent = parent;
|
||||
|
||||
if (blockElements[parent.name]) {
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (lastParent === parent) {
|
||||
textNode = new Node('#text', 3);
|
||||
textNode.value = '\u00a0';
|
||||
node.replace(textNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
|
||||
if (!settings.allow_html_in_named_anchor) {
|
||||
self.addAttributeFilter('id,name', function(nodes) {
|
||||
var i = nodes.length, sibling, prevSibling, parent, node;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
if (node.name === 'a' && node.firstChild && !node.attr('href')) {
|
||||
parent = node.parent;
|
||||
|
||||
// Move children after current node
|
||||
sibling = node.lastChild;
|
||||
do {
|
||||
prevSibling = sibling.prev;
|
||||
parent.insert(sibling, node);
|
||||
sibling = prevSibling;
|
||||
} while (sibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
262
public/tinymce/classes/html/Entities.js
Normal file
262
public/tinymce/classes/html/Entities.js
Normal file
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Entities.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*jshint bitwise:false */
|
||||
|
||||
/**
|
||||
* Entity encoder class.
|
||||
*
|
||||
* @class tinymce.html.Entities
|
||||
* @static
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Entities", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var makeMap = Tools.makeMap;
|
||||
|
||||
var namedEntities, baseEntities, reverseEntities,
|
||||
attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
rawCharsRegExp = /[<>&\"\']/g,
|
||||
entityRegExp = /&(#x|#)?([\w]+);/g,
|
||||
asciiMap = {
|
||||
128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020",
|
||||
135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152",
|
||||
142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022",
|
||||
150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A",
|
||||
156: "\u0153", 158: "\u017E", 159: "\u0178"
|
||||
};
|
||||
|
||||
// Raw entities
|
||||
baseEntities = {
|
||||
'\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&'
|
||||
};
|
||||
|
||||
// Reverse lookup table for raw entities
|
||||
reverseEntities = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
''': "'"
|
||||
};
|
||||
|
||||
// Decodes text by using the browser
|
||||
function nativeDecode(text) {
|
||||
var elm;
|
||||
|
||||
elm = document.createElement("div");
|
||||
elm.innerHTML = text;
|
||||
|
||||
return elm.textContent || elm.innerText || text;
|
||||
}
|
||||
|
||||
// Build a two way lookup table for the entities
|
||||
function buildEntitiesLookup(items, radix) {
|
||||
var i, chr, entity, lookup = {};
|
||||
|
||||
if (items) {
|
||||
items = items.split(',');
|
||||
radix = radix || 10;
|
||||
|
||||
// Build entities lookup table
|
||||
for (i = 0; i < items.length; i += 2) {
|
||||
chr = String.fromCharCode(parseInt(items[i], radix));
|
||||
|
||||
// Only add non base entities
|
||||
if (!baseEntities[chr]) {
|
||||
entity = '&' + items[i + 1] + ';';
|
||||
lookup[chr] = entity;
|
||||
lookup[entity] = chr;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
|
||||
// Unpack entities lookup where the numbers are in radix 32 to reduce the size
|
||||
namedEntities = buildEntitiesLookup(
|
||||
'50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
|
||||
'5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
|
||||
'5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
|
||||
'5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
|
||||
'68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
|
||||
'6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
|
||||
'6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
|
||||
'75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
|
||||
'7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
|
||||
'7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
|
||||
'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
|
||||
'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
|
||||
't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
|
||||
'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
|
||||
'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
|
||||
'81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
|
||||
'8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
|
||||
'8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
|
||||
'8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
|
||||
'8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
|
||||
'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
|
||||
'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
|
||||
'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
|
||||
'80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
|
||||
'811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32);
|
||||
|
||||
var Entities = {
|
||||
/**
|
||||
* Encodes the specified string using raw entities. This means only the required XML base entities will be endoded.
|
||||
*
|
||||
* @method encodeRaw
|
||||
* @param {String} text Text to encode.
|
||||
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
||||
* @return {String} Entity encoded text.
|
||||
*/
|
||||
encodeRaw: function(text, attr) {
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || chr;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Encoded the specified text with both the attributes and text entities. This function will produce larger text contents
|
||||
* since it doesn't know if the context is within a attribute or text node. This was added for compatibility
|
||||
* and is exposed as the DOMUtils.encode function.
|
||||
*
|
||||
* @method encodeAllRaw
|
||||
* @param {String} text Text to encode.
|
||||
* @return {String} Entity encoded text.
|
||||
*/
|
||||
encodeAllRaw: function(text) {
|
||||
return ('' + text).replace(rawCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || chr;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes the specified string using numeric entities. The core entities will be
|
||||
* encoded as named ones but all non lower ascii characters will be encoded into numeric entities.
|
||||
*
|
||||
* @method encodeNumeric
|
||||
* @param {String} text Text to encode.
|
||||
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
||||
* @return {String} Entity encoded text.
|
||||
*/
|
||||
encodeNumeric: function(text, attr) {
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
// Multi byte sequence convert it to a single entity
|
||||
if (chr.length > 1) {
|
||||
return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';';
|
||||
}
|
||||
|
||||
return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';';
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes the specified string using named entities. The core entities will be encoded
|
||||
* as named ones but all non lower ascii characters will be encoded into named entities.
|
||||
*
|
||||
* @method encodeNamed
|
||||
* @param {String} text Text to encode.
|
||||
* @param {Boolean} attr Optional flag to specify if the text is attribute contents.
|
||||
* @param {Object} entities Optional parameter with entities to use.
|
||||
* @return {String} Entity encoded text.
|
||||
*/
|
||||
encodeNamed: function(text, attr, entities) {
|
||||
entities = entities || namedEntities;
|
||||
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || entities[chr] || chr;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an encode function based on the name(s) and it's optional entities.
|
||||
*
|
||||
* @method getEncodeFunc
|
||||
* @param {String} name Comma separated list of encoders for example named,numeric.
|
||||
* @param {String} entities Optional parameter with entities to use instead of the built in set.
|
||||
* @return {function} Encode function to be used.
|
||||
*/
|
||||
getEncodeFunc: function(name, entities) {
|
||||
entities = buildEntitiesLookup(entities) || namedEntities;
|
||||
|
||||
function encodeNamedAndNumeric(text, attr) {
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
|
||||
});
|
||||
}
|
||||
|
||||
function encodeCustomNamed(text, attr) {
|
||||
return Entities.encodeNamed(text, attr, entities);
|
||||
}
|
||||
|
||||
// Replace + with , to be compatible with previous TinyMCE versions
|
||||
name = makeMap(name.replace(/\+/g, ','));
|
||||
|
||||
// Named and numeric encoder
|
||||
if (name.named && name.numeric) {
|
||||
return encodeNamedAndNumeric;
|
||||
}
|
||||
|
||||
// Named encoder
|
||||
if (name.named) {
|
||||
// Custom names
|
||||
if (entities) {
|
||||
return encodeCustomNamed;
|
||||
}
|
||||
|
||||
return Entities.encodeNamed;
|
||||
}
|
||||
|
||||
// Numeric
|
||||
if (name.numeric) {
|
||||
return Entities.encodeNumeric;
|
||||
}
|
||||
|
||||
// Raw encoder
|
||||
return Entities.encodeRaw;
|
||||
},
|
||||
|
||||
/**
|
||||
* Decodes the specified string, this will replace entities with raw UTF characters.
|
||||
*
|
||||
* @method decode
|
||||
* @param {String} text Text to entity decode.
|
||||
* @return {String} Entity decoded string.
|
||||
*/
|
||||
decode: function(text) {
|
||||
return text.replace(entityRegExp, function(all, numeric, value) {
|
||||
if (numeric) {
|
||||
value = parseInt(value, numeric.length === 2 ? 16 : 10);
|
||||
|
||||
// Support upper UTF
|
||||
if (value > 0xFFFF) {
|
||||
value -= 0x10000;
|
||||
|
||||
return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
|
||||
} else {
|
||||
return asciiMap[value] || String.fromCharCode(value);
|
||||
}
|
||||
}
|
||||
|
||||
return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return Entities;
|
||||
});
|
496
public/tinymce/classes/html/Node.js
Normal file
496
public/tinymce/classes/html/Node.js
Normal file
@ -0,0 +1,496 @@
|
||||
/**
|
||||
* Node.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is a minimalistic implementation of a DOM like node used by the DomParser class.
|
||||
*
|
||||
* @example
|
||||
* var node = new tinymce.html.Node('strong', 1);
|
||||
* someRoot.append(node);
|
||||
*
|
||||
* @class tinymce.html.Node
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Node", [], function() {
|
||||
var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
|
||||
'#text': 3,
|
||||
'#comment': 8,
|
||||
'#cdata': 4,
|
||||
'#pi': 7,
|
||||
'#doctype': 10,
|
||||
'#document-fragment': 11
|
||||
};
|
||||
|
||||
// Walks the tree left/right
|
||||
function walk(node, root_node, prev) {
|
||||
var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
|
||||
|
||||
// Walk into nodes if it has a start
|
||||
if (node[startName]) {
|
||||
return node[startName];
|
||||
}
|
||||
|
||||
// Return the sibling if it has one
|
||||
if (node !== root_node) {
|
||||
sibling = node[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
|
||||
// Walk up the parents to look for siblings
|
||||
for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
|
||||
sibling = parent[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Node instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Node
|
||||
* @param {String} name Name of the node type.
|
||||
* @param {Number} type Numeric type representing the node.
|
||||
*/
|
||||
function Node(name, type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
|
||||
if (type === 1) {
|
||||
this.attributes = [];
|
||||
this.attributes.map = {};
|
||||
}
|
||||
}
|
||||
|
||||
Node.prototype = {
|
||||
/**
|
||||
* Replaces the current node with the specified one.
|
||||
*
|
||||
* @example
|
||||
* someNode.replace(someNewNode);
|
||||
*
|
||||
* @method replace
|
||||
* @param {tinymce.html.Node} node Node to replace the current node with.
|
||||
* @return {tinymce.html.Node} The old node that got replaced.
|
||||
*/
|
||||
replace: function(node) {
|
||||
var self = this;
|
||||
|
||||
if (node.parent) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
self.insert(node, self);
|
||||
self.remove();
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets/sets or removes an attribute by name.
|
||||
*
|
||||
* @example
|
||||
* someNode.attr("name", "value"); // Sets an attribute
|
||||
* console.log(someNode.attr("name")); // Gets an attribute
|
||||
* someNode.attr("name", null); // Removes an attribute
|
||||
*
|
||||
* @method attr
|
||||
* @param {String} name Attribute name to set or get.
|
||||
* @param {String} value Optional value to set.
|
||||
* @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation.
|
||||
*/
|
||||
attr: function(name, value) {
|
||||
var self = this, attrs, i, undef;
|
||||
|
||||
if (typeof name !== "string") {
|
||||
for (i in name) {
|
||||
self.attr(i, name[i]);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if ((attrs = self.attributes)) {
|
||||
if (value !== undef) {
|
||||
// Remove attribute
|
||||
if (value === null) {
|
||||
if (name in attrs.map) {
|
||||
delete attrs.map[name];
|
||||
|
||||
i = attrs.length;
|
||||
while (i--) {
|
||||
if (attrs[i].name === name) {
|
||||
attrs = attrs.splice(i, 1);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// Set attribute
|
||||
if (name in attrs.map) {
|
||||
// Set attribute
|
||||
i = attrs.length;
|
||||
while (i--) {
|
||||
if (attrs[i].name === name) {
|
||||
attrs[i].value = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attrs.push({name: name, value: value});
|
||||
}
|
||||
|
||||
attrs.map[name] = value;
|
||||
|
||||
return self;
|
||||
} else {
|
||||
return attrs.map[name];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Does a shallow clones the node into a new node. It will also exclude id attributes since
|
||||
* there should only be one id per document.
|
||||
*
|
||||
* @example
|
||||
* var clonedNode = node.clone();
|
||||
*
|
||||
* @method clone
|
||||
* @return {tinymce.html.Node} New copy of the original node.
|
||||
*/
|
||||
clone: function() {
|
||||
var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs;
|
||||
|
||||
// Clone element attributes
|
||||
if ((selfAttrs = self.attributes)) {
|
||||
cloneAttrs = [];
|
||||
cloneAttrs.map = {};
|
||||
|
||||
for (i = 0, l = selfAttrs.length; i < l; i++) {
|
||||
selfAttr = selfAttrs[i];
|
||||
|
||||
// Clone everything except id
|
||||
if (selfAttr.name !== 'id') {
|
||||
cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
|
||||
cloneAttrs.map[selfAttr.name] = selfAttr.value;
|
||||
}
|
||||
}
|
||||
|
||||
clone.attributes = cloneAttrs;
|
||||
}
|
||||
|
||||
clone.value = self.value;
|
||||
clone.shortEnded = self.shortEnded;
|
||||
|
||||
return clone;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the node in in another node.
|
||||
*
|
||||
* @example
|
||||
* node.wrap(wrapperNode);
|
||||
*
|
||||
* @method wrap
|
||||
*/
|
||||
wrap: function(wrapper) {
|
||||
var self = this;
|
||||
|
||||
self.parent.insert(wrapper, self);
|
||||
wrapper.append(self);
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unwraps the node in other words it removes the node but keeps the children.
|
||||
*
|
||||
* @example
|
||||
* node.unwrap();
|
||||
*
|
||||
* @method unwrap
|
||||
*/
|
||||
unwrap: function() {
|
||||
var self = this, node, next;
|
||||
|
||||
for (node = self.firstChild; node; ) {
|
||||
next = node.next;
|
||||
self.insert(node, self, true);
|
||||
node = next;
|
||||
}
|
||||
|
||||
self.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the node from it's parent.
|
||||
*
|
||||
* @example
|
||||
* node.remove();
|
||||
*
|
||||
* @method remove
|
||||
* @return {tinymce.html.Node} Current node that got removed.
|
||||
*/
|
||||
remove: function() {
|
||||
var self = this, parent = self.parent, next = self.next, prev = self.prev;
|
||||
|
||||
if (parent) {
|
||||
if (parent.firstChild === self) {
|
||||
parent.firstChild = next;
|
||||
|
||||
if (next) {
|
||||
next.prev = null;
|
||||
}
|
||||
} else {
|
||||
prev.next = next;
|
||||
}
|
||||
|
||||
if (parent.lastChild === self) {
|
||||
parent.lastChild = prev;
|
||||
|
||||
if (prev) {
|
||||
prev.next = null;
|
||||
}
|
||||
} else {
|
||||
next.prev = prev;
|
||||
}
|
||||
|
||||
self.parent = self.next = self.prev = null;
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends a new node as a child of the current node.
|
||||
*
|
||||
* @example
|
||||
* node.append(someNode);
|
||||
*
|
||||
* @method append
|
||||
* @param {tinymce.html.Node} node Node to append as a child of the current one.
|
||||
* @return {tinymce.html.Node} The node that got appended.
|
||||
*/
|
||||
append: function(node) {
|
||||
var self = this, last;
|
||||
|
||||
if (node.parent) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
last = self.lastChild;
|
||||
if (last) {
|
||||
last.next = node;
|
||||
node.prev = last;
|
||||
self.lastChild = node;
|
||||
} else {
|
||||
self.lastChild = self.firstChild = node;
|
||||
}
|
||||
|
||||
node.parent = self;
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a node at a specific position as a child of the current node.
|
||||
*
|
||||
* @example
|
||||
* parentNode.insert(newChildNode, oldChildNode);
|
||||
*
|
||||
* @method insert
|
||||
* @param {tinymce.html.Node} node Node to insert as a child of the current node.
|
||||
* @param {tinymce.html.Node} ref_node Reference node to set node before/after.
|
||||
* @param {Boolean} before Optional state to insert the node before the reference node.
|
||||
* @return {tinymce.html.Node} The node that got inserted.
|
||||
*/
|
||||
insert: function(node, ref_node, before) {
|
||||
var parent;
|
||||
|
||||
if (node.parent) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
parent = ref_node.parent || this;
|
||||
|
||||
if (before) {
|
||||
if (ref_node === parent.firstChild) {
|
||||
parent.firstChild = node;
|
||||
} else {
|
||||
ref_node.prev.next = node;
|
||||
}
|
||||
|
||||
node.prev = ref_node.prev;
|
||||
node.next = ref_node;
|
||||
ref_node.prev = node;
|
||||
} else {
|
||||
if (ref_node === parent.lastChild) {
|
||||
parent.lastChild = node;
|
||||
} else {
|
||||
ref_node.next.prev = node;
|
||||
}
|
||||
|
||||
node.next = ref_node.next;
|
||||
node.prev = ref_node;
|
||||
ref_node.next = node;
|
||||
}
|
||||
|
||||
node.parent = parent;
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all children by name.
|
||||
*
|
||||
* @method getAll
|
||||
* @param {String} name Name of the child nodes to collect.
|
||||
* @return {Array} Array with child nodes matchin the specified name.
|
||||
*/
|
||||
getAll: function(name) {
|
||||
var self = this, node, collection = [];
|
||||
|
||||
for (node = self.firstChild; node; node = walk(node, self)) {
|
||||
if (node.name === name) {
|
||||
collection.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all children of the current node.
|
||||
*
|
||||
* @method empty
|
||||
* @return {tinymce.html.Node} The current node that got cleared.
|
||||
*/
|
||||
empty: function() {
|
||||
var self = this, nodes, i, node;
|
||||
|
||||
// Remove all children
|
||||
if (self.firstChild) {
|
||||
nodes = [];
|
||||
|
||||
// Collect the children
|
||||
for (node = self.firstChild; node; node = walk(node, self)) {
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
// Remove the children
|
||||
i = nodes.length;
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
node.parent = node.firstChild = node.lastChild = node.next = node.prev = null;
|
||||
}
|
||||
}
|
||||
|
||||
self.firstChild = self.lastChild = null;
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true/false if the node is to be considered empty or not.
|
||||
*
|
||||
* @example
|
||||
* node.isEmpty({img: true});
|
||||
* @method isEmpty
|
||||
* @param {Object} elements Name/value object with elements that are automatically treated as non empty elements.
|
||||
* @return {Boolean} true/false if the node is empty or not.
|
||||
*/
|
||||
isEmpty: function(elements) {
|
||||
var self = this, node = self.firstChild, i, name;
|
||||
|
||||
if (node) {
|
||||
do {
|
||||
if (node.type === 1) {
|
||||
// Ignore bogus elements
|
||||
if (node.attributes.map['data-mce-bogus']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep empty elements like <img />
|
||||
if (elements[node.name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep elements with data attributes or name attribute like <a name="1"></a>
|
||||
i = node.attributes.length;
|
||||
while (i--) {
|
||||
name = node.attributes[i].name;
|
||||
if (name === "name" || name.indexOf('data-mce-') === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep comments
|
||||
if (node.type === 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep non whitespace text nodes
|
||||
if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) {
|
||||
return false;
|
||||
}
|
||||
} while ((node = walk(node, self)));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Walks to the next or previous node and returns that node or null if it wasn't found.
|
||||
*
|
||||
* @method walk
|
||||
* @param {Boolean} prev Optional previous node state defaults to false.
|
||||
* @return {tinymce.html.Node} Node that is next to or previous of the current node.
|
||||
*/
|
||||
walk: function(prev) {
|
||||
return walk(this, null, prev);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a node of a specific type.
|
||||
*
|
||||
* @static
|
||||
* @method create
|
||||
* @param {String} name Name of the node type to create for example "b" or "#text".
|
||||
* @param {Object} attrs Name/value collection of attributes that will be applied to elements.
|
||||
*/
|
||||
Node.create = function(name, attrs) {
|
||||
var node, attrName;
|
||||
|
||||
// Create node
|
||||
node = new Node(name, typeLookup[name] || 1);
|
||||
|
||||
// Add attributes if needed
|
||||
if (attrs) {
|
||||
for (attrName in attrs) {
|
||||
node.attr(attrName, attrs[attrName]);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
return Node;
|
||||
});
|
405
public/tinymce/classes/html/SaxParser.js
Normal file
405
public/tinymce/classes/html/SaxParser.js
Normal file
@ -0,0 +1,405 @@
|
||||
/**
|
||||
* SaxParser.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
|
||||
* always execute the events in the right order for tag soup code like <b><p></b></p>. It will also remove elements
|
||||
* and attributes that doesn't fit the schema if the validate setting is enabled.
|
||||
*
|
||||
* @example
|
||||
* var parser = new tinymce.html.SaxParser({
|
||||
* validate: true,
|
||||
*
|
||||
* comment: function(text) {
|
||||
* console.log('Comment:', text);
|
||||
* },
|
||||
*
|
||||
* cdata: function(text) {
|
||||
* console.log('CDATA:', text);
|
||||
* },
|
||||
*
|
||||
* text: function(text, raw) {
|
||||
* console.log('Text:', text, 'Raw:', raw);
|
||||
* },
|
||||
*
|
||||
* start: function(name, attrs, empty) {
|
||||
* console.log('Start:', name, attrs, empty);
|
||||
* },
|
||||
*
|
||||
* end: function(name) {
|
||||
* console.log('End:', name);
|
||||
* },
|
||||
*
|
||||
* pi: function(name, text) {
|
||||
* console.log('PI:', name, text);
|
||||
* },
|
||||
*
|
||||
* doctype: function(text) {
|
||||
* console.log('DocType:', text);
|
||||
* }
|
||||
* }, schema);
|
||||
* @class tinymce.html.SaxParser
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/SaxParser", [
|
||||
"tinymce/html/Schema",
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/util/Tools"
|
||||
], function(Schema, Entities, Tools) {
|
||||
var each = Tools.each;
|
||||
|
||||
/**
|
||||
* Constructs a new SaxParser instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method SaxParser
|
||||
* @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
|
||||
* @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
|
||||
*/
|
||||
return function(settings, schema) {
|
||||
var self = this, noop = function() {};
|
||||
|
||||
settings = settings || {};
|
||||
self.schema = schema = schema || new Schema();
|
||||
|
||||
if (settings.fix_self_closing !== false) {
|
||||
settings.fix_self_closing = true;
|
||||
}
|
||||
|
||||
// Add handler functions from settings and setup default handlers
|
||||
each('comment cdata text start end pi doctype'.split(' '), function(name) {
|
||||
if (name) {
|
||||
self[name] = settings[name] || noop;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Parses the specified HTML string and executes the callbacks for each item it finds.
|
||||
*
|
||||
* @example
|
||||
* new SaxParser({...}).parse('<b>text</b>');
|
||||
* @method parse
|
||||
* @param {String} html Html string to sax parse.
|
||||
*/
|
||||
self.parse = function(html) {
|
||||
var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
|
||||
var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
|
||||
var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
|
||||
var attributesRequired, attributesDefault, attributesForced;
|
||||
var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
|
||||
var decode = Entities.decode, fixSelfClosing, filteredAttrs = Tools.makeMap('src,href');
|
||||
|
||||
function processEndTag(name) {
|
||||
var pos, i;
|
||||
|
||||
// Find position of parent of the same type
|
||||
pos = stack.length;
|
||||
while (pos--) {
|
||||
if (stack[pos].name === name) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Found parent
|
||||
if (pos >= 0) {
|
||||
// Close all the open elements
|
||||
for (i = stack.length - 1; i >= pos; i--) {
|
||||
name = stack[i];
|
||||
|
||||
if (name.valid) {
|
||||
self.end(name.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
|
||||
function parseAttribute(match, name, value, val2, val3) {
|
||||
var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
|
||||
|
||||
name = name.toLowerCase();
|
||||
value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
|
||||
|
||||
// Validate name and value pass through all data- attributes
|
||||
if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
|
||||
attrRule = validAttributesMap[name];
|
||||
|
||||
// Find rule by pattern matching
|
||||
if (!attrRule && validAttributePatterns) {
|
||||
i = validAttributePatterns.length;
|
||||
while (i--) {
|
||||
attrRule = validAttributePatterns[i];
|
||||
if (attrRule.pattern.test(name)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No rule matched
|
||||
if (i === -1) {
|
||||
attrRule = null;
|
||||
}
|
||||
}
|
||||
|
||||
// No attribute rule found
|
||||
if (!attrRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate value
|
||||
if (attrRule.validValues && !(value in attrRule.validValues)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredAttrs[name] && !settings.allow_script_urls) {
|
||||
if (/(java|vb)script:/i.test(decodeURIComponent(value.replace(trimRegExp, '')))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add attribute to list and map
|
||||
attrList.map[name] = value;
|
||||
attrList.push({
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
// Precompile RegExps and map objects
|
||||
tokenRegExp = new RegExp('<(?:' +
|
||||
'(?:!--([\\w\\W]*?)-->)|' + // Comment
|
||||
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
|
||||
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
|
||||
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
|
||||
'(?:\\/([^>]+)>)|' + // End element
|
||||
'(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
|
||||
')', 'g');
|
||||
|
||||
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
|
||||
|
||||
// Setup lookup tables for empty elements and boolean attributes
|
||||
shortEndedElements = schema.getShortEndedElements();
|
||||
selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
|
||||
fillAttrsMap = schema.getBoolAttrs();
|
||||
validate = settings.validate;
|
||||
removeInternalElements = settings.remove_internals;
|
||||
fixSelfClosing = settings.fix_self_closing;
|
||||
specialElements = schema.getSpecialElements();
|
||||
|
||||
while ((matches = tokenRegExp.exec(html))) {
|
||||
// Text
|
||||
if (index < matches.index) {
|
||||
self.text(decode(html.substr(index, matches.index - index)));
|
||||
}
|
||||
|
||||
if ((value = matches[6])) { // End element
|
||||
value = value.toLowerCase();
|
||||
|
||||
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
|
||||
if (value.charAt(0) === ':') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
processEndTag(value);
|
||||
} else if ((value = matches[7])) { // Start element
|
||||
value = value.toLowerCase();
|
||||
|
||||
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
|
||||
if (value.charAt(0) === ':') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
isShortEnded = value in shortEndedElements;
|
||||
|
||||
// Is self closing tag for example an <li> after an open <li>
|
||||
if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
|
||||
processEndTag(value);
|
||||
}
|
||||
|
||||
// Validate element
|
||||
if (!validate || (elementRule = schema.getElementRule(value))) {
|
||||
isValidElement = true;
|
||||
|
||||
// Grab attributes map and patters when validation is enabled
|
||||
if (validate) {
|
||||
validAttributesMap = elementRule.attributes;
|
||||
validAttributePatterns = elementRule.attributePatterns;
|
||||
}
|
||||
|
||||
// Parse attributes
|
||||
if ((attribsValue = matches[8])) {
|
||||
isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
|
||||
|
||||
// If the element has internal attributes then remove it if we are told to do so
|
||||
if (isInternalElement && removeInternalElements) {
|
||||
isValidElement = false;
|
||||
}
|
||||
|
||||
attrList = [];
|
||||
attrList.map = {};
|
||||
|
||||
attribsValue.replace(attrRegExp, parseAttribute);
|
||||
} else {
|
||||
attrList = [];
|
||||
attrList.map = {};
|
||||
}
|
||||
|
||||
// Process attributes if validation is enabled
|
||||
if (validate && !isInternalElement) {
|
||||
attributesRequired = elementRule.attributesRequired;
|
||||
attributesDefault = elementRule.attributesDefault;
|
||||
attributesForced = elementRule.attributesForced;
|
||||
anyAttributesRequired = elementRule.removeEmptyAttrs;
|
||||
|
||||
// Check if any attribute exists
|
||||
if (anyAttributesRequired && !attrList.length) {
|
||||
isValidElement = false;
|
||||
}
|
||||
|
||||
// Handle forced attributes
|
||||
if (attributesForced) {
|
||||
i = attributesForced.length;
|
||||
while (i--) {
|
||||
attr = attributesForced[i];
|
||||
name = attr.name;
|
||||
attrValue = attr.value;
|
||||
|
||||
if (attrValue === '{$uid}') {
|
||||
attrValue = 'mce_' + idCount++;
|
||||
}
|
||||
|
||||
attrList.map[name] = attrValue;
|
||||
attrList.push({name: name, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle default attributes
|
||||
if (attributesDefault) {
|
||||
i = attributesDefault.length;
|
||||
while (i--) {
|
||||
attr = attributesDefault[i];
|
||||
name = attr.name;
|
||||
|
||||
if (!(name in attrList.map)) {
|
||||
attrValue = attr.value;
|
||||
|
||||
if (attrValue === '{$uid}') {
|
||||
attrValue = 'mce_' + idCount++;
|
||||
}
|
||||
|
||||
attrList.map[name] = attrValue;
|
||||
attrList.push({name: name, value: attrValue});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle required attributes
|
||||
if (attributesRequired) {
|
||||
i = attributesRequired.length;
|
||||
while (i--) {
|
||||
if (attributesRequired[i] in attrList.map) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// None of the required attributes where found
|
||||
if (i === -1) {
|
||||
isValidElement = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate element if it's marked as bogus
|
||||
if (attrList.map['data-mce-bogus']) {
|
||||
isValidElement = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidElement) {
|
||||
self.start(value, attrList, isShortEnded);
|
||||
}
|
||||
} else {
|
||||
isValidElement = false;
|
||||
}
|
||||
|
||||
// Treat script, noscript and style a bit different since they may include code that looks like elements
|
||||
if ((endRegExp = specialElements[value])) {
|
||||
endRegExp.lastIndex = index = matches.index + matches[0].length;
|
||||
|
||||
if ((matches = endRegExp.exec(html))) {
|
||||
if (isValidElement) {
|
||||
text = html.substr(index, matches.index - index);
|
||||
}
|
||||
|
||||
index = matches.index + matches[0].length;
|
||||
} else {
|
||||
text = html.substr(index);
|
||||
index = html.length;
|
||||
}
|
||||
|
||||
if (isValidElement) {
|
||||
if (text.length > 0) {
|
||||
self.text(text, true);
|
||||
}
|
||||
|
||||
self.end(value);
|
||||
}
|
||||
|
||||
tokenRegExp.lastIndex = index;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Push value on to stack
|
||||
if (!isShortEnded) {
|
||||
if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) {
|
||||
stack.push({name: value, valid: isValidElement});
|
||||
} else if (isValidElement) {
|
||||
self.end(value);
|
||||
}
|
||||
}
|
||||
} else if ((value = matches[1])) { // Comment
|
||||
// Padd comment value to avoid browsers from parsing invalid comments as HTML
|
||||
if (value.charAt(0) === '>') {
|
||||
value = ' ' + value;
|
||||
}
|
||||
|
||||
if (!settings.allow_conditional_comments && value.substr(0, 3) === '[if') {
|
||||
value = ' ' + value;
|
||||
}
|
||||
|
||||
self.comment(value);
|
||||
} else if ((value = matches[2])) { // CDATA
|
||||
self.cdata(value);
|
||||
} else if ((value = matches[3])) { // DOCTYPE
|
||||
self.doctype(value);
|
||||
} else if ((value = matches[4])) { // PI
|
||||
self.pi(value, matches[5]);
|
||||
}
|
||||
|
||||
index = matches.index + matches[0].length;
|
||||
}
|
||||
|
||||
// Text
|
||||
if (index < html.length) {
|
||||
self.text(decode(html.substr(index)));
|
||||
}
|
||||
|
||||
// Close any open elements
|
||||
for (i = stack.length - 1; i >= 0; i--) {
|
||||
value = stack[i];
|
||||
|
||||
if (value.valid) {
|
||||
self.end(value.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
930
public/tinymce/classes/html/Schema.js
Normal file
930
public/tinymce/classes/html/Schema.js
Normal file
@ -0,0 +1,930 @@
|
||||
/**
|
||||
* Schema.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Schema validator class.
|
||||
*
|
||||
* @class tinymce.html.Schema
|
||||
* @example
|
||||
* if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
|
||||
* alert('span is valid child of p.');
|
||||
*
|
||||
* if (tinymce.activeEditor.schema.getElementRule('p'))
|
||||
* alert('P is a valid element.');
|
||||
*
|
||||
* @class tinymce.html.Schema
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Schema", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var mapCache = {};
|
||||
var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;
|
||||
|
||||
function split(items, delim) {
|
||||
return items ? items.split(delim || ' ') : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a schema lookup table
|
||||
*
|
||||
* @private
|
||||
* @param {String} type html4, html5 or html5-strict schema type.
|
||||
* @return {Object} Schema lookup table.
|
||||
*/
|
||||
function compileSchema(type) {
|
||||
var schema = {}, globalAttributes, eventAttributes, blockContent;
|
||||
var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;
|
||||
|
||||
function add(name, attributes, children) {
|
||||
var ni, i, attributesOrder, args = arguments;
|
||||
|
||||
function arrayToMap(array) {
|
||||
var map = {}, i, l;
|
||||
|
||||
for (i = 0, l = array.length; i < l; i++) {
|
||||
map[array[i]] = {};
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
children = children || [];
|
||||
attributes = attributes || "";
|
||||
|
||||
if (typeof(children) === "string") {
|
||||
children = split(children);
|
||||
}
|
||||
|
||||
// Split string children
|
||||
for (i = 3; i < args.length; i++) {
|
||||
if (typeof(args[i]) === "string") {
|
||||
args[i] = split(args[i]);
|
||||
}
|
||||
|
||||
children.push.apply(children, args[i]);
|
||||
}
|
||||
|
||||
name = split(name);
|
||||
ni = name.length;
|
||||
while (ni--) {
|
||||
attributesOrder = [].concat(globalAttributes, split(attributes));
|
||||
schema[name[ni]] = {
|
||||
attributes: arrayToMap(attributesOrder),
|
||||
attributesOrder: attributesOrder,
|
||||
children: arrayToMap(children)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addAttrs(name, attributes) {
|
||||
var ni, schemaItem, i, l;
|
||||
|
||||
name = split(name);
|
||||
ni = name.length;
|
||||
attributes = split(attributes);
|
||||
while (ni--) {
|
||||
schemaItem = schema[name[ni]];
|
||||
for (i = 0, l = attributes.length; i < l; i++) {
|
||||
schemaItem.attributes[attributes[i]] = {};
|
||||
schemaItem.attributesOrder.push(attributes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use cached schema
|
||||
if (mapCache[type]) {
|
||||
return mapCache[type];
|
||||
}
|
||||
|
||||
// Attributes present on all elements
|
||||
globalAttributes = split("id accesskey class dir lang style tabindex title");
|
||||
|
||||
// Event attributes can be opt-in/opt-out
|
||||
eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
|
||||
"ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
|
||||
"onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
|
||||
"onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
|
||||
"onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
|
||||
"onwaiting"
|
||||
);
|
||||
|
||||
// Block content elements
|
||||
blockContent = split(
|
||||
"address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul"
|
||||
);
|
||||
|
||||
// Phrasing content elements from the HTML5 spec (inline)
|
||||
phrasingContent = split(
|
||||
"a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
|
||||
"label map noscript object q s samp script select small span strong sub sup " +
|
||||
"textarea u var #text #comment"
|
||||
);
|
||||
|
||||
// Add HTML5 items to globalAttributes, blockContent, phrasingContent
|
||||
if (type != "html4") {
|
||||
globalAttributes.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " +
|
||||
"hidden spellcheck translate"));
|
||||
blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav"));
|
||||
phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " +
|
||||
"video ruby bdi keygen"));
|
||||
}
|
||||
|
||||
// Add HTML4 elements unless it's html5-strict
|
||||
if (type != "html5-strict") {
|
||||
globalAttributes.push("xml:lang");
|
||||
|
||||
html4PhrasingContent = split("acronym applet basefont big font strike tt");
|
||||
phrasingContent.push.apply(phrasingContent, html4PhrasingContent);
|
||||
|
||||
each(html4PhrasingContent, function(name) {
|
||||
add(name, "", phrasingContent);
|
||||
});
|
||||
|
||||
html4BlockContent = split("center dir isindex noframes");
|
||||
blockContent.push.apply(blockContent, html4BlockContent);
|
||||
|
||||
// Flow content elements from the HTML5 spec (block+inline)
|
||||
flowContent = [].concat(blockContent, phrasingContent);
|
||||
|
||||
each(html4BlockContent, function(name) {
|
||||
add(name, "", flowContent);
|
||||
});
|
||||
}
|
||||
|
||||
// Flow content elements from the HTML5 spec (block+inline)
|
||||
flowContent = flowContent || [].concat(blockContent, phrasingContent);
|
||||
|
||||
// HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
|
||||
// Schema items <element name>, <specific attributes>, <children ..>
|
||||
add("html", "manifest", "head body");
|
||||
add("head", "", "base command link meta noscript script style title");
|
||||
add("title hr noscript br");
|
||||
add("base", "href target");
|
||||
add("link", "href rel media hreflang type sizes hreflang");
|
||||
add("meta", "name http-equiv content charset");
|
||||
add("style", "media type scoped");
|
||||
add("script", "src async defer type charset");
|
||||
add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
|
||||
"onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
|
||||
"onpopstate onresize onscroll onstorage onunload", flowContent);
|
||||
add("address dt dd div caption", "", flowContent);
|
||||
add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
|
||||
add("blockquote", "cite", flowContent);
|
||||
add("ol", "reversed start type", "li");
|
||||
add("ul", "", "li");
|
||||
add("li", "value", flowContent);
|
||||
add("dl", "", "dt dd");
|
||||
add("a", "href target rel media hreflang type", phrasingContent);
|
||||
add("q", "cite", phrasingContent);
|
||||
add("ins del", "cite datetime", flowContent);
|
||||
add("img", "src alt usemap ismap width height");
|
||||
add("iframe", "src name width height", flowContent);
|
||||
add("embed", "src type width height");
|
||||
add("object", "data type typemustmatch name usemap form width height", flowContent, "param");
|
||||
add("param", "name value");
|
||||
add("map", "name", flowContent, "area");
|
||||
add("area", "alt coords shape href target rel media hreflang type");
|
||||
add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
|
||||
add("colgroup", "span", "col");
|
||||
add("col", "span");
|
||||
add("tbody thead tfoot", "", "tr");
|
||||
add("tr", "", "td th");
|
||||
add("td", "colspan rowspan headers", flowContent);
|
||||
add("th", "colspan rowspan headers scope abbr", flowContent);
|
||||
add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
|
||||
add("fieldset", "disabled form name", flowContent, "legend");
|
||||
add("label", "form for", phrasingContent);
|
||||
add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
|
||||
"formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
|
||||
);
|
||||
add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
|
||||
type == "html4" ? flowContent : phrasingContent);
|
||||
add("select", "disabled form multiple name required size", "option optgroup");
|
||||
add("optgroup", "disabled label", "option");
|
||||
add("option", "disabled label selected value");
|
||||
add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
|
||||
add("menu", "type label", flowContent, "li");
|
||||
add("noscript", "", flowContent);
|
||||
|
||||
// Extend with HTML5 elements
|
||||
if (type != "html4") {
|
||||
add("wbr");
|
||||
add("ruby", "", phrasingContent, "rt rp");
|
||||
add("figcaption", "", flowContent);
|
||||
add("mark rt rp summary bdi", "", phrasingContent);
|
||||
add("canvas", "width height", flowContent);
|
||||
add("video", "src crossorigin poster preload autoplay mediagroup loop " +
|
||||
"muted controls width height", flowContent, "track source");
|
||||
add("audio", "src crossorigin preload autoplay mediagroup loop muted controls", flowContent, "track source");
|
||||
add("source", "src type media");
|
||||
add("track", "kind src srclang label default");
|
||||
add("datalist", "", phrasingContent, "option");
|
||||
add("article section nav aside header footer", "", flowContent);
|
||||
add("hgroup", "", "h1 h2 h3 h4 h5 h6");
|
||||
add("figure", "", flowContent, "figcaption");
|
||||
add("time", "datetime", phrasingContent);
|
||||
add("dialog", "open", flowContent);
|
||||
add("command", "type label icon disabled checked radiogroup command");
|
||||
add("output", "for form name", phrasingContent);
|
||||
add("progress", "value max", phrasingContent);
|
||||
add("meter", "value min max low high optimum", phrasingContent);
|
||||
add("details", "open", flowContent, "summary");
|
||||
add("keygen", "autofocus challenge disabled form keytype name");
|
||||
}
|
||||
|
||||
// Extend with HTML4 attributes unless it's html5-strict
|
||||
if (type != "html5-strict") {
|
||||
addAttrs("script", "language xml:space");
|
||||
addAttrs("style", "xml:space");
|
||||
addAttrs("object", "declare classid codebase codetype archive standby align border hspace vspace");
|
||||
addAttrs("param", "valuetype type");
|
||||
addAttrs("a", "charset name rev shape coords");
|
||||
addAttrs("br", "clear");
|
||||
addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
|
||||
addAttrs("img", "name longdesc align border hspace vspace");
|
||||
addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
|
||||
addAttrs("font basefont", "size color face");
|
||||
addAttrs("input", "usemap align");
|
||||
addAttrs("select", "onchange");
|
||||
addAttrs("textarea");
|
||||
addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
|
||||
addAttrs("ul", "type compact");
|
||||
addAttrs("li", "type");
|
||||
addAttrs("ol dl menu dir", "compact");
|
||||
addAttrs("pre", "width xml:space");
|
||||
addAttrs("hr", "align noshade size width");
|
||||
addAttrs("isindex", "prompt");
|
||||
addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
|
||||
addAttrs("col", "width align char charoff valign");
|
||||
addAttrs("colgroup", "width align char charoff valign");
|
||||
addAttrs("thead", "align char charoff valign");
|
||||
addAttrs("tr", "align char charoff valign bgcolor");
|
||||
addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
|
||||
addAttrs("form", "accept");
|
||||
addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
|
||||
addAttrs("tfoot", "align char charoff valign");
|
||||
addAttrs("tbody", "align char charoff valign");
|
||||
addAttrs("area", "nohref");
|
||||
addAttrs("body", "background bgcolor text link vlink alink");
|
||||
}
|
||||
|
||||
// Extend with HTML5 attributes unless it's html4
|
||||
if (type != "html4") {
|
||||
addAttrs("input button select textarea", "autofocus");
|
||||
addAttrs("input textarea", "placeholder");
|
||||
addAttrs("a", "download");
|
||||
addAttrs("link script img", "crossorigin");
|
||||
addAttrs("iframe", "srcdoc sandbox seamless allowfullscreen");
|
||||
}
|
||||
|
||||
// Special: iframe, ruby, video, audio, label
|
||||
|
||||
// Delete children of the same name from it's parent
|
||||
// For example: form can't have a child of the name form
|
||||
each(split('a form meter progress dfn'), function(name) {
|
||||
if (schema[name]) {
|
||||
delete schema[name].children[name];
|
||||
}
|
||||
});
|
||||
|
||||
// Delete header, footer, sectioning and heading content descendants
|
||||
/*each('dt th address', function(name) {
|
||||
delete schema[name].children[name];
|
||||
});*/
|
||||
|
||||
// Caption can't have tables
|
||||
delete schema.caption.children.table;
|
||||
|
||||
// TODO: LI:s can only have value if parent is OL
|
||||
|
||||
// TODO: Handle transparent elements
|
||||
// a ins del canvas map
|
||||
|
||||
mapCache[type] = schema;
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Schema instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Schema
|
||||
* @param {Object} settings Name/value settings object.
|
||||
*/
|
||||
return function(settings) {
|
||||
var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems;
|
||||
var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap;
|
||||
var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, customElementsMap = {}, specialElements = {};
|
||||
|
||||
// Creates an lookup table map object for the specified option or the default value
|
||||
function createLookupTable(option, default_value, extendWith) {
|
||||
var value = settings[option];
|
||||
|
||||
if (!value) {
|
||||
// Get cached default map or make it if needed
|
||||
value = mapCache[option];
|
||||
|
||||
if (!value) {
|
||||
value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' '));
|
||||
value = extend(value, extendWith);
|
||||
|
||||
mapCache[option] = value;
|
||||
}
|
||||
} else {
|
||||
// Create custom map
|
||||
value = makeMap(value, ',', makeMap(value.toUpperCase(), ' '));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
settings = settings || {};
|
||||
schemaItems = compileSchema(settings.schema);
|
||||
|
||||
// Allow all elements and attributes if verify_html is set to false
|
||||
if (settings.verify_html === false) {
|
||||
settings.valid_elements = '*[*]';
|
||||
}
|
||||
|
||||
// Build styles list
|
||||
if (settings.valid_styles) {
|
||||
validStyles = {};
|
||||
|
||||
// Convert styles into a rule list
|
||||
each(settings.valid_styles, function(value, key) {
|
||||
validStyles[key] = explode(value);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup map objects
|
||||
whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object');
|
||||
selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
|
||||
shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
|
||||
'meta param embed source wbr track');
|
||||
boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
|
||||
'noshade nowrap readonly selected autoplay loop controls');
|
||||
nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap);
|
||||
textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
|
||||
'blockquote center dir fieldset header footer article section hgroup aside nav figure');
|
||||
blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
|
||||
'th tr td li ol ul caption dl dt dd noscript menu isindex samp option ' +
|
||||
'datalist select optgroup', textBlockElementsMap);
|
||||
|
||||
each((settings.special || 'script noscript style textarea').split(' '), function(name) {
|
||||
specialElements[name] = new RegExp('<\/' + name + '[^>]*>','gi');
|
||||
});
|
||||
|
||||
// Converts a wildcard expression string to a regexp for example *a will become /.*a/.
|
||||
function patternToRegExp(str) {
|
||||
return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
|
||||
}
|
||||
|
||||
// Parses the specified valid_elements string and adds to the current rules
|
||||
// This function is a bit hard to read since it's heavily optimized for speed
|
||||
function addValidElements(valid_elements) {
|
||||
var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
|
||||
prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
|
||||
elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
|
||||
attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
|
||||
hasPatternsRegExp = /[*?+]/;
|
||||
|
||||
if (valid_elements) {
|
||||
// Split valid elements into an array with rules
|
||||
valid_elements = split(valid_elements, ',');
|
||||
|
||||
if (elements['@']) {
|
||||
globalAttributes = elements['@'].attributes;
|
||||
globalAttributesOrder = elements['@'].attributesOrder;
|
||||
}
|
||||
|
||||
// Loop all rules
|
||||
for (ei = 0, el = valid_elements.length; ei < el; ei++) {
|
||||
// Parse element rule
|
||||
matches = elementRuleRegExp.exec(valid_elements[ei]);
|
||||
if (matches) {
|
||||
// Setup local names for matches
|
||||
prefix = matches[1];
|
||||
elementName = matches[2];
|
||||
outputName = matches[3];
|
||||
attrData = matches[5];
|
||||
|
||||
// Create new attributes and attributesOrder
|
||||
attributes = {};
|
||||
attributesOrder = [];
|
||||
|
||||
// Create the new element
|
||||
element = {
|
||||
attributes: attributes,
|
||||
attributesOrder: attributesOrder
|
||||
};
|
||||
|
||||
// Padd empty elements prefix
|
||||
if (prefix === '#') {
|
||||
element.paddEmpty = true;
|
||||
}
|
||||
|
||||
// Remove empty elements prefix
|
||||
if (prefix === '-') {
|
||||
element.removeEmpty = true;
|
||||
}
|
||||
|
||||
if (matches[4] === '!') {
|
||||
element.removeEmptyAttrs = true;
|
||||
}
|
||||
|
||||
// Copy attributes from global rule into current rule
|
||||
if (globalAttributes) {
|
||||
for (key in globalAttributes) {
|
||||
attributes[key] = globalAttributes[key];
|
||||
}
|
||||
|
||||
attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
|
||||
}
|
||||
|
||||
// Attributes defined
|
||||
if (attrData) {
|
||||
attrData = split(attrData, '|');
|
||||
for (ai = 0, al = attrData.length; ai < al; ai++) {
|
||||
matches = attrRuleRegExp.exec(attrData[ai]);
|
||||
if (matches) {
|
||||
attr = {};
|
||||
attrType = matches[1];
|
||||
attrName = matches[2].replace(/::/g, ':');
|
||||
prefix = matches[3];
|
||||
value = matches[4];
|
||||
|
||||
// Required
|
||||
if (attrType === '!') {
|
||||
element.attributesRequired = element.attributesRequired || [];
|
||||
element.attributesRequired.push(attrName);
|
||||
attr.required = true;
|
||||
}
|
||||
|
||||
// Denied from global
|
||||
if (attrType === '-') {
|
||||
delete attributes[attrName];
|
||||
attributesOrder.splice(inArray(attributesOrder, attrName), 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default value
|
||||
if (prefix) {
|
||||
// Default value
|
||||
if (prefix === '=') {
|
||||
element.attributesDefault = element.attributesDefault || [];
|
||||
element.attributesDefault.push({name: attrName, value: value});
|
||||
attr.defaultValue = value;
|
||||
}
|
||||
|
||||
// Forced value
|
||||
if (prefix === ':') {
|
||||
element.attributesForced = element.attributesForced || [];
|
||||
element.attributesForced.push({name: attrName, value: value});
|
||||
attr.forcedValue = value;
|
||||
}
|
||||
|
||||
// Required values
|
||||
if (prefix === '<') {
|
||||
attr.validValues = makeMap(value, '?');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for attribute patterns
|
||||
if (hasPatternsRegExp.test(attrName)) {
|
||||
element.attributePatterns = element.attributePatterns || [];
|
||||
attr.pattern = patternToRegExp(attrName);
|
||||
element.attributePatterns.push(attr);
|
||||
} else {
|
||||
// Add attribute to order list if it doesn't already exist
|
||||
if (!attributes[attrName]) {
|
||||
attributesOrder.push(attrName);
|
||||
}
|
||||
|
||||
attributes[attrName] = attr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global rule, store away these for later usage
|
||||
if (!globalAttributes && elementName == '@') {
|
||||
globalAttributes = attributes;
|
||||
globalAttributesOrder = attributesOrder;
|
||||
}
|
||||
|
||||
// Handle substitute elements such as b/strong
|
||||
if (outputName) {
|
||||
element.outputName = elementName;
|
||||
elements[outputName] = element;
|
||||
}
|
||||
|
||||
// Add pattern or exact element
|
||||
if (hasPatternsRegExp.test(elementName)) {
|
||||
element.pattern = patternToRegExp(elementName);
|
||||
patternElements.push(element);
|
||||
} else {
|
||||
elements[elementName] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setValidElements(valid_elements) {
|
||||
elements = {};
|
||||
patternElements = [];
|
||||
|
||||
addValidElements(valid_elements);
|
||||
|
||||
each(schemaItems, function(element, name) {
|
||||
children[name] = element.children;
|
||||
});
|
||||
}
|
||||
|
||||
// Adds custom non HTML elements to the schema
|
||||
function addCustomElements(custom_elements) {
|
||||
var customElementRegExp = /^(~)?(.+)$/;
|
||||
|
||||
if (custom_elements) {
|
||||
each(split(custom_elements, ','), function(rule) {
|
||||
var matches = customElementRegExp.exec(rule),
|
||||
inline = matches[1] === '~',
|
||||
cloneName = inline ? 'span' : 'div',
|
||||
name = matches[2];
|
||||
|
||||
children[name] = children[cloneName];
|
||||
customElementsMap[name] = cloneName;
|
||||
|
||||
// If it's not marked as inline then add it to valid block elements
|
||||
if (!inline) {
|
||||
blockElementsMap[name.toUpperCase()] = {};
|
||||
blockElementsMap[name] = {};
|
||||
}
|
||||
|
||||
// Add elements clone if needed
|
||||
if (!elements[name]) {
|
||||
var customRule = elements[cloneName];
|
||||
|
||||
customRule = extend({}, customRule);
|
||||
delete customRule.removeEmptyAttrs;
|
||||
delete customRule.removeEmpty;
|
||||
|
||||
elements[name] = customRule;
|
||||
}
|
||||
|
||||
// Add custom elements at span/div positions
|
||||
each(children, function(element) {
|
||||
if (element[cloneName]) {
|
||||
element[name] = element[cloneName];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Adds valid children to the schema object
|
||||
function addValidChildren(valid_children) {
|
||||
var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
|
||||
|
||||
if (valid_children) {
|
||||
each(split(valid_children, ','), function(rule) {
|
||||
var matches = childRuleRegExp.exec(rule), parent, prefix;
|
||||
|
||||
if (matches) {
|
||||
prefix = matches[1];
|
||||
|
||||
// Add/remove items from default
|
||||
if (prefix) {
|
||||
parent = children[matches[2]];
|
||||
} else {
|
||||
parent = children[matches[2]] = {'#comment': {}};
|
||||
}
|
||||
|
||||
parent = children[matches[2]];
|
||||
|
||||
each(split(matches[3], '|'), function(child) {
|
||||
if (prefix === '-') {
|
||||
delete parent[child];
|
||||
} else {
|
||||
parent[child] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getElementRule(name) {
|
||||
var element = elements[name], i;
|
||||
|
||||
// Exact match found
|
||||
if (element) {
|
||||
return element;
|
||||
}
|
||||
|
||||
// No exact match then try the patterns
|
||||
i = patternElements.length;
|
||||
while (i--) {
|
||||
element = patternElements[i];
|
||||
|
||||
if (element.pattern.test(name)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.valid_elements) {
|
||||
// No valid elements defined then clone the elements from the schema spec
|
||||
each(schemaItems, function(element, name) {
|
||||
elements[name] = {
|
||||
attributes: element.attributes,
|
||||
attributesOrder: element.attributesOrder
|
||||
};
|
||||
|
||||
children[name] = element.children;
|
||||
});
|
||||
|
||||
// Switch these on HTML4
|
||||
if (settings.schema != "html5") {
|
||||
each(split('strong/b em/i'), function(item) {
|
||||
item = split(item, '/');
|
||||
elements[item[1]].outputName = item[0];
|
||||
});
|
||||
}
|
||||
|
||||
// Add default alt attribute for images
|
||||
elements.img.attributesDefault = [{name: 'alt', value: ''}];
|
||||
|
||||
// Remove these if they are empty by default
|
||||
each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function(name) {
|
||||
if (elements[name]) {
|
||||
elements[name].removeEmpty = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Padd these by default
|
||||
each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function(name) {
|
||||
elements[name].paddEmpty = true;
|
||||
});
|
||||
|
||||
// Remove these if they have no attributes
|
||||
each(split('span'), function(name) {
|
||||
elements[name].removeEmptyAttrs = true;
|
||||
});
|
||||
|
||||
// Remove these by default
|
||||
// TODO: Reenable in 4.1
|
||||
/*each(split('script style'), function(name) {
|
||||
delete elements[name];
|
||||
});*/
|
||||
} else {
|
||||
setValidElements(settings.valid_elements);
|
||||
}
|
||||
|
||||
addCustomElements(settings.custom_elements);
|
||||
addValidChildren(settings.valid_children);
|
||||
addValidElements(settings.extended_valid_elements);
|
||||
|
||||
// Todo: Remove this when we fix list handling to be valid
|
||||
addValidChildren('+ol[ul|ol],+ul[ul|ol]');
|
||||
|
||||
// Delete invalid elements
|
||||
if (settings.invalid_elements) {
|
||||
each(explode(settings.invalid_elements), function(item) {
|
||||
if (elements[item]) {
|
||||
delete elements[item];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If the user didn't allow span only allow internal spans
|
||||
if (!getElementRule('span')) {
|
||||
addValidElements('span[!data-mce-type|*]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Name/value map object with valid parents and children to those parents.
|
||||
*
|
||||
* @example
|
||||
* children = {
|
||||
* div:{p:{}, h1:{}}
|
||||
* };
|
||||
* @field children
|
||||
* @type Object
|
||||
*/
|
||||
self.children = children;
|
||||
|
||||
/**
|
||||
* Name/value map object with valid styles for each element.
|
||||
*
|
||||
* @field styles
|
||||
* @type Object
|
||||
*/
|
||||
self.styles = validStyles;
|
||||
|
||||
/**
|
||||
* Returns a map with boolean attributes.
|
||||
*
|
||||
* @method getBoolAttrs
|
||||
* @return {Object} Name/value lookup map for boolean attributes.
|
||||
*/
|
||||
self.getBoolAttrs = function() {
|
||||
return boolAttrMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with block elements.
|
||||
*
|
||||
* @method getBlockElements
|
||||
* @return {Object} Name/value lookup map for block elements.
|
||||
*/
|
||||
self.getBlockElements = function() {
|
||||
return blockElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with text block elements. Such as: p,h1-h6,div,address
|
||||
*
|
||||
* @method getTextBlockElements
|
||||
* @return {Object} Name/value lookup map for block elements.
|
||||
*/
|
||||
self.getTextBlockElements = function() {
|
||||
return textBlockElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with short ended elements such as BR or IMG.
|
||||
*
|
||||
* @method getShortEndedElements
|
||||
* @return {Object} Name/value lookup map for short ended elements.
|
||||
*/
|
||||
self.getShortEndedElements = function() {
|
||||
return shortEndedElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with self closing tags such as <li>.
|
||||
*
|
||||
* @method getSelfClosingElements
|
||||
* @return {Object} Name/value lookup map for self closing tags elements.
|
||||
*/
|
||||
self.getSelfClosingElements = function() {
|
||||
return selfClosingElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with elements that should be treated as contents regardless if it has text
|
||||
* content in them or not such as TD, VIDEO or IMG.
|
||||
*
|
||||
* @method getNonEmptyElements
|
||||
* @return {Object} Name/value lookup map for non empty elements.
|
||||
*/
|
||||
self.getNonEmptyElements = function() {
|
||||
return nonEmptyElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
|
||||
*
|
||||
* @method getWhiteSpaceElements
|
||||
* @return {Object} Name/value lookup map for white space elements.
|
||||
*/
|
||||
self.getWhiteSpaceElements = function() {
|
||||
return whiteSpaceElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with special elements. These are elements that needs to be parsed
|
||||
* in a special way such as script, style, textarea etc. The map object values
|
||||
* are regexps used to find the end of the element.
|
||||
*
|
||||
* @method getSpecialElements
|
||||
* @return {Object} Name/value lookup map for special elements.
|
||||
*/
|
||||
self.getSpecialElements = function() {
|
||||
return specialElements;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified element and it's child is valid or not
|
||||
* according to the schema.
|
||||
*
|
||||
* @method isValidChild
|
||||
* @param {String} name Element name to check for.
|
||||
* @param {String} child Element child to verify.
|
||||
* @return {Boolean} True/false if the element is a valid child of the specified parent.
|
||||
*/
|
||||
self.isValidChild = function(name, child) {
|
||||
var parent = children[name];
|
||||
|
||||
return !!(parent && parent[child]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified element name and optional attribute is
|
||||
* valid according to the schema.
|
||||
*
|
||||
* @method isValid
|
||||
* @param {String} name Name of element to check.
|
||||
* @param {String} attr Optional attribute name to check for.
|
||||
* @return {Boolean} True/false if the element and attribute is valid.
|
||||
*/
|
||||
self.isValid = function(name, attr) {
|
||||
var attrPatterns, i, rule = getElementRule(name);
|
||||
|
||||
// Check if it's a valid element
|
||||
if (rule) {
|
||||
if (attr) {
|
||||
// Check if attribute name exists
|
||||
if (rule.attributes[attr]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if attribute matches a regexp pattern
|
||||
attrPatterns = rule.attributePatterns;
|
||||
if (attrPatterns) {
|
||||
i = attrPatterns.length;
|
||||
while (i--) {
|
||||
if (attrPatterns[i].pattern.test(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified element is valid or not
|
||||
* according to the schema.
|
||||
*
|
||||
* @method getElementRule
|
||||
* @param {String} name Element name to check for.
|
||||
* @return {Object} Element object or undefined if the element isn't valid.
|
||||
*/
|
||||
self.getElementRule = getElementRule;
|
||||
|
||||
/**
|
||||
* Returns an map object of all custom elements.
|
||||
*
|
||||
* @method getCustomElements
|
||||
* @return {Object} Name/value map object of all custom elements.
|
||||
*/
|
||||
self.getCustomElements = function() {
|
||||
return customElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a valid elements string and adds it to the schema. The valid elements
|
||||
format is for example "element[attr=default|otherattr]".
|
||||
* Existing rules will be replaced with the ones specified, so this extends the schema.
|
||||
*
|
||||
* @method addValidElements
|
||||
* @param {String} valid_elements String in the valid elements format to be parsed.
|
||||
*/
|
||||
self.addValidElements = addValidElements;
|
||||
|
||||
/**
|
||||
* Parses a valid elements string and sets it to the schema. The valid elements
|
||||
* format is for example "element[attr=default|otherattr]".
|
||||
* Existing rules will be replaced with the ones specified, so this extends the schema.
|
||||
*
|
||||
* @method setValidElements
|
||||
* @param {String} valid_elements String in the valid elements format to be parsed.
|
||||
*/
|
||||
self.setValidElements = setValidElements;
|
||||
|
||||
/**
|
||||
* Adds custom non HTML elements to the schema.
|
||||
*
|
||||
* @method addCustomElements
|
||||
* @param {String} custom_elements Comma separated list of custom elements to add.
|
||||
*/
|
||||
self.addCustomElements = addCustomElements;
|
||||
|
||||
/**
|
||||
* Parses a valid children string and adds them to the schema structure. The valid children
|
||||
* format is for example: "element[child1|child2]".
|
||||
*
|
||||
* @method addValidChildren
|
||||
* @param {String} valid_children Valid children elements string to parse
|
||||
*/
|
||||
self.addValidChildren = addValidChildren;
|
||||
|
||||
self.elements = elements;
|
||||
};
|
||||
});
|
156
public/tinymce/classes/html/Serializer.js
Normal file
156
public/tinymce/classes/html/Serializer.js
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Serializer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to serialize down the DOM tree into a string using a Writer instance.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
|
||||
* @class tinymce.html.Serializer
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Serializer", [
|
||||
"tinymce/html/Writer",
|
||||
"tinymce/html/Schema"
|
||||
], function(Writer, Schema) {
|
||||
/**
|
||||
* Constructs a new Serializer instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Serializer
|
||||
* @param {Object} settings Name/value settings object.
|
||||
* @param {tinymce.html.Schema} schema Schema instance to use.
|
||||
*/
|
||||
return function(settings, schema) {
|
||||
var self = this, writer = new Writer(settings);
|
||||
|
||||
settings = settings || {};
|
||||
settings.validate = "validate" in settings ? settings.validate : true;
|
||||
|
||||
self.schema = schema = schema || new Schema();
|
||||
self.writer = writer;
|
||||
|
||||
/**
|
||||
* Serializes the specified node into a string.
|
||||
*
|
||||
* @example
|
||||
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
|
||||
* @method serialize
|
||||
* @param {tinymce.html.Node} node Node instance to serialize.
|
||||
* @return {String} String with HTML based on DOM tree.
|
||||
*/
|
||||
self.serialize = function(node) {
|
||||
var handlers, validate;
|
||||
|
||||
validate = settings.validate;
|
||||
|
||||
handlers = {
|
||||
// #text
|
||||
3: function(node) {
|
||||
writer.text(node.value, node.raw);
|
||||
},
|
||||
|
||||
// #comment
|
||||
8: function(node) {
|
||||
writer.comment(node.value);
|
||||
},
|
||||
|
||||
// Processing instruction
|
||||
7: function(node) {
|
||||
writer.pi(node.name, node.value);
|
||||
},
|
||||
|
||||
// Doctype
|
||||
10: function(node) {
|
||||
writer.doctype(node.value);
|
||||
},
|
||||
|
||||
// CDATA
|
||||
4: function(node) {
|
||||
writer.cdata(node.value);
|
||||
},
|
||||
|
||||
// Document fragment
|
||||
11: function(node) {
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
walk(node);
|
||||
} while ((node = node.next));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writer.reset();
|
||||
|
||||
function walk(node) {
|
||||
var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
|
||||
|
||||
if (!handler) {
|
||||
name = node.name;
|
||||
isEmpty = node.shortEnded;
|
||||
attrs = node.attributes;
|
||||
|
||||
// Sort attributes
|
||||
if (validate && attrs && attrs.length > 1) {
|
||||
sortedAttrs = [];
|
||||
sortedAttrs.map = {};
|
||||
|
||||
elementRule = schema.getElementRule(node.name);
|
||||
for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
|
||||
attrName = elementRule.attributesOrder[i];
|
||||
|
||||
if (attrName in attrs.map) {
|
||||
attrValue = attrs.map[attrName];
|
||||
sortedAttrs.map[attrName] = attrValue;
|
||||
sortedAttrs.push({name: attrName, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, l = attrs.length; i < l; i++) {
|
||||
attrName = attrs[i].name;
|
||||
|
||||
if (!(attrName in sortedAttrs.map)) {
|
||||
attrValue = attrs.map[attrName];
|
||||
sortedAttrs.map[attrName] = attrValue;
|
||||
sortedAttrs.push({name: attrName, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
attrs = sortedAttrs;
|
||||
}
|
||||
|
||||
writer.start(node.name, attrs, isEmpty);
|
||||
|
||||
if (!isEmpty) {
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
walk(node);
|
||||
} while ((node = node.next));
|
||||
}
|
||||
|
||||
writer.end(name);
|
||||
}
|
||||
} else {
|
||||
handler(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize element and treat all non elements as fragments
|
||||
if (node.type == 1 && !settings.inner) {
|
||||
walk(node);
|
||||
} else {
|
||||
handlers[11](node);
|
||||
}
|
||||
|
||||
return writer.getContent();
|
||||
};
|
||||
};
|
||||
});
|
314
public/tinymce/classes/html/Styles.js
Normal file
314
public/tinymce/classes/html/Styles.js
Normal file
@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Styles.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to parse CSS styles it also compresses styles to reduce the output size.
|
||||
*
|
||||
* @example
|
||||
* var Styles = new tinymce.html.Styles({
|
||||
* url_converter: function(url) {
|
||||
* return url;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* styles = Styles.parse('border: 1px solid red');
|
||||
* styles.color = 'red';
|
||||
*
|
||||
* console.log(new tinymce.html.StyleSerializer().serialize(styles));
|
||||
*
|
||||
* @class tinymce.html.Styles
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Styles", [], function() {
|
||||
return function(settings, schema) {
|
||||
/*jshint maxlen:255 */
|
||||
var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
|
||||
urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
|
||||
styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
|
||||
trimRightRegExp = /\s+$/,
|
||||
undef, i, encodingLookup = {}, encodingItems, invisibleChar = '\uFEFF';
|
||||
|
||||
settings = settings || {};
|
||||
|
||||
encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
|
||||
for (i = 0; i < encodingItems.length; i++) {
|
||||
encodingLookup[encodingItems[i]] = invisibleChar + i;
|
||||
encodingLookup[invisibleChar + i] = encodingItems[i];
|
||||
}
|
||||
|
||||
function toHex(match, r, g, b) {
|
||||
function hex(val) {
|
||||
val = parseInt(val, 10).toString(16);
|
||||
|
||||
return val.length > 1 ? val : '0' + val; // 0 -> 00
|
||||
}
|
||||
|
||||
return '#' + hex(r) + hex(g) + hex(b);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Parses the specified RGB color value and returns a hex version of that color.
|
||||
*
|
||||
* @method toHex
|
||||
* @param {String} color RGB string value like rgb(1,2,3)
|
||||
* @return {String} Hex version of that RGB value like #FF00FF.
|
||||
*/
|
||||
toHex: function(color) {
|
||||
return color.replace(rgbRegExp, toHex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the specified style value into an object collection. This parser will also
|
||||
* merge and remove any redundant items that browsers might have added. It will also convert non hex
|
||||
* colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings.
|
||||
*
|
||||
* @method parse
|
||||
* @param {String} css Style value to parse for example: border:1px solid red;.
|
||||
* @return {Object} Object representation of that style like {border: '1px solid red'}
|
||||
*/
|
||||
parse: function(css) {
|
||||
var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter;
|
||||
var urlConverterScope = settings.url_converter_scope || this;
|
||||
|
||||
function compress(prefix, suffix) {
|
||||
var top, right, bottom, left;
|
||||
|
||||
// IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
|
||||
// So lets asume it shouldn't be there
|
||||
if (styles['border-image'] === 'none') {
|
||||
delete styles['border-image'];
|
||||
}
|
||||
|
||||
// Get values and check it it needs compressing
|
||||
top = styles[prefix + '-top' + suffix];
|
||||
if (!top) {
|
||||
return;
|
||||
}
|
||||
|
||||
right = styles[prefix + '-right' + suffix];
|
||||
if (top != right) {
|
||||
return;
|
||||
}
|
||||
|
||||
bottom = styles[prefix + '-bottom' + suffix];
|
||||
if (right != bottom) {
|
||||
return;
|
||||
}
|
||||
|
||||
left = styles[prefix + '-left' + suffix];
|
||||
if (bottom != left) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compress
|
||||
styles[prefix + suffix] = left;
|
||||
delete styles[prefix + '-top' + suffix];
|
||||
delete styles[prefix + '-right' + suffix];
|
||||
delete styles[prefix + '-bottom' + suffix];
|
||||
delete styles[prefix + '-left' + suffix];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specific style can be compressed in other words if all border-width are equal.
|
||||
*/
|
||||
function canCompress(key) {
|
||||
var value = styles[key], i;
|
||||
|
||||
if (!value || value.indexOf(' ') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = value.split(' ');
|
||||
i = value.length;
|
||||
while (i--) {
|
||||
if (value[i] !== value[0]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
styles[key] = value[0];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses multiple styles into one style.
|
||||
*/
|
||||
function compress2(target, a, b, c) {
|
||||
if (!canCompress(a)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canCompress(b)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canCompress(c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compress
|
||||
styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
|
||||
delete styles[a];
|
||||
delete styles[b];
|
||||
delete styles[c];
|
||||
}
|
||||
|
||||
// Encodes the specified string by replacing all \" \' ; : with _<num>
|
||||
function encode(str) {
|
||||
isEncoded = true;
|
||||
|
||||
return encodingLookup[str];
|
||||
}
|
||||
|
||||
// Decodes the specified string by replacing all _<num> with it's original value \" \' etc
|
||||
// It will also decode the \" \' if keep_slashes is set to fale or omitted
|
||||
function decode(str, keep_slashes) {
|
||||
if (isEncoded) {
|
||||
str = str.replace(/\uFEFF[0-9]/g, function(str) {
|
||||
return encodingLookup[str];
|
||||
});
|
||||
}
|
||||
|
||||
if (!keep_slashes) {
|
||||
str = str.replace(/\\([\'\";:])/g, "$1");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function processUrl(match, url, url2, url3, str, str2) {
|
||||
str = str || str2;
|
||||
|
||||
if (str) {
|
||||
str = decode(str);
|
||||
|
||||
// Force strings into single quote format
|
||||
return "'" + str.replace(/\'/g, "\\'") + "'";
|
||||
}
|
||||
|
||||
url = decode(url || url2 || url3);
|
||||
|
||||
if (!settings.allow_script_urls && /(java|vb)script:/i.test(url.replace(/[\s\r\n]+/, ''))) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Convert the URL to relative/absolute depending on config
|
||||
if (urlConverter) {
|
||||
url = urlConverter.call(urlConverterScope, url, 'style');
|
||||
}
|
||||
|
||||
// Output new URL format
|
||||
return "url('" + url.replace(/\'/g, "\\'") + "')";
|
||||
}
|
||||
|
||||
if (css) {
|
||||
css = css.replace(/[\u0000-\u001F]/g, '');
|
||||
|
||||
// Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing
|
||||
css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function(str) {
|
||||
return str.replace(/[;:]/g, encode);
|
||||
});
|
||||
|
||||
// Parse styles
|
||||
while ((matches = styleRegExp.exec(css))) {
|
||||
name = matches[1].replace(trimRightRegExp, '').toLowerCase();
|
||||
value = matches[2].replace(trimRightRegExp, '');
|
||||
|
||||
if (name && value.length > 0) {
|
||||
if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(/.test(value))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Opera will produce 700 instead of bold in their style values
|
||||
if (name === 'font-weight' && value === '700') {
|
||||
value = 'bold';
|
||||
} else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
|
||||
// Convert RGB colors to HEX
|
||||
value = value.replace(rgbRegExp, toHex);
|
||||
|
||||
// Convert URLs and force them into url('value') format
|
||||
value = value.replace(urlOrStrRegExp, processUrl);
|
||||
styles[name] = isEncoded ? decode(value, true) : value;
|
||||
}
|
||||
|
||||
styleRegExp.lastIndex = matches.index + matches[0].length;
|
||||
}
|
||||
|
||||
// Compress the styles to reduce it's size for example IE will expand styles
|
||||
compress("border", "");
|
||||
compress("border", "-width");
|
||||
compress("border", "-color");
|
||||
compress("border", "-style");
|
||||
compress("padding", "");
|
||||
compress("margin", "");
|
||||
compress2('border', 'border-width', 'border-style', 'border-color');
|
||||
|
||||
// Remove pointless border, IE produces these
|
||||
if (styles.border === 'medium none') {
|
||||
delete styles.border;
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes the specified style object into a string.
|
||||
*
|
||||
* @method serialize
|
||||
* @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
|
||||
* @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized.
|
||||
* @return {String} String representation of the style object for example: border: 1px solid red.
|
||||
*/
|
||||
serialize: function(styles, element_name) {
|
||||
var css = '', name, value;
|
||||
|
||||
function serializeStyles(name) {
|
||||
var styleList, i, l, value;
|
||||
|
||||
styleList = schema.styles[name];
|
||||
if (styleList) {
|
||||
for (i = 0, l = styleList.length; i < l; i++) {
|
||||
name = styleList[i];
|
||||
value = styles[name];
|
||||
|
||||
if (value !== undef && value.length > 0) {
|
||||
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize styles according to schema
|
||||
if (element_name && schema && schema.styles) {
|
||||
// Serialize global styles and element specific styles
|
||||
serializeStyles('*');
|
||||
serializeStyles(element_name);
|
||||
} else {
|
||||
// Output the styles in the order they are inside the object
|
||||
for (name in styles) {
|
||||
value = styles[name];
|
||||
|
||||
if (value !== undef && value.length > 0) {
|
||||
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
199
public/tinymce/classes/html/Writer.js
Normal file
199
public/tinymce/classes/html/Writer.js
Normal file
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Writer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to write HTML tags out it can be used with the Serializer or the SaxParser.
|
||||
*
|
||||
* @class tinymce.html.Writer
|
||||
* @example
|
||||
* var writer = new tinymce.html.Writer({indent: true});
|
||||
* var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>');
|
||||
* console.log(writer.getContent());
|
||||
*
|
||||
* @class tinymce.html.Writer
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Writer", [
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/util/Tools"
|
||||
], function(Entities, Tools) {
|
||||
var makeMap = Tools.makeMap;
|
||||
|
||||
/**
|
||||
* Constructs a new Writer instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Writer
|
||||
* @param {Object} settings Name/value settings object.
|
||||
*/
|
||||
return function(settings) {
|
||||
var html = [], indent, indentBefore, indentAfter, encode, htmlOutput;
|
||||
|
||||
settings = settings || {};
|
||||
indent = settings.indent;
|
||||
indentBefore = makeMap(settings.indent_before || '');
|
||||
indentAfter = makeMap(settings.indent_after || '');
|
||||
encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities);
|
||||
htmlOutput = settings.element_format == "html";
|
||||
|
||||
return {
|
||||
/**
|
||||
* Writes the a start element such as <p id="a">.
|
||||
*
|
||||
* @method start
|
||||
* @param {String} name Name of the element.
|
||||
* @param {Array} attrs Optional attribute array or undefined if it hasn't any.
|
||||
* @param {Boolean} empty Optional empty state if the tag should end like <br />.
|
||||
*/
|
||||
start: function(name, attrs, empty) {
|
||||
var i, l, attr, value;
|
||||
|
||||
if (indent && indentBefore[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
html.push('<', name);
|
||||
|
||||
if (attrs) {
|
||||
for (i = 0, l = attrs.length; i < l; i++) {
|
||||
attr = attrs[i];
|
||||
html.push(' ', attr.name, '="', encode(attr.value, true), '"');
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty || htmlOutput) {
|
||||
html[html.length] = '>';
|
||||
} else {
|
||||
html[html.length] = ' />';
|
||||
}
|
||||
|
||||
if (empty && indent && indentAfter[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes the a end element such as </p>.
|
||||
*
|
||||
* @method end
|
||||
* @param {String} name Name of the element.
|
||||
*/
|
||||
end: function(name) {
|
||||
var value;
|
||||
|
||||
/*if (indent && indentBefore[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n')
|
||||
html.push('\n');
|
||||
}*/
|
||||
|
||||
html.push('</', name, '>');
|
||||
|
||||
if (indent && indentAfter[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a text node.
|
||||
*
|
||||
* @method text
|
||||
* @param {String} text String to write out.
|
||||
* @param {Boolean} raw Optional raw state if true the contents wont get encoded.
|
||||
*/
|
||||
text: function(text, raw) {
|
||||
if (text.length > 0) {
|
||||
html[html.length] = raw ? text : encode(text);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a cdata node such as <![CDATA[data]]>.
|
||||
*
|
||||
* @method cdata
|
||||
* @param {String} text String to write out inside the cdata.
|
||||
*/
|
||||
cdata: function(text) {
|
||||
html.push('<![CDATA[', text, ']]>');
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a comment node such as <!-- Comment -->.
|
||||
*
|
||||
* @method cdata
|
||||
* @param {String} text String to write out inside the comment.
|
||||
*/
|
||||
comment: function(text) {
|
||||
html.push('<!--', text, '-->');
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a PI node such as <?xml attr="value" ?>.
|
||||
*
|
||||
* @method pi
|
||||
* @param {String} name Name of the pi.
|
||||
* @param {String} text String to write out inside the pi.
|
||||
*/
|
||||
pi: function(name, text) {
|
||||
if (text) {
|
||||
html.push('<?', name, ' ', text, '?>');
|
||||
} else {
|
||||
html.push('<?', name, '?>');
|
||||
}
|
||||
|
||||
if (indent) {
|
||||
html.push('\n');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a doctype node such as <!DOCTYPE data>.
|
||||
*
|
||||
* @method doctype
|
||||
* @param {String} text String to write out inside the doctype.
|
||||
*/
|
||||
doctype: function(text) {
|
||||
html.push('<!DOCTYPE', text, '>', indent ? '\n' : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets the internal buffer if one wants to reuse the writer.
|
||||
*
|
||||
* @method reset
|
||||
*/
|
||||
reset: function() {
|
||||
html.length = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the contents that got serialized.
|
||||
*
|
||||
* @method getContent
|
||||
* @return {String} HTML contents that got written down.
|
||||
*/
|
||||
getContent: function() {
|
||||
return html.join('').replace(/\n$/, '');
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user