1 /* 2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved. 3 For licensing, see LICENSE.html or http://ckeditor.com/license 4 */ 5 6 /** 7 * A lightweight representation of an HTML DOM structure. 8 * @constructor 9 * @example 10 */ 11 CKEDITOR.htmlParser.fragment = function() 12 { 13 /** 14 * The nodes contained in the root of this fragment. 15 * @type Array 16 * @example 17 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); 18 * alert( fragment.children.length ); "2" 19 */ 20 this.children = []; 21 22 /** 23 * Get the fragment parent. Should always be null. 24 * @type Object 25 * @default null 26 * @example 27 */ 28 this.parent = null; 29 30 /** @private */ 31 this._ = 32 { 33 isBlockLike : true, 34 hasInlineStarted : false 35 }; 36 }; 37 38 (function() 39 { 40 // Elements which the end tag is marked as optional in the HTML 4.01 DTD 41 // (expect empty elements). 42 var optionalClose = {colgroup:1,dd:1,dt:1,li:1,option:1,p:1,td:1,tfoot:1,th:1,thead:1,tr:1}; 43 44 // Block-level elements whose internal structure should be respected during 45 // parser fixing. 46 var nonBreakingBlocks = CKEDITOR.tools.extend( 47 {table:1,ul:1,ol:1,dl:1}, 48 CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl ); 49 50 /** 51 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. 52 * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. 53 * @param {Number} [fixForBody=false] Wrap body with specified element if needed. 54 * @returns CKEDITOR.htmlParser.fragment The fragment created. 55 * @example 56 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); 57 * alert( fragment.children[0].name ); "b" 58 * alert( fragment.children[1].value ); " Text" 59 */ 60 CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody ) 61 { 62 var parser = new CKEDITOR.htmlParser(), 63 html = [], 64 fragment = new CKEDITOR.htmlParser.fragment(), 65 pendingInline = [], 66 currentNode = fragment, 67 // Indicate we're inside a <pre> element, spaces should be touched differently. 68 inPre = false, 69 returnPoint; 70 71 function checkPending( newTagName ) 72 { 73 if ( pendingInline.length > 0 ) 74 { 75 for ( var i = 0 ; i < pendingInline.length ; i++ ) 76 { 77 var pendingElement = pendingInline[ i ], 78 pendingName = pendingElement.name, 79 pendingDtd = CKEDITOR.dtd[ pendingName ], 80 currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; 81 82 if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) 83 { 84 // Get a clone for the pending element. 85 pendingElement = pendingElement.clone(); 86 87 // Add it to the current node and make it the current, 88 // so the new element will be added inside of it. 89 pendingElement.parent = currentNode; 90 currentNode = pendingElement; 91 92 // Remove the pending element (back the index by one 93 // to properly process the next entry). 94 pendingInline.splice( i, 1 ); 95 i--; 96 } 97 } 98 } 99 } 100 101 function addElement( element, target, enforceCurrent ) 102 { 103 target = target || currentNode || fragment; 104 105 // If the target is the fragment and this element can't go inside 106 // body (if fixForBody). 107 if ( fixForBody && !target.type && !CKEDITOR.dtd.$body[ element.name ] ) 108 { 109 var savedCurrent = currentNode; 110 111 // Create a <p> in the fragment. 112 currentNode = target; 113 parser.onTagOpen( fixForBody, {} ); 114 115 // The new target now is the <p>. 116 target = currentNode; 117 118 if ( enforceCurrent ) 119 currentNode = savedCurrent; 120 } 121 122 // Rtrim empty spaces on block end boundary. (#3585) 123 if ( element._.isBlockLike 124 && !inPre ) 125 { 126 127 var length = element.children.length, 128 lastChild = element.children[ length - 1 ], 129 text; 130 if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) 131 { 132 if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) ) 133 element.children.length = length -1; 134 else 135 lastChild.value = text; 136 } 137 } 138 139 target.add( element ); 140 141 if ( element.returnPoint ) 142 { 143 currentNode = element.returnPoint; 144 delete element.returnPoint; 145 } 146 } 147 148 parser.onTagOpen = function( tagName, attributes, selfClosing ) 149 { 150 var element = new CKEDITOR.htmlParser.element( tagName, attributes ); 151 152 // "isEmpty" will be always "false" for unknown elements, so we 153 // must force it if the parser has identified it as a selfClosing tag. 154 if ( element.isUnknown && selfClosing ) 155 element.isEmpty = true; 156 157 // This is a tag to be removed if empty, so do not add it immediately. 158 if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) 159 { 160 pendingInline.push( element ); 161 return; 162 } 163 else if ( tagName == 'pre' ) 164 inPre = true; 165 else if ( tagName == 'br' && inPre ) 166 { 167 currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) ); 168 return; 169 } 170 171 var currentName = currentNode.name, 172 currentDtd = ( currentName && CKEDITOR.dtd[ currentName ] ) || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ); 173 174 // If the element cannot be child of the current element. 175 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) 176 { 177 // If this is the fragment node, just ignore this tag and add 178 // its children. 179 if ( !currentName ) 180 return; 181 182 var reApply = false; 183 184 // If the element name is the same as the current element name, 185 // then just close the current one and append the new one to the 186 // parent. This situation usually happens with <p>, <li>, <dt> and 187 // <dd>, specially in IE. Do not enter in this if block in this case. 188 if ( tagName == currentName ) 189 { 190 addElement( currentNode, currentNode.parent ); 191 } 192 else 193 { 194 if ( nonBreakingBlocks[ currentName ] ) 195 { 196 if ( !returnPoint ) 197 returnPoint = currentNode; 198 } 199 else 200 { 201 addElement( currentNode, currentNode.parent, true ); 202 203 if ( !optionalClose[ currentName ] ) 204 { 205 // The current element is an inline element, which 206 // cannot hold the new one. Put it in the pending list, 207 // and try adding the new one after it. 208 pendingInline.unshift( currentNode ); 209 } 210 } 211 212 reApply = true; 213 } 214 215 // In any of the above cases, we'll be adding, or trying to 216 // add it to the parent. 217 currentNode = currentNode.returnPoint || currentNode.parent; 218 219 if ( reApply ) 220 { 221 parser.onTagOpen.apply( this, arguments ); 222 return; 223 } 224 } 225 226 checkPending( tagName ); 227 228 element.parent = currentNode; 229 element.returnPoint = returnPoint; 230 returnPoint = 0; 231 232 if ( element.isEmpty ) 233 addElement( element ); 234 else 235 currentNode = element; 236 }; 237 238 parser.onTagClose = function( tagName ) 239 { 240 var index = 0, 241 pendingAdd = [], 242 candidate = currentNode; 243 244 while ( candidate.type && candidate.name != tagName ) 245 { 246 // If this is an inline element, add it to the pending list, so 247 // it will continue after the closing tag. 248 if ( !candidate._.isBlockLike ) 249 { 250 pendingInline.unshift( candidate ); 251 252 // Increase the index, so it will not get checked again in 253 // the pending list check that follows. 254 index++; 255 } 256 257 // This node should be added to it's parent at this point. But, 258 // it should happen only if the closing tag is really closing 259 // one of the nodes. So, for now, we just cache it. 260 pendingAdd.push( candidate ); 261 262 candidate = candidate.parent; 263 } 264 265 if ( candidate.type ) 266 { 267 // Add all elements that have been found in the above loop. 268 for ( var i = 0 ; i < pendingAdd.length ; i++ ) 269 { 270 var node = pendingAdd[ i ]; 271 addElement( node, node.parent ); 272 } 273 274 currentNode = candidate; 275 276 if( currentNode.name == 'pre' ) 277 inPre = false; 278 279 addElement( candidate, candidate.parent ); 280 281 // The parent should start receiving new nodes now, except if 282 // addElement changed the currentNode. 283 if ( candidate == currentNode ) 284 currentNode = currentNode.parent; 285 } 286 287 // Check if there is any pending tag to be closed. 288 for ( ; index < pendingInline.length ; index++ ) 289 { 290 // If found, just remove it from the list. 291 if ( tagName == pendingInline[ index ].name ) 292 { 293 pendingInline.splice( index, 1 ); 294 295 // Decrease the index so we continue from the next one. 296 index--; 297 } 298 } 299 }; 300 301 parser.onText = function( text ) 302 { 303 // Trim empty spaces at beginning of element contents except <pre>. 304 if ( !currentNode._.hasInlineStarted && !inPre ) 305 { 306 text = CKEDITOR.tools.ltrim( text ); 307 308 if ( text.length === 0 ) 309 return; 310 } 311 312 checkPending(); 313 314 if ( fixForBody && !currentNode.type ) 315 this.onTagOpen( fixForBody, {} ); 316 317 // Shrinking consequential spaces into one single for all elements 318 // text contents. 319 if ( !inPre ) 320 text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' ); 321 322 currentNode.add( new CKEDITOR.htmlParser.text( text ) ); 323 }; 324 325 parser.onCDATA = function( cdata ) 326 { 327 currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) ); 328 }; 329 330 parser.onComment = function( comment ) 331 { 332 currentNode.add( new CKEDITOR.htmlParser.comment( comment ) ); 333 }; 334 335 // Parse it. 336 parser.parse( fragmentHtml ); 337 338 // Close all pending nodes. 339 while ( currentNode.type ) 340 { 341 var parent = currentNode.parent, 342 node = currentNode; 343 344 if ( fixForBody && !parent.type && !CKEDITOR.dtd.$body[ node.name ] ) 345 { 346 currentNode = parent; 347 parser.onTagOpen( fixForBody, {} ); 348 parent = currentNode; 349 } 350 351 parent.add( node ); 352 currentNode = parent; 353 } 354 355 return fragment; 356 }; 357 358 CKEDITOR.htmlParser.fragment.prototype = 359 { 360 /** 361 * Adds a node to this fragment. 362 * @param {Object} node The node to be added. It can be any of of the 363 * following types: {@link CKEDITOR.htmlParser.element}, 364 * {@link CKEDITOR.htmlParser.text} and 365 * {@link CKEDITOR.htmlParser.comment}. 366 * @example 367 */ 368 add : function( node ) 369 { 370 var len = this.children.length, 371 previous = len > 0 && this.children[ len - 1 ] || null; 372 373 if ( previous ) 374 { 375 // If the block to be appended is following text, trim spaces at 376 // the right of it. 377 if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) 378 { 379 previous.value = CKEDITOR.tools.rtrim( previous.value ); 380 381 // If we have completely cleared the previous node. 382 if ( previous.value.length === 0 ) 383 { 384 // Remove it from the list and add the node again. 385 this.children.pop(); 386 this.add( node ); 387 return; 388 } 389 } 390 391 previous.next = node; 392 } 393 394 node.previous = previous; 395 node.parent = this; 396 397 this.children.push( node ); 398 399 this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike ); 400 }, 401 402 /** 403 * Writes the fragment HTML to a CKEDITOR.htmlWriter. 404 * @param {CKEDITOR.htmlWriter} writer The writer to which write the HTML. 405 * @example 406 * var writer = new CKEDITOR.htmlWriter(); 407 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' ); 408 * fragment.writeHtml( writer ) 409 * alert( writer.getHtml() ); "<p><b>Example</b></p>" 410 */ 411 writeHtml : function( writer, filter ) 412 { 413 for ( var i = 0, len = this.children.length ; i < len ; i++ ) 414 this.children[i].writeHtml( writer, filter ); 415 } 416 }; 417 })(); 418