]>
git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blob - sources/core/htmlparser/fragment.js
c06298652af2710c01aa53c3476008288e857d9d
2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
9 * A lightweight representation of an HTML DOM structure.
12 * @constructor Creates a fragment class instance.
14 CKEDITOR
.htmlParser
.fragment = function() {
16 * The nodes contained in the root of this fragment.
18 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
19 * alert( fragment.children.length ); // 2
24 * Get the fragment parent. Should always be null.
26 * @property {Object} [=null]
33 hasInlineStarted: false
38 // Block-level elements whose internal structure should be respected during
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
);
42 var listBlocks
= { ol: 1, ul: 1 };
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 } );
47 // Which element to create when encountered not allowed content.
48 var structureFixes
= {
59 function isRemoveEmpty( node
) {
60 // Keep marked element event if it is empty.
61 if ( node
.attributes
[ 'data-cke-survive' ] )
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
];
69 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
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'
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
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`.
90 CKEDITOR
.htmlParser
.fragment
.fromHtml = function( fragmentHtml
, parent
, fixingBlock
) {
91 var parser
= new CKEDITOR
.htmlParser();
93 var root
= parent
instanceof CKEDITOR
.htmlParser
.element
? parent : typeof parent
== 'string' ? new CKEDITOR
.htmlParser
.element( parent
) : new CKEDITOR
.htmlParser
.fragment();
95 var pendingInline
= [],
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';
103 function checkPending( newTagName
) {
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
];
113 if ( ( !currentDtd
|| currentDtd
[ pendingName
] ) && ( !newTagName
|| !pendingDtd
|| pendingDtd
[ newTagName
] || !CKEDITOR
.dtd
[ newTagName
] ) ) {
114 if ( !pendingBRsSent
) {
119 // Get a clone for the pending element.
120 pendingElement
= pendingElement
.clone();
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
;
127 // Remove the pending element (back the index by one
128 // to properly process the next entry).
129 pendingInline
.splice( i
, 1 );
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
--;
141 function sendPendingBRs() {
142 while ( pendingBRs
.length
)
143 addElement( pendingBRs
.shift(), currentNode
);
146 // Rtrim empty spaces on block end boundary. (#3585)
147 function removeTailWhitespace( element
) {
148 if ( element
._
.isBlockLike
&& element
.name
!= 'pre' && element
.name
!= 'textarea' ) {
150 var length
= element
.children
.length
,
151 lastChild
= element
.children
[ length
- 1 ],
153 if ( lastChild
&& lastChild
.type
== CKEDITOR
.NODE_TEXT
) {
154 if ( !( text
= CKEDITOR
.tools
.rtrim( lastChild
.value
) ) )
155 element
.children
.length
= length
- 1;
157 lastChild
.value
= text
;
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.
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.
171 function addElement( element
, target
, moveCurrent
) {
172 target
= target
|| currentNode
|| root
;
174 // Current element might be mangled by fix body below,
175 // save it for restore later.
176 var savedCurrent
= currentNode
;
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
, {} );
185 // The new target now is the <p>.
186 element
.returnPoint
= target
= currentNode
;
189 removeTailWhitespace( element
);
191 // Avoid adding empty inline.
192 if ( !( isRemoveEmpty( element
) && !element
.children
.length
) )
193 target
.add( element
);
195 if ( element
.name
== 'pre' )
198 if ( element
.name
== 'textarea' )
202 if ( element
.returnPoint
) {
203 currentNode
= element
.returnPoint
;
204 delete element
.returnPoint
;
206 currentNode
= moveCurrent
? target : savedCurrent
;
210 // Auto paragraphing should happen when inline content enters the root element.
211 function checkAutoParagraphing( parent
, node
) {
213 // Check for parent that can contain block.
214 if ( ( parent
== root
|| parent
.name
== 'body' ) && fixingBlock
&&
215 ( !parent
.name
|| CKEDITOR
.dtd
[ parent
.name
][ fixingBlock
] ) ) {
218 if ( node
.attributes
&& ( realName
= node
.attributes
[ 'data-cke-real-element-type' ] ) )
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
) &&
227 node
.type
== CKEDITOR
.NODE_TEXT
;
231 // Judge whether two element tag names are likely the siblings from the same
232 // structural element.
233 function possiblySibling( tag1
, tag2
) {
235 if ( tag1
in CKEDITOR
.dtd
.$listItem
|| tag1
in CKEDITOR
.dtd
.$tableContent
)
236 return tag1
== tag2
|| tag1
== 'dt' && tag2
== 'dd' || tag1
== 'dd' && tag2
== 'dt';
241 parser
.onTagOpen = function( tagName
, attributes
, selfClosing
, optionalClose
) {
242 var element
= new CKEDITOR
.htmlParser
.element( tagName
, attributes
);
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;
249 // Check for optional closed elements, including browser quirks and manually opened blocks.
250 element
.isOptionalClose
= optionalClose
;
252 // This is a tag to be removed if empty, so do not add it immediately.
253 if ( isRemoveEmpty( element
) ) {
254 pendingInline
.push( element
);
256 } else if ( tagName
== 'pre' )
258 else if ( tagName
== 'br' && inPre
) {
259 currentNode
.add( new CKEDITOR
.htmlParser
.text( '\n' ) );
261 } else if ( tagName
== 'textarea' ) {
265 if ( tagName
== 'br' ) {
266 pendingBRs
.push( element
);
271 var currentName
= currentNode
.name
;
273 var currentDtd
= currentName
? ( CKEDITOR
.dtd
[ currentName
] || ( currentNode
._
.isBlockLike
? CKEDITOR
.dtd
.div : CKEDITOR
.dtd
.span
) ) : rootDtd
;
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 ];
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
);
290 !element
.returnPoint
&& ( element
.returnPoint
= currentNode
);
291 currentNode
= lastChild
;
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 );
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
;
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
);
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.
324 element
.isOrphan
= 1;
333 checkPending( tagName
);
336 element
.parent
= currentNode
;
338 if ( element
.isEmpty
)
339 addElement( element
);
341 currentNode
= element
;
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 );
355 newPendingInline
= [],
356 candidate
= currentNode
;
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
362 if ( !candidate
._
.isBlockLike
)
363 newPendingInline
.unshift( candidate
);
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
);
370 // Make sure return point is properly restored.
371 candidate
= candidate
.returnPoint
|| candidate
.parent
;
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
);
381 currentNode
= candidate
;
383 if ( candidate
._
.isBlockLike
)
386 addElement( candidate
, candidate
.parent
);
388 // The parent should start receiving new nodes now, except if
389 // addElement changed the currentNode.
390 if ( candidate
== currentNode
)
391 currentNode
= currentNode
.parent
;
393 pendingInline
= pendingInline
.concat( newPendingInline
);
396 if ( tagName
== 'body' )
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
);
405 if ( text
.length
=== 0 )
409 var currentName
= currentNode
.name
,
410 currentDtd
= currentName
? ( CKEDITOR
.dtd
[ currentName
] || ( currentNode
._
.isBlockLike
? CKEDITOR
.dtd
.div : CKEDITOR
.dtd
.span
) ) : rootDtd
;
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
);
422 // Shrinking consequential spaces into one single for all elements
424 if ( !inPre
&& !inTextarea
)
425 text
= text
.replace( /[\t\r\n ]{2,}|[\t\r\n]/g
, ' ' );
427 text
= new CKEDITOR
.htmlParser
.text( text
);
430 if ( checkAutoParagraphing( currentNode
, text
) )
431 this.onTagOpen( fixingBlock
, {}, 0, 1 );
433 currentNode
.add( text
);
436 parser
.onCDATA = function( cdata
) {
437 currentNode
.add( new CKEDITOR
.htmlParser
.cdata( cdata
) );
440 parser
.onComment = function( comment
) {
443 currentNode
.add( new CKEDITOR
.htmlParser
.comment( comment
) );
447 parser
.parse( fragmentHtml
);
451 // Close all pending nodes, make sure return point is properly restored.
452 while ( currentNode
!= root
)
453 addElement( currentNode
, currentNode
.parent
, 1 );
455 removeTailWhitespace( root
);
460 CKEDITOR
.htmlParser
.fragment
.prototype = {
463 * The node type. This is a constant value set to {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
466 * @property {Number} [=CKEDITOR.NODE_DOCUMENT_FRAGMENT]
468 type: CKEDITOR
.NODE_DOCUMENT_FRAGMENT
,
471 * Adds a node to this fragment.
473 * @param {CKEDITOR.htmlParser.node} node The node to be added.
474 * @param {Number} [index] From where the insertion happens.
476 add: function( node
, index
) {
477 isNaN( index
) && ( index
= this.children
.length
);
479 var previous
= index
> 0 ? this.children
[ index
- 1 ] : null;
481 // If the block to be appended is following text, trim spaces at
483 if ( node
._
.isBlockLike
&& previous
.type
== CKEDITOR
.NODE_TEXT
) {
484 previous
.value
= CKEDITOR
.tools
.rtrim( previous
.value
);
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.
495 previous
.next
= node
;
498 node
.previous
= previous
;
501 this.children
.splice( index
, 0, node
);
503 if ( !this._
.hasInlineStarted
)
504 this._
.hasInlineStarted
= node
.type
== CKEDITOR
.NODE_TEXT
|| ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& !node
._
.isBlockLike
);
508 * Filter this fragment's content with given filter.
511 * @param {CKEDITOR.htmlParser.filter} filter
513 filter: function( filter
, context
) {
514 context
= this.getFilterContext( context
);
516 // Apply the root filter.
517 filter
.onRoot( context
, this );
519 this.filterChildren( filter
, false, context
);
523 * Filter this fragment's children with given filter.
525 * Element's children may only be filtered once by one
526 * instance of filter.
529 * @param {CKEDITOR.htmlParser.filter} filter
530 * @param {Boolean} [filterRoot] Whether to apply the "root" filter rule specified in the `filter`.
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
)
542 context
= this.getFilterContext( context
);
544 // Filtering root if enforced.
545 if ( filterRoot
&& !this.parent
)
546 filter
.onRoot( context
, this );
548 this.childrenFilteredBy
= filter
.id
;
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 )
560 * Writes the fragment HTML to a {@link CKEDITOR.htmlParser.basicWriter}.
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>'
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.
570 writeHtml: function( writer
, filter
) {
572 this.filter( filter
);
574 this.writeChildrenHtml( writer
);
578 * Write and filtering the child nodes of this fragment.
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`.
584 writeChildrenHtml: function( writer
, filter
, filterRoot
) {
585 var context
= this.getFilterContext();
587 // Filtering root if enforced.
588 if ( filterRoot
&& !this.parent
&& filter
)
589 filter
.onRoot( context
, this );
592 this.filterChildren( filter
, false, context
);
594 for ( var i
= 0, children
= this.children
, l
= children
.length
; i
< l
; i
++ )
595 children
[ i
].writeHtml( writer
);
599 * Execute callback on each node (of given type) in this document fragment.
601 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<p>foo<b>bar</b>bom</p>' );
602 * fragment.forEach( function( node ) {
603 * console.log( node );
606 * // 1. document fragment,
608 * // 3. "foo" text node,
610 * // 5. "bar" text node,
611 * // 6. "bom" text node.
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.
620 forEach: function( callback
, type
, skipRoot
) {
621 if ( !skipRoot
&& ( !type
|| this.type
== type
) )
622 var ret
= callback( this );
624 // Do not filter children if callback returned false.
628 var children
= this.children
,
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
)
642 getFilterContext: function( context
) {
643 return context
|| {};