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 /** 45 * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. 46 * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. 47 * @returns CKEDITOR.htmlParser.fragment The fragment created. 48 * @example 49 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' ); 50 * alert( fragment.children[0].name ); "b" 51 * alert( fragment.children[1].value ); " Text" 52 */ 53 CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody ) 54 { 55 var parser = new CKEDITOR.htmlParser(), 56 html = [], 57 fragment = new CKEDITOR.htmlParser.fragment(), 58 pendingInline = [], 59 currentNode = fragment; 60 61 var checkPending = function( newTagName ) 62 { 63 if ( pendingInline.length > 0 ) 64 { 65 for ( var i = 0 ; i < pendingInline.length ; i++ ) 66 { 67 var pendingElement = pendingInline[ i ], 68 pendingName = pendingElement.name, 69 pendingDtd = CKEDITOR.dtd[ pendingName ], 70 currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ]; 71 72 if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) ) 73 { 74 // Get a clone for the pending element. 75 pendingElement = pendingElement.clone(); 76 77 // Add it to the current node and make it the current, 78 // so the new element will be added inside of it. 79 currentNode.add( pendingElement ); 80 currentNode = pendingElement; 81 82 // Remove the pending element (back the index by one 83 // to properly process the next entry). 84 pendingInline.splice( i, 1 ); 85 i--; 86 } 87 } 88 } 89 }; 90 91 parser.onTagOpen = function( tagName, attributes, selfClosing ) 92 { 93 if ( fixForBody && !currentNode.type && !CKEDITOR.dtd.$body[ tagName ] ) 94 this.onTagOpen( 'p', {} ); 95 96 var element = new CKEDITOR.htmlParser.element( tagName, attributes ); 97 98 // "isEmpty" will be always "false" for unknown elements, so we 99 // must force it if the parser has identified it as a selfClosing tag. 100 if ( element.isUnknown && selfClosing ) 101 element.isEmpty = true; 102 103 // This is a tag to be removed if empty, so do not add it immediately. 104 if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) 105 { 106 pendingInline.push( element ); 107 return; 108 } 109 110 var currentName = currentNode.name, 111 currentDtd = ( currentName && CKEDITOR.dtd[ currentName ] ) || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ); 112 113 // If the element cannot be child of the current element. 114 if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) 115 { 116 // If this is the fragment node, just ignore this tag and add 117 // its children. 118 if ( !currentName ) 119 return; 120 121 var reApply = false; 122 123 // If the element name is the same as the current element name, 124 // then just close the current one and append the new one to the 125 // parent. This situation usually happens with <p>, <li>, <dt> and 126 // <dd>, specially in IE. 127 if ( tagName != currentName ) 128 { 129 // If it is optional to close the current element, then 130 // close it at this point and simply add the new 131 // element after it. 132 if ( !optionalClose[ currentName ] ) 133 { 134 // The current element is an inline element, which 135 // cannot hold the new one. Put it in the pending list, 136 // and try adding the new one after it. 137 pendingInline.unshift( currentNode ); 138 } 139 140 reApply = true; 141 } 142 143 // In any of the above cases, we'll be adding, or trying to 144 // add it to the parent. 145 currentNode = currentNode.parent; 146 147 if ( reApply ) 148 { 149 parser.onTagOpen.apply( this, arguments ); 150 return; 151 } 152 } 153 154 checkPending( tagName ); 155 156 currentNode.add( element ); 157 158 if ( !element.isEmpty ) 159 currentNode = element; 160 }; 161 162 parser.onTagClose = function( tagName ) 163 { 164 var closingElement = currentNode, 165 index = 0; 166 167 while ( closingElement && closingElement.name != tagName ) 168 { 169 // If this is an inline element, add it to the pending list, so 170 // it will continue after the closing tag. 171 if ( !closingElement._.isBlockLike ) 172 { 173 pendingInline.unshift( closingElement ); 174 175 // Increase the index, so it will not get checked again in 176 // the pending list check that follows. 177 index++; 178 } 179 180 closingElement = closingElement.parent; 181 } 182 183 if ( closingElement ) 184 currentNode = closingElement.parent; 185 else if ( pendingInline.length > index ) 186 { 187 // If we didn't find any parent to be closed, let's check the 188 // pending list. 189 for ( ; index < pendingInline.length ; index++ ) 190 { 191 // If found, just remove it from the list. 192 if ( tagName == pendingInline[ index ].name ) 193 { 194 pendingInline.splice( index, 1 ); 195 196 // Decrease the index so we continue from the next one. 197 index--; 198 } 199 } 200 } 201 }; 202 203 parser.onText = function( text ) 204 { 205 if ( !currentNode._.hasInlineStarted ) 206 { 207 text = CKEDITOR.tools.ltrim( text ); 208 209 if ( text.length === 0 ) 210 return; 211 } 212 213 checkPending(); 214 215 if ( fixForBody && !currentNode.type ) 216 this.onTagOpen( 'p', {} ); 217 218 currentNode.add( new CKEDITOR.htmlParser.text( text ) ); 219 }; 220 221 parser.onComment = function( comment ) 222 { 223 currentNode.add( new CKEDITOR.htmlParser.comment( comment ) ); 224 }; 225 226 parser.parse( fragmentHtml ); 227 228 return fragment; 229 }; 230 231 CKEDITOR.htmlParser.fragment.prototype = 232 { 233 /** 234 * Adds a node to this fragment. 235 * @param {Object} node The node to be added. It can be any of of the 236 * following types: {@link CKEDITOR.htmlParser.element}, 237 * {@link CKEDITOR.htmlParser.text} and 238 * {@link CKEDITOR.htmlParser.comment}. 239 * @example 240 */ 241 add : function( node ) 242 { 243 var len = this.children.length, 244 previous = len > 0 && this.children[ len - 1 ] || null; 245 246 if ( previous ) 247 { 248 // If the block to be appended is following text, trim spaces at 249 // the right of it. 250 if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT ) 251 { 252 previous.value = CKEDITOR.tools.rtrim( previous.value ); 253 254 // If we have completely cleared the previous node. 255 if ( previous.value.length === 0 ) 256 { 257 // Remove it from the list and add the node again. 258 this.children.pop(); 259 this.add( node ); 260 return; 261 } 262 } 263 264 previous.next = node; 265 } 266 267 node.previous = previous; 268 node.parent = this; 269 270 this.children.push( node ); 271 272 this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike ); 273 }, 274 275 /** 276 * Writes the fragment HTML to a CKEDITOR.htmlWriter. 277 * @param {CKEDITOR.htmlWriter} writer The writer to which write the HTML. 278 * @example 279 * var writer = new CKEDITOR.htmlWriter(); 280 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' ); 281 * fragment.writeHtml( writer ) 282 * alert( writer.getHtml() ); "<p><b>Example</b></p>" 283 */ 284 writeHtml : function( writer, filter ) 285 { 286 for ( var i = 0, len = this.children.length ; i < len ; i++ ) 287 this.children[i].writeHtml( writer, filter ); 288 } 289 }; 290 })(); 291