src/pyams_skin/resources/js/ext/tinymce/dev/classes/html/DomParser.js
changeset 566 a1707c607eec
parent 565 318533413200
child 567 bca1726b1d85
equal deleted inserted replaced
565:318533413200 566:a1707c607eec
     1 /**
       
     2  * DomParser.js
       
     3  *
       
     4  * Copyright, Moxiecode Systems AB
       
     5  * Released under LGPL License.
       
     6  *
       
     7  * License: http://www.tinymce.com/license
       
     8  * Contributing: http://www.tinymce.com/contributing
       
     9  */
       
    10 
       
    11 /**
       
    12  * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
       
    13  * sure that the node tree is valid according to the specified schema.
       
    14  * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
       
    15  *
       
    16  * @example
       
    17  * var parser = new tinymce.html.DomParser({validate: true}, schema);
       
    18  * var rootNode = parser.parse('<h1>content</h1>');
       
    19  *
       
    20  * @class tinymce.html.DomParser
       
    21  * @version 3.4
       
    22  */
       
    23 define("tinymce/html/DomParser", [
       
    24 	"tinymce/html/Node",
       
    25 	"tinymce/html/Schema",
       
    26 	"tinymce/html/SaxParser",
       
    27 	"tinymce/util/Tools"
       
    28 ], function(Node, Schema, SaxParser, Tools) {
       
    29 	var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
       
    30 
       
    31 	/**
       
    32 	 * Constructs a new DomParser instance.
       
    33 	 *
       
    34 	 * @constructor
       
    35 	 * @method DomParser
       
    36 	 * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
       
    37 	 * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
       
    38 	 */
       
    39 	return function(settings, schema) {
       
    40 		var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
       
    41 
       
    42 		settings = settings || {};
       
    43 		settings.validate = "validate" in settings ? settings.validate : true;
       
    44 		settings.root_name = settings.root_name || 'body';
       
    45 		self.schema = schema = schema || new Schema();
       
    46 
       
    47 		function fixInvalidChildren(nodes) {
       
    48 			var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
       
    49 			var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode;
       
    50 
       
    51 			nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
       
    52 			nonEmptyElements = schema.getNonEmptyElements();
       
    53 			textBlockElements = schema.getTextBlockElements();
       
    54 
       
    55 			for (ni = 0; ni < nodes.length; ni++) {
       
    56 				node = nodes[ni];
       
    57 
       
    58 				// Already removed or fixed
       
    59 				if (!node.parent || node.fixed) {
       
    60 					continue;
       
    61 				}
       
    62 
       
    63 				// If the invalid element is a text block and the text block is within a parent LI element
       
    64 				// Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
       
    65 				if (textBlockElements[node.name] && node.parent.name == 'li') {
       
    66 					// Move sibling text blocks after LI element
       
    67 					sibling = node.next;
       
    68 					while (sibling) {
       
    69 						if (textBlockElements[sibling.name]) {
       
    70 							sibling.name = 'li';
       
    71 							sibling.fixed = true;
       
    72 							node.parent.insert(sibling, node.parent);
       
    73 						} else {
       
    74 							break;
       
    75 						}
       
    76 
       
    77 						sibling = sibling.next;
       
    78 					}
       
    79 
       
    80 					// Unwrap current text block
       
    81 					node.unwrap(node);
       
    82 					continue;
       
    83 				}
       
    84 
       
    85 				// Get list of all parent nodes until we find a valid parent to stick the child into
       
    86 				parents = [node];
       
    87 				for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
       
    88 					!nonSplitableElements[parent.name]; parent = parent.parent) {
       
    89 					parents.push(parent);
       
    90 				}
       
    91 
       
    92 				// Found a suitable parent
       
    93 				if (parent && parents.length > 1) {
       
    94 					// Reverse the array since it makes looping easier
       
    95 					parents.reverse();
       
    96 
       
    97 					// Clone the related parent and insert that after the moved node
       
    98 					newParent = currentNode = self.filterNode(parents[0].clone());
       
    99 
       
   100 					// Start cloning and moving children on the left side of the target node
       
   101 					for (i = 0; i < parents.length - 1; i++) {
       
   102 						if (schema.isValidChild(currentNode.name, parents[i].name)) {
       
   103 							tempNode = self.filterNode(parents[i].clone());
       
   104 							currentNode.append(tempNode);
       
   105 						} else {
       
   106 							tempNode = currentNode;
       
   107 						}
       
   108 
       
   109 						for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) {
       
   110 							nextNode = childNode.next;
       
   111 							tempNode.append(childNode);
       
   112 							childNode = nextNode;
       
   113 						}
       
   114 
       
   115 						currentNode = tempNode;
       
   116 					}
       
   117 
       
   118 					if (!newParent.isEmpty(nonEmptyElements)) {
       
   119 						parent.insert(newParent, parents[0], true);
       
   120 						parent.insert(node, newParent);
       
   121 					} else {
       
   122 						parent.insert(node, parents[0], true);
       
   123 					}
       
   124 
       
   125 					// Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
       
   126 					parent = parents[0];
       
   127 					if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
       
   128 						parent.empty().remove();
       
   129 					}
       
   130 				} else if (node.parent) {
       
   131 					// If it's an LI try to find a UL/OL for it or wrap it
       
   132 					if (node.name === 'li') {
       
   133 						sibling = node.prev;
       
   134 						if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
       
   135 							sibling.append(node);
       
   136 							continue;
       
   137 						}
       
   138 
       
   139 						sibling = node.next;
       
   140 						if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
       
   141 							sibling.insert(node, sibling.firstChild, true);
       
   142 							continue;
       
   143 						}
       
   144 
       
   145 						node.wrap(self.filterNode(new Node('ul', 1)));
       
   146 						continue;
       
   147 					}
       
   148 
       
   149 					// Try wrapping the element in a DIV
       
   150 					if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
       
   151 						node.wrap(self.filterNode(new Node('div', 1)));
       
   152 					} else {
       
   153 						// We failed wrapping it, then remove or unwrap it
       
   154 						if (node.name === 'style' || node.name === 'script') {
       
   155 							node.empty().remove();
       
   156 						} else {
       
   157 							node.unwrap();
       
   158 						}
       
   159 					}
       
   160 				}
       
   161 			}
       
   162 		}
       
   163 
       
   164 		/**
       
   165 		 * Runs the specified node though the element and attributes filters.
       
   166 		 *
       
   167 		 * @method filterNode
       
   168 		 * @param {tinymce.html.Node} Node the node to run filters on.
       
   169 		 * @return {tinymce.html.Node} The passed in node.
       
   170 		 */
       
   171 		self.filterNode = function(node) {
       
   172 			var i, name, list;
       
   173 
       
   174 			// Run element filters
       
   175 			if (name in nodeFilters) {
       
   176 				list = matchedNodes[name];
       
   177 
       
   178 				if (list) {
       
   179 					list.push(node);
       
   180 				} else {
       
   181 					matchedNodes[name] = [node];
       
   182 				}
       
   183 			}
       
   184 
       
   185 			// Run attribute filters
       
   186 			i = attributeFilters.length;
       
   187 			while (i--) {
       
   188 				name = attributeFilters[i].name;
       
   189 
       
   190 				if (name in node.attributes.map) {
       
   191 					list = matchedAttributes[name];
       
   192 
       
   193 					if (list) {
       
   194 						list.push(node);
       
   195 					} else {
       
   196 						matchedAttributes[name] = [node];
       
   197 					}
       
   198 				}
       
   199 			}
       
   200 
       
   201 			return node;
       
   202 		};
       
   203 
       
   204 		/**
       
   205 		 * Adds a node filter function to the parser, the parser will collect the specified nodes by name
       
   206 		 * and then execute the callback ones it has finished parsing the document.
       
   207 		 *
       
   208 		 * @example
       
   209 		 * parser.addNodeFilter('p,h1', function(nodes, name) {
       
   210 		 *		for (var i = 0; i < nodes.length; i++) {
       
   211 		 *			console.log(nodes[i].name);
       
   212 		 *		}
       
   213 		 * });
       
   214 		 * @method addNodeFilter
       
   215 		 * @method {String} name Comma separated list of nodes to collect.
       
   216 		 * @param {function} callback Callback function to execute once it has collected nodes.
       
   217 		 */
       
   218 		self.addNodeFilter = function(name, callback) {
       
   219 			each(explode(name), function(name) {
       
   220 				var list = nodeFilters[name];
       
   221 
       
   222 				if (!list) {
       
   223 					nodeFilters[name] = list = [];
       
   224 				}
       
   225 
       
   226 				list.push(callback);
       
   227 			});
       
   228 		};
       
   229 
       
   230 		/**
       
   231 		 * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
       
   232 		 * and then execute the callback ones it has finished parsing the document.
       
   233 		 *
       
   234 		 * @example
       
   235 		 * parser.addAttributeFilter('src,href', function(nodes, name) {
       
   236 		 *		for (var i = 0; i < nodes.length; i++) {
       
   237 		 *			console.log(nodes[i].name);
       
   238 		 *		}
       
   239 		 * });
       
   240 		 * @method addAttributeFilter
       
   241 		 * @method {String} name Comma separated list of nodes to collect.
       
   242 		 * @param {function} callback Callback function to execute once it has collected nodes.
       
   243 		 */
       
   244 		self.addAttributeFilter = function(name, callback) {
       
   245 			each(explode(name), function(name) {
       
   246 				var i;
       
   247 
       
   248 				for (i = 0; i < attributeFilters.length; i++) {
       
   249 					if (attributeFilters[i].name === name) {
       
   250 						attributeFilters[i].callbacks.push(callback);
       
   251 						return;
       
   252 					}
       
   253 				}
       
   254 
       
   255 				attributeFilters.push({name: name, callbacks: [callback]});
       
   256 			});
       
   257 		};
       
   258 
       
   259 		/**
       
   260 		 * Parses the specified HTML string into a DOM like node tree and returns the result.
       
   261 		 *
       
   262 		 * @example
       
   263 		 * var rootNode = new DomParser({...}).parse('<b>text</b>');
       
   264 		 * @method parse
       
   265 		 * @param {String} html Html string to sax parse.
       
   266 		 * @param {Object} args Optional args object that gets passed to all filter functions.
       
   267 		 * @return {tinymce.html.Node} Root node containing the tree.
       
   268 		 */
       
   269 		self.parse = function(html, args) {
       
   270 			var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
       
   271 			var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
       
   272 			var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
       
   273 			var children, nonEmptyElements, rootBlockName;
       
   274 
       
   275 			args = args || {};
       
   276 			matchedNodes = {};
       
   277 			matchedAttributes = {};
       
   278 			blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
       
   279 			nonEmptyElements = schema.getNonEmptyElements();
       
   280 			children = schema.children;
       
   281 			validate = settings.validate;
       
   282 			rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
       
   283 
       
   284 			whiteSpaceElements = schema.getWhiteSpaceElements();
       
   285 			startWhiteSpaceRegExp = /^[ \t\r\n]+/;
       
   286 			endWhiteSpaceRegExp = /[ \t\r\n]+$/;
       
   287 			allWhiteSpaceRegExp = /[ \t\r\n]+/g;
       
   288 			isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
       
   289 
       
   290 			function addRootBlocks() {
       
   291 				var node = rootNode.firstChild, next, rootBlockNode;
       
   292 
       
   293 				// Removes whitespace at beginning and end of block so:
       
   294 				// <p> x </p> -> <p>x</p>
       
   295 				function trim(rootBlockNode) {
       
   296 					if (rootBlockNode) {
       
   297 						node = rootBlockNode.firstChild;
       
   298 						if (node && node.type == 3) {
       
   299 							node.value = node.value.replace(startWhiteSpaceRegExp, '');
       
   300 						}
       
   301 
       
   302 						node = rootBlockNode.lastChild;
       
   303 						if (node && node.type == 3) {
       
   304 							node.value = node.value.replace(endWhiteSpaceRegExp, '');
       
   305 						}
       
   306 					}
       
   307 				}
       
   308 
       
   309 				// Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
       
   310 				if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
       
   311 					return;
       
   312 				}
       
   313 
       
   314 				while (node) {
       
   315 					next = node.next;
       
   316 
       
   317 					if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
       
   318 						!blockElements[node.name] && !node.attr('data-mce-type'))) {
       
   319 						if (!rootBlockNode) {
       
   320 							// Create a new root block element
       
   321 							rootBlockNode = createNode(rootBlockName, 1);
       
   322 							rootBlockNode.attr(settings.forced_root_block_attrs);
       
   323 							rootNode.insert(rootBlockNode, node);
       
   324 							rootBlockNode.append(node);
       
   325 						} else {
       
   326 							rootBlockNode.append(node);
       
   327 						}
       
   328 					} else {
       
   329 						trim(rootBlockNode);
       
   330 						rootBlockNode = null;
       
   331 					}
       
   332 
       
   333 					node = next;
       
   334 				}
       
   335 
       
   336 				trim(rootBlockNode);
       
   337 			}
       
   338 
       
   339 			function createNode(name, type) {
       
   340 				var node = new Node(name, type), list;
       
   341 
       
   342 				if (name in nodeFilters) {
       
   343 					list = matchedNodes[name];
       
   344 
       
   345 					if (list) {
       
   346 						list.push(node);
       
   347 					} else {
       
   348 						matchedNodes[name] = [node];
       
   349 					}
       
   350 				}
       
   351 
       
   352 				return node;
       
   353 			}
       
   354 
       
   355 			function removeWhitespaceBefore(node) {
       
   356 				var textNode, textVal, sibling;
       
   357 
       
   358 				for (textNode = node.prev; textNode && textNode.type === 3;) {
       
   359 					textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
       
   360 
       
   361 					if (textVal.length > 0) {
       
   362 						textNode.value = textVal;
       
   363 						textNode = textNode.prev;
       
   364 					} else {
       
   365 						sibling = textNode.prev;
       
   366 						textNode.remove();
       
   367 						textNode = sibling;
       
   368 					}
       
   369 				}
       
   370 			}
       
   371 
       
   372 			function cloneAndExcludeBlocks(input) {
       
   373 				var name, output = {};
       
   374 
       
   375 				for (name in input) {
       
   376 					if (name !== 'li' && name != 'p') {
       
   377 						output[name] = input[name];
       
   378 					}
       
   379 				}
       
   380 
       
   381 				return output;
       
   382 			}
       
   383 
       
   384 			parser = new SaxParser({
       
   385 				validate: validate,
       
   386 				allow_script_urls: settings.allow_script_urls,
       
   387 				allow_conditional_comments: settings.allow_conditional_comments,
       
   388 
       
   389 				// Exclude P and LI from DOM parsing since it's treated better by the DOM parser
       
   390 				self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
       
   391 
       
   392 				cdata: function(text) {
       
   393 					node.append(createNode('#cdata', 4)).value = text;
       
   394 				},
       
   395 
       
   396 				text: function(text, raw) {
       
   397 					var textNode;
       
   398 
       
   399 					// Trim all redundant whitespace on non white space elements
       
   400 					if (!isInWhiteSpacePreservedElement) {
       
   401 						text = text.replace(allWhiteSpaceRegExp, ' ');
       
   402 
       
   403 						if (node.lastChild && blockElements[node.lastChild.name]) {
       
   404 							text = text.replace(startWhiteSpaceRegExp, '');
       
   405 						}
       
   406 					}
       
   407 
       
   408 					// Do we need to create the node
       
   409 					if (text.length !== 0) {
       
   410 						textNode = createNode('#text', 3);
       
   411 						textNode.raw = !!raw;
       
   412 						node.append(textNode).value = text;
       
   413 					}
       
   414 				},
       
   415 
       
   416 				comment: function(text) {
       
   417 					node.append(createNode('#comment', 8)).value = text;
       
   418 				},
       
   419 
       
   420 				pi: function(name, text) {
       
   421 					node.append(createNode(name, 7)).value = text;
       
   422 					removeWhitespaceBefore(node);
       
   423 				},
       
   424 
       
   425 				doctype: function(text) {
       
   426 					var newNode;
       
   427 
       
   428 					newNode = node.append(createNode('#doctype', 10));
       
   429 					newNode.value = text;
       
   430 					removeWhitespaceBefore(node);
       
   431 				},
       
   432 
       
   433 				start: function(name, attrs, empty) {
       
   434 					var newNode, attrFiltersLen, elementRule, attrName, parent;
       
   435 
       
   436 					elementRule = validate ? schema.getElementRule(name) : {};
       
   437 					if (elementRule) {
       
   438 						newNode = createNode(elementRule.outputName || name, 1);
       
   439 						newNode.attributes = attrs;
       
   440 						newNode.shortEnded = empty;
       
   441 
       
   442 						node.append(newNode);
       
   443 
       
   444 						// Check if node is valid child of the parent node is the child is
       
   445 						// unknown we don't collect it since it's probably a custom element
       
   446 						parent = children[node.name];
       
   447 						if (parent && children[newNode.name] && !parent[newNode.name]) {
       
   448 							invalidChildren.push(newNode);
       
   449 						}
       
   450 
       
   451 						attrFiltersLen = attributeFilters.length;
       
   452 						while (attrFiltersLen--) {
       
   453 							attrName = attributeFilters[attrFiltersLen].name;
       
   454 
       
   455 							if (attrName in attrs.map) {
       
   456 								list = matchedAttributes[attrName];
       
   457 
       
   458 								if (list) {
       
   459 									list.push(newNode);
       
   460 								} else {
       
   461 									matchedAttributes[attrName] = [newNode];
       
   462 								}
       
   463 							}
       
   464 						}
       
   465 
       
   466 						// Trim whitespace before block
       
   467 						if (blockElements[name]) {
       
   468 							removeWhitespaceBefore(newNode);
       
   469 						}
       
   470 
       
   471 						// Change current node if the element wasn't empty i.e not <br /> or <img />
       
   472 						if (!empty) {
       
   473 							node = newNode;
       
   474 						}
       
   475 
       
   476 						// Check if we are inside a whitespace preserved element
       
   477 						if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
       
   478 							isInWhiteSpacePreservedElement = true;
       
   479 						}
       
   480 					}
       
   481 				},
       
   482 
       
   483 				end: function(name) {
       
   484 					var textNode, elementRule, text, sibling, tempNode;
       
   485 
       
   486 					elementRule = validate ? schema.getElementRule(name) : {};
       
   487 					if (elementRule) {
       
   488 						if (blockElements[name]) {
       
   489 							if (!isInWhiteSpacePreservedElement) {
       
   490 								// Trim whitespace of the first node in a block
       
   491 								textNode = node.firstChild;
       
   492 								if (textNode && textNode.type === 3) {
       
   493 									text = textNode.value.replace(startWhiteSpaceRegExp, '');
       
   494 
       
   495 									// Any characters left after trim or should we remove it
       
   496 									if (text.length > 0) {
       
   497 										textNode.value = text;
       
   498 										textNode = textNode.next;
       
   499 									} else {
       
   500 										sibling = textNode.next;
       
   501 										textNode.remove();
       
   502 										textNode = sibling;
       
   503 
       
   504 										// Remove any pure whitespace siblings
       
   505 										while (textNode && textNode.type === 3) {
       
   506 											text = textNode.value;
       
   507 											sibling = textNode.next;
       
   508 
       
   509 											if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
       
   510 												textNode.remove();
       
   511 												textNode = sibling;
       
   512 											}
       
   513 
       
   514 											textNode = sibling;
       
   515 										}
       
   516 									}
       
   517 								}
       
   518 
       
   519 								// Trim whitespace of the last node in a block
       
   520 								textNode = node.lastChild;
       
   521 								if (textNode && textNode.type === 3) {
       
   522 									text = textNode.value.replace(endWhiteSpaceRegExp, '');
       
   523 
       
   524 									// Any characters left after trim or should we remove it
       
   525 									if (text.length > 0) {
       
   526 										textNode.value = text;
       
   527 										textNode = textNode.prev;
       
   528 									} else {
       
   529 										sibling = textNode.prev;
       
   530 										textNode.remove();
       
   531 										textNode = sibling;
       
   532 
       
   533 										// Remove any pure whitespace siblings
       
   534 										while (textNode && textNode.type === 3) {
       
   535 											text = textNode.value;
       
   536 											sibling = textNode.prev;
       
   537 
       
   538 											if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
       
   539 												textNode.remove();
       
   540 												textNode = sibling;
       
   541 											}
       
   542 
       
   543 											textNode = sibling;
       
   544 										}
       
   545 									}
       
   546 								}
       
   547 							}
       
   548 
       
   549 							// Trim start white space
       
   550 							// Removed due to: #5424
       
   551 							/*textNode = node.prev;
       
   552 							if (textNode && textNode.type === 3) {
       
   553 								text = textNode.value.replace(startWhiteSpaceRegExp, '');
       
   554 
       
   555 								if (text.length > 0)
       
   556 									textNode.value = text;
       
   557 								else
       
   558 									textNode.remove();
       
   559 							}*/
       
   560 						}
       
   561 
       
   562 						// Check if we exited a whitespace preserved element
       
   563 						if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
       
   564 							isInWhiteSpacePreservedElement = false;
       
   565 						}
       
   566 
       
   567 						// Handle empty nodes
       
   568 						if (elementRule.removeEmpty || elementRule.paddEmpty) {
       
   569 							if (node.isEmpty(nonEmptyElements)) {
       
   570 								if (elementRule.paddEmpty) {
       
   571 									node.empty().append(new Node('#text', '3')).value = '\u00a0';
       
   572 								} else {
       
   573 									// Leave nodes that have a name like <a name="name">
       
   574 									if (!node.attributes.map.name && !node.attributes.map.id) {
       
   575 										tempNode = node.parent;
       
   576 
       
   577 										if (blockElements[node.name]) {
       
   578 											node.empty().remove();
       
   579 										} else {
       
   580 											node.unwrap();
       
   581 										}
       
   582 
       
   583 										node = tempNode;
       
   584 										return;
       
   585 									}
       
   586 								}
       
   587 							}
       
   588 						}
       
   589 
       
   590 						node = node.parent;
       
   591 					}
       
   592 				}
       
   593 			}, schema);
       
   594 
       
   595 			rootNode = node = new Node(args.context || settings.root_name, 11);
       
   596 
       
   597 			parser.parse(html);
       
   598 
       
   599 			// Fix invalid children or report invalid children in a contextual parsing
       
   600 			if (validate && invalidChildren.length) {
       
   601 				if (!args.context) {
       
   602 					fixInvalidChildren(invalidChildren);
       
   603 				} else {
       
   604 					args.invalid = true;
       
   605 				}
       
   606 			}
       
   607 
       
   608 			// Wrap nodes in the root into block elements if the root is body
       
   609 			if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
       
   610 				addRootBlocks();
       
   611 			}
       
   612 
       
   613 			// Run filters only when the contents is valid
       
   614 			if (!args.invalid) {
       
   615 				// Run node filters
       
   616 				for (name in matchedNodes) {
       
   617 					list = nodeFilters[name];
       
   618 					nodes = matchedNodes[name];
       
   619 
       
   620 					// Remove already removed children
       
   621 					fi = nodes.length;
       
   622 					while (fi--) {
       
   623 						if (!nodes[fi].parent) {
       
   624 							nodes.splice(fi, 1);
       
   625 						}
       
   626 					}
       
   627 
       
   628 					for (i = 0, l = list.length; i < l; i++) {
       
   629 						list[i](nodes, name, args);
       
   630 					}
       
   631 				}
       
   632 
       
   633 				// Run attribute filters
       
   634 				for (i = 0, l = attributeFilters.length; i < l; i++) {
       
   635 					list = attributeFilters[i];
       
   636 
       
   637 					if (list.name in matchedAttributes) {
       
   638 						nodes = matchedAttributes[list.name];
       
   639 
       
   640 						// Remove already removed children
       
   641 						fi = nodes.length;
       
   642 						while (fi--) {
       
   643 							if (!nodes[fi].parent) {
       
   644 								nodes.splice(fi, 1);
       
   645 							}
       
   646 						}
       
   647 
       
   648 						for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
       
   649 							list.callbacks[fi](nodes, list.name, args);
       
   650 						}
       
   651 					}
       
   652 				}
       
   653 			}
       
   654 
       
   655 			return rootNode;
       
   656 		};
       
   657 
       
   658 		// Remove <br> at end of block elements Gecko and WebKit injects BR elements to
       
   659 		// make it possible to place the caret inside empty blocks. This logic tries to remove
       
   660 		// these elements and keep br elements that where intended to be there intact
       
   661 		if (settings.remove_trailing_brs) {
       
   662 			self.addNodeFilter('br', function(nodes) {
       
   663 				var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
       
   664 				var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
       
   665 				var elementRule, textNode;
       
   666 
       
   667 				// Remove brs from body element as well
       
   668 				blockElements.body = 1;
       
   669 
       
   670 				// Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
       
   671 				for (i = 0; i < l; i++) {
       
   672 					node = nodes[i];
       
   673 					parent = node.parent;
       
   674 
       
   675 					if (blockElements[node.parent.name] && node === parent.lastChild) {
       
   676 						// Loop all nodes to the left of the current node and check for other BR elements
       
   677 						// excluding bookmarks since they are invisible
       
   678 						prev = node.prev;
       
   679 						while (prev) {
       
   680 							prevName = prev.name;
       
   681 
       
   682 							// Ignore bookmarks
       
   683 							if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
       
   684 								// Found a non BR element
       
   685 								if (prevName !== "br") {
       
   686 									break;
       
   687 								}
       
   688 
       
   689 								// Found another br it's a <br><br> structure then don't remove anything
       
   690 								if (prevName === 'br') {
       
   691 									node = null;
       
   692 									break;
       
   693 								}
       
   694 							}
       
   695 
       
   696 							prev = prev.prev;
       
   697 						}
       
   698 
       
   699 						if (node) {
       
   700 							node.remove();
       
   701 
       
   702 							// Is the parent to be considered empty after we removed the BR
       
   703 							if (parent.isEmpty(nonEmptyElements)) {
       
   704 								elementRule = schema.getElementRule(parent.name);
       
   705 
       
   706 								// Remove or padd the element depending on schema rule
       
   707 								if (elementRule) {
       
   708 									if (elementRule.removeEmpty) {
       
   709 										parent.remove();
       
   710 									} else if (elementRule.paddEmpty) {
       
   711 										parent.empty().append(new Node('#text', 3)).value = '\u00a0';
       
   712 									}
       
   713 								}
       
   714 							}
       
   715 						}
       
   716 					} else {
       
   717 						// Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
       
   718 						// so they become <p><b><i>&nbsp;</i></b></p>
       
   719 						lastParent = node;
       
   720 						while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
       
   721 							lastParent = parent;
       
   722 
       
   723 							if (blockElements[parent.name]) {
       
   724 								break;
       
   725 							}
       
   726 
       
   727 							parent = parent.parent;
       
   728 						}
       
   729 
       
   730 						if (lastParent === parent) {
       
   731 							textNode = new Node('#text', 3);
       
   732 							textNode.value = '\u00a0';
       
   733 							node.replace(textNode);
       
   734 						}
       
   735 					}
       
   736 				}
       
   737 			});
       
   738 		}
       
   739 
       
   740 		// Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
       
   741 		if (!settings.allow_html_in_named_anchor) {
       
   742 			self.addAttributeFilter('id,name', function(nodes) {
       
   743 				var i = nodes.length, sibling, prevSibling, parent, node;
       
   744 
       
   745 				while (i--) {
       
   746 					node = nodes[i];
       
   747 					if (node.name === 'a' && node.firstChild && !node.attr('href')) {
       
   748 						parent = node.parent;
       
   749 
       
   750 						// Move children after current node
       
   751 						sibling = node.lastChild;
       
   752 						do {
       
   753 							prevSibling = sibling.prev;
       
   754 							parent.insert(sibling, node);
       
   755 							sibling = prevSibling;
       
   756 						} while (sibling);
       
   757 					}
       
   758 				}
       
   759 			});
       
   760 		}
       
   761 
       
   762 		if (settings.validate && schema.getValidClasses()) {
       
   763 			self.addAttributeFilter('class', function(nodes) {
       
   764 				var i = nodes.length, node, classList, ci, className, classValue;
       
   765 				var validClasses = schema.getValidClasses(), validClassesMap, valid;
       
   766 
       
   767 				while (i--) {
       
   768 					node = nodes[i];
       
   769 					classList = node.attr('class').split(' ');
       
   770 					classValue = '';
       
   771 
       
   772 					for (ci = 0; ci < classList.length; ci++) {
       
   773 						className = classList[ci];
       
   774 						valid = false;
       
   775 
       
   776 						validClassesMap = validClasses['*'];
       
   777 						if (validClassesMap && validClassesMap[className]) {
       
   778 							valid = true;
       
   779 						}
       
   780 
       
   781 						validClassesMap = validClasses[node.name];
       
   782 						if (!valid && validClassesMap && validClassesMap[className]) {
       
   783 							valid = true;
       
   784 						}
       
   785 
       
   786 						if (valid) {
       
   787 							if (classValue) {
       
   788 								classValue += ' ';
       
   789 							}
       
   790 
       
   791 							classValue += className;
       
   792 						}
       
   793 					}
       
   794 
       
   795 					if (!classValue.length) {
       
   796 						classValue = null;
       
   797 					}
       
   798 
       
   799 					node.attr('class', classValue);
       
   800 				}
       
   801 			});
       
   802 		}
       
   803 	};
       
   804 });