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