JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.5.3
[ckeditor.git] / _source / core / htmlparser / fragment.js
1 /*\r
2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license\r
4 */\r
5 \r
6 /**\r
7  * A lightweight representation of an HTML DOM structure.\r
8  * @constructor\r
9  * @example\r
10  */\r
11 CKEDITOR.htmlParser.fragment = function()\r
12 {\r
13         /**\r
14          * The nodes contained in the root of this fragment.\r
15          * @type Array\r
16          * @example\r
17          * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );\r
18          * alert( fragment.children.length );  "2"\r
19          */\r
20         this.children = [];\r
21 \r
22         /**\r
23          * Get the fragment parent. Should always be null.\r
24          * @type Object\r
25          * @default null\r
26          * @example\r
27          */\r
28         this.parent = null;\r
29 \r
30         /** @private */\r
31         this._ =\r
32         {\r
33                 isBlockLike : true,\r
34                 hasInlineStarted : false\r
35         };\r
36 };\r
37 \r
38 (function()\r
39 {\r
40         // Block-level elements whose internal structure should be respected during\r
41         // parser fixing.\r
42         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 );\r
43 \r
44         var listBlocks = { ol:1, ul:1 };\r
45 \r
46         // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>.\r
47         var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style:1,script:1 } );\r
48 \r
49         /**\r
50          * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.\r
51          * @param {String} fragmentHtml The HTML to be parsed, filling the fragment.\r
52          * @param {Number} [fixForBody=false] Wrap body with specified element if needed.\r
53          * @param {CKEDITOR.htmlParser.element} contextNode Parse the html as the content of this element.\r
54          * @returns CKEDITOR.htmlParser.fragment The fragment created.\r
55          * @example\r
56          * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );\r
57          * alert( fragment.children[0].name );  "b"\r
58          * alert( fragment.children[1].value );  " Text"\r
59          */\r
60         CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody, contextNode )\r
61         {\r
62                 var parser = new CKEDITOR.htmlParser(),\r
63                         fragment = contextNode || new CKEDITOR.htmlParser.fragment(),\r
64                         pendingInline = [],\r
65                         pendingBRs = [],\r
66                         currentNode = fragment,\r
67                     // Indicate we're inside a <pre> element, spaces should be touched differently.\r
68                         inPre = false;\r
69 \r
70                 function checkPending( newTagName )\r
71                 {\r
72                         var pendingBRsSent;\r
73 \r
74                         if ( pendingInline.length > 0 )\r
75                         {\r
76                                 for ( var i = 0 ; i < pendingInline.length ; i++ )\r
77                                 {\r
78                                         var pendingElement = pendingInline[ i ],\r
79                                                 pendingName = pendingElement.name,\r
80                                                 pendingDtd = CKEDITOR.dtd[ pendingName ],\r
81                                                 currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];\r
82 \r
83                                         if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) )\r
84                                         {\r
85                                                 if ( !pendingBRsSent )\r
86                                                 {\r
87                                                         sendPendingBRs();\r
88                                                         pendingBRsSent = 1;\r
89                                                 }\r
90 \r
91                                                 // Get a clone for the pending element.\r
92                                                 pendingElement = pendingElement.clone();\r
93 \r
94                                                 // Add it to the current node and make it the current,\r
95                                                 // so the new element will be added inside of it.\r
96                                                 pendingElement.parent = currentNode;\r
97                                                 currentNode = pendingElement;\r
98 \r
99                                                 // Remove the pending element (back the index by one\r
100                                                 // to properly process the next entry).\r
101                                                 pendingInline.splice( i, 1 );\r
102                                                 i--;\r
103                                         }\r
104                                 }\r
105                         }\r
106                 }\r
107 \r
108                 function sendPendingBRs()\r
109                 {\r
110                         while ( pendingBRs.length )\r
111                                 currentNode.add( pendingBRs.shift() );\r
112                 }\r
113 \r
114                 /*\r
115                 * Beside of simply append specified element to target, this function also takes\r
116                 * care of other dirty lifts like forcing block in body, trimming spaces at\r
117                 * the block boundaries etc.\r
118                 *\r
119                 * @param {Element} element  The element to be added as the last child of {@link target}.\r
120                 * @param {Element} target The parent element to relieve the new node.\r
121                 * @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless\r
122                 * there's a return point node specified on the element, otherwise move current onto {@link target} node.\r
123                  */\r
124                 function addElement( element, target, moveCurrent )\r
125                 {\r
126                         // Ignore any element that has already been added.\r
127                         if ( element.previous !== undefined )\r
128                                 return;\r
129 \r
130                         target = target || currentNode || fragment;\r
131 \r
132                         // Current element might be mangled by fix body below,\r
133                         // save it for restore later.\r
134                         var savedCurrent = currentNode;\r
135 \r
136                         // If the target is the fragment and this inline element can't go inside\r
137                         // body (if fixForBody).\r
138                         if ( fixForBody && ( !target.type || target.name == 'body' ) )\r
139                         {\r
140                                 var elementName, realElementName;\r
141                                 if ( element.attributes\r
142                                          && ( realElementName =\r
143                                                   element.attributes[ 'data-cke-real-element-type' ] ) )\r
144                                         elementName = realElementName;\r
145                                 else\r
146                                         elementName =  element.name;\r
147 \r
148                                 if ( elementName && !( elementName in CKEDITOR.dtd.$body || elementName == 'body' || element.isOrphan ) )\r
149                                 {\r
150                                         // Create a <p> in the fragment.\r
151                                         currentNode = target;\r
152                                         parser.onTagOpen( fixForBody, {} );\r
153 \r
154                                         // The new target now is the <p>.\r
155                                         element.returnPoint = target = currentNode;\r
156                                 }\r
157                         }\r
158 \r
159                         // Rtrim empty spaces on block end boundary. (#3585)\r
160                         if ( element._.isBlockLike\r
161                                  && element.name != 'pre' )\r
162                         {\r
163 \r
164                                 var length = element.children.length,\r
165                                         lastChild = element.children[ length - 1 ],\r
166                                         text;\r
167                                 if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT )\r
168                                 {\r
169                                         if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) )\r
170                                                 element.children.length = length -1;\r
171                                         else\r
172                                                 lastChild.value = text;\r
173                                 }\r
174                         }\r
175 \r
176                         target.add( element );\r
177 \r
178                         if ( element.returnPoint )\r
179                         {\r
180                                 currentNode = element.returnPoint;\r
181                                 delete element.returnPoint;\r
182                         }\r
183                         else\r
184                                 currentNode = moveCurrent ? target : savedCurrent;\r
185                 }\r
186 \r
187                 parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose )\r
188                 {\r
189                         var element = new CKEDITOR.htmlParser.element( tagName, attributes );\r
190 \r
191                         // "isEmpty" will be always "false" for unknown elements, so we\r
192                         // must force it if the parser has identified it as a selfClosing tag.\r
193                         if ( element.isUnknown && selfClosing )\r
194                                 element.isEmpty = true;\r
195 \r
196                         element.isOptionalClose = optionalClose;\r
197 \r
198                         // This is a tag to be removed if empty, so do not add it immediately.\r
199                         if ( CKEDITOR.dtd.$removeEmpty[ tagName ] )\r
200                         {\r
201                                 pendingInline.push( element );\r
202                                 return;\r
203                         }\r
204                         else if ( tagName == 'pre' )\r
205                                 inPre = true;\r
206                         else if ( tagName == 'br' && inPre )\r
207                         {\r
208                                 currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) );\r
209                                 return;\r
210                         }\r
211 \r
212                         if ( tagName == 'br' )\r
213                         {\r
214                                 pendingBRs.push( element );\r
215                                 return;\r
216                         }\r
217 \r
218                         while( 1 )\r
219                         {\r
220                                 var currentName = currentNode.name;\r
221 \r
222                                 var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ]\r
223                                                 || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) )\r
224                                                 : rootDtd;\r
225 \r
226                                 // If the element cannot be child of the current element.\r
227                                 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] )\r
228                                 {\r
229                                         // Current node doesn't have a close tag, time for a close\r
230                                         // as this element isn't fit in. (#7497)\r
231                                         if ( currentNode.isOptionalClose )\r
232                                                 parser.onTagClose( currentName );\r
233                                         // Fixing malformed nested lists by moving it into a previous list item. (#3828)\r
234                                         else if ( tagName in listBlocks\r
235                                                 && currentName in listBlocks )\r
236                                         {\r
237                                                 var children = currentNode.children,\r
238                                                         lastChild = children[ children.length - 1 ];\r
239 \r
240                                                 // Establish the list item if it's not existed.\r
241                                                 if ( !( lastChild && lastChild.name == 'li' ) )\r
242                                                         addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode );\r
243 \r
244                                                 !element.returnPoint && ( element.returnPoint = currentNode );\r
245                                                 currentNode = lastChild;\r
246                                         }\r
247                                         // Establish new list root for orphan list items.\r
248                                         else if ( tagName in CKEDITOR.dtd.$listItem && currentName != tagName )\r
249                                                 parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 );\r
250                                         // We're inside a structural block like table and list, AND the incoming element\r
251                                         // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it,\r
252                                         // and most importantly, return back to here once this element is added,\r
253                                         // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table>\r
254                                         else if ( currentName in nonBreakingBlocks && currentName != tagName )\r
255                                         {\r
256                                                 !element.returnPoint && ( element.returnPoint = currentNode );\r
257                                                 currentNode = currentNode.parent;\r
258                                         }\r
259                                         else\r
260                                         {\r
261                                                 // The current element is an inline element, which\r
262                                                 // need to be continued even after the close, so put\r
263                                                 // it in the pending list.\r
264                                                 if ( currentName in CKEDITOR.dtd.$inline )\r
265                                                         pendingInline.unshift( currentNode );\r
266 \r
267                                                 // The most common case where we just need to close the\r
268                                                 // current one and append the new one to the parent.\r
269                                                 if ( currentNode.parent )\r
270                                                         addElement( currentNode, currentNode.parent, 1 );\r
271                                                 // We've tried our best to fix the embarrassment here, while\r
272                                                 // this element still doesn't find it's parent, mark it as\r
273                                                 // orphan and show our tolerance to it.\r
274                                                 else\r
275                                                 {\r
276                                                         element.isOrphan = 1;\r
277                                                         break;\r
278                                                 }\r
279                                         }\r
280                                 }\r
281                                 else\r
282                                         break;\r
283                         }\r
284 \r
285                         checkPending( tagName );\r
286                         sendPendingBRs();\r
287 \r
288                         element.parent = currentNode;\r
289 \r
290                         if ( element.isEmpty )\r
291                                 addElement( element );\r
292                         else\r
293                                 currentNode = element;\r
294                 };\r
295 \r
296                 parser.onTagClose = function( tagName )\r
297                 {\r
298                         // Check if there is any pending tag to be closed.\r
299                         for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- )\r
300                         {\r
301                                 // If found, just remove it from the list.\r
302                                 if ( tagName == pendingInline[ i ].name )\r
303                                 {\r
304                                         pendingInline.splice( i, 1 );\r
305                                         return;\r
306                                 }\r
307                         }\r
308 \r
309                         var pendingAdd = [],\r
310                                 newPendingInline = [],\r
311                                 candidate = currentNode;\r
312 \r
313                         while ( candidate != fragment && candidate.name != tagName )\r
314                         {\r
315                                 // If this is an inline element, add it to the pending list, if we're\r
316                                 // really closing one of the parents element later, they will continue\r
317                                 // after it.\r
318                                 if ( !candidate._.isBlockLike )\r
319                                         newPendingInline.unshift( candidate );\r
320 \r
321                                 // This node should be added to it's parent at this point. But,\r
322                                 // it should happen only if the closing tag is really closing\r
323                                 // one of the nodes. So, for now, we just cache it.\r
324                                 pendingAdd.push( candidate );\r
325 \r
326                                 // Make sure return point is properly restored.\r
327                                 candidate = candidate.returnPoint || candidate.parent;\r
328                         }\r
329 \r
330                         if ( candidate != fragment )\r
331                         {\r
332                                 // Add all elements that have been found in the above loop.\r
333                                 for ( i = 0 ; i < pendingAdd.length ; i++ )\r
334                                 {\r
335                                         var node = pendingAdd[ i ];\r
336                                         addElement( node, node.parent );\r
337                                 }\r
338 \r
339                                 currentNode = candidate;\r
340 \r
341                                 if ( currentNode.name == 'pre' )\r
342                                         inPre = false;\r
343 \r
344                                 if ( candidate._.isBlockLike )\r
345                                         sendPendingBRs();\r
346 \r
347                                 addElement( candidate, candidate.parent );\r
348 \r
349                                 // The parent should start receiving new nodes now, except if\r
350                                 // addElement changed the currentNode.\r
351                                 if ( candidate == currentNode )\r
352                                         currentNode = currentNode.parent;\r
353 \r
354                                 pendingInline = pendingInline.concat( newPendingInline );\r
355                         }\r
356 \r
357                         if ( tagName == 'body' )\r
358                                 fixForBody = false;\r
359                 };\r
360 \r
361                 parser.onText = function( text )\r
362                 {\r
363                         // Trim empty spaces at beginning of element contents except <pre>.\r
364                         if ( !currentNode._.hasInlineStarted && !inPre )\r
365                         {\r
366                                 text = CKEDITOR.tools.ltrim( text );\r
367 \r
368                                 if ( text.length === 0 )\r
369                                         return;\r
370                         }\r
371 \r
372                         sendPendingBRs();\r
373                         checkPending();\r
374 \r
375                         if ( fixForBody\r
376                                  && ( !currentNode.type || currentNode.name == 'body' )\r
377                                  && CKEDITOR.tools.trim( text ) )\r
378                         {\r
379                                 this.onTagOpen( fixForBody, {}, 0, 1 );\r
380                         }\r
381 \r
382                         // Shrinking consequential spaces into one single for all elements\r
383                         // text contents.\r
384                         if ( !inPre )\r
385                                 text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' );\r
386 \r
387                         currentNode.add( new CKEDITOR.htmlParser.text( text ) );\r
388                 };\r
389 \r
390                 parser.onCDATA = function( cdata )\r
391                 {\r
392                         currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) );\r
393                 };\r
394 \r
395                 parser.onComment = function( comment )\r
396                 {\r
397                         sendPendingBRs();\r
398                         checkPending();\r
399                         currentNode.add( new CKEDITOR.htmlParser.comment( comment ) );\r
400                 };\r
401 \r
402                 // Parse it.\r
403                 parser.parse( fragmentHtml );\r
404 \r
405                 // Send all pending BRs except one, which we consider a unwanted bogus. (#5293)\r
406                 sendPendingBRs( !CKEDITOR.env.ie && 1 );\r
407 \r
408                 // Close all pending nodes, make sure return point is properly restored.\r
409                 while ( currentNode != fragment )\r
410                         addElement( currentNode, currentNode.parent, 1 );\r
411 \r
412                 return fragment;\r
413         };\r
414 \r
415         CKEDITOR.htmlParser.fragment.prototype =\r
416         {\r
417                 /**\r
418                  * Adds a node to this fragment.\r
419                  * @param {Object} node The node to be added. It can be any of of the\r
420                  *              following types: {@link CKEDITOR.htmlParser.element},\r
421                  *              {@link CKEDITOR.htmlParser.text} and\r
422                  *              {@link CKEDITOR.htmlParser.comment}.\r
423                  * @example\r
424                  */\r
425                 add : function( node )\r
426                 {\r
427                         var len = this.children.length,\r
428                                 previous = len > 0 && this.children[ len - 1 ] || null;\r
429 \r
430                         if ( previous )\r
431                         {\r
432                                 // If the block to be appended is following text, trim spaces at\r
433                                 // the right of it.\r
434                                 if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT )\r
435                                 {\r
436                                         previous.value = CKEDITOR.tools.rtrim( previous.value );\r
437 \r
438                                         // If we have completely cleared the previous node.\r
439                                         if ( previous.value.length === 0 )\r
440                                         {\r
441                                                 // Remove it from the list and add the node again.\r
442                                                 this.children.pop();\r
443                                                 this.add( node );\r
444                                                 return;\r
445                                         }\r
446                                 }\r
447 \r
448                                 previous.next = node;\r
449                         }\r
450 \r
451                         node.previous = previous;\r
452                         node.parent = this;\r
453 \r
454                         this.children.push( node );\r
455 \r
456                         this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike );\r
457                 },\r
458 \r
459                 /**\r
460                  * Writes the fragment HTML to a CKEDITOR.htmlWriter.\r
461                  * @param {CKEDITOR.htmlWriter} writer The writer to which write the HTML.\r
462                  * @example\r
463                  * var writer = new CKEDITOR.htmlWriter();\r
464                  * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '&lt;P&gt;&lt;B&gt;Example' );\r
465                  * fragment.writeHtml( writer )\r
466                  * alert( writer.getHtml() );  "&lt;p&gt;&lt;b&gt;Example&lt;/b&gt;&lt;/p&gt;"\r
467                  */\r
468                 writeHtml : function( writer, filter )\r
469                 {\r
470                         var isChildrenFiltered;\r
471                         this.filterChildren = function()\r
472                         {\r
473                                 var writer = new CKEDITOR.htmlParser.basicWriter();\r
474                                 this.writeChildrenHtml.call( this, writer, filter, true );\r
475                                 var html = writer.getHtml();\r
476                                 this.children = new CKEDITOR.htmlParser.fragment.fromHtml( html ).children;\r
477                                 isChildrenFiltered = 1;\r
478                         };\r
479 \r
480                         // Filtering the root fragment before anything else.\r
481                         !this.name && filter && filter.onFragment( this );\r
482 \r
483                         this.writeChildrenHtml( writer, isChildrenFiltered ? null : filter );\r
484                 },\r
485 \r
486                 writeChildrenHtml : function( writer, filter )\r
487                 {\r
488                         for ( var i = 0 ; i < this.children.length ; i++ )\r
489                                 this.children[i].writeHtml( writer, filter );\r
490                 }\r
491         };\r
492 })();\r