X-Git-Url: https://scm.cri.ensmp.fr/git/ckeditor.git/blobdiff_plain/256592bf803e851aa7fc953e08a6e9e58d970f8c..871bad8291b6dbc29d489d95d185458caab25158:/skins/ckeditor/_source/core/htmlparser/fragment.js diff --git a/skins/ckeditor/_source/core/htmlparser/fragment.js b/skins/ckeditor/_source/core/htmlparser/fragment.js new file mode 100644 index 0000000..bfa5cc5 --- /dev/null +++ b/skins/ckeditor/_source/core/htmlparser/fragment.js @@ -0,0 +1,497 @@ +/* +Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved. +For licensing, see LICENSE.html or http://ckeditor.com/license +*/ + +/** + * A lightweight representation of an HTML DOM structure. + * @constructor + * @example + */ +CKEDITOR.htmlParser.fragment = function() +{ + /** + * The nodes contained in the root of this fragment. + * @type Array + * @example + * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' ); + * alert( fragment.children.length ); "2" + */ + this.children = []; + + /** + * Get the fragment parent. Should always be null. + * @type Object + * @default null + * @example + */ + this.parent = null; + + /** @private */ + this._ = + { + isBlockLike : true, + hasInlineStarted : false + }; +}; + +(function() +{ + // Block-level elements whose internal structure should be respected during + // parser fixing. + var nonBreakingBlocks = CKEDITOR.tools.extend( { table:1,ul:1,ol:1,dl:1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl ); + + // IE < 8 don't output the close tag on definition list items. (#6975) + var optionalCloseTags = CKEDITOR.env.ie && CKEDITOR.env.version < 8 ? { dd : 1, dt :1 } : {}; + + var listBlocks = { ol:1, ul:1 }; + + // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan
  • . + var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style:1,script:1 } ); + + /** + * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string. + * @param {String} fragmentHtml The HTML to be parsed, filling the fragment. + * @param {Number} [fixForBody=false] Wrap body with specified element if needed. + * @param {CKEDITOR.htmlParser.element} contextNode Parse the html as the content of this element. + * @returns CKEDITOR.htmlParser.fragment The fragment created. + * @example + * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' ); + * alert( fragment.children[0].name ); "b" + * alert( fragment.children[1].value ); " Text" + */ + CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody, contextNode ) + { + var parser = new CKEDITOR.htmlParser(), + fragment = contextNode || new CKEDITOR.htmlParser.fragment(), + pendingInline = [], + pendingBRs = [], + currentNode = fragment, + // Indicate we're inside a
     element, spaces should be touched differently.
    +			inPre = false;
    +
    +		function checkPending( newTagName )
    +		{
    +			var pendingBRsSent;
    +
    +			if ( pendingInline.length > 0 )
    +			{
    +				for ( var i = 0 ; i < pendingInline.length ; i++ )
    +				{
    +					var pendingElement = pendingInline[ i ],
    +						pendingName = pendingElement.name,
    +						pendingDtd = CKEDITOR.dtd[ pendingName ],
    +						currentDtd = currentNode.name && CKEDITOR.dtd[ currentNode.name ];
    +
    +					if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) )
    +					{
    +						if ( !pendingBRsSent )
    +						{
    +							sendPendingBRs();
    +							pendingBRsSent = 1;
    +						}
    +
    +						// Get a clone for the pending element.
    +						pendingElement = pendingElement.clone();
    +
    +						// Add it to the current node and make it the current,
    +						// so the new element will be added inside of it.
    +						pendingElement.parent = currentNode;
    +						currentNode = pendingElement;
    +
    +						// Remove the pending element (back the index by one
    +						// to properly process the next entry).
    +						pendingInline.splice( i, 1 );
    +						i--;
    +					}
    +				}
    +			}
    +		}
    +
    +		function sendPendingBRs()
    +		{
    +			while ( pendingBRs.length )
    +				currentNode.add( pendingBRs.shift() );
    +		}
    +
    +		/*
    +		* Beside of simply append specified element to target, this function also takes
    +		* care of other dirty lifts like forcing block in body, trimming spaces at
    +		* the block boundaries etc.
    +		*
    +		* @param {Element} element  The element to be added as the last child of {@link target}.
    +		* @param {Element} target The parent element to relieve the new node.
    +		* @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless
    +		* there's a return point node specified on the element, otherwise move current onto {@link target} node.
    +		 */
    +		function addElement( element, target, moveCurrent )
    +		{
    +			// Ignore any element that has already been added.
    +			if ( element.previous !== undefined )
    +				return;
    +
    +			target = target || currentNode || fragment;
    +
    +			// Current element might be mangled by fix body below,
    +			// save it for restore later.
    +			var savedCurrent = currentNode;
    +
    +			// If the target is the fragment and this inline element can't go inside
    +			// body (if fixForBody).
    +			if ( fixForBody && ( !target.type || target.name == 'body' ) )
    +			{
    +				var elementName, realElementName;
    +				if ( element.attributes
    +					 && ( realElementName =
    +						  element.attributes[ 'data-cke-real-element-type' ] ) )
    +					elementName = realElementName;
    +				else
    +					elementName =  element.name;
    +
    +				if ( elementName && !( elementName in CKEDITOR.dtd.$body || elementName == 'body' || element.isOrphan ) )
    +				{
    +					// Create a 

    in the fragment. + currentNode = target; + parser.onTagOpen( fixForBody, {} ); + + // The new target now is the

    . + element.returnPoint = target = currentNode; + } + } + + // Rtrim empty spaces on block end boundary. (#3585) + if ( element._.isBlockLike + && element.name != 'pre' ) + { + + var length = element.children.length, + lastChild = element.children[ length - 1 ], + text; + if ( lastChild && lastChild.type == CKEDITOR.NODE_TEXT ) + { + if ( !( text = CKEDITOR.tools.rtrim( lastChild.value ) ) ) + element.children.length = length -1; + else + lastChild.value = text; + } + } + + target.add( element ); + + if ( element.returnPoint ) + { + currentNode = element.returnPoint; + delete element.returnPoint; + } + else + currentNode = moveCurrent ? target : savedCurrent; + } + + parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) + { + var element = new CKEDITOR.htmlParser.element( tagName, attributes ); + + // "isEmpty" will be always "false" for unknown elements, so we + // must force it if the parser has identified it as a selfClosing tag. + if ( element.isUnknown && selfClosing ) + element.isEmpty = true; + + // Check for optional closed elements, including browser quirks and manually opened blocks. + element.isOptionalClose = tagName in optionalCloseTags || optionalClose; + + // This is a tag to be removed if empty, so do not add it immediately. + if ( CKEDITOR.dtd.$removeEmpty[ tagName ] ) + { + pendingInline.push( element ); + return; + } + else if ( tagName == 'pre' ) + inPre = true; + else if ( tagName == 'br' && inPre ) + { + currentNode.add( new CKEDITOR.htmlParser.text( '\n' ) ); + return; + } + + if ( tagName == 'br' ) + { + pendingBRs.push( element ); + return; + } + + while( 1 ) + { + var currentName = currentNode.name; + + var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ] + || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) ) + : rootDtd; + + // If the element cannot be child of the current element. + if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) + { + // Current node doesn't have a close tag, time for a close + // as this element isn't fit in. (#7497) + if ( currentNode.isOptionalClose ) + parser.onTagClose( currentName ); + // Fixing malformed nested lists by moving it into a previous list item. (#3828) + else if ( tagName in listBlocks + && currentName in listBlocks ) + { + var children = currentNode.children, + lastChild = children[ children.length - 1 ]; + + // Establish the list item if it's not existed. + if ( !( lastChild && lastChild.name == 'li' ) ) + addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode ); + + !element.returnPoint && ( element.returnPoint = currentNode ); + currentNode = lastChild; + } + // Establish new list root for orphan list items. + else if ( tagName in CKEDITOR.dtd.$listItem && currentName != tagName ) + parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 ); + // We're inside a structural block like table and list, AND the incoming element + // is not of the same type (e.g. td1td2), we simply add this new one before it, + // and most importantly, return back to here once this element is added, + // e.g.

    p1

    td1td2
    + else if ( currentName in nonBreakingBlocks && currentName != tagName ) + { + !element.returnPoint && ( element.returnPoint = currentNode ); + currentNode = currentNode.parent; + } + else + { + // The current element is an inline element, which + // need to be continued even after the close, so put + // it in the pending list. + if ( currentName in CKEDITOR.dtd.$inline ) + pendingInline.unshift( currentNode ); + + // The most common case where we just need to close the + // current one and append the new one to the parent. + if ( currentNode.parent ) + addElement( currentNode, currentNode.parent, 1 ); + // We've tried our best to fix the embarrassment here, while + // this element still doesn't find it's parent, mark it as + // orphan and show our tolerance to it. + else + { + element.isOrphan = 1; + break; + } + } + } + else + break; + } + + checkPending( tagName ); + sendPendingBRs(); + + element.parent = currentNode; + + if ( element.isEmpty ) + addElement( element ); + else + currentNode = element; + }; + + parser.onTagClose = function( tagName ) + { + // Check if there is any pending tag to be closed. + for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- ) + { + // If found, just remove it from the list. + if ( tagName == pendingInline[ i ].name ) + { + pendingInline.splice( i, 1 ); + return; + } + } + + var pendingAdd = [], + newPendingInline = [], + candidate = currentNode; + + while ( candidate != fragment && candidate.name != tagName ) + { + // If this is an inline element, add it to the pending list, if we're + // really closing one of the parents element later, they will continue + // after it. + if ( !candidate._.isBlockLike ) + newPendingInline.unshift( candidate ); + + // This node should be added to it's parent at this point. But, + // it should happen only if the closing tag is really closing + // one of the nodes. So, for now, we just cache it. + pendingAdd.push( candidate ); + + // Make sure return point is properly restored. + candidate = candidate.returnPoint || candidate.parent; + } + + if ( candidate != fragment ) + { + // Add all elements that have been found in the above loop. + for ( i = 0 ; i < pendingAdd.length ; i++ ) + { + var node = pendingAdd[ i ]; + addElement( node, node.parent ); + } + + currentNode = candidate; + + if ( currentNode.name == 'pre' ) + inPre = false; + + if ( candidate._.isBlockLike ) + sendPendingBRs(); + + addElement( candidate, candidate.parent ); + + // The parent should start receiving new nodes now, except if + // addElement changed the currentNode. + if ( candidate == currentNode ) + currentNode = currentNode.parent; + + pendingInline = pendingInline.concat( newPendingInline ); + } + + if ( tagName == 'body' ) + fixForBody = false; + }; + + parser.onText = function( text ) + { + // Trim empty spaces at beginning of text contents except

    .
    +			if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre )
    +			{
    +				text = CKEDITOR.tools.ltrim( text );
    +
    +				if ( text.length === 0 )
    +					return;
    +			}
    +
    +			sendPendingBRs();
    +			checkPending();
    +
    +			if ( fixForBody
    +				 && ( !currentNode.type || currentNode.name == 'body' )
    +				 && CKEDITOR.tools.trim( text ) )
    +			{
    +				this.onTagOpen( fixForBody, {}, 0, 1 );
    +			}
    +
    +			// Shrinking consequential spaces into one single for all elements
    +			// text contents.
    +			if ( !inPre )
    +				text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' );
    +
    +			currentNode.add( new CKEDITOR.htmlParser.text( text ) );
    +		};
    +
    +		parser.onCDATA = function( cdata )
    +		{
    +			currentNode.add( new CKEDITOR.htmlParser.cdata( cdata ) );
    +		};
    +
    +		parser.onComment = function( comment )
    +		{
    +			sendPendingBRs();
    +			checkPending();
    +			currentNode.add( new CKEDITOR.htmlParser.comment( comment ) );
    +		};
    +
    +		// Parse it.
    +		parser.parse( fragmentHtml );
    +
    +		// Send all pending BRs except one, which we consider a unwanted bogus. (#5293)
    +		sendPendingBRs( !CKEDITOR.env.ie && 1 );
    +
    +		// Close all pending nodes, make sure return point is properly restored.
    +		while ( currentNode != fragment )
    +			addElement( currentNode, currentNode.parent, 1 );
    +
    +		return fragment;
    +	};
    +
    +	CKEDITOR.htmlParser.fragment.prototype =
    +	{
    +		/**
    +		 * Adds a node to this fragment.
    +		 * @param {Object} node The node to be added. It can be any of of the
    +		 *		following types: {@link CKEDITOR.htmlParser.element},
    +		 *		{@link CKEDITOR.htmlParser.text} and
    +		 *		{@link CKEDITOR.htmlParser.comment}.
    +		 *	@param {Number} [index] From where the insertion happens.
    +		 * @example
    +		 */
    +		add : function( node, index )
    +		{
    +			isNaN( index ) && ( index = this.children.length );
    +
    +			var previous = index > 0 ? this.children[ index - 1 ] : null;
    +			if ( previous )
    +			{
    +				// If the block to be appended is following text, trim spaces at
    +				// the right of it.
    +				if ( node._.isBlockLike && previous.type == CKEDITOR.NODE_TEXT )
    +				{
    +					previous.value = CKEDITOR.tools.rtrim( previous.value );
    +
    +					// If we have completely cleared the previous node.
    +					if ( previous.value.length === 0 )
    +					{
    +						// Remove it from the list and add the node again.
    +						this.children.pop();
    +						this.add( node );
    +						return;
    +					}
    +				}
    +
    +				previous.next = node;
    +			}
    +
    +			node.previous = previous;
    +			node.parent = this;
    +
    +			this.children.splice( index, 0, node );
    +
    +			this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike );
    +		},
    +
    +		/**
    +		 * Writes the fragment HTML to a CKEDITOR.htmlWriter.
    +		 * @param {CKEDITOR.htmlWriter} writer The writer to which write the HTML.
    +		 * @example
    +		 * var writer = new CKEDITOR.htmlWriter();
    +		 * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<P><B>Example' );
    +		 * fragment.writeHtml( writer )
    +		 * alert( writer.getHtml() );  "<p><b>Example</b></p>"
    +		 */
    +		writeHtml : function( writer, filter )
    +		{
    +			var isChildrenFiltered;
    +			this.filterChildren = function()
    +			{
    +				var writer = new CKEDITOR.htmlParser.basicWriter();
    +				this.writeChildrenHtml.call( this, writer, filter, true );
    +				var html = writer.getHtml();
    +				this.children = new CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
    +				isChildrenFiltered = 1;
    +			};
    +
    +			// Filtering the root fragment before anything else.
    +			!this.name && filter && filter.onFragment( this );
    +
    +			this.writeChildrenHtml( writer, isChildrenFiltered ? null : filter );
    +		},
    +
    +		writeChildrenHtml : function( writer, filter )
    +		{
    +			for ( var i = 0 ; i < this.children.length ; i++ )
    +				this.children[i].writeHtml( writer, filter );
    +		}
    +	};
    +})();