]>
Commit | Line | Data |
---|---|---|
1 | /** | |
2 | * @license Copyright (c) 2003-2015, 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 | } )(); |