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