diff options
Diffstat (limited to 'sources/core/htmlparser/fragment.js')
-rw-r--r-- | sources/core/htmlparser/fragment.js | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/sources/core/htmlparser/fragment.js b/sources/core/htmlparser/fragment.js new file mode 100644 index 0000000..c062986 --- /dev/null +++ b/sources/core/htmlparser/fragment.js | |||
@@ -0,0 +1,646 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | 'use strict'; | ||
7 | |||
8 | /** | ||
9 | * A lightweight representation of an HTML DOM structure. | ||
10 | * | ||
11 | * @class | ||
12 | * @constructor Creates a fragment class instance. | ||
13 | */ | ||
14 | CKEDITOR.htmlParser.fragment = function() { | ||
15 | /** | ||
16 | * The nodes contained in the root of this fragment. | ||
17 | * | ||
18 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); | ||
19 | * alert( fragment.children.length ); // 2 | ||
20 | */ | ||
21 | this.children = []; | ||
22 | |||
23 | /** | ||
24 | * Get the fragment parent. Should always be null. | ||
25 | * | ||
26 | * @property {Object} [=null] | ||
27 | */ | ||
28 | this.parent = null; | ||
29 | |||
30 | /** @private */ | ||
31 | this._ = { | ||
32 | isBlockLike: true, | ||
33 | hasInlineStarted: false | ||
34 | }; | ||
35 | }; | ||
36 | |||
37 | ( function() { | ||
38 | // Block-level elements whose internal structure should be respected during | ||
39 | // parser fixing. | ||
40 | var nonBreakingBlocks = CKEDITOR.tools.extend( { table: 1, ul: 1, ol: 1, dl: 1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl ); | ||
41 | |||
42 | var listBlocks = { ol: 1, ul: 1 }; | ||
43 | |||
44 | // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>. | ||
45 | var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style: 1, script: 1 } ); | ||
46 | |||
47 | // Which element to create when encountered not allowed content. | ||
48 | var structureFixes = { | ||
49 | ul: 'li', | ||
50 | ol: 'li', | ||
51 | dl: 'dd', | ||
52 | table: 'tbody', | ||
53 | tbody: 'tr', | ||
54 | thead: 'tr', | ||
55 | tfoot: 'tr', | ||
56 | tr: 'td' | ||
57 | }; | ||
58 | |||
59 | function isRemoveEmpty( node ) { | ||
60 | // Keep marked element event if it is empty. | ||
61 | if ( node.attributes[ 'data-cke-survive' ] ) | ||
62 | return false; | ||
63 | |||
64 | // Empty link is to be removed when empty but not anchor. (#7894) | ||
65 | return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ]; | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. | ||
70 | * | ||
71 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); | ||
72 | * alert( fragment.children[ 0 ].name ); // 'b' | ||
73 | * alert( fragment.children[ 1 ].value ); // ' Text' | ||
74 | * | ||
75 | * @static | ||
76 | * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. | ||
77 | * @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual | ||
78 | * element which makes the content been parsed as the content of this element and fix | ||
79 | * to match it. | ||
80 | * If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used | ||
81 | * as the parent and it will be returned. | ||
82 | * @param {String/Boolean} [fixingBlock] When `parent` is a block limit element, | ||
83 | * and the param is a string value other than `false`, it is to | ||
84 | * avoid having block-less content as the direct children of parent by wrapping | ||
85 | * the content with a block element of the specified tag, e.g. | ||
86 | * when `fixingBlock` specified as `p`, the content `<body><i>foo</i></body>` | ||
87 | * will be fixed into `<body><p><i>foo</i></p></body>`. | ||
88 | * @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`. | ||
89 | */ | ||
90 | CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) { | ||
91 | var parser = new CKEDITOR.htmlParser(); | ||
92 | |||
93 | var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment(); | ||
94 | |||
95 | var pendingInline = [], | ||
96 | pendingBRs = [], | ||
97 | currentNode = root, | ||
98 | // Indicate we're inside a <textarea> element, spaces should be touched differently. | ||
99 | inTextarea = root.name == 'textarea', | ||
100 | // Indicate we're inside a <pre> element, spaces should be touched differently. | ||
101 | inPre = root.name == 'pre'; | ||
102 | |||
103 | function checkPending( newTagName ) { | ||
104 | var pendingBRsSent; | ||
105 | |||
106 | if ( pendingInline.length > 0 ) { | ||
107 | for ( var i = 0; i < pendingInline.length; i++ ) { | ||
108 | var pendingElement = pendingInline[ i ], | ||
109 | pendingName = pendingElement.name, | ||
110 | pendingDtd = CKEDITOR.dtd[ pendingName ], | ||
111 | currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; | ||
112 | |||
113 | if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) { | ||
114 | if ( !pendingBRsSent ) { | ||
115 | sendPendingBRs(); | ||
116 | pendingBRsSent = 1; | ||
117 | } | ||
118 | |||
119 | // Get a clone for the pending element. | ||
120 | pendingElement = pendingElement.clone(); | ||
121 | |||
122 | // Add it to the current node and make it the current, | ||
123 | // so the new element will be added inside of it. | ||
124 | pendingElement.parent = currentNode; | ||
125 | currentNode = pendingElement; | ||
126 | |||
127 | // Remove the pending element (back the index by one | ||
128 | // to properly process the next entry). | ||
129 | pendingInline.splice( i, 1 ); | ||
130 | i--; | ||
131 | } else { | ||
132 | // Some element of the same type cannot be nested, flat them, | ||
133 | // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894) | ||
134 | if ( pendingName == currentNode.name ) | ||
135 | addElement( currentNode, currentNode.parent, 1 ), i--; | ||
136 | } | ||
137 | } | ||
138 | } | ||
139 | } | ||
140 | |||
141 | function sendPendingBRs() { | ||
142 | while ( pendingBRs.length ) | ||
143 | addElement( pendingBRs.shift(), currentNode ); | ||
144 | } | ||
145 | |||
146 | // Rtrim empty spaces on block end boundary. (#3585) | ||
147 | function removeTailWhitespace( element ) { | ||
148 | if ( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) { | ||
149 | |||
150 | var length = element.children.length, | ||
151 | lastChild = element.children[ length - 1 ], | ||
152 | text; | ||
153 | if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) { | ||
154 | if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) ) | ||
155 | element.children.length = length - 1; | ||
156 | else | ||
157 | lastChild.value = text; | ||
158 | } | ||
159 | } | ||
160 | } | ||
161 | |||
162 | // Beside of simply append specified element to target, this function also takes | ||
163 | // care of other dirty lifts like forcing block in body, trimming spaces at | ||
164 | // the block boundaries etc. | ||
165 | // | ||
166 | // @param {Element} element The element to be added as the last child of {@link target}. | ||
167 | // @param {Element} target The parent element to relieve the new node. | ||
168 | // @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless | ||
169 | // there's a return point node specified on the element, otherwise move current onto {@link target} node. | ||
170 | // | ||
171 | function addElement( element, target, moveCurrent ) { | ||
172 | target = target || currentNode || root; | ||
173 | |||
174 | // Current element might be mangled by fix body below, | ||
175 | // save it for restore later. | ||
176 | var savedCurrent = currentNode; | ||
177 | |||
178 | // Ignore any element that has already been added. | ||
179 | if ( element.previous === undefined ) { | ||
180 | if ( checkAutoParagraphing( target, element ) ) { | ||
181 | // Create a <p> in the fragment. | ||
182 | currentNode = target; | ||
183 | parser.onTagOpen( fixingBlock, {} ); | ||
184 | |||
185 | // The new target now is the <p>. | ||
186 | element.returnPoint = target = currentNode; | ||
187 | } | ||
188 | |||
189 | removeTailWhitespace( element ); | ||
190 | |||
191 | // Avoid adding empty inline. | ||
192 | if ( !( isRemoveEmpty( element ) && !element.children.length ) ) | ||
193 | target.add( element ); | ||
194 | |||
195 | if ( element.name == 'pre' ) | ||
196 | inPre = false; | ||
197 | |||
198 | if ( element.name == 'textarea' ) | ||
199 | inTextarea = false; | ||
200 | } | ||
201 | |||
202 | if ( element.returnPoint ) { | ||
203 | currentNode = element.returnPoint; | ||
204 | delete element.returnPoint; | ||
205 | } else { | ||
206 | currentNode = moveCurrent ? target : savedCurrent; | ||
207 | } | ||
208 | } | ||
209 | |||
210 | // Auto paragraphing should happen when inline content enters the root element. | ||
211 | function checkAutoParagraphing( parent, node ) { | ||
212 | |||
213 | // Check for parent that can contain block. | ||
214 | if ( ( parent == root || parent.name == 'body' ) && fixingBlock && | ||
215 | ( !parent.name || CKEDITOR.dtd[ parent.name ][ fixingBlock ] ) ) { | ||
216 | var name, realName; | ||
217 | |||
218 | if ( node.attributes && ( realName = node.attributes[ 'data-cke-real-element-type' ] ) ) | ||
219 | name = realName; | ||
220 | else | ||
221 | name = node.name; | ||
222 | |||
223 | // Text node, inline elements are subjected, except for <script>/<style>. | ||
224 | return name && name in CKEDITOR.dtd.$inline && | ||
225 | !( name in CKEDITOR.dtd.head ) && | ||
226 | !node.isOrphan || | ||
227 | node.type == CKEDITOR.NODE_TEXT; | ||
228 | } | ||
229 | } | ||
230 | |||
231 | // Judge whether two element tag names are likely the siblings from the same | ||
232 | // structural element. | ||
233 | function possiblySibling( tag1, tag2 ) { | ||
234 | |||
235 | if ( tag1 in CKEDITOR.dtd.$listItem || tag1 in CKEDITOR.dtd.$tableContent ) | ||
236 | return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt'; | ||
237 | |||
238 | return false; | ||
239 | } | ||
240 | |||
241 | parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) { | ||
242 | var element = new CKEDITOR.htmlParser.element( tagName, attributes ); | ||
243 | |||
244 | // "isEmpty" will be always "false" for unknown elements, so we | ||
245 | // must force it if the parser has identified it as a selfClosing tag. | ||
246 | if ( element.isUnknown && selfClosing ) | ||
247 | element.isEmpty = true; | ||
248 | |||
249 | // Check for optional closed elements, including browser quirks and manually opened blocks. | ||
250 | element.isOptionalClose = optionalClose; | ||
251 | |||
252 | // This is a tag to be removed if empty, so do not add it immediately. | ||
253 | if ( isRemoveEmpty( element ) ) { | ||
254 | pendingInline.push( element ); | ||
255 | return; | ||
256 | } else if ( tagName == 'pre' ) | ||
257 | inPre = true; | ||
258 | else if ( tagName == 'br' && inPre ) { | ||
259 | currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) ); | ||
260 | return; | ||
261 | } else if ( tagName == 'textarea' ) { | ||
262 | inTextarea = true; | ||
263 | } | ||
264 | |||
265 | if ( tagName == 'br' ) { | ||
266 | pendingBRs.push( element ); | ||
267 | return; | ||
268 | } | ||
269 | |||
270 | while ( 1 ) { | ||
271 | var currentName = currentNode.name; | ||
272 | |||
273 | var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; | ||
274 | |||
275 | // If the element cannot be child of the current element. | ||
276 | if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) { | ||
277 | // Current node doesn't have a close tag, time for a close | ||
278 | // as this element isn't fit in. (#7497) | ||
279 | if ( currentNode.isOptionalClose ) | ||
280 | parser.onTagClose( currentName ); | ||
281 | // Fixing malformed nested lists by moving it into a previous list item. (#3828) | ||
282 | else if ( tagName in listBlocks && currentName in listBlocks ) { | ||
283 | var children = currentNode.children, | ||
284 | lastChild = children[ children.length - 1 ]; | ||
285 | |||
286 | // Establish the list item if it's not existed. | ||
287 | if ( !( lastChild && lastChild.name == 'li' ) ) | ||
288 | addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode ); | ||
289 | |||
290 | !element.returnPoint && ( element.returnPoint = currentNode ); | ||
291 | currentNode = lastChild; | ||
292 | } | ||
293 | // Establish new list root for orphan list items, but NOT to create | ||
294 | // new list for the following ones, fix them instead. (#6975) | ||
295 | // <dl><dt>foo<dd>bar</dl> | ||
296 | // <ul><li>foo<li>bar</ul> | ||
297 | else if ( tagName in CKEDITOR.dtd.$listItem && | ||
298 | !possiblySibling( tagName, currentName ) ) { | ||
299 | parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 ); | ||
300 | } | ||
301 | // We're inside a structural block like table and list, AND the incoming element | ||
302 | // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it, | ||
303 | // and most importantly, return back to here once this element is added, | ||
304 | // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table> | ||
305 | else if ( currentName in nonBreakingBlocks && | ||
306 | !possiblySibling( tagName, currentName ) ) { | ||
307 | !element.returnPoint && ( element.returnPoint = currentNode ); | ||
308 | currentNode = currentNode.parent; | ||
309 | } else { | ||
310 | // The current element is an inline element, which | ||
311 | // need to be continued even after the close, so put | ||
312 | // it in the pending list. | ||
313 | if ( currentName in CKEDITOR.dtd.$inline ) | ||
314 | pendingInline.unshift( currentNode ); | ||
315 | |||
316 | // The most common case where we just need to close the | ||
317 | // current one and append the new one to the parent. | ||
318 | if ( currentNode.parent ) | ||
319 | addElement( currentNode, currentNode.parent, 1 ); | ||
320 | // We've tried our best to fix the embarrassment here, while | ||
321 | // this element still doesn't find it's parent, mark it as | ||
322 | // orphan and show our tolerance to it. | ||
323 | else { | ||
324 | element.isOrphan = 1; | ||
325 | break; | ||
326 | } | ||
327 | } | ||
328 | } else { | ||
329 | break; | ||
330 | } | ||
331 | } | ||
332 | |||
333 | checkPending( tagName ); | ||
334 | sendPendingBRs(); | ||
335 | |||
336 | element.parent = currentNode; | ||
337 | |||
338 | if ( element.isEmpty ) | ||
339 | addElement( element ); | ||
340 | else | ||
341 | currentNode = element; | ||
342 | }; | ||
343 | |||
344 | parser.onTagClose = function( tagName ) { | ||
345 | // Check if there is any pending tag to be closed. | ||
346 | for ( var i = pendingInline.length - 1; i >= 0; i-- ) { | ||
347 | // If found, just remove it from the list. | ||
348 | if ( tagName == pendingInline[ i ].name ) { | ||
349 | pendingInline.splice( i, 1 ); | ||
350 | return; | ||
351 | } | ||
352 | } | ||
353 | |||
354 | var pendingAdd = [], | ||
355 | newPendingInline = [], | ||
356 | candidate = currentNode; | ||
357 | |||
358 | while ( candidate != root && candidate.name != tagName ) { | ||
359 | // If this is an inline element, add it to the pending list, if we're | ||
360 | // really closing one of the parents element later, they will continue | ||
361 | // after it. | ||
362 | if ( !candidate._.isBlockLike ) | ||
363 | newPendingInline.unshift( candidate ); | ||
364 | |||
365 | // This node should be added to it's parent at this point. But, | ||
366 | // it should happen only if the closing tag is really closing | ||
367 | // one of the nodes. So, for now, we just cache it. | ||
368 | pendingAdd.push( candidate ); | ||
369 | |||
370 | // Make sure return point is properly restored. | ||
371 | candidate = candidate.returnPoint || candidate.parent; | ||
372 | } | ||
373 | |||
374 | if ( candidate != root ) { | ||
375 | // Add all elements that have been found in the above loop. | ||
376 | for ( i = 0; i < pendingAdd.length; i++ ) { | ||
377 | var node = pendingAdd[ i ]; | ||
378 | addElement( node, node.parent ); | ||
379 | } | ||
380 | |||
381 | currentNode = candidate; | ||
382 | |||
383 | if ( candidate._.isBlockLike ) | ||
384 | sendPendingBRs(); | ||
385 | |||
386 | addElement( candidate, candidate.parent ); | ||
387 | |||
388 | // The parent should start receiving new nodes now, except if | ||
389 | // addElement changed the currentNode. | ||
390 | if ( candidate == currentNode ) | ||
391 | currentNode = currentNode.parent; | ||
392 | |||
393 | pendingInline = pendingInline.concat( newPendingInline ); | ||
394 | } | ||
395 | |||
396 | if ( tagName == 'body' ) | ||
397 | fixingBlock = false; | ||
398 | }; | ||
399 | |||
400 | parser.onText = function( text ) { | ||
401 | // Trim empty spaces at beginning of text contents except <pre> and <textarea>. | ||
402 | if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) { | ||
403 | text = CKEDITOR.tools.ltrim( text ); | ||
404 | |||
405 | if ( text.length === 0 ) | ||
406 | return; | ||
407 | } | ||
408 | |||
409 | var currentName = currentNode.name, | ||
410 | currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) : rootDtd; | ||
411 | |||
412 | // Fix orphan text in list/table. (#8540) (#8870) | ||
413 | if ( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) { | ||
414 | parser.onTagOpen( structureFixes[ currentName ] || '' ); | ||
415 | parser.onText( text ); | ||
416 | return; | ||
417 | } | ||
418 | |||
419 | sendPendingBRs(); | ||
420 | checkPending(); | ||
421 | |||
422 | // Shrinking consequential spaces into one single for all elements | ||
423 | // text contents. | ||
424 | if ( !inPre && !inTextarea ) | ||
425 | text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' ); | ||
426 | |||
427 | text = new CKEDITOR.htmlParser.text( text ); | ||
428 | |||
429 | |||
430 | if ( checkAutoParagraphing( currentNode, text ) ) | ||
431 | this.onTagOpen( fixingBlock, {}, 0, 1 ); | ||
432 | |||
433 | currentNode.add( text ); | ||
434 | }; | ||
435 | |||
436 | parser.onCDATA = function( cdata ) { | ||
437 | currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) ); | ||
438 | }; | ||
439 | |||
440 | parser.onComment = function( comment ) { | ||
441 | sendPendingBRs(); | ||
442 | checkPending(); | ||
443 | currentNode.add( new CKEDITOR.htmlParser.comment( comment ) ); | ||
444 | }; | ||
445 | |||
446 | // Parse it. | ||
447 | parser.parse( fragmentHtml ); | ||
448 | |||
449 | sendPendingBRs(); | ||
450 | |||
451 | // Close all pending nodes, make sure return point is properly restored. | ||
452 | while ( currentNode != root ) | ||
453 | addElement( currentNode, currentNode.parent, 1 ); | ||
454 | |||
455 | removeTailWhitespace( root ); | ||
456 | |||
457 | return root; | ||
458 | }; | ||
459 | |||
460 | CKEDITOR.htmlParser.fragment.prototype = { | ||
461 | |||
462 | /** | ||
463 | * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | ||
464 | * | ||
465 | * @readonly | ||
466 | * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT] | ||
467 | */ | ||
468 | type: CKEDITOR.NODE_DOCUMENT_FRAGMENT, | ||
469 | |||
470 | /** | ||
471 | * Adds a node to this fragment. | ||
472 | * | ||
473 | * @param {CKEDITOR.htmlParser.node} node The node to be added. | ||
474 | * @param {Number} [index] From where the insertion happens. | ||
475 | */ | ||
476 | add: function( node, index ) { | ||
477 | isNaN( index ) && ( index = this.children.length ); | ||
478 | |||
479 | var previous = index > 0 ? this.children[ index - 1 ] : null; | ||
480 | if ( previous ) { | ||
481 | // If the block to be appended is following text, trim spaces at | ||
482 | // the right of it. | ||
483 | if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) { | ||
484 | previous.value = CKEDITOR.tools.rtrim( previous.value ); | ||
485 | |||
486 | // If we have completely cleared the previous node. | ||
487 | if ( previous.value.length === 0 ) { | ||
488 | // Remove it from the list and add the node again. | ||
489 | this.children.pop(); | ||
490 | this.add( node ); | ||
491 | return; | ||
492 | } | ||
493 | } | ||
494 | |||
495 | previous.next = node; | ||
496 | } | ||
497 | |||
498 | node.previous = previous; | ||
499 | node.parent = this; | ||
500 | |||
501 | this.children.splice( index, 0, node ); | ||
502 | |||
503 | if ( !this._.hasInlineStarted ) | ||
504 | this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike ); | ||
505 | }, | ||
506 | |||
507 | /** | ||
508 | * Filter this fragment's content with given filter. | ||
509 | * | ||
510 | * @since 4.1 | ||
511 | * @param {CKEDITOR.htmlParser.filter} filter | ||
512 | */ | ||
513 | filter: function( filter, context ) { | ||
514 | context = this.getFilterContext( context ); | ||
515 | |||
516 | // Apply the root filter. | ||
517 | filter.onRoot( context, this ); | ||
518 | |||
519 | this.filterChildren( filter, false, context ); | ||
520 | }, | ||
521 | |||
522 | /** | ||
523 | * Filter this fragment's children with given filter. | ||
524 | * | ||
525 | * Element's children may only be filtered once by one | ||
526 | * instance of filter. | ||
527 | * | ||
528 | * @since 4.1 | ||
529 | * @param {CKEDITOR.htmlParser.filter} filter | ||
530 | * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. | ||
531 | */ | ||
532 | filterChildren: function( filter, filterRoot, context ) { | ||
533 | // If this element's children were already filtered | ||
534 | // by current filter, don't filter them 2nd time. | ||
535 | // This situation may occur when filtering bottom-up | ||
536 | // (filterChildren() called manually in element's filter), | ||
537 | // or in unpredictable edge cases when filter | ||
538 | // is manipulating DOM structure. | ||
539 | if ( this.childrenFilteredBy == filter.id ) | ||
540 | return; | ||
541 | |||
542 | context = this.getFilterContext( context ); | ||
543 | |||
544 | // Filtering root if enforced. | ||
545 | if ( filterRoot && !this.parent ) | ||
546 | filter.onRoot( context, this ); | ||
547 | |||
548 | this.childrenFilteredBy = filter.id; | ||
549 | |||
550 | // Don't cache anything, children array may be modified by filter rule. | ||
551 | for ( var i = 0; i < this.children.length; i++ ) { | ||
552 | // Stay in place if filter returned false, what means | ||
553 | // that node has been removed. | ||
554 | if ( this.children[ i ].filter( filter, context ) === false ) | ||
555 | i--; | ||
556 | } | ||
557 | }, | ||
558 | |||
559 | /** | ||
560 | * Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}. | ||
561 | * | ||
562 | * var writer = new CKEDITOR.htmlWriter(); | ||
563 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' ); | ||
564 | * fragment.writeHtml( writer ); | ||
565 | * alert( writer.getHtml() ); // '<p><b>Example</b></p>' | ||
566 | * | ||
567 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
568 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. | ||
569 | */ | ||
570 | writeHtml: function( writer, filter ) { | ||
571 | if ( filter ) | ||
572 | this.filter( filter ); | ||
573 | |||
574 | this.writeChildrenHtml( writer ); | ||
575 | }, | ||
576 | |||
577 | /** | ||
578 | * Write and filtering the child nodes of this fragment. | ||
579 | * | ||
580 | * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML. | ||
581 | * @param {CKEDITOR.htmlParser.filter} [filter] The filter to use when writing the HTML. | ||
582 | * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`. | ||
583 | */ | ||
584 | writeChildrenHtml: function( writer, filter, filterRoot ) { | ||
585 | var context = this.getFilterContext(); | ||
586 | |||
587 | // Filtering root if enforced. | ||
588 | if ( filterRoot && !this.parent && filter ) | ||
589 | filter.onRoot( context, this ); | ||
590 | |||
591 | if ( filter ) | ||
592 | this.filterChildren( filter, false, context ); | ||
593 | |||
594 | for ( var i = 0, children = this.children, l = children.length; i < l; i++ ) | ||
595 | children[ i ].writeHtml( writer ); | ||
596 | }, | ||
597 | |||
598 | /** | ||
599 | * Execute callback on each node (of given type) in this document fragment. | ||
600 | * | ||
601 | * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' ); | ||
602 | * fragment.forEach( function( node ) { | ||
603 | * console.log( node ); | ||
604 | * } ); | ||
605 | * // Will log: | ||
606 | * // 1. document fragment, | ||
607 | * // 2. <p> element, | ||
608 | * // 3. "foo" text node, | ||
609 | * // 4. <b> element, | ||
610 | * // 5. "bar" text node, | ||
611 | * // 6. "bom" text node. | ||
612 | * | ||
613 | * @since 4.1 | ||
614 | * @param {Function} callback Function to be executed on every node. | ||
615 | * **Since 4.3** if `callback` returned `false` descendants of current node will be ignored. | ||
616 | * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument. | ||
617 | * @param {Number} [type] If specified `callback` will be executed only on nodes of this type. | ||
618 | * @param {Boolean} [skipRoot] Don't execute `callback` on this fragment. | ||
619 | */ | ||
620 | forEach: function( callback, type, skipRoot ) { | ||
621 | if ( !skipRoot && ( !type || this.type == type ) ) | ||
622 | var ret = callback( this ); | ||
623 | |||
624 | // Do not filter children if callback returned false. | ||
625 | if ( ret === false ) | ||
626 | return; | ||
627 | |||
628 | var children = this.children, | ||
629 | node, | ||
630 | i = 0; | ||
631 | |||
632 | // We do not cache the size, because the list of nodes may be changed by the callback. | ||
633 | for ( ; i < children.length; i++ ) { | ||
634 | node = children[ i ]; | ||
635 | if ( node.type == CKEDITOR.NODE_ELEMENT ) | ||
636 | node.forEach( callback, type ); | ||
637 | else if ( !type || node.type == type ) | ||
638 | callback( node ); | ||
639 | } | ||
640 | }, | ||
641 | |||
642 | getFilterContext: function( context ) { | ||
643 | return context || {}; | ||
644 | } | ||
645 | }; | ||
646 | } )(); | ||