You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jspwiki.apache.org by aj...@apache.org on 2008/10/05 05:44:23 UTC

svn commit: r701746 [1/2] - /incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/

Author: ajaquith
Date: Sat Oct  4 20:44:22 2008
New Revision: 701746

URL: http://svn.apache.org/viewvc?rev=701746&view=rev
Log:
Even more refactoring of JSP migration code. Now able to chomp through quite a sizable chunk of the existing JSPs, although there are still some NPEs that need chasing.

Added:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigratorTest.java
Removed:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDirective.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Markup.java
Modified:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/AbstractNode.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Attribute.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformer.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformerTest.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDocumentTest.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigrator.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParser.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParserTest.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Node.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/NodeType.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformer.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformerTest.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Tag.java
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Text.java

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/AbstractNode.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/AbstractNode.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/AbstractNode.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/AbstractNode.java Sat Oct  4 20:44:22 2008
@@ -119,28 +119,6 @@
     /*
      * (non-Javadoc)
      * 
-     * @see com.ecyrd.jspwiki.ui.stripes.Node#getLevel()
-     */
-    public int getLevel()
-    {
-        if( m_parent == null )
-        {
-            return -1;
-        }
-
-        int level = 0;
-        Node node = this;
-        while ( node.getType() != NodeType.ROOT )
-        {
-            level++;
-            node = node.getParent();
-        }
-        return level;
-    }
-
-    /*
-     * (non-Javadoc)
-     * 
      * @see com.ecyrd.jspwiki.ui.stripes.Node#getLine()
      */
     public int getLine()

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Attribute.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Attribute.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Attribute.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Attribute.java Sat Oct  4 20:44:22 2008
@@ -28,7 +28,11 @@
     @Override
     public void setType( NodeType type )
     {
-        throw new UnsupportedOperationException( "Attributes are always of type NodeType.ATTRIBUTE; illegal to call this method." );
+        if ( type != NodeType.ATTRIBUTE && type != NodeType.DYNAMIC_ATTRIBUTE )
+        {
+            throw new UnsupportedOperationException( "Attributes are always of type NodeType.ATTRIBUTE or NodeType.DYNAMIC_ATTRIBUTE." );
+        }
+        super.setType( type );
     }
 
     /**
@@ -37,14 +41,21 @@
     public String toString()
     {
         StringBuilder sb = new StringBuilder();
-        sb.append(  m_name );
-        sb.append( '=' );
-        sb.append( m_quote );
-        for ( Node valueNode : m_children )
+        if ( m_type == NodeType.ATTRIBUTE )
+        {
+            sb.append(  m_name );
+            sb.append( '=' );
+            sb.append( m_quote );
+            for ( Node valueNode : m_children )
+            {
+                sb.append( valueNode.toString() );
+            }
+            sb.append( m_quote );
+        }
+        else
         {
-            sb.append( valueNode.toString() );
+            sb.append( getValue() );
         }
-        sb.append( m_quote );
         return sb.toString();
     }
 }

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformer.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformer.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformer.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformer.java Sat Oct  4 20:44:22 2008
@@ -30,9 +30,9 @@
                     removeSetBundle( tag );
                 }
 
-                // Advise user about <input type="hidden"> tags
+                // Advise user about <input type="hidden"> or <stripes:hidden> tags
                 boolean isTypeHidden = false;
-                isTypeHidden = "stripes:form".equals( tag.getName() );
+                isTypeHidden = "stripes:hidden".equals( tag.getName() ) && tag.getType() != NodeType.HTML_END_TAG;
                 if( "input".equals( tag.getName() ) )
                 {
                     Attribute attribute = tag.getAttribute( "type" );
@@ -41,7 +41,7 @@
                 if( isTypeHidden )
                 {
                     Attribute hidden = tag.getAttribute( "name" );
-                    message( hidden, "NOTE: hidden form input \"" + hidden.getValue()
+                    message( hidden, "NOTE: hidden form input with name \"" + hidden.getValue()
                                      + "\" should probably correspond to a Stripes ActionBean getter/settter. Refactor?" );
                 }
 

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformerTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformerTest.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformerTest.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JSPWikiJspTransformerTest.java Sat Oct  4 20:44:22 2008
@@ -1,5 +1,6 @@
 package com.ecyrd.jspwiki.ui.stripes;
 
+import java.io.File;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -72,6 +73,14 @@
         assertEquals( "form", node.getName() );
     }
     
+    public void testParseWeirdDoc() throws Exception
+    {
+        File src = new File( "src/webdocs/Captcha.jsp" );
+        String s = JspMigrator.readSource( src );
+        
+        JspDocument doc = new JspParser().parse( s );
+        m_transformer.transform( m_sharedState, doc );
+    }
 
     public static Test suite()
     {

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDocumentTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDocumentTest.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDocumentTest.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspDocumentTest.java Sat Oct  4 20:44:22 2008
@@ -30,8 +30,6 @@
         assertEquals( 5, endTag.getStart() );
         assertEquals( 11, endTag.getEnd() );
         assertEquals( "foo", endTag.getName() );
-
-        Node child = new Tag( doc, NodeType.HTML_COMBINED_TAG );
     }
 
     public static Test suite()

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigrator.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigrator.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigrator.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigrator.java Sat Oct  4 20:44:22 2008
@@ -151,7 +151,7 @@
         writer.close();
     }
 
-    protected String readSource( File src ) throws IOException
+    protected static String readSource( File src ) throws IOException
     {
         // Read in the file
         FileReader reader = new FileReader( src );

Added: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigratorTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigratorTest.java?rev=701746&view=auto
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigratorTest.java (added)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMigratorTest.java Sat Oct  4 20:44:22 2008
@@ -0,0 +1,27 @@
+package com.ecyrd.jspwiki.ui.stripes;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+public class JspMigratorTest extends TestCase
+{
+    protected Map<String, Object> m_sharedState = new HashMap<String, Object>();
+
+    protected JspTransformer m_transformer = new StripesJspTransformer();
+
+    protected JspDocument m_doc = new JspDocument();
+
+    public JspMigratorTest( String s )
+    {
+        super( s );
+    }
+
+    public static Test suite()
+    {
+        return new TestSuite( JspMigratorTest.class );
+    }
+}

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParser.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParser.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParser.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParser.java Sat Oct  4 20:44:22 2008
@@ -8,15 +8,18 @@
  */
 public class JspParser
 {
-    private static final ParserDelegate TEXT_PARSER = new TextParser();
+    private static final Parser TEXT_PARSER = new TextParser();
 
-    private static final JspTagParser JSP_TAG_PARSER = new JspTagParser();
-
-    private static final ParserDelegate HTML_TAG_PARSER = new HtmlTagParser();
+    private static final Parser TAG_PARSER = new TagParser();
 
     private static final AttributeParser ATTRIBUTE_PARSER = new AttributeParser();
+    
+    private static final DynamicAttributeParser DYNAMIC_ATTRIBUTE_PARSER = new DynamicAttributeParser();
 
-    public static class AttributeParser extends ParserDelegate
+    /**
+     * Parses dynamic attributes in a Tag declaration.
+     */
+    public static class DynamicAttributeParser implements Parser
     {
         public void beginStage()
         {
@@ -26,10 +29,75 @@
         {
         }
 
+        public void handle( char ch )
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            int leftAngleBrackets = 1;
+            boolean increment = false;
+            do
+            {
+                // Get character at current position
+                // Increment position
+                if ( increment )
+                {
+                    ctx.incrementPosition();
+                }
+                ch = ctx.getSource().charAt( ctx.position() );
+               
+               switch (ch) {
+                   
+                   case ('<'):
+                   {
+                       leftAngleBrackets++;
+                       break;
+                   }
+                   case ('>'):
+                   {
+                       leftAngleBrackets--;
+                       break;
+                   }
+                   default:
+                   {
+                       // Ignore any other character
+                   }
+                   increment = true;
+               }
+                
+            } while ( leftAngleBrackets != 0 );
+            
+            // Set the end position, name and value.
+            Attribute attribute = (Attribute)ctx.getNode();
+            ctx.setEndPosition( attribute );
+            attribute.setType( NodeType.DYNAMIC_ATTRIBUTE );
+            attribute.setValue( ctx.getSource().substring( ctx.getMarkerForStage( Stage.CODE_OR_COMMENT), attribute.getEnd() ) );
+            
+            // Add to parent
+            Tag parent = (Tag)ctx.getParentContext().getNode();
+            parent.addAttribute( attribute );
+            
+            // Pop the ParseContext and exit
+            ParseContext.pop();
+        }
+    }
+
+    public static class AttributeParser implements Parser
+    {
+        public void beginStage()
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            Node attribute = new Attribute( ctx.getDocument() );
+            ctx.setNode( attribute );
+        }
+
+        public void endStage()
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            Node attribute = ctx.getNode();
+            attribute.setEnd( ctx.position() );
+        }
+
         /**
-         * Handles {@link NodeLifecycle#TAG_WHITESPACE},
-         * {@link NodeLifecycle#ATTRIBUTE_EQUALS},
-         * {@link NodeLifecycle#ATTRIBUTE_VALUE}.
+         * {@link Stage#NAME}.
          */
         public void handle( char ch )
         {
@@ -38,76 +106,38 @@
 
             switch( ctx.getStage() )
             {
-                // Whitespace between node name and first attribute, or
-                // between attributes.
-                case TAG_WHITESPACE: {
-                    switch( ch )
-                    {
-                        case (' '): {
-                            break;
-                        }
-                        case ('/'): {
-                            // If we see /, switch back to HTML tag parser
-                            ctx.setParser( HTML_TAG_PARSER, NodeLifecycle.TAG_WHITESPACE );
-                            break;
-                        }
-                        case ('%'): {
-                            // Ignore the % because we might be in a JSP
-                            // directive/element
-                            break;
-                        }
-                        case ('>'): {
-                            // Switch back to HTML tag parser so its endStage()
-                            // method is called
-                            ctx.setParser( HTML_TAG_PARSER );
-                            ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
-                            break;
-                        }
-                        default: {
-                            // Create new attribute
-                            Attribute attribute = new Attribute( ctx.getDocument() );
-                            attribute.setParent( ctx.getNode() );
-                            ctx.setAttribute( attribute );
-                            ctx.setStage( NodeLifecycle.ATTRIBUTE_NAME );
-                        }
-                    }
-                    break;
-                }
-
-                    // Characters that name an attribute, after whitespace but
-                    // before equals (=).
-                case ATTRIBUTE_NAME: {
+                // Characters that name an attribute, after whitespace but
+                // before equals (=).
+                case NAME: {
                     if( ch == '=' )
                     {
                         // Set the attribute name
-                        Attribute attribute = ctx.getAttribute();
+                        Attribute attribute = (Attribute) ctx.getNode();
                         attribute.setName( ctx.getSource().substring( attribute.getStart(), pos ) );
-                        ctx.setStage( NodeLifecycle.ATTRIBUTE_EQUALS );
+
+                        // The next character will be a single/double quote
+                        ctx.incrementPosition();
+                        char delimiter = ctx.getSource().charAt( ctx.position() );
+                        attribute.setAttributeDelimiter( delimiter );
+
+                        // Start a new ParseContext, and Text node in position
+                        // after quote
+                        ctx = ParseContext.push();
+                        ctx.setStartPosition( ctx.getNode(), ctx.position() + 1 );
                     }
                     break;
                 }
 
-                    // The equals (=) that separates the attribute name and
-                    // value.
-                case ATTRIBUTE_EQUALS: {
-                    if( ch == '\'' || ch == '\"' )
-                    {
-                        // Save the quote delimiter for later
-                        Attribute attribute = ctx.getAttribute();
-                        attribute.setAttributeDelimiter( ch );
-
-                        // Push current ParseContext and Node onto stack
-                        ctx = ctx.push();
-                        ctx.pushNode( attribute );
-
-                        // Assign new text node to ParseContext, set as child of
-                        // attribute
-                        Text text = new Text( ctx.getDocument() );
-                        text.setParent( ctx.getParentNode() );
-                        ctx.setNode( text );
-                        ctx.setStartPosition( text, ctx.position() );
-                        ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
-                    }
+                case ATTRIBUTE_END: {
+                    // Add the finished attribute to the parent
+                    Attribute attribute = (Attribute) ctx.getNode();
+                    Tag parent = (Tag) attribute.getParent();
+                    parent.addAttribute( attribute );
+
+                    // Retrieve the Tag ParseContext & fire the handler method
+                    ctx = ParseContext.pop();
+                    Parser parser = ctx.getParser();
+                    parser.handle( ch );
                     break;
                 }
             }
@@ -115,85 +145,182 @@
 
     }
 
-    public static class HtmlTagParser extends ParserDelegate
+    public static class TagParser implements Parser
     {
-        public void beginStage()
+        /**
+         * Factory method that initializes a supplied Node. When initialized,
+         * the node's start position, line number, column number and level are
+         * set automatically based on JspDocument's internal cache of
+         * line-breaks and nodes. Note that the new node is not actually added
+         * to the internal node tree until the method
+         * {@link #finalizeNode(Node, int)} is called.
+         * 
+         * @param type the node type
+         * @param the stage to set at the end of the initialization
+         */
+        private void initNode( Node node, Stage stage )
         {
+            ParseContext ctx = ParseContext.currentContext();
+            ctx.setNode( node );
+
+            // Skip ahead if tag start > 1 char long
+            int increment = node.getType().getTagStart().length() - 1;
+            for( int i = 0; i < increment; i++ )
+            {
+                ctx.incrementPosition();
+            }
+
+            // Set the new stage and mark it at the next character
+            ctx.setStage( stage, 1 );
         }
 
-        public void endStage()
+        public void beginStage()
         {
             ParseContext ctx = ParseContext.currentContext();
-            Node node = ctx.getNode();
 
-            // Finalize the node type if it is still undefined
-            if( node.getType() == NodeType.UNRESOLVED_HTML_TAG )
+            // Figure out what this tag is
+            String lookahead = ctx.lookahead( 4 );
+            String lookahead9 = ctx.lookahead( 9 );
+            JspDocument doc = ctx.getDocument();
+
+            // <%- means hidden JSP comment
+            if( lookahead.startsWith( NodeType.JSP_COMMENT.getTagStart() ) )
             {
-                char lastCh = ctx.getSource().charAt( ctx.position() - 1 );
-                if( lastCh == '/' )
-                {
-                    node.setType( NodeType.HTML_COMBINED_TAG );
-                }
-                else
+                initNode( new Text( doc, NodeType.JSP_COMMENT ), Stage.CODE_OR_COMMENT );
+            }
+
+            // <%! means JSP declaration
+            else if( lookahead.startsWith( NodeType.JSP_DECLARATION.getTagStart() ) )
+            {
+                initNode( new Text( doc, NodeType.JSP_DECLARATION ), Stage.CODE_OR_COMMENT );
+            }
+
+            // <%= means JSP expression
+            else if( lookahead.startsWith( NodeType.JSP_EXPRESSION.getTagStart() ) )
+            {
+                initNode( new Text( doc, NodeType.JSP_EXPRESSION ), Stage.CODE_OR_COMMENT );
+            }
+
+            // <%@ + space means JSP directive
+            else if( lookahead.startsWith( NodeType.JSP_DIRECTIVE.getTagStart() ) )
+            {
+                initNode( new Tag( doc, NodeType.JSP_DIRECTIVE ), Stage.NAME );
+            }
+
+            // <!-- means HTML comment
+            else if( lookahead.startsWith( NodeType.HTML_COMMENT.getTagStart() ) )
+            {
+                initNode( new Text( doc, NodeType.HTML_COMMENT ), Stage.CODE_OR_COMMENT );
+            }
+
+            // Whitespace after <% means
+            // scriptlet
+            else if( lookahead.startsWith( NodeType.SCRIPTLET.getTagStart() ) )
+            {
+                if( lookahead.length() >= 3 && Character.isWhitespace( lookahead.charAt( 2 ) ) )
                 {
-                    // If no /, it's an HTML start tag, and new nodes should be
-                    // children of it
-                    node.setType( NodeType.HTML_START_TAG );
-                    ctx.pushNode( node );
+                    initNode( new Text( doc, NodeType.SCRIPTLET ), Stage.CODE_OR_COMMENT );
                 }
             }
+            
+            // <![CDATA[ means CDATA
+            else if( lookahead9.startsWith( NodeType.CDATA.getTagStart() ) )
+            {
+                initNode( new Text( doc, NodeType.CDATA ), Stage.CODE_OR_COMMENT );
+            }
 
-            // Set the end position
-            ctx.setEndPosition( node );
+            // If </, it's an HTML end tag
+            else if( lookahead.startsWith( NodeType.HTML_END_TAG.getTagStart() ) )
+            {
+                initNode( new Tag( doc, NodeType.HTML_END_TAG ), Stage.NAME );
+            }
 
-            // If node length is > 0, add it to the parent
-            if( node.getEnd() > node.getStart() )
+            // Any other char means its HTML start tag
+            // or combined tag
+            else
             {
-                node.getParent().addChild( node );
+                initNode( new Tag( doc, NodeType.UNRESOLVED_HTML_TAG ), Stage.NAME );
+            }
+        }
+
+        public void endStage()
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            Node node = ctx.getNode();
+
+            if( node != null )
+            {
+                // Set the end position
+                ctx.setEndPosition( node );
+
+                // If node length is > 0, add it to the parent
+                if( node.getEnd() > node.getStart() )
+                {
+                    node.getParent().addChild( node );
+                }
             }
         }
 
         /**
-         * Handles {@link NodeLifecycle#TAG_NAME}.
+         * Handles {@link Stage#NAME}.
          */
         public void handle( char ch )
         {
             ParseContext ctx = ParseContext.currentContext();
+            Node node = ctx.getNode();
 
             switch( ctx.getStage() )
             {
-                // After < but before whitespace that delimit attributes.
-                case TAG_NAME: {
+                // Characters that supply the tag name; immediately after <
+                case NAME: {
                     switch( ch )
                     {
-                        // If current character is whitespace, set the name and
-                        // move
-                        // to attributes stage
+                        // Whitespace == end of the name
                         case (' '): {
-                            finalizeTagName();
-                            ctx.setParser( ATTRIBUTE_PARSER );
-                            ctx.setStage( NodeLifecycle.TAG_WHITESPACE );
+                            int nameStart = ctx.getMarkerForStage( Stage.NAME );
+                            int nameEnd = ctx.position();
+                            node.setName( ctx.getSource().substring( nameStart, nameEnd ) );
+                            ctx.setStage( Stage.WHITESPACE, 0 );
                             break;
                         }
 
                             // Right angle bracket == end of the node
                         case ('>'): {
-                            finalizeTagName();
-                            ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                            handleTagEnd();
                             break;
                         }
                     }
                     break;
                 }
 
-                    // Whitespace immediately before >
-                case TAG_WHITESPACE: {
+                    // Whitespace between <%! and name, or between attributes.
+                case WHITESPACE: {
                     switch( ch )
                     {
+                        case ('/'):
+                        case (' '):
+                        case ('%'): {
+                            break;
+                        }
+                        
+                        // It's the start of a new dynamic attribute
+                        case ('<'): {
+                            ctx = ParseContext.push();
+                            ctx.setParser( DYNAMIC_ATTRIBUTE_PARSER, Stage.CODE_OR_COMMENT, 0 );
+                            ctx.setNode( new Attribute( ctx.getDocument() ) );
+                            break;
+                        }
+
                         case ('>'): {
-                            ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                            handleTagEnd();
                             break;
                         }
+
+                        default: {
+                            // It's the start of a new attribute
+                            ctx = ParseContext.push();
+                            ctx.setParser( ATTRIBUTE_PARSER, Stage.NAME, 0 );
+                        }
                     }
                     break;
                 }
@@ -201,11 +328,11 @@
                 case CODE_OR_COMMENT: {
                     switch( ch )
                     {
-                        // Terminating %> means the end of the comment
+                        // Terminating %> or --> means the end of the comment
                         case ('>'): {
-                            Node node = ctx.getNode();
-                            String lookbehind = ctx.lookbehind( 3 );
-                            if( lookbehind.equals( NodeType.HTML_COMMENT.getTagEnd() ) )
+                            String tagEnd = node.getType().getTagEnd();
+                            String lookbehind = ctx.lookbehind( tagEnd.length() );
+                            if( lookbehind.equals( tagEnd ) )
                             {
                                 // Set the end position
                                 node.setEnd( ctx.position() + 1 );
@@ -215,7 +342,7 @@
                                 node.setValue( ctx.getSource().substring( node.getStart() + type.getTagStart().length(),
                                                                           node.getEnd() - type.getTagEnd().length() ) );
 
-                                ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                                ctx.setParser( TEXT_PARSER, Stage.TEXT, 1 );
                             }
                             break;
                         }
@@ -225,119 +352,89 @@
             }
         }
 
-        private void finalizeTagName()
-        {
-            ParseContext ctx = ParseContext.currentContext();
-            Node node = ctx.getNode();
-            int nameStart = node.getStart() + node.getType().getTagStart().length();
-            int nameEnd = ctx.position();
-            if( nameEnd - nameStart > 0 )
-            {
-                if( ctx.getSource().charAt( nameEnd - 1 ) == '/' )
-                {
-                    nameEnd--;
-                }
-                node.setName( ctx.getSource().substring( nameStart, nameEnd ) );
-            }
-        }
-    }
-
-    public static class JspTagParser extends ParserDelegate
-    {
-        public void beginStage()
-        {
-        }
-
-        public void endStage()
-        {
-        }
-
         /**
-         * Handles {@link NodeLifecycle#TAG_WHITESPACE},
-         * {@link NodeLifecycle#TAG_NAME},
-         * {@link NodeLifecycle#CODE_OR_COMMENT}.
+         * <p>
+         * Finishes up an HTML or JSP tag when &gt; is detected by the handler
+         * method {@link #handle(char)}, but before {@link #endStage()}
+         * executes. Because HTML parsing can be messy, this method resolves
+         * several edge cases:
+         * </p>
+         * <ul>
+         * <li>If the tag's type has not been determined (that is, its current
+         * type is {@link NodeType#UNRESOLVED_HTML_TAG}, its type is resolved
+         * to either {@link NodeType#HTML_COMBINED_TAG} or
+         * {@link NodeType#HTML_START_TAG}, depending on whether the last
+         * character was /.
+         * <li>
+         * <li>If the tag's name has not been set, because the parser has not
+         * encountered whitespace that delimits attributes, its name is set.</li>
+         * <li>If the tag is of type {@link NodeType#HTML_START_TAG} or
+         * {@link NodeType#HTML_START_TAG}, the current ParseContext is pushed
+         * on, or popped off, of the stack. In addition, if the tag is an end
+         * tag, its parent is re-wired to the same parent as the start tag. This
+         * makes the start and end tag logical peers, as they should be.</li>
+         * </ul>
+         * <p>
+         * After these cases are resolved, the current node is re-set to a Text
+         * node, in the character position starting after the right angle
+         * bracket (&gt;).
+         * </p>
          */
-        public void handle( char ch )
+        private void handleTagEnd()
         {
             ParseContext ctx = ParseContext.currentContext();
             Node node = ctx.getNode();
 
-            switch( ctx.getStage() )
+            // Resolve tag type if not set
+            if( node.getType() == NodeType.UNRESOLVED_HTML_TAG )
             {
-                // Whitespace between node name and first attribute, or
-                // between attributes.
-                case TAG_WHITESPACE: {
-                    switch( ch )
-                    {
-                        case (' '): {
-                            break;
-                        }
-                        default: {
-                            // If directive name not set, start new ParseContext (and set marker)
-                            if ( node.getName() == null )
-                            {
-                                ctx = ctx.push();
-                                ctx.setParser( JSP_TAG_PARSER );
-                                ctx.setStage( NodeLifecycle.TAG_NAME );     // Sets the marker too
-                            }
-                            
-                            // Otherwise, it's the start of a new attribute
-                            else
-                            {
-                            }
-                        }
-                    }
-                    break;
+                String lookbehind = ctx.lookbehind( 2 );
+                if( NodeType.HTML_COMBINED_TAG.getTagEnd().equals( lookbehind ) )
+                {
+                    node.setType( NodeType.HTML_COMBINED_TAG );
                 }
-                    // Characters that supply the JSP directive name.
-                case TAG_NAME: {
-                    if( ch == ' ' )
-                    {
-                        int nameStart = ctx.getMarker( NodeLifecycle.TAG_NAME );    // Retrieve the marker
-                        ctx = ctx.pop();
-                        Node directive = ctx.getNode();
-                        directive.setName( ctx.getSource().substring( nameStart, ctx.position() ) );
-                        ctx.setParser( ATTRIBUTE_PARSER );
-                        ctx.setStage( NodeLifecycle.TAG_WHITESPACE );
-                    }
-                    break;
+                else
+                {
+                    node.setType( NodeType.HTML_START_TAG );
                 }
+            }
 
-                case CODE_OR_COMMENT: {
-                    switch( ch )
-                    {
-                        // Terminating %> means the end of the scriptlet
-                        case ('>'): {
-                            String lookbehind = ctx.lookbehind( 2 );
-                            if( lookbehind.equals( NodeType.SCRIPTLET.getTagEnd() ) )
-                            {
-                                // Set the end position
-                                node.setEnd( ctx.position() + 1 );
-
-                                // Set the value
-                                NodeType type = node.getType();
-                                node.setValue( ctx.getSource().substring( node.getStart() + type.getTagStart().length(),
-                                                                          node.getEnd() - type.getTagEnd().length() ) );
+            // Set the name if not set
+            if( node.getName() == null )
+            {
+                int nameStart = ctx.getMarkerForStage( Stage.NAME );
+                int nameEnd = ctx.position() + 1 - node.getType().getTagEnd().length();
+                node.setName( ctx.getSource().substring( nameStart, nameEnd ) );
+            }
 
-                                // If node length is > 0, add it to the parent
-                                if( node.getEnd() > node.getStart() )
-                                {
-                                    node.getParent().addChild( node );
-                                }
-                                ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
-                            }
-                            break;
-                        }
-                    }
+            // If start or end tag, push/pop as needed;
+            // otherwise start new Text
+            switch( node.getType() )
+            {
+                case HTML_START_TAG: {
+                    // Add the start tag to parent, and push it onto stack
+                    endStage();
+                    ctx = ParseContext.push();
+                    break;
+                }
+                case HTML_END_TAG: {
+                    // Make end tag the peer of the start tag
+                    Node startTag = node.getParent();
+                    node.setParent( startTag.getParent() );
+
+                    // Get rid of the current node in the parent context
+                    ctx = ParseContext.pop();
+                    ctx.setNode( null );
                     break;
                 }
             }
 
+            // Start new Text node after the >
+            ctx.setParser( TEXT_PARSER, Stage.TEXT, 1 );
         }
-
     }
 
-    public static abstract class ParserDelegate
+    public interface Parser
     {
         public abstract void beginStage();
 
@@ -346,24 +443,16 @@
         public abstract void handle( char ch );
     }
 
-    public static class TextParser extends ParserDelegate
+    public static class TextParser implements Parser
     {
 
         public void beginStage()
         {
+            // Create Text node and set start at next character
             ParseContext ctx = ParseContext.currentContext();
-            int pos = ctx.position() + 1;
-
-            // Create new Text
             Text text = new Text( ctx.getDocument() );
-
-            // Set parent relationship
-            text.setParent( ctx.getParentNode() );
             ctx.setNode( text );
-            ctx.setAttribute( null );
-
-            // Set the start, end, linebreak
-            ctx.setStartPosition( text, pos );
+            ctx.setStartPosition( ctx.getNode(), ctx.position() + 1 );
         }
 
         public void endStage()
@@ -389,122 +478,44 @@
         }
 
         /**
-         * Handles {@link NodeLifecycle#PARSING_TEXT}.
+         * Handles {@link Stage#TEXT}.
          */
         public void handle( char ch )
         {
             ParseContext ctx = ParseContext.currentContext();
             switch( ctx.getStage() )
             {
-                case PARSING_TEXT: {
+                case TEXT: {
                     switch( ch )
                     {
                         // If we see a quote, check to see if it's a part of a
                         // parent attribute
                         case ('\''):
                         case ('"'): {
-                            if( ctx.hasParentContext() )
+                            Node node = ctx.getNode();
+                            Node parent = node.getParent();
+                            if( parent instanceof Attribute )
                             {
-                                Attribute attribute = ctx.getParentContext().getAttribute();
-                                if( attribute != null && ch == attribute.getAttributeDelimiter() )
+                                Attribute attribute = (Attribute) parent;
+                                if( ch == attribute.getAttributeDelimiter() )
                                 {
-                                    // Pop the ParseContext (and run its
-                                    // endStage method) and Node
-                                    ctx = ctx.pop();
-                                    ctx.popNode();
-
-                                    // Finish the parent attribute
-                                    Node node = attribute.getParent();
-                                    attribute.setEnd( ctx.position() + 1 );
-                                    if( node.isHtmlNode() )
-                                    {
-                                        ((Tag) node).addAttribute( attribute );
-                                    }
-                                    else if( node.getType() == NodeType.JSP_DIRECTIVE )
-                                    {
-                                        ((JspDirective) node).addAttribute( attribute );
-                                    }
-                                    ctx.setAttribute( null );
-                                    ctx.setParser( ATTRIBUTE_PARSER, NodeLifecycle.TAG_WHITESPACE );
+                                    ctx = ParseContext.pop();
+                                    ctx.setStage( Stage.ATTRIBUTE_END, 0 );
                                 }
                             }
                             break;
                         }
                         case ('<'): {
-
-                            // Figure out what this tag is
-                            String lookahead = ctx.lookahead( 4 );
-                            JspDocument doc = ctx.getDocument();
-
-                            // <%- means hidden JSP comment
-                            if( lookahead.startsWith( NodeType.JSP_COMMENT.getTagStart() ) )
-                            {
-                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
-                                initNode( new Markup( doc, NodeType.JSP_COMMENT ) );
-                            }
-
-                            // <%! means JSP declaration
-                            else if( lookahead.startsWith( NodeType.JSP_DECLARATION.getTagStart() ) )
-                            {
-                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
-                                initNode( new Markup( doc, NodeType.JSP_DECLARATION ) );
-                            }
-
-                            // <%= means JSP expression
-                            else if( lookahead.startsWith( NodeType.JSP_EXPRESSION.getTagStart() ) )
+                            // Valid XML or JSP tag start?
+                            String lookahead = ctx.lookahead( 2 );
+                            if( lookahead.length() == 2 )
                             {
-                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
-                                initNode( new Markup( doc, NodeType.JSP_EXPRESSION ) );
-                            }
-
-                            // <%@ means JSP directive
-                            else if( lookahead.startsWith( NodeType.JSP_DIRECTIVE.getTagStart() ) )
-                            {
-                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.TAG_WHITESPACE );
-                                initNode( new JspDirective( doc ) );
-                            }
-
-                            // <!-- means HTML comment
-                            else if( lookahead.startsWith( NodeType.HTML_COMMENT.getTagStart() ) )
-                            {
-                                ctx.setParser( HTML_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
-                                initNode( new Markup( doc, NodeType.HTML_COMMENT ) );
-                            }
-
-                            // Whitespace after <% means
-                            // scriptlet
-                            else if( lookahead.startsWith( NodeType.SCRIPTLET.getTagStart() ) )
-                            {
-                                if( lookahead.length() >= 3 && Character.isWhitespace( lookahead.charAt( 2 ) ) )
+                                char nextChar = lookahead.charAt( 1 );
+                                if( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!/%_:".indexOf( nextChar ) != -1 )
                                 {
-                                    ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
-                                    initNode( new Markup( doc, NodeType.SCRIPTLET ) );
+                                    ctx.setParser( TAG_PARSER, Stage.TAG_START, 0 );
                                 }
                             }
-
-                            // If </, it's an HTML end tag
-                            else if( lookahead.startsWith( NodeType.HTML_END_TAG.getTagStart() ) )
-                            {
-                                ctx.setParser( HTML_TAG_PARSER, NodeLifecycle.TAG_NAME );
-                                initNode( new Tag( doc, NodeType.HTML_END_TAG ) );
-                            }
-
-                            // If < plus space, it's just ordinary
-                            // (albeit sloppy) markup
-                            else if( "< ".equals( lookahead.subSequence( 0, 2 ) ) )
-                            {
-                                ctx.setStage( NodeLifecycle.PARSING_TEXT );
-                                initNode( new Text( doc ) );
-                            }
-
-                            // Any other char means its HTML start tag
-                            // or combined tag
-                            else
-                            {
-                                ctx.setParser( HTML_TAG_PARSER, NodeLifecycle.TAG_NAME );
-                                initNode( new Tag( doc, NodeType.UNRESOLVED_HTML_TAG ) );
-                            }
-
                             break;
                         }
                     }
@@ -514,42 +525,6 @@
 
         }
 
-        /**
-         * Factory method that initializes a supplied Node. When initialized,
-         * the node's start position, line number, column number and level are
-         * set automatically based on JspDocument's internal cache of
-         * line-breaks and nodes. Note that the new node is not actually added
-         * to the internal node tree until the method
-         * {@link #finalizeNode(Node, int)} is called.
-         * 
-         * @param type the node type
-         */
-        private void initNode( Node node )
-        {
-            ParseContext ctx = ParseContext.currentContext();
-
-            // If HTML end tag, pop the node stack first
-            if( node.getType() == NodeType.HTML_END_TAG )
-            {
-                ctx.popNode();
-            }
-
-            // Set parent relationship
-            node.setParent( ctx.getParentNode() );
-            ctx.setNode( node );
-            ctx.setAttribute( null );
-
-            // Set the start, end, linebreak
-            ctx.setStartPosition( node, ctx.position() );
-
-            // Skip ahead if tag start > 1 char long
-            int increment = node.getType().getTagStart().length() - 1;
-            for( int i = 0; i < increment; i++ )
-            {
-                ctx.incrementPosition();
-            }
-        }
-
     }
 
     private static final class Counter
@@ -572,37 +547,30 @@
         }
     }
 
-    private enum NodeLifecycle
+    private enum Stage
     {
+        /**
+         * Parsing the left-angle bracket that starts the node (&lt;).
+         */
+        TAG_START,
         /** Parsing any text outside of a tag or element. */
-        PARSING_TEXT,
+        TEXT,
         /**
-         * Characters after the opening left bracket (&lt;), but before
-         * whitespace that delimit the attributes.
+         * Characters that name a node. For tags, the node name begins after the
+         * opening left bracket (&lt;) and ends before the first whitespace
+         * character. For attributes, the name begins after any whitespace and
+         * ends before the equals (=) character.
          */
-        TAG_NAME,
+        NAME,
         /**
          * Whitespace between the node or directive name and the first
          * attribute, and between attributes.
          */
-        TAG_WHITESPACE,
-        /**
-         * Characters that name an attribute, after whitespace but before the
-         * equals (=) character.
-         */
-        ATTRIBUTE_NAME,
-        /**
-         * When the current character is the equals (=) character that separates
-         * the attribute name and value.
-         */
-        ATTRIBUTE_EQUALS,
-        /**
-         * The opening quote, closing quote, and all characters in between, that
-         * denote an attribute's value.
-         */
-        ATTRIBUTE_VALUE,
+        WHITESPACE,
         /** Any text inside of a scriptlet, JSP comment, or JSP declaration. */
         CODE_OR_COMMENT,
+        /** Finishing an attribute. */
+        ATTRIBUTE_END
     }
 
     /**
@@ -610,8 +578,6 @@
      */
     private static class ParseContext
     {
-        private static final Stack<Node> NODE_STACK = new Stack<Node>();
-
         private static final Stack<ParseContext> CONTEXT_STACK = new Stack<ParseContext>();
 
         private static ParseContext CURRENT_CONTEXT;
@@ -623,42 +589,36 @@
 
         public static ParseContext initialContext( JspDocument doc, String source )
         {
-            NODE_STACK.clear();
             CONTEXT_STACK.clear();
             ParseContext context = new ParseContext( doc, source, new Counter() );
-            context.pushNode( doc.getRoot() );
             CURRENT_CONTEXT = context;
 
             // Set the default stage to TEXT
             Text text = new Text( doc );
-            text.setParent( doc.getRoot() );
             context.setNode( text );
-            context.setAttribute( null );
             context.setStartPosition( text, 0 );
-            context.m_stage = NodeLifecycle.PARSING_TEXT;
+            context.m_stage = Stage.TEXT;
             context.m_parser = TEXT_PARSER;
 
             // Return the init'ed context
             return context;
         }
 
-        private ParserDelegate m_parser = null;
+        private Parser m_parser = null;
 
         private final List<Integer> lineBreaks = new ArrayList<Integer>();
 
         private Node m_node = null;
 
-        private Attribute m_attribute = null;
-
         private final Counter m_counter;
 
         private final String m_source;
 
-        private NodeLifecycle m_stage = NodeLifecycle.PARSING_TEXT;
+        private Stage m_stage = Stage.TEXT;
 
         private JspDocument m_doc;
 
-        private Map<NodeLifecycle, Integer> m_markers = new HashMap<NodeLifecycle, Integer>();
+        private Map<Stage, Integer> m_markers = new HashMap<Stage, Integer>();
 
         private ParseContext( JspDocument doc, String source, Counter counter )
         {
@@ -706,16 +666,6 @@
             return m_source.substring( startPos, endPos );
         }
 
-        public Node getParentNode()
-        {
-            return NODE_STACK.peek();
-        }
-
-        public Attribute getAttribute()
-        {
-            return m_attribute;
-        }
-
         public Counter getCounter()
         {
             return m_counter;
@@ -734,7 +684,7 @@
          * @param stage the stage for which the marker position is desired
          * @return the position of the marker.
          */
-        public int getMarker( NodeLifecycle stage )
+        public int getMarkerForStage( Stage stage )
         {
             Integer mark = m_markers.get( stage );
             return mark == null ? Node.POSITION_NOT_SET : mark.intValue();
@@ -748,22 +698,21 @@
         /**
          * Sets the active Parser without changing the stage or incrementing the
          * position. Calling this method does <em>not</em> execute either the
-         * {@link ParserDelegate#beginStage()} or
-         * {@link ParserDelegate#endStage()} methods.
+         * {@link Parser#beginStage()} or {@link Parser#endStage()} methods.
          * 
          * @param parser the parser to set
          */
-        public void setParser( ParserDelegate parser )
+        public void setParser( Parser parser )
         {
             m_parser = parser;
         }
 
         public ParseContext getParentContext()
         {
-            return CONTEXT_STACK.peek();
+            return CONTEXT_STACK.size() == 0 ? null : CONTEXT_STACK.peek();
         }
 
-        public ParserDelegate getParser()
+        public Parser getParser()
         {
             return m_parser;
         }
@@ -773,7 +722,7 @@
             return m_source;
         }
 
-        public NodeLifecycle getStage()
+        public Stage getStage()
         {
             return m_stage;
         }
@@ -803,26 +752,12 @@
         }
 
         /**
-         * Sets a "marker" for the current stage (as reported by
-         * {@link #getStage()} at the current position (as reported by
-         * {@link #position()}. The marker can be retrieved later via
-         * {@link #getMarker(com.ecyrd.jspwiki.ui.stripes.JspParser.NodeLifecycle)}.
-         * Callers may place only one marker per lifecycle stage. Generally,
-         * markers are used to set character positions that are important to
-         * retrieve later.
-         */
-        public void mark()
-        {
-            m_markers.put( getStage(), position() );
-        }
-
-        /**
          * Pops the topmost ParseContext from the stack and replaces the
          * JspParser's current ParseContext with it. Before popping the
-         * ParseContext, the current stage's {@link ParserDelegate#endStage()}
-         * method is executed.
+         * ParseContext, the current stage's {@link Parser#endStage()} method is
+         * executed.
          */
-        public ParseContext pop()
+        public static ParseContext pop()
         {
             // Run the endStage method for the current stage
             ParseContext ctx = ParseContext.currentContext();
@@ -833,11 +768,6 @@
             return ctx;
         }
 
-        public void popNode()
-        {
-            NODE_STACK.pop();
-        }
-
         public int position()
         {
             return m_counter.position();
@@ -846,76 +776,98 @@
         /**
          * Pushes this ParseContext onto the stack and replaces the JspParser's
          * current ParseContext with a new one. This method does <em>not</em>
-         * call the current ParserDelegate's {@link ParserDelegate#endStage()}
-         * method.
+         * call the current Parser's {@link Parser#endStage()} method.
          * 
          * @return the new ParseContext
          */
-        public ParseContext push()
+        public static ParseContext push()
         {
-            CONTEXT_STACK.push( this );
-            ParseContext context = new ParseContext( m_doc, m_source, m_counter );
-            CURRENT_CONTEXT = context;
+            ParseContext oldCtx = currentContext();
+            CONTEXT_STACK.push( oldCtx );
+            ParseContext ctx = new ParseContext( oldCtx.m_doc, oldCtx.m_source, oldCtx.m_counter );
+            CURRENT_CONTEXT = ctx;
 
             // Set the default stage to TEXT
-            context.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
-
-            return context;
-        }
-
-        public void pushNode( Node node )
-        {
-            NODE_STACK.push( node );
+            Node text = new Text( ctx.m_doc );
+            ctx.setNode( text );
+            ctx.m_stage = Stage.TEXT;
+            ctx.m_parser = TEXT_PARSER;
+            return ctx;
         }
 
         /**
-         * Sets the ParseContext's current attribute, and sets it start position
-         * if not null.
+         * Sets the current Node to a supplied node. If the start position is
+         * not set, it set to the current position. If not already set, the
+         * parent is set to the parent ParseContext's node, or else the
+         * JspDocument's root node.
          * 
-         * @param attribute
+         * @param node the node to set. If <code>null</code>, the current
+         *            node is removed
          */
-        public void setAttribute( Attribute attribute )
+        public void setNode( Node node )
         {
-            m_attribute = attribute;
-            if( attribute != null )
+            m_node = node;
+            if( node == null )
             {
-                setStartPosition( attribute, position() );
+                return;
             }
-        }
 
-        public void setNode( Node node )
-        {
-            m_node = node;
+            // If start not set, set it now
+            if( node.getStart() == Node.POSITION_NOT_SET )
+            {
+                setStartPosition( node, position() );
+            }
+
+            // Set the parent relationship
+            if( node.getParent() == null )
+            {
+                if( getParentContext() == null )
+                {
+                    node.setParent( m_doc.getRoot() );
+                }
+                else
+                {
+                    node.setParent( getParentContext().getNode() );
+                }
+            }
         }
 
         /**
-         * Sets the current lifecycle stage, without resetting the current
-         * parser.
+         * Sets the current lifecycle stage and mark its position, without
+         * resetting the current parser. The "marker" is set relative to the
+         * current position (as reported by {@link #position()}. The marker can
+         * be retrieved later via
+         * {@link #getMarkerForStage(com.ecyrd.jspwiki.ui.stripes.JspParser.Stage)}.
          * 
          * @param stage
+         * @param increment the number of characters ahead of the current
+         *            position to set the marker
          */
-        public void setStage( NodeLifecycle stage )
+        public void setStage( Stage stage, int increment )
         {
-            // Set the new stage and set a marker at the current position
+            // Set the new stage.
             m_stage = stage;
-            mark();
+            m_markers.put( getStage(), position() + increment );
         }
 
         /**
          * <p>
-         * Ends the current {@link NodeLifecycle} and starts another, and sets a
-         * marker for the current position. When this method is called, the
-         * active {@link ParserDelegate} is finalized for the previous stage by
-         * calling its {@link ParserDelegate#endStage()} method. Then, the
-         * current text parser is replaced with the one that corresponds to the
-         * correct one for the new stage, and a marker is set. Finally, the new
+         * Ends the current {@link Stage} and starts another, and sets a marker
+         * for the next character position. When this method is called, the
+         * active {@link Parser} is finalized for the previous stage by calling
+         * its {@link Parser#endStage()} method. Then, the current text parser
+         * is replaced with the one that corresponds to the correct one for the
+         * new stage, and a marker is set at the next position. Finally, the new
          * stage is initialized by calling the new parser's
-         * {@link ParserDelegate #beginStage()} method.
+         * {@link Parser #beginStage()} method.
          * </p>
          * 
-         * @param stage
+         * @param parser the parser to set
+         * @param stage the stage to set
+         * @increment the number of characters ahead to set the marker for the
+         *            new stage. Must be zero or higher.
          */
-        public void setParser( ParserDelegate parser, NodeLifecycle stage )
+        public void setParser( Parser parser, Stage stage, int increment )
         {
             // Finish the parser's current stage
             if( parser != null && m_parser != null )
@@ -923,9 +875,9 @@
                 m_parser.endStage();
             }
 
-            // Set the new stage and set a marker at the current position
+            // Set the new stage and set a marker at the next position
             m_stage = stage;
-            mark();
+            m_markers.put( stage, position() + increment );
 
             // Replace the parser and start it up
             if( parser != null )
@@ -1002,7 +954,7 @@
             char ch = isWhitespace ? ' ' : currentChar; // For case statements
 
             // Handle the current character
-            ParserDelegate parser = ctx.getParser();
+            Parser parser = ctx.getParser();
             parser.handle( ch );
 
             // Increment the character position
@@ -1015,11 +967,7 @@
         node.setEnd( ctx.position() );
         if( node.getType() == NodeType.TEXT )
         {
-            node.setValue( ctx.getSource().substring( node.getStart(), node.getEnd() ) );
-            if( node.getEnd() > node.getStart() )
-            {
-                node.getParent().addChild( node );
-            }
+            ctx.getParser().endStage();
         }
 
         return doc;

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParserTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParserTest.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParserTest.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspParserTest.java Sat Oct  4 20:44:22 2008
@@ -14,19 +14,141 @@
         super( s );
     }
     
-    public void testCombinedTag() throws Exception
+    public void testNestedAttributes() throws Exception
     {
-        String s = "<foo />";
+        String s = "<a <b test=\"c\">selected=\"d\"</b> >Foo</a>";
         
         // Parse the contents
         JspParser parser = new JspParser();
         JspDocument doc = parser.parse( s );
         
+        // Verify three nodes total
+        List<Node> nodes = doc.getNodes();
+        assertEquals( 3, nodes.size() );
+        
+        // First node is a start tag
+        Node node;
+        node = nodes.get( 0 );
+        assertEquals( "a", node.getName() );
+        assertEquals( NodeType.HTML_START_TAG, node.getType() );
+        assertEquals( "<a <b test=\"c\">selected=\"d\"</b> >", node.toString() );
+        
+        // Second node is a Text node
+        node = nodes.get( 1 );
+        assertEquals( NodeType.TEXT, node.getType() );
+        assertEquals( "Foo", node.getValue() );
+        
+        // Third node is an end tag
+        node = nodes.get( 2 );
+        assertEquals( "a", node.getName() );
+        assertEquals( NodeType.HTML_END_TAG, node.getType() );
+        
+        // First and third node are children of the root
+        assertEquals( 2, doc.getRoot().getChildren().size() );
+        assertEquals( NodeType.HTML_START_TAG, doc.getRoot().getChildren().get( 0 ).getType() );
+        assertEquals( NodeType.HTML_END_TAG, doc.getRoot().getChildren().get( 1 ).getType() );
+        
+        // Test first node: should have 3 attributes (2 dynamic)
+        Tag tag = (Tag)nodes.get( 0 );
+        assertEquals( 3, tag.getAttributes().size() );
+        Attribute attribute;
+        attribute = tag.getAttributes().get( 0 );
+        assertEquals( null, attribute.getName() );
+        assertEquals( NodeType.DYNAMIC_ATTRIBUTE, attribute.getType() );
+        assertEquals( "<b test=\"c\">", attribute.getValue() );
+        assertEquals( "<b test=\"c\">", attribute.toString() );
+        attribute = tag.getAttributes().get( 1 );
+        assertEquals( "selected", attribute.getName() );
+        assertEquals( "d", attribute.getValue() );
+        assertEquals( "selected=\"d\"", attribute.toString() );
+        attribute = tag.getAttributes().get( 2 );
+        assertEquals( null, attribute.getName() );
+        assertEquals( NodeType.DYNAMIC_ATTRIBUTE, attribute.getType() );
+        assertEquals( "</b>", attribute.getValue() );
+        assertEquals( "</b>", attribute.toString() );
+    }
+
+    public void testAttributes() throws Exception
+    {
+        String s = "<a b=\"cd\"/>";
+
+        // Parse the contents
+        JspParser parser = new JspParser();
+        JspDocument doc = parser.parse( s );
+
         // Results in one node
         List<Node> nodes = doc.getNodes();
         assertEquals( 1, nodes.size() );
         Tag node;
+
+        // Verify HTML start tag
+        node = (Tag) nodes.get( 0 );
+        assertEquals( "a", node.getName() );
+        assertEquals( null, node.getValue() );
+        assertEquals( 0, node.getChildren().size() );
+        assertEquals( NodeType.HTML_COMBINED_TAG, node.getType() );
+        assertEquals( "<a b=\"cd\"/>", node.toString() );
+
+        // Verify attributes
+        assertEquals( 1, node.getAttributes().size() );
+        Attribute attribute = node.getAttribute( "b" );
+        assertEquals( "b", attribute.getName() );
+        assertEquals( "cd", attribute.getValue() );
+        assertEquals( 3, attribute.getStart() );
+        assertEquals( 9, attribute.getEnd() );
+    }
+
+    public void testCdata() throws Exception
+    {
+        String s = "  <![CDATA[ foo ]]>  ";
+
+        // Parse the contents
+        JspParser parser = new JspParser();
+        JspDocument doc = parser.parse( s );
+
+        // Results in 3 nodes
+        List<Node> nodes = doc.getNodes();
+        assertEquals( 3, nodes.size() );
+        Node node;
+
+        // Verify text tag (0)
+        node = nodes.get( 0 );
+        assertEquals( "(TEXT)", node.getName() );
+        assertEquals( "  ", node.getValue() );
+        assertEquals( 0, node.getChildren().size() );
+        assertEquals( NodeType.TEXT, node.getType() );
+        assertEquals( "  ", node.toString() );
+
+        // Verify CDATA tag (1)
+        node = nodes.get( 1 );
+        assertEquals( "(TEXT)", node.getName() );
+        assertEquals( " foo ", node.getValue() );
+        assertEquals( 0, node.getChildren().size() );
+        assertEquals( NodeType.CDATA, node.getType() );
+        assertEquals( "<![CDATA[ foo ]]>", node.toString() );
         
+        // Verify text tag (2)
+        node = nodes.get( 2 );
+        assertEquals( "(TEXT)", node.getName() );
+        assertEquals( "  ", node.getValue() );
+        assertEquals( 0, node.getChildren().size() );
+        assertEquals( NodeType.TEXT, node.getType() );
+        assertEquals( "  ", node.toString() );
+    }
+    
+    public void testCombinedTag() throws Exception
+    {
+        String s = "<foo />";
+
+        // Parse the contents
+        JspParser parser = new JspParser();
+        JspDocument doc = parser.parse( s );
+
+        // Results in one node
+        List<Node> nodes = doc.getNodes();
+        assertEquals( 1, nodes.size() );
+        Tag node;
+
         // Verify HTML combined tag
         node = (Tag) nodes.get( 0 );
         assertEquals( "foo", node.getName() );
@@ -47,15 +169,15 @@
         // Results in one node
         List<Node> nodes = doc.getNodes();
         assertEquals( 1, nodes.size() );
-        Markup node;
+        Text node;
 
         // Verify comment
-        node = (Markup) nodes.get( 0 );
-        assertEquals( "(MARKUP)", node.getName() );
+        node = (Text) nodes.get( 0 );
+        assertEquals( "(TEXT)", node.getName() );
         assertEquals( " This is a comment ", node.getValue() );
         assertEquals( 0, node.getChildren().size() );
     }
-    
+
     public void testParseDirective() throws Exception
     {
         String s = "<%@ page import=\"org.apache.log4j.*\" %>";
@@ -67,11 +189,11 @@
         // Results in one node
         List<Node> nodes = doc.getNodes();
         assertEquals( 1, nodes.size() );
-        JspDirective node;
+        Tag node;
         Node attribute;
 
         // Verify directive
-        node = (JspDirective) nodes.get( 0 );
+        node = (Tag) nodes.get( 0 );
         assertEquals( "page", node.getName() );
         assertEquals( 1, node.getAttributes().size() );
         attribute = node.getAttributes().get( 0 );
@@ -81,9 +203,8 @@
 
     public void testParse() throws Exception
     {
-        JspMigrator m = new JspMigrator();
         File src = new File( "src/webdocs/LoginForm.jsp" );
-        String s = m.readSource( src );
+        String s = JspMigrator.readSource( src );
 
         // Parse the contents of the file
         JspParser parser = new JspParser();
@@ -106,11 +227,10 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "page", node.getName() );
-        assertEquals( 1, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 1, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "import", attribute.getName() );
         assertEquals( "org.apache.log4j.*", attribute.getValue() );
         i++;
@@ -122,7 +242,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -136,11 +255,10 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "page", node.getName() );
-        assertEquals( 1, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 1, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "import", attribute.getName() );
         assertEquals( "com.ecyrd.jspwiki.*", attribute.getValue() );
         i++;
@@ -152,7 +270,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -166,11 +283,10 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "page", node.getName() );
-        assertEquals( 1, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 1, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "import", attribute.getName() );
         assertEquals( "com.ecyrd.jspwiki.action.*", attribute.getValue() );
         i++;
@@ -182,7 +298,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -196,11 +311,10 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "page", node.getName() );
-        assertEquals( 1, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 1, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "errorPage", attribute.getName() );
         assertEquals( "/Error.jsp", attribute.getValue() );
         i++;
@@ -212,7 +326,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -226,14 +339,13 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "taglib", node.getName() );
-        assertEquals( 2, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 2, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "uri", attribute.getName() );
         assertEquals( "/WEB-INF/jspwiki.tld", attribute.getValue() );
-        attribute = ((JspDirective) node).getAttributes().get( 1 );
+        attribute = ((Tag) node).getAttributes().get( 1 );
         assertEquals( "prefix", attribute.getName() );
         assertEquals( "wiki", attribute.getValue() );
         i++;
@@ -245,7 +357,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -259,14 +370,13 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "taglib", node.getName() );
-        assertEquals( 2, ((JspDirective) node).getAttributes().size() );
-        attribute = ((JspDirective) node).getAttributes().get( 0 );
+        assertEquals( 2, ((Tag) node).getAttributes().size() );
+        attribute = ((Tag) node).getAttributes().get( 0 );
         assertEquals( "uri", attribute.getName() );
         assertEquals( "/WEB-INF/stripes.tld", attribute.getValue() );
-        attribute = ((JspDirective) node).getAttributes().get( 1 );
+        attribute = ((Tag) node).getAttributes().get( 1 );
         assertEquals( "prefix", attribute.getName() );
         assertEquals( "stripes", attribute.getValue() );
         i++;
@@ -278,7 +388,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -292,7 +401,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.HTML_COMBINED_TAG, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "stripes:useActionBean", node.getName() );
 
@@ -315,7 +423,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -328,9 +435,8 @@
         assertEquals( 767, node.getEnd() );
         assertEquals( NodeType.JSP_DECLARATION, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
-        assertEquals( "(MARKUP)", node.getName() );
+        assertEquals( "(TEXT)", node.getName() );
         i++;
         node = nodes.get( i );
         assertEquals( 19, node.getLine() );
@@ -340,7 +446,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.TEXT, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "(TEXT)", node.getName() );
         i++;
@@ -354,9 +459,8 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.SCRIPTLET, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
-        assertEquals( "(MARKUP)", node.getName() );
+        assertEquals( "(TEXT)", node.getName() );
         i++;
 
         // Test second tag on line 33 aka node 17
@@ -368,7 +472,6 @@
         assertEquals( 0, node.getChildren().size() );
         assertEquals( NodeType.HTML_COMBINED_TAG, node.getType() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 17, node.getSiblings().size() );
         assertEquals( "wiki:Include", node.getName() );
 
@@ -509,7 +612,6 @@
         assertEquals( "  ", node.getValue() );
         assertEquals( "(TEXT)", node.getName() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 2, node.getSiblings().size() );
         assertEquals( 0, node.getChildren().size() );
 
@@ -524,7 +626,6 @@
         assertEquals( "wiki:Include", node.getName() );
         assertEquals( null, node.getValue() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 2, node.getSiblings().size() );
         assertEquals( 0, node.getChildren().size() );
 
@@ -557,7 +658,6 @@
         assertEquals( "  ", node.getValue() );
         assertEquals( "(TEXT)", node.getName() );
         assertEquals( NodeType.ROOT, node.getParent().getType() );
-        assertEquals( 1, node.getLevel() );
         assertEquals( 2, node.getSiblings().size() );
         assertEquals( 0, node.getChildren().size() );
     }

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Node.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Node.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Node.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Node.java Sat Oct  4 20:44:22 2008
@@ -62,15 +62,6 @@
     public abstract int getEnd();
 
     /**
-     * Returns the "level" of the tag; that is, how far from the top-level nodes
-     * (which are level 1). If the parent of this node is not set, this method
-     * returns -1.
-     * 
-     * @return
-     */
-    public abstract int getLevel();
-
-    /**
      * Returns the line of the source text the node starts on.
      * 
      * @return the line

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/NodeType.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/NodeType.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/NodeType.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/NodeType.java Sat Oct  4 20:44:22 2008
@@ -8,9 +8,13 @@
     /** Root node */
     ROOT(null, null),
     /** Attribute node */
-    ATTRIBUTE(null,null),
+    ATTRIBUTE("",""),
+    /** Attribute node generated by an embedded JSP tag. */
+    DYNAMIC_ATTRIBUTE("<",">"),
     /** Text node */
     TEXT("", ""),
+    /** CDATA node. */
+    CDATA("<![CDATA[","]]>"),
     /** HTML comment tag */
     HTML_COMMENT("<!--", "-->"),
     /** HTML start tag */
@@ -39,7 +43,7 @@
      * JSP page, import or taglib directive, e.g., &lt;%@ include... %&gt;
      * &lt;%@ page... %&gt; &lt;%@ taglib... %&gt;
      */
-    JSP_DIRECTIVE("<%@", "%>");
+    JSP_DIRECTIVE("<%@ ", "%>");
     private final String m_tagStart;
 
     private final String m_tagEnd;

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformer.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformer.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformer.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformer.java Sat Oct  4 20:44:22 2008
@@ -46,7 +46,7 @@
         Node lastTaglib = null;
         for ( Node node : nodes )
         {
-            JspDirective directive = (JspDirective)node;
+            Tag directive = (Tag)node;
             if ( "taglib".equals( node.getName() ) )
             {
                 lastTaglib = node;
@@ -62,7 +62,8 @@
         {
             Text linebreak = new Text( doc );
             linebreak.setValue( System.getProperty( "line.separator" ) );
-            JspDirective directive = new JspDirective( doc );
+            linebreak.setParent( doc.getRoot() );
+            Tag directive = new Tag( doc, NodeType.JSP_DIRECTIVE );
             directive.setName( "taglib" );
             Attribute attribute = new Attribute( doc );
             attribute.setName( "uri" );
@@ -165,7 +166,7 @@
             if( actionUrl != null )
             {
                 int qmark = actionUrl.indexOf( '?' );
-                if( qmark < actionUrl.length() - 1 )
+                if( qmark != -1 && qmark < actionUrl.length() - 1 )
                 {
                     // Change "action" attribute"
                     String trimmedPath = actionUrl.substring( 0, qmark );

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformerTest.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformerTest.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformerTest.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/StripesJspTransformerTest.java Sat Oct  4 20:44:22 2008
@@ -1,5 +1,6 @@
 package com.ecyrd.jspwiki.ui.stripes;
 
+import java.io.File;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -108,9 +109,9 @@
         node = doc.getNodes().get( 0 );
         assertEquals( NodeType.JSP_DIRECTIVE, node.getType() );
         assertEquals( "taglib", node.getName() );
-        attribute = ((JspDirective) node).getAttribute( "prefix" );
+        attribute = ((Tag) node).getAttribute( "prefix" );
         assertEquals( "stripes", attribute.getValue() );
-        attribute = ((JspDirective) node).getAttribute( "uri" );
+        attribute = ((Tag) node).getAttribute( "uri" );
         assertEquals( "/WEB-INF/stripes.tld", attribute.getValue() );
 
         // Verify linebreak

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Tag.java
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Tag.java?rev=701746&r1=701745&r2=701746&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Tag.java (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Tag.java Sat Oct  4 20:44:22 2008
@@ -69,43 +69,6 @@
         m_attributes.remove( attribute );
     }
 
-    private String diagnostic()
-    {
-        StringBuilder sb = new StringBuilder();
-        sb.append( "[" );
-        sb.append( m_type.toString() );
-        sb.append( "(pos=" );
-        sb.append( m_line );
-        sb.append( ":" );
-        sb.append( m_col );
-        sb.append( ",chars=" );
-        sb.append( m_start );
-        sb.append( ":" );
-        sb.append( m_end );
-        sb.append( ",L" );
-        sb.append( getLevel() );
-        sb.append( ")," );
-        sb.append( "name=\"" );
-        sb.append( m_name );
-        sb.append( "\"," );
-        if( m_attributes.size() > 0 )
-        {
-            sb.append( "attributes=" );
-            for( Attribute attr : m_attributes )
-            {
-                sb.append( "[" );
-                sb.append( attr.toString() );
-                sb.append( "\"]" );
-            }
-            sb.append( "," );
-        }
-        sb.append( "value=\"" );
-        sb.append( getValue() );
-        sb.append( "\"]" );
-
-        return sb.toString();
-    }
-
     /**
      * Adds a child to the current Node. If the Node is of type
      * {@link NodeType#HTML_COMBINED_TAG}, the tag will be split into two nodes
@@ -190,23 +153,43 @@
      */
     public String toString()
     {
+        // Root node is easy!
+        if ( m_type == NodeType.ROOT )
+        {
+            return "ROOT";
+        }
+        
         StringBuilder sb = new StringBuilder();
-        sb.append( m_type.getTagStart() );
+        
+        // Calculate start and end nodes
+        String tagStart= m_type.getTagStart();
+        String tagEnd = m_type.getTagEnd();
+        if ( tagStart == null ) tagStart = "?";
+        if ( tagEnd == null ) tagEnd = "?";
+
+        // Print tag start
+        sb.append( tagStart );
 
-        // HTML nodes and JSP directives are formatted in mostly the same way.
+        // If Tag, print start/end plus attributes.
         if( isHtmlNode() || m_type == NodeType.JSP_DIRECTIVE )
         {
-            if( m_type == NodeType.JSP_DIRECTIVE )
-            {
-                sb.append( ' ' );
-            }
             sb.append( m_name );
             if( m_attributes.size() > 0 )
             {
+                sb.append( ' ' );
+                NodeType lastType = null;
                 for( Attribute attr : m_attributes )
                 {
-                    sb.append( ' ' );
+                    if ( attr.getType() == lastType )
+                    {
+                        sb.append( ' ' );
+                    }
                     sb.append( attr.toString() );
+                    lastType = attr.getType();
+                }
+                if ( lastType == NodeType.DYNAMIC_ATTRIBUTE )
+                {
+                    sb.append( ' ' );
                 }
             }
         }
@@ -220,7 +203,8 @@
             }
         }
 
-        sb.append( m_type.getTagEnd() );
+        // Print tag end
+        sb.append( tagEnd );
         return sb.toString();
     }