--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_skin/resources/js/ext/tinymce/dev/classes/html/DomParser.js Wed Jun 17 10:00:10 2015 +0200
@@ -0,0 +1,804 @@
+/**
+ * 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;
+
+ if (blockElements[node.name]) {
+ node.empty().remove();
+ } else {
+ node.unwrap();
+ }
+
+ 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);
+ }
+ }
+ });
+ }
+
+ if (settings.validate && schema.getValidClasses()) {
+ self.addAttributeFilter('class', function(nodes) {
+ var i = nodes.length, node, classList, ci, className, classValue;
+ var validClasses = schema.getValidClasses(), validClassesMap, valid;
+
+ while (i--) {
+ node = nodes[i];
+ classList = node.attr('class').split(' ');
+ classValue = '';
+
+ for (ci = 0; ci < classList.length; ci++) {
+ className = classList[ci];
+ valid = false;
+
+ validClassesMap = validClasses['*'];
+ if (validClassesMap && validClassesMap[className]) {
+ valid = true;
+ }
+
+ validClassesMap = validClasses[node.name];
+ if (!valid && validClassesMap && validClassesMap[className]) {
+ valid = true;
+ }
+
+ if (valid) {
+ if (classValue) {
+ classValue += ' ';
+ }
+
+ classValue += className;
+ }
+ }
+
+ if (!classValue.length) {
+ classValue = null;
+ }
+
+ node.attr('class', classValue);
+ }
+ });
+ }
+ };
+});