JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.1
[ckeditor.git] / _source / core / htmlparser / fragment.js
index 7f6c6df..bfa5cc5 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.\r
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
 For licensing, see LICENSE.html or http://ckeditor.com/license\r
 */\r
 \r
@@ -37,39 +37,43 @@ CKEDITOR.htmlParser.fragment = function()
 \r
 (function()\r
 {\r
-       // Elements which the end tag is marked as optional in the HTML 4.01 DTD\r
-       // (expect empty elements).\r
-       var optionalClose = {colgroup:1,dd:1,dt:1,li:1,option:1,p:1,td:1,tfoot:1,th:1,thead:1,tr:1};\r
-\r
        // Block-level elements whose internal structure should be respected during\r
        // parser fixing.\r
-       var nonBreakingBlocks = CKEDITOR.tools.extend(\r
-                       {table:1,ul:1,ol:1,dl:1},\r
-                       CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl );\r
+       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 );\r
+\r
+       // IE < 8 don't output the close tag on definition list items. (#6975)\r
+       var optionalCloseTags = CKEDITOR.env.ie && CKEDITOR.env.version < 8 ? { dd : 1, dt :1 } : {};\r
+\r
+       var listBlocks = { ol:1, ul:1 };\r
+\r
+       // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>.\r
+       var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style:1,script:1 } );\r
 \r
        /**\r
         * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.\r
         * @param {String} fragmentHtml The HTML to be parsed, filling the fragment.\r
         * @param {Number} [fixForBody=false] Wrap body with specified element if needed.\r
+        * @param {CKEDITOR.htmlParser.element} contextNode Parse the html as the content of this element.\r
         * @returns CKEDITOR.htmlParser.fragment The fragment created.\r
         * @example\r
         * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );\r
         * alert( fragment.children[0].name );  "b"\r
         * alert( fragment.children[1].value );  " Text"\r
         */\r
-       CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody )\r
+       CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, fixForBody, contextNode )\r
        {\r
                var parser = new CKEDITOR.htmlParser(),\r
-                       html = [],\r
-                       fragment = new CKEDITOR.htmlParser.fragment(),\r
+                       fragment = contextNode || new CKEDITOR.htmlParser.fragment(),\r
                        pendingInline = [],\r
+                       pendingBRs = [],\r
                        currentNode = fragment,\r
                    // Indicate we're inside a <pre> element, spaces should be touched differently.\r
-                       inPre = false,\r
-                       returnPoint;\r
+                       inPre = false;\r
 \r
                function checkPending( newTagName )\r
                {\r
+                       var pendingBRsSent;\r
+\r
                        if ( pendingInline.length > 0 )\r
                        {\r
                                for ( var i = 0 ; i < pendingInline.length ; i++ )\r
@@ -81,6 +85,12 @@ CKEDITOR.htmlParser.fragment = function()
 \r
                                        if ( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !CKEDITOR.dtd[ newTagName ] ) )\r
                                        {\r
+                                               if ( !pendingBRsSent )\r
+                                               {\r
+                                                       sendPendingBRs();\r
+                                                       pendingBRsSent = 1;\r
+                                               }\r
+\r
                                                // Get a clone for the pending element.\r
                                                pendingElement = pendingElement.clone();\r
 \r
@@ -98,34 +108,54 @@ CKEDITOR.htmlParser.fragment = function()
                        }\r
                }\r
 \r
-               function addElement( element, target, enforceCurrent )\r
+               function sendPendingBRs()\r
+               {\r
+                       while ( pendingBRs.length )\r
+                               currentNode.add( pendingBRs.shift() );\r
+               }\r
+\r
+               /*\r
+               * Beside of simply append specified element to target, this function also takes\r
+               * care of other dirty lifts like forcing block in body, trimming spaces at\r
+               * the block boundaries etc.\r
+               *\r
+               * @param {Element} element  The element to be added as the last child of {@link target}.\r
+               * @param {Element} target The parent element to relieve the new node.\r
+               * @param {Boolean} [moveCurrent=false] Don't change the "currentNode" global unless\r
+               * there's a return point node specified on the element, otherwise move current onto {@link target} node.\r
+                */\r
+               function addElement( element, target, moveCurrent )\r
                {\r
+                       // Ignore any element that has already been added.\r
+                       if ( element.previous !== undefined )\r
+                               return;\r
+\r
                        target = target || currentNode || fragment;\r
 \r
-                       // If the target is the fragment and this element can't go inside\r
+                       // Current element might be mangled by fix body below,\r
+                       // save it for restore later.\r
+                       var savedCurrent = currentNode;\r
+\r
+                       // If the target is the fragment and this inline element can't go inside\r
                        // body (if fixForBody).\r
-                       if ( fixForBody && !target.type )\r
+                       if ( fixForBody && ( !target.type || target.name == 'body' ) )\r
                        {\r
                                var elementName, realElementName;\r
                                if ( element.attributes\r
                                         && ( realElementName =\r
-                                                 element.attributes[ '_cke_real_element_type' ] ) )\r
+                                                 element.attributes[ 'data-cke-real-element-type' ] ) )\r
                                        elementName = realElementName;\r
                                else\r
                                        elementName =  element.name;\r
-                               if ( !( elementName in CKEDITOR.dtd.$body ) )\r
-                               {\r
-                                       var savedCurrent = currentNode;\r
 \r
+                               if ( elementName && !( elementName in CKEDITOR.dtd.$body || elementName == 'body' || element.isOrphan ) )\r
+                               {\r
                                        // Create a <p> in the fragment.\r
                                        currentNode = target;\r
                                        parser.onTagOpen( fixForBody, {} );\r
 \r
                                        // The new target now is the <p>.\r
-                                       target = currentNode;\r
-\r
-                                       if ( enforceCurrent )\r
-                                               currentNode = savedCurrent;\r
+                                       element.returnPoint = target = currentNode;\r
                                }\r
                        }\r
 \r
@@ -153,9 +183,11 @@ CKEDITOR.htmlParser.fragment = function()
                                currentNode = element.returnPoint;\r
                                delete element.returnPoint;\r
                        }\r
+                       else\r
+                               currentNode = moveCurrent ? target : savedCurrent;\r
                }\r
 \r
-               parser.onTagOpen = function( tagName, attributes, selfClosing )\r
+               parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose )\r
                {\r
                        var element = new CKEDITOR.htmlParser.element( tagName, attributes );\r
 \r
@@ -164,6 +196,9 @@ CKEDITOR.htmlParser.fragment = function()
                        if ( element.isUnknown && selfClosing )\r
                                element.isEmpty = true;\r
 \r
+                       // Check for optional closed elements, including browser quirks and manually opened blocks.\r
+                       element.isOptionalClose = tagName in optionalCloseTags || optionalClose;\r
+\r
                        // This is a tag to be removed if empty, so do not add it immediately.\r
                        if ( CKEDITOR.dtd.$removeEmpty[ tagName ] )\r
                        {\r
@@ -178,66 +213,83 @@ CKEDITOR.htmlParser.fragment = function()
                                return;\r
                        }\r
 \r
-                       var currentName = currentNode.name,\r
-                               currentDtd = ( currentName && CKEDITOR.dtd[ currentName ] ) || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span );\r
+                       if ( tagName == 'br' )\r
+                       {\r
+                               pendingBRs.push( element );\r
+                               return;\r
+                       }\r
 \r
-                       // If the element cannot be child of the current element.\r
-                       if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] )\r
+                       while( 1 )\r
                        {\r
-                               // If this is the fragment node, just ignore this tag and add\r
-                               // its children.\r
-                               if ( !currentName )\r
-                                       return;\r
+                               var currentName = currentNode.name;\r
 \r
-                               var reApply = false;\r
+                               var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ]\r
+                                               || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) )\r
+                                               : rootDtd;\r
 \r
-                               // If the element name is the same as the current element name,\r
-                               // then just close the current one and append the new one to the\r
-                               // parent. This situation usually happens with <p>, <li>, <dt> and\r
-                               // <dd>, specially in IE. Do not enter in this if block in this case.\r
-                               if ( tagName == currentName )\r
-                               {\r
-                                       addElement( currentNode, currentNode.parent );\r
-                               }\r
-                               else\r
+                               // If the element cannot be child of the current element.\r
+                               if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] )\r
                                {\r
-                                       if ( nonBreakingBlocks[ currentName ] )\r
+                                       // Current node doesn't have a close tag, time for a close\r
+                                       // as this element isn't fit in. (#7497)\r
+                                       if ( currentNode.isOptionalClose )\r
+                                               parser.onTagClose( currentName );\r
+                                       // Fixing malformed nested lists by moving it into a previous list item. (#3828)\r
+                                       else if ( tagName in listBlocks\r
+                                               && currentName in listBlocks )\r
                                        {\r
-                                               if ( !returnPoint )\r
-                                                       returnPoint = currentNode;\r
+                                               var children = currentNode.children,\r
+                                                       lastChild = children[ children.length - 1 ];\r
+\r
+                                               // Establish the list item if it's not existed.\r
+                                               if ( !( lastChild && lastChild.name == 'li' ) )\r
+                                                       addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode );\r
+\r
+                                               !element.returnPoint && ( element.returnPoint = currentNode );\r
+                                               currentNode = lastChild;\r
+                                       }\r
+                                       // Establish new list root for orphan list items.\r
+                                       else if ( tagName in CKEDITOR.dtd.$listItem && currentName != tagName )\r
+                                               parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 );\r
+                                       // We're inside a structural block like table and list, AND the incoming element\r
+                                       // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it,\r
+                                       // and most importantly, return back to here once this element is added,\r
+                                       // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table>\r
+                                       else if ( currentName in nonBreakingBlocks && currentName != tagName )\r
+                                       {\r
+                                               !element.returnPoint && ( element.returnPoint = currentNode );\r
+                                               currentNode = currentNode.parent;\r
                                        }\r
                                        else\r
                                        {\r
-                                               addElement( currentNode, currentNode.parent, true );\r
+                                               // The current element is an inline element, which\r
+                                               // need to be continued even after the close, so put\r
+                                               // it in the pending list.\r
+                                               if ( currentName in CKEDITOR.dtd.$inline )\r
+                                                       pendingInline.unshift( currentNode );\r
 \r
-                                               if ( !optionalClose[ currentName ] )\r
+                                               // The most common case where we just need to close the\r
+                                               // current one and append the new one to the parent.\r
+                                               if ( currentNode.parent )\r
+                                                       addElement( currentNode, currentNode.parent, 1 );\r
+                                               // We've tried our best to fix the embarrassment here, while\r
+                                               // this element still doesn't find it's parent, mark it as\r
+                                               // orphan and show our tolerance to it.\r
+                                               else\r
                                                {\r
-                                                       // The current element is an inline element, which\r
-                                                       // cannot hold the new one. Put it in the pending list,\r
-                                                       // and try adding the new one after it.\r
-                                                       pendingInline.unshift( currentNode );\r
+                                                       element.isOrphan = 1;\r
+                                                       break;\r
                                                }\r
                                        }\r
-\r
-                                       reApply = true;\r
-                               }\r
-\r
-                               // In any of the above cases, we'll be adding, or trying to\r
-                               // add it to the parent.\r
-                               currentNode = currentNode.returnPoint || currentNode.parent;\r
-\r
-                               if ( reApply )\r
-                               {\r
-                                       parser.onTagOpen.apply( this, arguments );\r
-                                       return;\r
                                }\r
+                               else\r
+                                       break;\r
                        }\r
 \r
                        checkPending( tagName );\r
+                       sendPendingBRs();\r
 \r
                        element.parent = currentNode;\r
-                       element.returnPoint = returnPoint;\r
-                       returnPoint = 0;\r
 \r
                        if ( element.isEmpty )\r
                                addElement( element );\r
@@ -247,35 +299,42 @@ CKEDITOR.htmlParser.fragment = function()
 \r
                parser.onTagClose = function( tagName )\r
                {\r
-                       var index = 0,\r
-                               pendingAdd = [],\r
+                       // Check if there is any pending tag to be closed.\r
+                       for ( var i = pendingInline.length - 1 ; i >= 0 ; i-- )\r
+                       {\r
+                               // If found, just remove it from the list.\r
+                               if ( tagName == pendingInline[ i ].name )\r
+                               {\r
+                                       pendingInline.splice( i, 1 );\r
+                                       return;\r
+                               }\r
+                       }\r
+\r
+                       var pendingAdd = [],\r
+                               newPendingInline = [],\r
                                candidate = currentNode;\r
 \r
-                       while ( candidate.type && candidate.name != tagName )\r
+                       while ( candidate != fragment && candidate.name != tagName )\r
                        {\r
-                               // If this is an inline element, add it to the pending list, so\r
-                               // it will continue after the closing tag.\r
+                               // If this is an inline element, add it to the pending list, if we're\r
+                               // really closing one of the parents element later, they will continue\r
+                               // after it.\r
                                if ( !candidate._.isBlockLike )\r
-                               {\r
-                                       pendingInline.unshift( candidate );\r
-\r
-                                       // Increase the index, so it will not get checked again in\r
-                                       // the pending list check that follows.\r
-                                       index++;\r
-                               }\r
+                                       newPendingInline.unshift( candidate );\r
 \r
                                // This node should be added to it's parent at this point. But,\r
                                // it should happen only if the closing tag is really closing\r
                                // one of the nodes. So, for now, we just cache it.\r
                                pendingAdd.push( candidate );\r
 \r
-                               candidate = candidate.parent;\r
+                               // Make sure return point is properly restored.\r
+                               candidate = candidate.returnPoint || candidate.parent;\r
                        }\r
 \r
-                       if ( candidate.type )\r
+                       if ( candidate != fragment )\r
                        {\r
                                // Add all elements that have been found in the above loop.\r
-                               for ( var i = 0 ; i < pendingAdd.length ; i++ )\r
+                               for ( i = 0 ; i < pendingAdd.length ; i++ )\r
                                {\r
                                        var node = pendingAdd[ i ];\r
                                        addElement( node, node.parent );\r
@@ -283,42 +342,30 @@ CKEDITOR.htmlParser.fragment = function()
 \r
                                currentNode = candidate;\r
 \r
-                               if( currentNode.name == 'pre' )\r
+                               if ( currentNode.name == 'pre' )\r
                                        inPre = false;\r
 \r
+                               if ( candidate._.isBlockLike )\r
+                                       sendPendingBRs();\r
+\r
                                addElement( candidate, candidate.parent );\r
 \r
                                // The parent should start receiving new nodes now, except if\r
                                // addElement changed the currentNode.\r
                                if ( candidate == currentNode )\r
                                        currentNode = currentNode.parent;\r
-                       }\r
-                       // The tag is not actually closing anything, thus we need invalidate\r
-                       // the pending elements.(#3862)\r
-                       else\r
-                       {\r
-                               pendingInline.splice( 0, index );\r
-                               index = 0;\r
-                       }\r
-\r
-                       // Check if there is any pending tag to be closed.\r
-                       for ( ; index < pendingInline.length ; index++ )\r
-                       {\r
-                               // If found, just remove it from the list.\r
-                               if ( tagName == pendingInline[ index ].name )\r
-                               {\r
-                                       pendingInline.splice( index, 1 );\r
 \r
-                                       // Decrease the index so we continue from the next one.\r
-                                       index--;\r
-                               }\r
+                               pendingInline = pendingInline.concat( newPendingInline );\r
                        }\r
+\r
+                       if ( tagName == 'body' )\r
+                               fixForBody = false;\r
                };\r
 \r
                parser.onText = function( text )\r
                {\r
-                       // Trim empty spaces at beginning of element contents except <pre>.\r
-                       if ( !currentNode._.hasInlineStarted && !inPre )\r
+                       // Trim empty spaces at beginning of text contents except <pre>.\r
+                       if ( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre )\r
                        {\r
                                text = CKEDITOR.tools.ltrim( text );\r
 \r
@@ -326,10 +373,15 @@ CKEDITOR.htmlParser.fragment = function()
                                        return;\r
                        }\r
 \r
+                       sendPendingBRs();\r
                        checkPending();\r
 \r
-                       if ( fixForBody && !currentNode.type )\r
-                               this.onTagOpen( fixForBody, {} );\r
+                       if ( fixForBody\r
+                                && ( !currentNode.type || currentNode.name == 'body' )\r
+                                && CKEDITOR.tools.trim( text ) )\r
+                       {\r
+                               this.onTagOpen( fixForBody, {}, 0, 1 );\r
+                       }\r
 \r
                        // Shrinking consequential spaces into one single for all elements\r
                        // text contents.\r
@@ -346,28 +398,20 @@ CKEDITOR.htmlParser.fragment = function()
 \r
                parser.onComment = function( comment )\r
                {\r
+                       sendPendingBRs();\r
+                       checkPending();\r
                        currentNode.add( new CKEDITOR.htmlParser.comment( comment ) );\r
                };\r
 \r
                // Parse it.\r
                parser.parse( fragmentHtml );\r
 \r
-               // Close all pending nodes.\r
-               while ( currentNode.type )\r
-               {\r
-                       var parent = currentNode.parent,\r
-                               node = currentNode;\r
+               // Send all pending BRs except one, which we consider a unwanted bogus. (#5293)\r
+               sendPendingBRs( !CKEDITOR.env.ie && 1 );\r
 \r
-                       if ( fixForBody && !parent.type && !CKEDITOR.dtd.$body[ node.name ] )\r
-                       {\r
-                               currentNode = parent;\r
-                               parser.onTagOpen( fixForBody, {} );\r
-                               parent = currentNode;\r
-                       }\r
-\r
-                       parent.add( node );\r
-                       currentNode = parent;\r
-               }\r
+               // Close all pending nodes, make sure return point is properly restored.\r
+               while ( currentNode != fragment )\r
+                       addElement( currentNode, currentNode.parent, 1 );\r
 \r
                return fragment;\r
        };\r
@@ -380,13 +424,14 @@ CKEDITOR.htmlParser.fragment = function()
                 *              following types: {@link CKEDITOR.htmlParser.element},\r
                 *              {@link CKEDITOR.htmlParser.text} and\r
                 *              {@link CKEDITOR.htmlParser.comment}.\r
+                *      @param {Number} [index] From where the insertion happens.\r
                 * @example\r
                 */\r
-               add : function( node )\r
+               add : function( node, index )\r
                {\r
-                       var len = this.children.length,\r
-                               previous = len > 0 && this.children[ len - 1 ] || null;\r
+                       isNaN( index ) && ( index = this.children.length );\r
 \r
+                       var previous = index > 0 ? this.children[ index - 1 ] : null;\r
                        if ( previous )\r
                        {\r
                                // If the block to be appended is following text, trim spaces at\r
@@ -411,7 +456,7 @@ CKEDITOR.htmlParser.fragment = function()
                        node.previous = previous;\r
                        node.parent = this;\r
 \r
-                       this.children.push( node );\r
+                       this.children.splice( index, 0, node );\r
 \r
                        this._.hasInlineStarted = node.type == CKEDITOR.NODE_TEXT || ( node.type == CKEDITOR.NODE_ELEMENT && !node._.isBlockLike );\r
                },\r
@@ -427,7 +472,25 @@ CKEDITOR.htmlParser.fragment = function()
                 */\r
                writeHtml : function( writer, filter )\r
                {\r
-                       for ( var i = 0, len = this.children.length ; i < len ; i++ )\r
+                       var isChildrenFiltered;\r
+                       this.filterChildren = function()\r
+                       {\r
+                               var writer = new CKEDITOR.htmlParser.basicWriter();\r
+                               this.writeChildrenHtml.call( this, writer, filter, true );\r
+                               var html = writer.getHtml();\r
+                               this.children = new CKEDITOR.htmlParser.fragment.fromHtml( html ).children;\r
+                               isChildrenFiltered = 1;\r
+                       };\r
+\r
+                       // Filtering the root fragment before anything else.\r
+                       !this.name && filter && filter.onFragment( this );\r
+\r
+                       this.writeChildrenHtml( writer, isChildrenFiltered ? null : filter );\r
+               },\r
+\r
+               writeChildrenHtml : function( writer, filter )\r
+               {\r
+                       for ( var i = 0 ; i < this.children.length ; i++ )\r
                                this.children[i].writeHtml( writer, filter );\r
                }\r
        };\r