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> </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 }); |
|