JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.6.1
[ckeditor.git] / _source / core / htmlparser / fragment.js
index 244e298..bfa5cc5 100644 (file)
@@ -1,5 +1,5 @@
 /*\r
-Copyright (c) 2003-2010, 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,38 +37,38 @@ 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
-               listBlocks = CKEDITOR.dtd.$list, listItems = CKEDITOR.dtd.$listItem;\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
@@ -114,36 +114,48 @@ CKEDITOR.htmlParser.fragment = function()
                                currentNode.add( pendingBRs.shift() );\r
                }\r
 \r
-               function addElement( element, target, enforceCurrent )\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\r
-                                               && !( elementName in CKEDITOR.dtd.$body )\r
-                                               && !( elementName in CKEDITOR.dtd.$nonBodyContent )  )\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
@@ -171,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
@@ -182,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
@@ -202,83 +219,77 @@ CKEDITOR.htmlParser.fragment = function()
                                return;\r
                        }\r
 \r
-                       var currentName = currentNode.name;\r
-\r
-                       var currentDtd = currentName\r
-                               && ( CKEDITOR.dtd[ currentName ]\r
-                                       || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) );\r
-\r
-                       // If the element cannot be child of the current element.\r
-                       if ( currentDtd   // Fragment could receive any elements.\r
-                                && !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] )\r
+                       while( 1 )\r
                        {\r
+                               var currentName = currentNode.name;\r
 \r
-                               var reApply = false,\r
-                                       addPoint;   // New position to start adding nodes.\r
+                               var currentDtd = currentName ? ( CKEDITOR.dtd[ currentName ]\r
+                                               || ( currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span ) )\r
+                                               : rootDtd;\r
 \r
-                               // Fixing malformed nested lists by moving it into a previous list item. (#3828)\r
-                               if ( tagName in listBlocks\r
-                                       && currentName in listBlocks )\r
+                               // If the element cannot be child of the current element.\r
+                               if ( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] )\r
                                {\r
-                                       var children = currentNode.children,\r
-                                               lastChild = children[ children.length - 1 ];\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
+                                               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 in listItems ) )\r
-                                               addElement( ( lastChild = new CKEDITOR.htmlParser.element( 'li' ) ), currentNode );\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
-                                       returnPoint = currentNode, addPoint = lastChild;\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
-                               else if ( tagName == currentName )\r
-                               {\r
-                                       addElement( currentNode, currentNode.parent );\r
-                               }\r
-                               else\r
-                               {\r
-                                       if ( nonBreakingBlocks[ currentName ] )\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
-                                               if ( !returnPoint )\r
-                                                       returnPoint = currentNode;\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
-                               if ( addPoint )\r
-                                       currentNode = addPoint;\r
-                               // Try adding it to the return point, or the parent element.\r
                                else\r
-                                       currentNode = currentNode.returnPoint || currentNode.parent;\r
-\r
-                               if ( reApply )\r
-                               {\r
-                                       parser.onTagOpen.apply( this, arguments );\r
-                                       return;\r
-                               }\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
@@ -303,7 +314,7 @@ CKEDITOR.htmlParser.fragment = function()
                                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, if we're\r
                                // really closing one of the parents element later, they will continue\r
@@ -316,10 +327,11 @@ CKEDITOR.htmlParser.fragment = function()
                                // 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 ( i = 0 ; i < pendingAdd.length ; i++ )\r
@@ -352,8 +364,8 @@ CKEDITOR.htmlParser.fragment = function()
 \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
@@ -368,7 +380,7 @@ CKEDITOR.htmlParser.fragment = function()
                                 && ( !currentNode.type || currentNode.name == 'body' )\r
                                 && CKEDITOR.tools.trim( text ) )\r
                        {\r
-                               this.onTagOpen( fixForBody, {} );\r
+                               this.onTagOpen( fixForBody, {}, 0, 1 );\r
                        }\r
 \r
                        // Shrinking consequential spaces into one single for all elements\r
@@ -386,32 +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
-               sendPendingBRs();\r
+               // Send all pending BRs except one, which we consider a unwanted bogus. (#5293)\r
+               sendPendingBRs( !CKEDITOR.env.ie && 1 );\r
 \r
-               // Close all pending nodes.\r
-               while ( currentNode.type )\r
-               {\r
-                       var parent = currentNode.parent,\r
-                               node = currentNode;\r
-\r
-                       if ( fixForBody\r
-                                && ( !parent.type || parent.name == 'body' )\r
-                                && !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
@@ -424,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
@@ -455,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