home *** CD-ROM | disk | FTP | other *** search
/ .net 1999 December / netCD65.iso / pc / Software / Top20 / DreamWeaver / PC / data1.cab / Program_Files / Configuration / Commands / Clean Up HTML.js < prev    next >
Encoding:
JavaScript  |  1998-11-30  |  35.8 KB  |  1,064 lines

  1. //
  2. // Copyright 1998 Macromedia, Inc. All rights reserved. 
  3. // ----------------------------------------------------
  4. //
  5. // Clean Up HTML.js
  6. //
  7. // This command cleans up certain categories of superfluous
  8. // HTML within the user document without effecting document
  9. // layout.  This command makes two passes over the users 
  10. // document depending on the options selected; see 
  11. // cleanUpDocument() in Clean Up HTML.js for more details.
  12. //
  13. // ----------------------------------------------------
  14.  
  15. //
  16. // Global variables -- see initialize() for more comments.  
  17. // Dreamweaver doesn't currently initialize any globals 
  18. // loaded in auxilliary scripts through <SCRIPT SRC=..>, 
  19. // so we explicitly initialize these initialize().
  20. //
  21. var cbRemoveEmptyTags;
  22. var cbRemoveRedundant;
  23. var cbCombineFonts;
  24. var cbRemoveTags;
  25. var cbRemoveComments;
  26. var cbRemoveDWComments;
  27. var cbShowLog;
  28. var tbTagsToRemove;
  29. var numEmptyRemoved;
  30. var numRedundantRemoved;
  31. var numTagsRemoved;
  32. var numCommentsRemoved;
  33. var numFontsCombined;
  34. var arrTagsToRemove;
  35. var strClassAttrib;
  36. var strStyleAttrib;
  37. var arrDWCommentTags;
  38. var emptyRemovalCandidates;
  39. var redundantTagCandidates;
  40. var combinableTagCandidates;
  41. var bPreserveEmptyHeader;
  42. var bRemovedTracing;
  43.                                           
  44. // ----------- Object prototype extension ------------
  45.  
  46. function arrayContains( item )
  47. {
  48.    var nElements = this.length;
  49.    for( var i = 0; i < nElements; i++ )
  50.       if ( this[i] == item )
  51.          return true;
  52.  
  53.    return false;   
  54. }
  55.  
  56. Array.prototype.contains = arrayContains;
  57.    
  58. //   
  59. // ------- Local Clean Up command functions ----------
  60. //
  61.  
  62. function isQuote( c )
  63. {
  64.    return( c == '\"' || c == '\'' );
  65. }
  66.  
  67. function isAlpha( c )
  68. {
  69.    var isoval = c.charCodeAt(0);
  70.    return( (isoval >= "A".charCodeAt(0) && isoval <= "Z".charCodeAt(0)) ||
  71.            (isoval >= "a".charCodeAt(0) && isoval <= "z".charCodeAt(0)));
  72. }              
  73.  
  74. // Match a <xxx or </xxx tag; note that xxx must be alphabetical
  75. function isTagBegin( currentchar, nextchar )
  76. {
  77.    return( currentchar == '<' && (isAlpha( nextchar ) || nextchar == '/') );
  78. }
  79.  
  80. // Note that '>' should be ignored within quotes inside tag brackets
  81. function isTagEnd( c )
  82. {
  83.    return( c == '>' );
  84. }
  85.  
  86. function isWhite( c )
  87. {
  88.    return( c == ' ' || c == '\t' || c == '\n' || c == '\r' );
  89. }
  90.  
  91. function isAllWhite( str )
  92. {
  93.    for( var i = 0; i < str.length; i++ )
  94.    {
  95.       if ( !isWhite( str.charAt( i ) ) )
  96.          return( false );
  97.    }
  98.    
  99.    return( true ); 
  100. }
  101.  
  102. // parseAttributes()
  103. //
  104. // Parse the attributes from within the start tag of a given
  105. // node per the rules found here: http://www.w3.org/TR/WD-html-lex/
  106. //    
  107. // Return an array of arrays (unfortunately; the associative 
  108. // aspect of arrays is overloaded with "instance properties",
  109. // so arrays already contain prototype methods/properties
  110. // value pairs):
  111. //
  112. //          arr[0] --> attributes array
  113. //          arr[1] --> values array
  114. //
  115. // The value for a given attribute are in the same position
  116. // at the attribute within the values array.  Singleton name
  117. // tokens have an empty ("") or undefined value.
  118. // 
  119. // If bStripQuotes is true, then any "outer" quotes around an
  120. // attribute value are stripped, e.g., the value in
  121. //
  122. //    NAME="bob's name"
  123. //
  124. // is returned as: bob's name.  If bStripQuotes is false, that 
  125. // value is returned as "bob's name"
  126. //
  127. // If bMakeUpper is true, all attribute/value strings are normalized
  128. // to upper case
  129. //
  130. function parseAttributes( node, bStripQuotes, bMakeUpper )
  131. {
  132.    var   tagstr         = node.outerHTML;
  133.    var   pos            = 0;
  134.    var   prevChar       = null;
  135.    var   currentChar    = null;
  136.    var   currentQuote   = null;
  137.    var   arrAttribs     = new Array();
  138.    var   arrValues      = new Array();
  139.    var   arrIdx         = 0;
  140.    var   attrib         = "";
  141.    var   value          = "";
  142.    var   bValueIsEmpty  = false;
  143.    var   bInsideQuote   = false;
  144.    var   bAccumValue    = false;
  145.    var   bAttribReady   = false;
  146.    var   bSkipToWhite   = true;  // initially true to skip "<tag "
  147.    
  148.    while( pos < node.outerHTML.length )
  149.    {
  150.       prevChar     = currentChar;
  151.       currentChar  = tagstr.charAt( pos++ );         
  152.     
  153.       // Handle quote state; remember actual quote that 
  154.       // flipped the state so we match ' and " right
  155.       //                                     
  156.       if ( isQuote( currentChar ) )
  157.       {
  158.          if ( bInsideQuote )
  159.          {
  160.             if ( currentChar == currentQuote )
  161.             {
  162.                // Coming out of quoted region; turn quotes off
  163.                bInsideQuote = false;               
  164.                currentQuote = null;
  165.                if ( bStripQuotes )
  166.                {
  167.                   // Careful; make sure ATTR="" works even when we're
  168.                   // stripping quotes off values
  169.                   MM_assert( bAccumValue, MSG_ParseErrEndQuote );
  170.                   bValueIsEmpty = true;                     
  171.                   continue;
  172.                }
  173.             }                           
  174.          }
  175.          else
  176.          if ( bAccumValue && value == "" ) // only turn quotes on after '=' and
  177.          {                                 // before accumulating anything; e.g.,
  178.             // Turn quotes on              // ignore the quote in ATTR=xxx"xxx
  179.             bInsideQuote = true;
  180.             currentQuote = currentChar;
  181.             if ( bStripQuotes )
  182.                continue;
  183.          }
  184.       }
  185.       
  186.       // Handle the terminating character; write any attribute/value
  187.       // we may have been accumulating and we're done.
  188.       //
  189.       if ( !bInsideQuote && isTagEnd( currentChar ) )
  190.       {
  191.          if ( attrib != "" )
  192.          {         
  193.             arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  194.             arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  195.             attrib = "";
  196.             value  = "";
  197.             bAttribReady = false;
  198.             bAccumValue  = false;      
  199.          }
  200.          break; 
  201.       }               
  202.       
  203.       // Accumulate characters; if bAccumValue is true, we're on the
  204.       // right side of an "=", otherwise we're on the left side or accumulating
  205.       // a singleton name token.  I don't think quoted regions make sense 
  206.       // on the left side either.
  207.       //                                   
  208.       if ( !bInsideQuote && !bAccumValue )
  209.       {
  210.          // first skip to white after tag name <xxxx 
  211.          if ( bSkipToWhite && !isWhite( currentChar ) )
  212.             continue;
  213.             
  214.          bSkipToWhite = false;              
  215.          
  216.          // Whitespace not inside quotes; if we're accumulating
  217.          // an attribute, it's ready (the whitespace terminates it);
  218.          if ( isWhite( currentChar ) )
  219.          {
  220.             bAttribReady = attrib != "";
  221.          }
  222.          else
  223.          {
  224.             // Non-white space; if we have an equals sign, switch
  225.             // over to accumulate the value
  226.             if ( currentChar == '=' )
  227.             {
  228.                bAttribReady = attrib != "";
  229.                bAccumValue  = true;
  230.                MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  231.             }
  232.             else
  233.             {
  234.                // Unquoted non-white non-value -- accumulate
  235.                // as name token.  If there's a name token ready, 
  236.                // save it as a singleton first.
  237.                //
  238.                if ( bAttribReady )
  239.                {
  240.                   arrAttribs[ arrIdx++ ] = bMakeUpper ? attrib.toUpperCase() : attrib;
  241.                   attrib = "";
  242.                   bAttribReady = false;                  
  243.                }
  244.             
  245.                attrib += currentChar;
  246.             }
  247.          }
  248.       }
  249.       else
  250.       {
  251.          // We're accumulating a value
  252.          //
  253.          MM_assert( bAttribReady, MSG_ParseErrUnexpectedEQU );
  254.          
  255.          if ( !bInsideQuote && isWhite( currentChar ) )
  256.          {
  257.             // Swallow whitespace until we either get a value
  258.             // or we terminate
  259.             
  260.             if ( value != "" || bValueIsEmpty )
  261.             {
  262.                arrAttribs[ arrIdx ]  = bMakeUpper ? attrib.toUpperCase() : attrib;
  263.                arrValues[ arrIdx++ ] = bMakeUpper ? value.toUpperCase() : value;
  264.                attrib = "";
  265.                value  = "";
  266.                bAttribReady  = false;
  267.                bAccumValue   = false;      
  268.                bValueIsEmpty = false;
  269.             }
  270.          }            
  271.          else
  272.          {
  273.             // We're inside a quote, or we're not terminated -- keep
  274.             // accumulating
  275.             //            
  276.             value += currentChar;
  277.          }
  278.       }
  279.    }
  280.    
  281.    // We're done; package up our arrays and return them
  282.    //
  283.    MM_assert( !bAccumValue, MSG_ParseErrValue );
  284.    return new Array( arrAttribs, arrValues );
  285. }
  286.  
  287. // findCombinableParent()
  288. //
  289. // Return a parent node with which the given node may have
  290. // its attributes combined with.  This routine trusts that
  291. // caller has verified that the combineTagName is a member
  292. // of combinableTagCandidates!  A combinable parent is 
  293. // a direct parent up the tree who is the parent of no
  294. // other children (which would not want to inherit the
  295. // characteristics of the given child whose attributes
  296. // will migrate up), e.g.:
  297. //
  298. // <FONT face="arial"><FONT color="blue">text</FONT></FONT>
  299. // 
  300. // and
  301. //
  302. // <FONT face="arial"><B><FONT color="blue">text</FONT></B></FONT>
  303. //
  304. // are combinable, but 
  305. //
  306. // <FONT face="arial"><B>x<FONT color"blue">text</FONT></B></FONT>
  307. //
  308. // is not as the 'x' textual child should not inherit the blue 
  309. // characteristic.  This routine recursively calls itself to 
  310. // walk the "direct" (childNodes.length == 1) parent chain.
  311. // 
  312. function findCombinableParent( node, combineTagName )
  313. {
  314.    MM_assert( combinableTagCandidates.contains( combineTagName ) );
  315.    
  316.    if ( (node.parentNode != null)    &&
  317.         (node.parentNode.childNodes.length == 1) )
  318.    {
  319.       if ( combineTagName == node.parentNode.tagName )
  320.          return( node.parentNode );
  321.          
  322.       if ( node.parentNode.innerHTML == node.outerHTML ) // parent contains only this child tree
  323.          return( findCombinableParent( node.parentNode, combineTagName ) );  
  324.    }
  325.  
  326.    return null;   
  327. }
  328.  
  329. // hasRedundantParent()
  330. //
  331. // Return true if the given node is redundant with a
  332. // controlling parent.  Redundant parent/children must
  333. // have identical attribute/value sets.
  334. //   
  335. function hasRedundantParent( node )
  336. {
  337.    var rc = false;
  338.    
  339.    if ( redundantTagCandidates.contains( node.tagName ) )
  340.    {      
  341.       var parent  = node.parentNode;
  342.       
  343.       // Find controlling parent
  344.       while( parent != null )
  345.       {
  346.          if ( node.tagName == parent.tagName )
  347.          {
  348.             // Compare parent and child attribute name/value pairs
  349.             var cArrs   = parseAttributes( node, true, true );
  350.             var pArrs   = parseAttributes( parent, true, true );
  351.             var cNames  = cArrs[0];
  352.             var cValues = cArrs[1];
  353.             var pNames  = pArrs[0];
  354.             var pValues = pArrs[1];
  355.  
  356.             if ( cNames.length == pNames.length && cValues.length == pValues.length )
  357.             {
  358.                cNames.sort();
  359.                pNames.sort();
  360.                cValues.sort();
  361.                pValues.sort();
  362.                
  363.                var len = cNames.length;
  364.                for( var i = 0; i < len; i++ )
  365.                {
  366.                   // note in js: undefined == undefined is true
  367.                   if ( pNames[i]  != cNames[i] || cValues[i] != pValues[i] )
  368.                      break;                          
  369.                }
  370.                
  371.                rc = (i == len); // if we got through everything they're the same
  372.             }
  373.             
  374.             if ( rc )  // if we're redundant, we're done
  375.                break;
  376.             
  377.             // Otherwise, if we're not actually overriding anything on this
  378.             // parent, we may still be redundant with an uber parent.  Cycle through
  379.             // the child's attributes and if none are present on parent keep going
  380.             //
  381.             var bKeepGoing = true;
  382.             for( var i = 0; i < cNames.length; i++ )
  383.             {
  384.                if ( pNames.contains( cNames[i] ) )
  385.                {
  386.                   bKeepGoing = false;
  387.                   break;
  388.                }
  389.             }            
  390.             
  391.             if ( !bKeepGoing )
  392.                break;            
  393.          }
  394.       
  395.          parent = parent.parentNode;
  396.       }
  397.    }
  398.  
  399.    return rc;
  400. }
  401.  
  402. // isAllWhiteNodeSignificant()
  403. //
  404. // Given a node whose inner html is all white, this
  405. // routine examines the node's siblings and returns 
  406. // true if the whitespace is significant and false
  407. // otherwise.
  408. //
  409. function isAllWhiteNodeSignificant( node )
  410. {
  411.    var siblings   = node.parentNode.childNodes;
  412.    var nSiblings  = siblings.length;
  413.    var siblingIdx = 0;
  414.    
  415.    // If we're an only child, then we really need
  416.    // to look at uncles and aunts.  
  417.    if ( (nSiblings == 1) && (node.parentNode != null) && (node.parentNode.nodeType != Node.DOCUMENT_NODE) )
  418.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  419.    
  420.    // Find self as parent's child first      
  421.    for( ; siblingIdx < nSiblings; siblingIdx++ )
  422.       if ( siblings.item( siblingIdx ) == node )
  423.          break;
  424.    
  425.    MM_assert( siblingIdx < nSiblings, MSG_ErrParentChild );
  426.  
  427.    // If sibling to the left has trailing whitespace, 
  428.    // our current all white node isn't significant.  Note
  429.    // we can just look to our immediate left rather than go
  430.    // to zero because any empty siblings to the left will
  431.    // have already been gobbled.
  432.    //
  433.    var lSibling = siblingIdx > 0 ? siblings.item( siblingIdx - 1) : null;
  434.    if ( lSibling != null )
  435.    {
  436.       if ( lSibling.nodeType == Node.TEXT_NODE )
  437.       {
  438.          if ( (lSibling.data.length > 0) &&
  439.               isWhite( lSibling.data[ lSibling.data.length - 1 ] ) )
  440.             return false;
  441.       }   
  442.       else
  443.       if ( lSibling.nodeType == Node.ELEMENT_NODE )
  444.       {
  445.          // non text left sibling
  446.          if ( (lSibling.innerHTML.length > 0) &&
  447.               isWhite( lSibling.innerHTML[ lSibling.innerHTML.length - 1 ] ) )
  448.             return false;
  449.       }
  450.       // else go on to our right to determine our significance
  451.    }
  452.       
  453.    // Now see if there's significant leading whitespace to 
  454.    // the immediate right that might render our all white 
  455.    // current node insignificant
  456.    //
  457.    var rSibling = null;
  458.    siblingIdx++;
  459.    while( siblingIdx < nSiblings )
  460.    {
  461.       rSibling = siblings.item( siblingIdx );
  462.       
  463.       if ( rSibling.nodeType == Node.TEXT_NODE )
  464.       {
  465.          // We have a textual sibling to the right; if
  466.          // this guy doesn't have leading whitespace, 
  467.          // we're significant, otherwise we're not.
  468.          if ( rSibling.data.length > 0 )
  469.             return( !isWhite( rSibling.data[0] ) );
  470.             
  471.          // else empty text node
  472.       }
  473.       else
  474.       if ( rSibling.nodeType == Node.ELEMENT_NODE )
  475.       {
  476.          // We have a non-empty non-text node to the
  477.          // right; if this guy doesn't have leading
  478.          // whitespace we're significant, otherwise not
  479.          if ( rSibling.innerHTML.length > 0 ) 
  480.             return( !isWhite( rSibling.innerHTML[0] ) );
  481.             
  482.          // else empty non-text node...
  483.       }
  484.          
  485.       siblingIdx++;
  486.    }   
  487.    
  488.    // If we got here there's nothing interesting to the
  489.    // right of this all white node, so it's as if we're
  490.    // an only child.  The DOCUMENT_NODE check is just for
  491.    // safety; there shouldn't be a way to get that high on
  492.    // empty markup node removal...
  493.    
  494.    if ( node.parentNode != null && node.nodeType != Node.DOCUMENT_NODE )
  495.       return( isAllWhiteNodeSignificant( node.parentNode ) );
  496.    
  497.    // otherwise nothing left -- we really are insignificant...
  498.    return false;         
  499. }
  500.  
  501. // isRemovableEmptyTag()
  502. //
  503. // Return true if this tag can be safely removed from the
  504. // document, false otherwise.
  505. //
  506. function isRemovableEmptyTag( tagNode )
  507. {
  508.    // First this tag must be an empty removal candidate with no class info
  509.    //
  510.    if ( emptyRemovalCandidates.contains( tagNode.tagName ) && !hasClassAttribute( tagNode ) )
  511.    {
  512.       // Short-circuit for named anchor tags; empty named anchors
  513.       // should be left alone
  514.       if ( "A" == tagNode.tagName && (null != tagNode.getAttribute( "NAME" )) )
  515.          return false;
  516.       
  517.       // If the innerHTML length is zero, it's empty and
  518.       // can be safely removed *unless* it's a heading
  519.       // tag -- the first empty heading tag after text 
  520.       // forces a carriage return.  
  521.       //      
  522.       if ( tagNode.innerHTML.length == 0 )
  523.       {
  524.          if ( ('H' == tagNode.tagName.charAt( 0 )) && bPreserveEmptyHeader )
  525.          {
  526.             // Preserve this empty header but gobble the next one we hit
  527.             bPreserveEmptyHeader = false;
  528.             return false;
  529.          }
  530.          
  531.          return true;
  532.       }
  533.       else
  534.       if ( isAllWhite( tagNode.innerHTML ) && !isAllWhiteNodeSignificant( tagNode ) )
  535.       {
  536.          // All empty tag candidates (generally character markup)
  537.          // spanning only whitespace can also be removed if the 
  538.          // tag is not within text, or if the tag to the right of
  539.          // text that ends in whitespace or to the left of text
  540.          // that begins with whitespace....
  541.          return true;
  542.       }
  543.    }
  544.  
  545.    return false;              
  546. }
  547.  
  548. // Using a tracing image in Dreamweaver attaches up to four proprietary
  549. // attributes to the body tag. We want to remove these attributes if Remove
  550. // Dreamweaver Comments is checked.
  551. //
  552. function removeTracingAttrs()
  553. {
  554.   var bodyNode = dreamweaver.getDocumentDOM("document").body;
  555.     
  556.   //look for tracing attributes - if any are found, toggle
  557.   //the global boolean to true and remove all attributes   
  558.   if (cbRemoveDWComments.checked){
  559.     if (bodyNode.getAttribute("tracingsrc") ||
  560.         bodyNode.getAttribute("tracingopacity") ||
  561.         bodyNode.getAttribute("tracingx") ||
  562.         bodyNode.getAttribute("tracingy"))
  563.    {
  564.      //remove all tracing image attributes  
  565.      bodyNode.removeAttribute("tracingsrc");
  566.      bodyNode.removeAttribute("tracingopacity");
  567.      bodyNode.removeAttribute("tracingx");
  568.      bodyNode.removeAttribute("tracingy");
  569.      bRemovedTracing=true;
  570.    }
  571.   }   
  572. }
  573.  
  574. // hasStyleAttribute()
  575. //
  576. // Return true if the given ELEMENT tag has a STYLE set
  577. //   
  578. function hasStyleAttribute( tagNode )
  579. {
  580.    return( tagNode.getAttribute( strStyleAttrib ) != null );
  581. }
  582.  
  583. // hasClassAttribute()
  584. //
  585. // Return true if the given ELEMENT tag has a CLASSID set
  586. //
  587. function hasClassAttribute( tagNode )
  588. {
  589.    return( tagNode.getAttribute( strClassAttrib ) != null );
  590. }
  591.  
  592. // loadNDWCommentOffsets()
  593. //
  594. // This callback is used by the comment removal traversal 
  595. // to push offsets of non-Dreamweaver comment nodes into
  596. // the userData variable passed by the comment removal pass
  597. //
  598. function loadNDWCommentOffsets( commentNode, userData )
  599. {
  600.    // MM_note( "Processing NDW comment:" + commentNode.data );
  601.    
  602.    // Server-side include comments of the form "<!-- #include... -->"
  603.    // should always be left alone!
  604.  
  605.    // eat up any leading white in comment data   
  606.    var i;
  607.    for( i = 0; i < commentNode.data.length; i++ )
  608.       if ( !isWhite( commentNode.data.charAt( i ) ) )
  609.          break;
  610.  
  611.    // if we have a #include skip it, otherwise push offsets for
  612.    // removal
  613.    //   
  614.    var bSkipComment = commentNode.data.substr( i, 8 ).toLowerCase() == "#include";
  615.    
  616.    if ( !bSkipComment )
  617.       userData.push( dreamweaver.nodeToOffsets( commentNode ) );               
  618.       
  619.    return true;   
  620. }
  621.  
  622. // processElement()
  623. //
  624. // Process a node of ELEMENT type within the user's document
  625. // This is a callback from traverse() used during the main 
  626. // removal traversal.
  627. //
  628. function processElement( elementNode )
  629. {
  630.    // MM_note( "Processing element: " + elementNode.tagName );
  631.    // Remove specific tag(s) check
  632.    //
  633.    if ( cbRemoveTags.checked &&
  634.         arrTagsToRemove.contains( elementNode.tagName ) )
  635.    {
  636.       // MM_note( "* Removing specified tag " + elementNode.outerHTML );
  637.       if ( elementNode.outerHTML == elementNode.innerHTML )
  638.          elementNode.outerHTML = "";
  639.       else         
  640.          elementNode.outerHTML = elementNode.innerHTML;      
  641.          
  642.       numTagsRemoved++;
  643.    }           
  644.    else
  645.    {
  646.       // Don't touch tags with style information
  647.       //
  648.       if ( !hasStyleAttribute( elementNode ) ) 
  649.       {
  650.          // Empty tag check
  651.          //
  652.          if ( cbRemoveEmptyTags.checked && isRemovableEmptyTag( elementNode ) )
  653.          {
  654.             var parent = elementNode.parentNode;
  655.             
  656.             // MM_note( "* Removing empty tag: " + elementNode.outerHTML );
  657.             elementNode.outerHTML = "";
  658.             numEmptyRemoved++;
  659.  
  660.             // Small work around DW behavior -- paragraph tags with 
  661.             // children are considered "not collapsable" even if the 
  662.             // children are empty.  When we remove all empty children 
  663.             // of a p tag then, DW sticks in a   to keep the 
  664.             // remaining <p> from being collapsed -- this makes the <p> 
  665.             // then come alive in the browser layout.  So if we've just 
  666.             // zapped the last child of a p tag, rewrite the P tag without 
  667.             // the   so it remains collapsed in the browser layout.  
  668.             // Note that if the p tag originally had text or an  
  669.             // it would still have textual children after the empty tag 
  670.             // removal and would be untouched.
  671.             //
  672.             if ( parent.tagName == "P" && !(parent.hasChildNodes()) )
  673.                parent.outerHTML = "<p>";
  674.          }
  675.          // Redundant child check
  676.          //
  677.          else
  678.          if ( cbRemoveRedundant.checked &&
  679.               hasRedundantParent( elementNode ) )
  680.          {
  681.             // MM_note( "* Removing redundant tag: " + elementNode.outerHTML );
  682.             elementNode.outerHTML = elementNode.innerHTML;
  683.             numRedundantRemoved++;
  684.          }
  685.          // Child/parent coalesce check
  686.          //
  687.          else
  688.          if ( cbCombineFonts.checked &&
  689.               combinableTagCandidates.contains( elementNode.tagName ) )
  690.          {
  691.             var parent  = findCombinableParent( elementNode, elementNode.tagName );
  692.             if ( parent != null )
  693.             {
  694.                // MM_note( "* Combining font tags: " + elementNode.outerHTML );
  695.                
  696.                // Set all child attributes on parent and remove child
  697.                //
  698.                var arrs    = parseAttributes( elementNode, true, false );
  699.                var attribs = arrs[0];
  700.                var values  = arrs[1];
  701.                
  702.                for( var i = 0; i < attribs.length; i++ )
  703.                   parent.setAttribute( attribs[i], values[i] ); // The value part 
  704.                                                                 // here may be null
  705.                elementNode.outerHTML = elementNode.innerHTML;
  706.                numFontsCombined++;
  707.             }
  708.          }
  709.          // Dreamweaver comment check -- dreamweaver comments
  710.          // come back to us as element nodes rather than comment nodes
  711.          else
  712.          if ( cbRemoveDWComments.checked && 
  713.               arrDWCommentTags.contains( elementNode.tagName ) )
  714.          {
  715.             // MM_note( "Removing DW comment: " + elementNode.tagName );
  716.             dreamweaver.editLockedRegions(true);
  717.             elementNode.outerHTML = elementNode.innerHTML;
  718.             numCommentsRemoved++;
  719.          }
  720.       }
  721.    }
  722.    
  723.    return true; // continue traverse
  724. }
  725.  
  726. // emptyHeaderStateTextHandler()
  727. //
  728. // This text node callback is used by pass two to flip
  729. // the global bPreserveEmptyHeader state to true -- we
  730. // just encountered text, so the next empty header 
  731. // found will force a carriage return and thus can't
  732. // be removed.  Empty headers after that however can
  733. // be removed until the next piece of text is encountered...
  734. //
  735. function emptyHeaderStateTextHandler( node )
  736. {
  737.    bPreserveEmptyHeader = true;
  738.    return true;
  739. }
  740.  
  741. // traverse()
  742. //
  743. // Do a recursive depth-first traversal of the user's 
  744. // document starting from the given node.  
  745. //
  746. // Callers provide up to three callback functions which
  747. // accept a node argument, one each (or the same one)
  748. // to process nodes of ELEMENT, TEXT, or COMMENT type.
  749. // At least one callback function is required.
  750. //
  751. // The handlers may stop the traversal by returning false;
  752. // returning true will continue the traversal to its 
  753. // completion.
  754. //
  755. // A fourth argument, a handle to a some user variable to
  756. // be passed on to each callback, may also be provided.
  757. //
  758. function traverse( node, fElementHandler ) // optional: fTextHandler, fCommentHandler, userData )
  759. {
  760.    var fTextHandler  = traverse.arguments.length >= 3 ? traverse.arguments[2] : null;  
  761.    var fCmmtHandler  = traverse.arguments.length >= 4 ? traverse.arguments[3] : null;
  762.    var userData      = traverse.arguments.length >= 5 ? traverse.arguments[4] : null;
  763.    var children      = node.childNodes;
  764.    var nChildren     = children.length;
  765.    var bContinue     = true;
  766.    var current       = null;
  767.    
  768.    for( var i = 0; bContinue && (i < nChildren); i++ )
  769.    {
  770.       current = children.item( i );
  771.    
  772.       // descend to any children first
  773.       if ( current.hasChildNodes() )
  774.          traverse( current, fElementHandler, fTextHandler, fCmmtHandler, userData );
  775.  
  776.       // process current node
  777.       switch( current.nodeType )
  778.       {
  779.          case Node.ELEMENT_NODE:
  780.             if ( userData != null )
  781.                bContinue = fElementHandler( current, userData );
  782.             else
  783.                bContinue = fElementHandler( current );
  784.             break;
  785.                
  786.          case Node.COMMENT_NODE:
  787.             if ( fCmmtHandler != null )
  788.                if ( userData != null )
  789.                   bContinue = fCmmtHandler( current, userData );
  790.                else
  791.                   bContinue = fCmmtHandler( current );
  792.             break;
  793.             
  794.          case Node.TEXT_NODE:
  795.             if ( fTextHandler != null )
  796.                if ( userData != null )
  797.                   bContinue = fTextHandler( current, userData )
  798.                else
  799.                   bContinue = fTextHandler( current )
  800.             break;
  801.          
  802.          case Node.DOCUMENT_NODE:
  803.          default:
  804.              MM_error( MSG_UnknownNodeType, current.nodeType );
  805.       }
  806.    }
  807. }
  808.  
  809. // doPassOne()
  810. //
  811. // Pass one does cleanup based on the HTML source string for 
  812. // the user's document; currently that means comment and extra 
  813. // whitespace removal.
  814. //
  815. // BW 8/17/98 Removed "remove extra whitespace" option for 
  816. //            performance reasons
  817. //
  818. function doPassOne()
  819. {
  820.    if ( cbRemoveComments.checked )  // pass one options
  821.    {
  822.       var htmlstr = dreamweaver.getDocumentDOM( "document" ).documentElement.outerHTML;
  823.       var htmlpos = 0;                                    
  824.       var htmlarr = new Array(); // array to save newing of intermediate
  825.                                  // string copies of doc
  826.  
  827.       // To remove comments, traverse over the entire DOM gathering
  828.       // offsets into the HTML source of the comments to be removed, 
  829.       // then remove those comments from the HTML source string.
  830.       //
  831.       var root           = dreamweaver.getDocumentDOM("document");
  832.       var commentOffsets = new Array();
  833.       var stubCallback   = new Function( "node", "userData", "return true;" );
  834.  
  835.       if ( root != null && root.hasChildNodes() )
  836.          traverse( root
  837.                  , stubCallback 
  838.                  , stubCallback
  839.                  , loadNDWCommentOffsets
  840.                  , commentOffsets ); 
  841.                  
  842.       // Now use offsets to delete sections of text from
  843.       // within the document source string.  
  844.       //
  845.       if ( commentOffsets.length > 0 )
  846.       {                 
  847.          var lastpos = 0;
  848.          for( var i = 0; i < commentOffsets.length; i++ )
  849.          {
  850.             htmlarr[htmlpos++] = htmlstr.substring( lastpos
  851.                                                   , commentOffsets[i][0] );
  852.             lastpos = commentOffsets[i][1];
  853.             numCommentsRemoved++;
  854.          }
  855.          
  856.          htmlarr[htmlpos++] = htmlstr.substring( lastpos );
  857.       }
  858.  
  859.       if ( htmlarr.length > 0 )
  860.          dreamweaver.getDocumentDOM( "document" ).documentElement.outerHTML = htmlarr.join("");
  861.    }
  862. }
  863.  
  864. // doPassTwo()
  865. //
  866. // Pass two does cleanup on DOM objects as appropriate over the
  867. // course of traversing the DOM heirarchy.  The actual work in this 
  868. // pass is done in the processElement() callback.
  869. //
  870. function doPassTwo()
  871. {   
  872.    // Load up comma-separated list of tags to remove if any; warn
  873.    // if option is checked but no tags specified
  874.    //      
  875.    arrTagsToRemove = dreamweaver.getTokens( tbTagsToRemove.value.toUpperCase(), ", " );
  876.    if ( cbRemoveTags.checked && arrTagsToRemove.length == 0 )
  877.       MM_error( MSG_NoTagsToRemove ); 
  878.       
  879.    // Traverse document, processing leaves
  880.    //
  881.    var root = dreamweaver.getDocumentDOM("document");
  882.    
  883.    if ( root != null && root.hasChildNodes() )
  884.    {
  885.       traverse( root
  886.               , processElement 
  887.               , emptyHeaderStateTextHandler ); // no comment handler for this pass
  888.               
  889.       // and finally attempt to remove tracingsrc attributes
  890.       // in body tag 
  891.       //
  892.       removeTracingAttrs();
  893.    }        
  894.    else
  895.       MM_error( MSG_ErrEmptyDoc );
  896.    
  897. }
  898.  
  899. // cleanUpDocument()
  900. //
  901. // Main routine for performing clean up when user hits OK.
  902. // Clean up is done in three passes:
  903. //
  904. // Pass 1: Clean up certain items based on the entire HTML 
  905. //         document as a string
  906. // Pass 2: Clean up certain items while traversing the DOM
  907. //
  908. function cleanUpDocument()
  909. {
  910.    // Set up logging particulars
  911.    //
  912.    if ( cbShowLog.checked )
  913.    {  
  914.       MM_enableLogging();
  915.       MM_clearLog();
  916.    }
  917.    else
  918.       MM_disableLogging();
  919.  
  920.    // Do cleanup in two passes -- the first pass , the second pass 
  921.    // cleans up certain items based on a hierarchy traversal of the DOM.
  922.    //
  923.    doPassOne();
  924.    doPassTwo();
  925.    
  926.    finalize();         
  927. }
  928.  
  929. // initialize()
  930. //
  931. // This is called on BODY onLoad; initialize all script globals
  932. //
  933. function initialize()
  934. {
  935.    // Counters for logging output
  936.    //
  937.    numEmptyRemoved      = 0;
  938.    numRedundantRemoved  = 0;
  939.    numTagsRemoved       = 0;
  940.    numCommentsRemoved   = 0;
  941.    numFontsCombined     = 0;
  942.    bRemovedTracing      = false;
  943.  
  944.    arrTagsToRemove      = null;
  945.  
  946.    strClassAttrib       = "CLASS";
  947.    strStyleAttrib       = "STYLE";
  948.  
  949.    // The following tags represent the tag names of Dreamweaver-
  950.    // specific comments, which are processed through the Dreamweaver
  951.    // JS API/DOM as named element nodes rather than comment nodes
  952.    //
  953.    arrDWCommentTags     = new Array( "MM:EDITABLE"      
  954.                                    , "MM:LIBITEM"       // variable library item (currently unused)
  955.                                    , "MM:TEMPLATE"      
  956.                                    , "MM:UNLOCKATTRS"   // currently unused
  957.                                    , "{#CUSTOMOBJ}"
  958.                                    , "{#MEINLINE}"      // used by Japanese DW
  959.                                    , "{#LIBITEM}" );
  960.  
  961.    // The following tags can be harmlessly removed from the user's
  962.    // document if they're empty.  Note that the Heading tags are
  963.    // not always safe and require special further handling; see
  964.    // isEmptyRemoveableTag().
  965.    // 
  966.    emptyRemovalCandidates = new Array( "H1", "H2", "H3", "H4", "H5", "H6"
  967.                                       , "TT", "I", "B", "U", "STRIKE", "BIG"
  968.                                       , "SMALL", "SUB", "SUP", "EM", "STRONG"
  969.                                       , "DFN", "CODE", "SAMP", "KBD", "VAR"
  970.                                       , "CITE", "XMP", "BLINK"
  971.                                       , "ADDRESS"
  972.                                       , "A", "MAP"
  973.                                       // , "P"
  974.                                       , "PRE"
  975.                                       , "FONT"
  976.                                       , "SPAN"
  977.                                       , "TABLE"                                 
  978.                                       , "BLOCKQUOTE"
  979.                                       , "LI", "OL", "UL"
  980.                                       , "DD", "DT", "DL"
  981.                                       , "DIR", "MENU"
  982.                                       , "DIV", "CENTER" );
  983.                                  
  984.    // These tags can be safely removed if they're redundant
  985.    // with their immediate parent, i.e., this tags have
  986.    // no nesting semantics.
  987.    //
  988.    redundantTagCandidates = new Array( "TT", "I", "B", "U", "STRIKE", "BIG"
  989.                                      , "SMALL", "SUB", "SUP", "EM", "STRONG"
  990.                                      , "DFN", "CODE", "SAMP", "KBD", "VAR"
  991.                                      , "CITE", "XMP"
  992.                                      , "FONT"
  993.                                      , "CENTER"
  994.                                      , "SPAN" );
  995.  
  996.    // These tags can be safely coalesced with parents with identical
  997.    // regions of influence.  Currently this is only done for FONT tags.
  998.    //                                     
  999.    combinableTagCandidates = new Array( "FONT" );
  1000.    
  1001.  
  1002.    // Global used by pass two to indicate if the next empty 
  1003.    // header we encounter should be preserved -- the first
  1004.    // empty header after text is significant as a carriage
  1005.    // return is forced; after that they can be gobbled until
  1006.    // there's more text.
  1007.    //
  1008.    bPreserveEmptyHeader = false;
  1009.                                           
  1010.                                  
  1011.    // And finally reference actual form element names 
  1012.    // here once
  1013.    //
  1014.    with( document.optionsForm )
  1015.    {
  1016.       cbRemoveEmptyTags       = removeEmptyTags;
  1017.       cbRemoveRedundant       = removeRedundantChildren;
  1018.       cbRemoveComments        = removeNDWComments;
  1019.       cbRemoveDWComments      = removeDWComments;
  1020.       cbRemoveTags            = removeTag;
  1021.       cbCombineFonts          = combineFonts;
  1022.       cbShowLog               = showLog;
  1023.       tbTagsToRemove          = tagsToRemove;
  1024.    }
  1025. }
  1026.  
  1027. function finalize()
  1028. {
  1029.    // Show what we did if show log is enabled
  1030.    //
  1031.    if ( cbShowLog.checked )
  1032.    {
  1033.       MM_note( MSG_TrcSummaryHeader );
  1034.       
  1035.       var bDidSomething = (numEmptyRemoved > 0)      ||
  1036.                           (numRedundantRemoved > 0)  ||
  1037.                           (numTagsRemoved > 0)       ||
  1038.                           (numCommentsRemoved > 0)   ||
  1039.                           (numFontsCombined > 0)     ||
  1040.                           (bRemovedTracing);
  1041.                           
  1042.       if ( bDidSomething )
  1043.       {                             
  1044.          if ( numEmptyRemoved > 0 ) 
  1045.             MM_note( MSG_TrcEmptyRemoved, numEmptyRemoved );
  1046.          if ( numRedundantRemoved > 0 )   
  1047.             MM_note( MSG_TrcRedundantRemoved, numRedundantRemoved );
  1048.          if ( numTagsRemoved > 0 )   
  1049.             MM_note( MSG_TrcTagsRemoved, numTagsRemoved );
  1050.          if ( numCommentsRemoved > 0 )   
  1051.             MM_note( MSG_TrcCommentsRemoved, numCommentsRemoved );
  1052.          if ( numFontsCombined > 0 )   
  1053.             MM_note( MSG_TrcFontsCombined, numFontsCombined );
  1054.          if ( bRemovedTracing )   
  1055.             MM_note( MSG_TracingAttrsRemoved );
  1056.       }
  1057.       else
  1058.          MM_note( MSG_TrcDidNothing );
  1059.                      
  1060.       MM_showLog();
  1061.    }
  1062.  
  1063.    window.close();         
  1064. }