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