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/09/28 14:59:41 UTC

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

Author: ajaquith
Date: Sun Sep 28 05:59:41 2008
New Revision: 699812

URL: http://svn.apache.org/viewvc?rev=699812&view=rev
Log:
Additional refactoring of JSP transformer code. One more batch to go...

Added:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/Markup.java
      - copied, changed from r696723, incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMarkup.java
Removed:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/tests/com/ecyrd/jspwiki/ui/stripes/JspMarkup.java
Modified:
    incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/build.xml
    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/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/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/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

Modified: incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/build.xml
URL: http://svn.apache.org/viewvc/incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/build.xml?rev=699812&r1=699811&r2=699812&view=diff
==============================================================================
--- incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/build.xml (original)
+++ incubator/jspwiki/branches/JSPWIKI_2_9_STRIPES_BRANCH/build.xml Sun Sep 28 05:59:41 2008
@@ -279,6 +279,21 @@
     </javadoc>
 
   </target>
+  
+  <!-- ============================================================== -->
+  
+  <!-- This target migrates Stripes JSPs -->
+  
+  <target name="migrate" depends="jar,jartests">
+    <mkdir dir="build/migrated" />
+    <java classname="com.ecyrd.jspwiki.ui.stripes.JspMigrator" fork="yes" maxmemory="512m">
+      <classpath>
+         <path refid="path.tests" />
+      </classpath>
+      <arg value="src/webdocs" />
+      <arg value="build/migrated" />
+    </java>
+  </target>
 
   <!-- ============================================================== -->
 

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=699812&r1=699811&r2=699812&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 Sun Sep 28 05:59:41 2008
@@ -240,7 +240,7 @@
     public boolean isJspNode()
     {
         return m_type == NodeType.JSP_COMMENT || m_type == NodeType.JSP_DECLARATION || m_type == NodeType.JSP_EXPRESSION
-               || m_type == NodeType.SCRIPTLET || m_type == NodeType.JSP_DIRECTIVE || m_type == NodeType.UNRESOLVED_JSP_TAG;
+               || m_type == NodeType.SCRIPTLET || m_type == NodeType.JSP_DIRECTIVE;
     }
 
     /**

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=699812&r1=699811&r2=699812&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 Sun Sep 28 05:59:41 2008
@@ -18,33 +18,18 @@
             // For all HTML tags...
             if( node.isHtmlNode() )
             {
-                Tag tag = (Tag)node;
-                
+                Tag tag = (Tag) node;
+
                 // Check any form or stripes:form elements
                 if( "form".equals( tag.getName() ) || "stripes:form".equals( tag.getName() ) )
                 {
-                    // Change "accept-charset" or "acceptcharset" values to UTF-8
-                    Attribute attribute = tag.getAttribute( "accept-charset" );
-                    if ( attribute == null )
-                    {
-                        attribute = tag.getAttribute( "acceptcharset" );
-                    }
-                    if( attribute != null )
-                    {
-                        message( attribute, "Changed value to \"UTF-8\"." );
-                        attribute.setValue( "UTF-8" );
-                    }
-                    
-                    // Remove onsubmit() attribute and warn the user
-                    attribute = tag.getAttribute( "onsubmit" );
-                    if( attribute != null )
-                    {
-                        String value = attribute.getValue();
-                        message( attribute, "Removed JavaScript call \"" + value + "\". REASON: it probably does not work with Stripes." );
-                        tag.removeAttribute( attribute );
-                    }
+                    processFormTag( tag );
+                }
+                else if( "fmt:setBundle".equals( tag.getName() ) )
+                {
+                    removeSetBundle( tag );
                 }
-                
+
                 // Advise user about <input type="hidden"> tags
                 boolean isTypeHidden = false;
                 isTypeHidden = "stripes:form".equals( tag.getName() );
@@ -56,16 +41,61 @@
                 if( isTypeHidden )
                 {
                     Attribute hidden = tag.getAttribute( "name" );
-                    message( hidden, "NOTE: hidden form input \"" + hidden.getValue() +"\" should probably correspond to a Stripes ActionBean getter/settter. Refactor?"  );
+                    message( hidden, "NOTE: hidden form input \"" + hidden.getValue()
+                                     + "\" should probably correspond to a Stripes ActionBean getter/settter. Refactor?" );
                 }
-                
+
                 // Tell user about <wiki:Messages> tags.
-                if ( "wiki:Messages".equals( tag.getName() ) )
+                if( "wiki:Messages".equals( tag.getName() ) )
                 {
-                    message( tag, "Consider using <stripes:errors> tags instead of <wiki:Messages> for displaying validation errors." );
+                    message( tag,
+                             "Consider using <stripes:errors> tags instead of <wiki:Messages> for displaying validation errors." );
                 }
             }
         }
     }
 
+    /**
+     * Removes the &lt;fmt:setBundle&gt; tag and advises the user.
+     * 
+     * @param tag the tag to remove
+     */
+    private void removeSetBundle( Tag tag )
+    {
+        Node parent = tag.getParent();
+        parent.removeChild( tag );
+        message( tag, "Removed <fmt:setBundle> tag because it is automatically set in web.xml." );
+    }
+
+    /**
+     * For &lt;form&gt; and &lt;stripes:form&gt; tags, changes
+     * <code>accept-charset</code> or <code>acceptcharset</code> attribute
+     * value to "UTF-8", and removes any <code>onsubmit</code> function calls.
+     * 
+     * @param tag the form tag
+     */
+    private void processFormTag( Tag tag )
+    {
+        // Change "accept-charset" or "acceptcharset" values to UTF-8
+        Attribute attribute = tag.getAttribute( "accept-charset" );
+        if( attribute == null )
+        {
+            attribute = tag.getAttribute( "acceptcharset" );
+        }
+        if( attribute != null )
+        {
+            message( attribute, "Changed value to \"UTF-8\"." );
+            attribute.setValue( "UTF-8" );
+        }
+
+        // Remove onsubmit() attribute and warn the user
+        attribute = tag.getAttribute( "onsubmit" );
+        if( attribute != null )
+        {
+            String value = attribute.getValue();
+            message( attribute, "Removed JavaScript call \"" + value + "\". REASON: it probably does not work with Stripes." );
+            tag.removeAttribute( attribute );
+        }
+    }
+
 }

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=699812&r1=699811&r2=699812&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 Sun Sep 28 05:59:41 2008
@@ -50,6 +50,28 @@
         assertEquals( "method", attribute.getName() );
         assertEquals( "POST", attribute.getValue() );
     }
+    
+    public void testSetBundle() throws Exception
+    {
+        String s = "<fmt:setBundle basename=\"templates.default\"/>\n<form method=\"POST\" onsubmit=\"return Wiki.submitOnce(this);\" />";
+        JspDocument doc = new JspParser().parse( s );
+   
+        // Should be 3 nodes: 2 HTML + 1 text
+        assertEquals( 3, doc.getNodes().size() );
+        assertEquals( 3, doc.getRoot().getChildren().size() );
+        
+        // Run the transformer
+        m_transformer.transform( m_sharedState, doc );
+        
+        // Now, should be only 2 nodes now because the <fmt:setBundle> tag was removed
+        assertEquals( 2, doc.getNodes().size() );
+        assertEquals( 2, doc.getRoot().getChildren().size() );
+        Node node = doc.getNodes().get( 0 );
+        assertEquals( NodeType.TEXT, node.getType() );
+        node = doc.getNodes().get( 1 );
+        assertEquals( "form", node.getName() );
+    }
+    
 
     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=699812&r1=699811&r2=699812&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 Sun Sep 28 05:59:41 2008
@@ -44,6 +44,8 @@
             throw new IllegalArgumentException( "Source and destination cannot be the same." );
         }
         JspMigrator migrator = new JspMigrator();
+        migrator.addTransformer( new StripesJspTransformer() );
+        migrator.addTransformer( new JSPWikiJspTransformer() );
         try
         {
             migrator.migrate( src, dest );

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=699812&r1=699811&r2=699812&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 Sun Sep 28 05:59:41 2008
@@ -6,721 +6,1023 @@
  * Parser that reads JSP document and constructs a {@link JspDocument} with the
  * results.
  */
-class JspParser
+public class JspParser
 {
-    private static final class Counter
-    {
-        private int m_pos;
+    private static final ParserDelegate TEXT_PARSER = new TextParser();
 
-        Counter()
-        {
-            m_pos = 0;
-        }
+    private static final JspTagParser JSP_TAG_PARSER = new JspTagParser();
 
-        public void increment()
+    private static final ParserDelegate HTML_TAG_PARSER = new HtmlTagParser();
+
+    private static final AttributeParser ATTRIBUTE_PARSER = new AttributeParser();
+
+    public static class AttributeParser extends ParserDelegate
+    {
+        public void beginStage()
         {
-            m_pos++;
         }
 
-        public int position()
+        public void endStage()
         {
-            return m_pos;
         }
-    }
 
-    private enum NodeLifecycle
-    {
-        TAG_RESOLUTION,
-        /**
-         * Characters after the directive tag start (&lt;) and whitespace, but
-         * before whitespace that delimit the attributes.
-         */
-        /**
-         * Characters after the opening left bracket (&lt;), but before
-         * whitespace that delimit the attributes.
-         */
-        TAG_NAME,
         /**
-         * After &lt;@ that determines that it's a JSP element, but before we
-         * know for sure what it is.
+         * Handles {@link NodeLifecycle#TAG_WHITESPACE},
+         * {@link NodeLifecycle#ATTRIBUTE_EQUALS},
+         * {@link NodeLifecycle#ATTRIBUTE_VALUE}.
          */
-        JSP_DIRECTIVE_NAME,
-        /**
-         * Whitespace between the node name and the first attribute, and between
-         * attributes.
-         */
-        BETWEEN_ATTRIBUTES,
-        /**
-         * 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,
-        /** Any outside of a tag or element (i.e., part of a text node). */
-        TEXT_NODE,
-        /** Any text inside of a scriptlet, JSP comment, or JSP declaration. */
-        CODE_OR_COMMENT,
-    }
+        public void handle( char ch )
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            int pos = ctx.position();
 
-    /**
-     * Encapsulates the current state of document parsing.
-     */
-    private static class ParseContext
-    {
-        private Node m_node = null;
+            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;
+                }
 
-        private Attribute m_attribute = null;
+                    // Characters that name an attribute, after whitespace but
+                    // before equals (=).
+                case ATTRIBUTE_NAME: {
+                    if( ch == '=' )
+                    {
+                        // Set the attribute name
+                        Attribute attribute = ctx.getAttribute();
+                        attribute.setName( ctx.getSource().substring( attribute.getStart(), pos ) );
+                        ctx.setStage( NodeLifecycle.ATTRIBUTE_EQUALS );
+                    }
+                    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 );
 
-        private Counter m_counter = null;;
+                        // 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 );
+                    }
+                    break;
+                }
+            }
+        }
 
-        private NodeLifecycle m_stage = NodeLifecycle.TEXT_NODE;
+    }
 
-        public ParseContext( Counter counter )
+    public static class HtmlTagParser extends ParserDelegate
+    {
+        public void beginStage()
         {
-            m_counter = counter;
         }
-        
-        private Map<NodeLifecycle,Integer> m_markers = new HashMap<NodeLifecycle,Integer>();
 
-        /**
-         * 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, for example the position of the left angle-bracket
-         * during the {@link NodeLifecycle#TAG_RESOLUTION} stage.
-         */
-        public void mark()
+        public void endStage()
         {
-            m_markers.put( getStage(), position() );
+            ParseContext ctx = ParseContext.currentContext();
+            Node node = ctx.getNode();
+
+            // Finalize the node type if it is still undefined
+            if( node.getType() == NodeType.UNRESOLVED_HTML_TAG )
+            {
+                char lastCh = ctx.getSource().charAt( ctx.position() - 1 );
+                if( lastCh == '/' )
+                {
+                    node.setType( NodeType.HTML_COMBINED_TAG );
+                }
+                else
+                {
+                    // 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 );
+                }
+            }
+
+            // 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 );
+            }
         }
 
         /**
-         * Retrieves the position of the marker set for the current stage (as set by {@link #mark()}).
-         * If no marker was set, this method returns {@link Node#POSITION_NOT_SET}.
-         * @param stage the stage for which the marker position is desired
-         * @return the position of the marker.
+         * Handles {@link NodeLifecycle#TAG_NAME}.
          */
-        public int getMarker( NodeLifecycle stage )
+        public void handle( char ch )
         {
-            Integer mark = m_markers.get( stage );
-            return mark == null ? Node.POSITION_NOT_SET : mark.intValue();
-        }
+            ParseContext ctx = ParseContext.currentContext();
 
-        public Attribute getAttribute()
-        {
-            return m_attribute;
-        }
+            switch( ctx.getStage() )
+            {
+                // After < but before whitespace that delimit attributes.
+                case TAG_NAME: {
+                    switch( ch )
+                    {
+                        // If current character is whitespace, set the name and
+                        // move
+                        // to attributes stage
+                        case (' '): {
+                            finalizeTagName();
+                            ctx.setParser( ATTRIBUTE_PARSER );
+                            ctx.setStage( NodeLifecycle.TAG_WHITESPACE );
+                            break;
+                        }
 
-        public Counter getCounter()
-        {
-            return m_counter;
-        }
+                            // Right angle bracket == end of the node
+                        case ('>'): {
+                            finalizeTagName();
+                            ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                            break;
+                        }
+                    }
+                    break;
+                }
 
-        public Node getNode()
-        {
-            return m_node;
-        }
+                    // Whitespace immediately before >
+                case TAG_WHITESPACE: {
+                    switch( ch )
+                    {
+                        case ('>'): {
+                            ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                            break;
+                        }
+                    }
+                    break;
+                }
 
-        public NodeLifecycle getStage()
-        {
-            return m_stage;
-        }
+                case CODE_OR_COMMENT: {
+                    switch( ch )
+                    {
+                        // Terminating %> means the end of the comment
+                        case ('>'): {
+                            Node node = ctx.getNode();
+                            String lookbehind = ctx.lookbehind( 3 );
+                            if( lookbehind.equals( NodeType.HTML_COMMENT.getTagEnd() ) )
+                            {
+                                // Set the end position
+                                node.setEnd( ctx.position() + 1 );
 
-        public void incrementPosition()
-        {
-            m_counter.increment();
+                                // Set the value
+                                NodeType type = node.getType();
+                                node.setValue( ctx.getSource().substring( node.getStart() + type.getTagStart().length(),
+                                                                          node.getEnd() - type.getTagEnd().length() ) );
+
+                                ctx.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+                            }
+                            break;
+                        }
+                    }
+                    break;
+                }
+            }
         }
 
-        public int position()
+        private void finalizeTagName()
         {
-            return m_counter.position();
+            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 void setAttribute( Attribute attribute )
+    public static class JspTagParser extends ParserDelegate
+    {
+        public void beginStage()
         {
-            m_attribute = attribute;
         }
 
-        public void setNode( Node node )
+        public void endStage()
         {
-            m_node = node;
         }
 
-        public void setStage( NodeLifecycle stage )
+        /**
+         * Handles {@link NodeLifecycle#TAG_WHITESPACE},
+         * {@link NodeLifecycle#TAG_NAME},
+         * {@link NodeLifecycle#CODE_OR_COMMENT}.
+         */
+        public void handle( char ch )
         {
-            m_stage = stage;
-        }
-    }
+            ParseContext ctx = ParseContext.currentContext();
+            Node node = ctx.getNode();
 
-    private final List<Integer> lineBreaks = new ArrayList<Integer>();
+            switch( ctx.getStage() )
+            {
+                // 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;
+                }
+                    // 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;
+                }
 
-    private JspDocument doc = null;
+                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 );
 
-    protected Stack<ParseContext> contextStack = new Stack<ParseContext>();
+                                // Set the value
+                                NodeType type = node.getType();
+                                node.setValue( ctx.getSource().substring( node.getStart() + type.getTagStart().length(),
+                                                                          node.getEnd() - type.getTagEnd().length() ) );
 
-    private Stack<Node> nodeStack = new Stack<Node>();
+                                // 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;
+                        }
+                    }
+                    break;
+                }
+            }
 
-    private String m_source;
+        }
 
-    /** The current parsing context. */
-    private ParseContext context;
+    }
 
-    /**
-     * Constructs a new JspDocument.
-     */
-    public JspParser()
+    public static abstract class ParserDelegate
     {
-        super();
+        public abstract void beginStage();
+
+        public abstract void endStage();
+
+        public abstract void handle( char ch );
     }
 
-    /**
-     * Parses a JSP file, supplied as a String, into Nodes.
-     * 
-     * @param m_source the JSP file contents
-     */
-    public JspDocument parse( String source )
+    public static class TextParser extends ParserDelegate
     {
-        // Initialize the cached document, m_source, and stack variables
-        this.doc = new JspDocument();
-        m_source = source;
-        contextStack.clear();
-        nodeStack.clear();
-        nodeStack.push( doc.getRoot() );
 
-        // Create new parse context and put it on the stack
-        context = new ParseContext( new Counter() );
-        initText( context.position() );
+        public void beginStage()
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            int pos = ctx.position() + 1;
 
-        // Initialize parser delegates
-        ParserDelegate textParser = new TextParser();
+            // Create new Text
+            Text text = new Text( ctx.getDocument() );
 
-        // Parse the file, character by character
-        for( char currentChar : source.toCharArray() )
+            // Set parent relationship
+            text.setParent( ctx.getParentNode() );
+            ctx.setNode( text );
+            ctx.setAttribute( null );
+
+            // Set the start, end, linebreak
+            ctx.setStartPosition( text, pos );
+        }
+
+        public void endStage()
         {
-            // Is the current character whitespace?
-            boolean isWhitespace = Character.isWhitespace( currentChar );
-            char ch = isWhitespace ? ' ' : currentChar; // For case statements
-            int pos = context.position();
+            // Finalize current node
+            ParseContext ctx = ParseContext.currentContext();
+            if( ctx.position() > 0 )
+            {
+                Node node = ctx.getNode();
 
-            switch( context.getStage() )
+                // Set the end position
+                node.setEnd( ctx.position() );
+
+                // Set the node value
+                node.setValue( ctx.getSource().substring( node.getStart(), node.getEnd() ) );
+
+                // If node length is > 0, add it to the parent
+                if( node.getEnd() > node.getStart() )
+                {
+                    node.getParent().addChild( node );
+                }
+            }
+        }
+
+        /**
+         * Handles {@link NodeLifecycle#PARSING_TEXT}.
+         */
+        public void handle( char ch )
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            switch( ctx.getStage() )
             {
-                // Part of a text node.
-                case TEXT_NODE: {
-                    textParser.handle( ch, context );
+                case PARSING_TEXT: {
                     switch( ch )
                     {
                         // If we see a quote, check to see if it's a part of a
                         // parent attribute
                         case ('\''):
                         case ('"'): {
-                            if( contextStack.size() > 0 )
+                            if( ctx.hasParentContext() )
                             {
-                                Attribute parentAttribute = contextStack.peek().getAttribute();
-                                if( parentAttribute != null && ch == parentAttribute.getAttributeDelimiter() )
+                                Attribute attribute = ctx.getParentContext().getAttribute();
+                                if( attribute != null && ch == attribute.getAttributeDelimiter() )
                                 {
-                                    // Finish the current text node and attach
-                                    // it to the parent attribute
-                                    finalizeText();
-
-                                    // Restore the parent ParseContext and Node
-                                    context = contextStack.pop();
-                                    nodeStack.pop();
+                                    // Pop the ParseContext (and run its
+                                    // endStage method) and Node
+                                    ctx = ctx.pop();
+                                    ctx.popNode();
 
                                     // Finish the parent attribute
-                                    finalizeAttribute();
+                                    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 );
                                 }
                             }
                             break;
                         }
                         case ('<'): {
-                            // Finalize current node and start a new
-                            // (unresolved) one
-                            finalizeText();
-                            initTag( NodeType.UNRESOLVED, context.position() );
-                            context.setStage( NodeLifecycle.TAG_RESOLUTION );
-                            break;
-                        }
-                    }
-                    break;
-                }
 
-                case TAG_RESOLUTION: {
-                    Node node = context.getNode();
-                    switch( node.getType() )
-                    {
+                            // Figure out what this tag is
+                            String lookahead = ctx.lookahead( 4 );
+                            JspDocument doc = ctx.getDocument();
 
-                        case UNRESOLVED: {
-                            switch( ch )
+                            // <%- means hidden JSP comment
+                            if( lookahead.startsWith( NodeType.JSP_COMMENT.getTagStart() ) )
                             {
-                                // If <%, it's a JSP element
-                                case ('%'): {
-                                    // Re-initialize the node as a JSPMarkup
-                                    // node
-                                    initJspMarkup();
-                                    break;
-                                }
-
-                                    // If </, it's an HTML end tag
-                                case ('/'): {
-                                    // Re-initialize the node as an end tag
-                                    initTag( NodeType.HTML_END_TAG, node.getStart() );
-                                    context.setStage( NodeLifecycle.TAG_NAME );
-                                    break;
-                                }
-
-                                    // If < plus space, it's just ordinary
-                                    // (albeit sloppy) markup
-                                case (' '): {
-                                    // Re-initialize the node as a text node
-                                    initText( node.getStart() );
-                                    context.setStage( NodeLifecycle.TEXT_NODE );
-                                    break;
-                                }
-
-                                    // Any other char means its HTML start tag
-                                    // or combined tag
-                                default: {
-                                    node.setType( NodeType.UNRESOLVED_HTML_TAG );
-                                    context.setStage( NodeLifecycle.TAG_NAME );
-                                }
+                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
+                                initNode( new Markup( doc, NodeType.JSP_COMMENT ) );
                             }
-                            break;
-                        }
 
-                            // If JSP element, next character narrows it down
-                        case UNRESOLVED_JSP_TAG: {
-                            switch( ch )
-                            {
-                                // Dash after <% means hidden JSP
-                                // comment
-                                case ('-'): {
-                                    node.setType( NodeType.JSP_COMMENT );
-                                    context.setStage( NodeLifecycle.CODE_OR_COMMENT );
-                                    break;
-                                }
+                            // <%! 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 ) );
+                            }
 
-                                    // Bang after <% means JSP
-                                    // declaration
-                                case ('!'): {
-                                    node.setType( NodeType.JSP_DECLARATION );
-                                    context.setStage( NodeLifecycle.CODE_OR_COMMENT );
-                                    break;
-                                }
+                            // <%= means JSP expression
+                            else if( lookahead.startsWith( NodeType.JSP_EXPRESSION.getTagStart() ) )
+                            {
+                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
+                                initNode( new Markup( doc, NodeType.JSP_EXPRESSION ) );
+                            }
 
-                                    // Equals after <% means JSP
-                                    // expression
-                                case ('='): {
-                                    node.setType( NodeType.JSP_EXPRESSION );
-                                    context.setStage( NodeLifecycle.CODE_OR_COMMENT );
-                                    break;
-                                }
+                            // <%@ means JSP directive
+                            else if( lookahead.startsWith( NodeType.JSP_DIRECTIVE.getTagStart() ) )
+                            {
+                                ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.TAG_WHITESPACE );
+                                initNode( new JspDirective( doc ) );
+                            }
 
-                                    // At-sign after <% means JSP
-                                    // directive
-                                case ('@'): {
-                                    // Re-initialize the node as a JspDirective
-                                    initJspDirective();
-                                    context.setStage( NodeLifecycle.BETWEEN_ATTRIBUTES );
-                                    break;
-                                }
+                            // <!-- 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
-                                case (' '): {
-                                    node.setType( NodeType.SCRIPTLET );
-                                    context.setStage( NodeLifecycle.CODE_OR_COMMENT );
-                                    break;
+                            // Whitespace after <% means
+                            // scriptlet
+                            else if( lookahead.startsWith( NodeType.SCRIPTLET.getTagStart() ) )
+                            {
+                                if( lookahead.length() >= 3 && Character.isWhitespace( lookahead.charAt( 2 ) ) )
+                                {
+                                    ctx.setParser( JSP_TAG_PARSER, NodeLifecycle.CODE_OR_COMMENT );
+                                    initNode( new Markup( doc, NodeType.SCRIPTLET ) );
                                 }
                             }
-                            break;
-                        }
-                    }
-                    break;
-                }
 
-                case CODE_OR_COMMENT: {
-                    switch( ch )
-                    {
-                        // Terminating %> means the end of the scriptlet
-                        case ('>'): {
-                            if( source.charAt( pos - 1 ) == '%' )
+                            // If </, it's an HTML end tag
+                            else if( lookahead.startsWith( NodeType.HTML_END_TAG.getTagStart() ) )
                             {
-                                finalizeJspMarkup();
-                                initText( pos + 1 );
+                                ctx.setParser( HTML_TAG_PARSER, NodeLifecycle.TAG_NAME );
+                                initNode( new Tag( doc, NodeType.HTML_END_TAG ) );
                             }
-                            break;
-                        }
-                    }
-                    break;
-                }
-
-                    // Characters that supply the JSP directive name.
-                case JSP_DIRECTIVE_NAME: {
-                    if( isWhitespace )
-                    {
-                        Node node = context.getNode();
-                        Attribute directive = context.getAttribute();
-                        node.setName( m_source.substring( directive.getStart(), context.position() ) );
-                        context.setAttribute( null );
-                        context.setStage( NodeLifecycle.BETWEEN_ATTRIBUTES );
-                    }
-                    break;
-                }
 
-                    // After < but before whitespace that delimit attributes.
-                case TAG_NAME: {
-                    switch( ch )
-                    {
-                        // If current character is whitespace, set the name and
-                        // move
-                        // to attributes stage
-                        case (' '): {
-                            finalizeTagName();
-                            context.setStage( NodeLifecycle.BETWEEN_ATTRIBUTES );
-                            break;
-                        }
+                            // 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 ) );
+                            }
 
-                            // Right angle bracket == end of the node
-                        case ('>'): {
-                            finalizeTagName();
-                            finalizeNode( pos + 1 );
-                            initText( pos + 1 );
-                            break;
-                        }
-                    }
-                    break;
-                }
+                            // 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 ) );
+                            }
 
-                    // Whitespace between node name and first attribute, or
-                    // between attributes.
-                case BETWEEN_ATTRIBUTES: {
-                    switch( ch )
-                    {
-                        case (' '): {
                             break;
                         }
-                        case ('/'): {
-                            // Ignore the / because we might be in HTML
-                            // combined tag
-                            break;
-                        }
-                        case ('%'): {
-                            // Ignore the % because we might be in a JSP
-                            // directive/element
-                            break;
-                        }
-                        case ('>'): {
-                            finalizeNode( pos + 1 );
-                            initText( pos + 1 );
-                            break;
-                        }
-                        default:
-                            initAttribute();
-                    }
-                    break;
-                }
-
-                    // Characters that name an attribute, after whitespace but
-                    // before equals (=).
-                case ATTRIBUTE_NAME: {
-                    if( ch == '=' )
-                    {
-                        Attribute attribute = context.getAttribute();
-                        attribute.setName( m_source.substring( attribute.getStart(), pos ) );
-                        context.setStage( NodeLifecycle.ATTRIBUTE_EQUALS );
-                    }
-                    break;
-                }
-
-                    // The equals (=) that separates the attribute name and
-                    // value.
-                case ATTRIBUTE_EQUALS: {
-                    if( ch == '\'' || ch == '\"' )
-                    {
-                        // Save the quote delimiter for later
-                        Attribute attribute = context.getAttribute();
-                        attribute.setAttributeDelimiter( ch );
-
-                        // Push current ParseContext and Node onto stack
-                        contextStack.push( context );
-                        nodeStack.push( attribute );
-
-                        // Create new context, with text node as child of
-                        // attribute
-                        context = new ParseContext( context.getCounter() );
-                        initText( context.position() + 1 );
-                        context.getNode().setParent( attribute );
                     }
                     break;
                 }
-
             }
 
-            // Reset the line/column counters if we encounter linebreaks
-            if( currentChar == '\r' || currentChar == '\n' )
+        }
+
+        /**
+         * 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 )
             {
-                lineBreaks.add( pos );
+                ctx.popNode();
             }
 
-            // Increment the character position
-            context.incrementPosition();
+            // 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();
+            }
         }
 
-        // Finalize the last node and return the parsed JSP
-        finalizeNode( context.position() );
-        return doc;
     }
 
-    private void finalizeJspMarkup()
+    private static final class Counter
     {
-        Node node = context.getNode();
-        NodeType type = node.getType();
+        private int m_pos;
 
-        // Set the end position
-        node.setEnd( context.position() + 1 );
+        Counter()
+        {
+            m_pos = 0;
+        }
 
-        node.setValue( m_source
-            .substring( node.getStart() + type.getTagStart().length(), node.getEnd() - type.getTagEnd().length() ) );
+        public void increment()
+        {
+            m_pos++;
+        }
 
-        // If node length is > 0, add it to the parent
-        if( node.getEnd() > node.getStart() )
+        public int position()
         {
-            node.getParent().addChild( node );
+            return m_pos;
         }
     }
 
-    private void finalizeAttribute()
+    private enum NodeLifecycle
+    {
+        /** Parsing any text outside of a tag or element. */
+        PARSING_TEXT,
+        /**
+         * Characters after the opening left bracket (&lt;), but before
+         * whitespace that delimit the attributes.
+         */
+        TAG_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,
+        /** Any text inside of a scriptlet, JSP comment, or JSP declaration. */
+        CODE_OR_COMMENT,
+    }
+
+    /**
+     * Encapsulates the current state of document parsing.
+     */
+    private static class ParseContext
     {
-        Attribute attribute = context.getAttribute();
-        Node node = attribute.getParent();
-        attribute.setEnd( context.position() + 1 );
-        if( node.isHtmlNode() )
+        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;
+
+        public static ParseContext currentContext()
+        {
+            return CURRENT_CONTEXT;
+        }
+
+        public static ParseContext initialContext( JspDocument doc, String source )
         {
-            ((Tag) node).addAttribute( attribute );
+            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_parser = TEXT_PARSER;
+
+            // Return the init'ed context
+            return context;
         }
-        else if( node.getType() == NodeType.JSP_DIRECTIVE )
+
+        private ParserDelegate 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 JspDocument m_doc;
+
+        private Map<NodeLifecycle, Integer> m_markers = new HashMap<NodeLifecycle, Integer>();
+
+        private ParseContext( JspDocument doc, String source, Counter counter )
         {
-            ((JspDirective) node).addAttribute( attribute );
+            super();
+            m_doc = doc;
+            m_source = source;
+            m_counter = counter;
         }
-        context.setAttribute( null );
-        context.setStage( NodeLifecycle.BETWEEN_ATTRIBUTES );
-    }
 
-    private void finalizeText()
-    {
-        Node node = context.getNode();
+        /**
+         * Returns an arbitrary number of characters ahead of the current
+         * position, starting with the current character. For example, if the
+         * current position returned by {@link #position()} is 70,
+         * <code>lookahead(3)</code> returns the characters at positions 70,
+         * 71 and 72. If the supplied length causes the lookahead position to
+         * exceed the length of the source string, the substring is truncated
+         * appropriately.
+         * 
+         * @param length the number of characters to return
+         * @return the substring
+         */
+        public String lookahead( int length )
+        {
+            int startPos = position();
+            int endPos = startPos + length > m_source.length() ? endPos = m_source.length() : startPos + length;
+            return m_source.substring( startPos, endPos );
+        }
 
-        // Set the end position
-        node.setEnd( context.position() );
+        /**
+         * Returns an arbitrary number of characters behind the current
+         * position, starting with the current character. For example, if the
+         * current position returned by {@link #position()} is 70,
+         * <code>lookbehind(3)</code> returns the characters at positions 68,
+         * 69 and 70. If the supplied length causes the lookbehind position to
+         * exceed the length of the source string, the substring is truncated
+         * appropriately.
+         * 
+         * @param length the number of characters to return
+         * @return the substring
+         */
+        public String lookbehind( int length )
+        {
+            int endPos = position() + 1;
+            int startPos = endPos - length < 0 ? 0 : endPos - length;
+            return m_source.substring( startPos, endPos );
+        }
 
-        node.setValue( m_source.substring( node.getStart(), node.getEnd() ) );
+        public Node getParentNode()
+        {
+            return NODE_STACK.peek();
+        }
 
-        // If node length is > 0, add it to the parent
-        if( node.getEnd() > node.getStart() )
+        public Attribute getAttribute()
         {
-            node.getParent().addChild( node );
+            return m_attribute;
         }
-    }
 
-    /**
-     * Finalizes the current Tag (returned by {@link ParseContext#getNode()}),
-     * adds it as a child to the parent node, and initializes a new {@link Text}
-     * node that will begin at a specified character position. The parent node
-     * is determined by taking the Node currently at the top of the internal
-     * node stack.
-     * 
-     * @param pos the desired start position for the new text node
-     */
-    private void finalizeNode( int pos )
-    {
-        Node node = context.getNode();
-        NodeType type = node.getType();
+        public Counter getCounter()
+        {
+            return m_counter;
+        }
 
-        // Set the end position
-        node.setEnd( pos );
+        public JspDocument getDocument()
+        {
+            return m_doc;
+        }
 
-        // Finalize the node type if it is still undefined
-        if( type == NodeType.UNRESOLVED_HTML_TAG )
+        /**
+         * Retrieves the position of the marker set for the current stage (as
+         * set by {@link #mark()}). If no marker was set, this method returns
+         * {@link Node#POSITION_NOT_SET}.
+         * 
+         * @param stage the stage for which the marker position is desired
+         * @return the position of the marker.
+         */
+        public int getMarker( NodeLifecycle stage )
         {
-            char lastCh = m_source.charAt( pos - 2 );
-            if( lastCh == '/' )
-            {
-                node.setType( NodeType.HTML_COMBINED_TAG );
-            }
-            else
+            Integer mark = m_markers.get( stage );
+            return mark == null ? Node.POSITION_NOT_SET : mark.intValue();
+        }
+
+        public Node getNode()
+        {
+            return m_node;
+        }
+
+        /**
+         * 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.
+         * 
+         * @param parser the parser to set
+         */
+        public void setParser( ParserDelegate parser )
+        {
+            m_parser = parser;
+        }
+
+        public ParseContext getParentContext()
+        {
+            return CONTEXT_STACK.peek();
+        }
+
+        public ParserDelegate getParser()
+        {
+            return m_parser;
+        }
+
+        public String getSource()
+        {
+            return m_source;
+        }
+
+        public NodeLifecycle getStage()
+        {
+            return m_stage;
+        }
+
+        /**
+         * Returns <code>true</code> if one or more ParseContexts have been
+         * pushed onto the stack (via {@link #push()}); <code>false</code>
+         * otherwise.
+         * 
+         * @return the result
+         */
+        public boolean hasParentContext()
+        {
+            return CONTEXT_STACK.size() > 0;
+        }
+
+        public void incrementPosition()
+        {
+            // Reset the line/column counters if we encounter linebreaks
+            char currentChar = m_source.charAt( position() );
+            if( currentChar == '\r' || currentChar == '\n' )
             {
-                // If no /, it's an HTML start tag, and new nodes should be
-                // children of it
-                node.setType( NodeType.HTML_START_TAG );
-                nodeStack.push( node );
+                lineBreaks.add( position() );
             }
+
+            m_counter.increment();
         }
-        else if( type == NodeType.TEXT )
+
+        /**
+         * 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()
         {
-            node.setValue( m_source.substring( node.getStart(), node.getEnd() ) );
+            m_markers.put( getStage(), position() );
         }
-        else if( node.isJspNode() )
+
+        /**
+         * 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.
+         */
+        public ParseContext pop()
         {
-            node.setValue( m_source.substring( node.getStart() + type.getTagStart().length(), node.getEnd()
-                                                                                              - type.getTagEnd().length() ) );
+            // Run the endStage method for the current stage
+            ParseContext ctx = ParseContext.currentContext();
+            ctx.getParser().endStage();
+
+            ctx = CONTEXT_STACK.pop();
+            CURRENT_CONTEXT = ctx;
+            return ctx;
         }
 
-        // If node length is > 0, add it to the parent
-        if( node.getEnd() > node.getStart() )
+        public void popNode()
         {
-            node.getParent().addChild( node );
+            NODE_STACK.pop();
         }
-    }
 
-    private void finalizeTagName()
-    {
-        Node node = context.getNode();
-        int nameStart = node.getStart() + node.getType().getTagStart().length();
-        int pos = context.position();
-        if( pos - nameStart > 0 )
+        public int position()
         {
-            node.setName( m_source.substring( nameStart, pos ) );
+            return m_counter.position();
         }
-    }
 
-    private void initAttribute()
-    {
-        Attribute attribute = new Attribute( doc );
-        attribute.setParent( context.getNode() );
-        context.setAttribute( attribute );
+        /**
+         * 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.
+         * 
+         * @return the new ParseContext
+         */
+        public ParseContext push()
+        {
+            CONTEXT_STACK.push( this );
+            ParseContext context = new ParseContext( m_doc, m_source, m_counter );
+            CURRENT_CONTEXT = context;
 
-        // Set the start, end, linebreak
-        updatePosition( attribute, context.position() );
+            // Set the default stage to TEXT
+            context.setParser( TEXT_PARSER, NodeLifecycle.PARSING_TEXT );
+
+            return context;
+        }
 
-        // Set the correct lifecycle stage
-        Node node = context.getNode();
-        if( node.getType() == NodeType.JSP_DIRECTIVE && node.getName() == null )
+        public void pushNode( Node node )
         {
-            context.setStage( NodeLifecycle.JSP_DIRECTIVE_NAME );
+            NODE_STACK.push( node );
         }
-        else
+
+        /**
+         * Sets the ParseContext's current attribute, and sets it start position
+         * if not null.
+         * 
+         * @param attribute
+         */
+        public void setAttribute( Attribute attribute )
         {
-            context.setStage( NodeLifecycle.ATTRIBUTE_NAME );
+            m_attribute = attribute;
+            if( attribute != null )
+            {
+                setStartPosition( attribute, position() );
+            }
         }
-    }
 
-    /**
-     * Sets the start, end and line/column positions for a supplied node, based
-     * on the position in the ParseContext.
-     * 
-     * @param node the node to set
-     */
-    private void updatePosition( Node node, int pos )
-    {
-        // Set the start, end, linebreak
-        node.setStart( pos );
-        int lastLineBreakPos = lineBreaks.size() == 0 ? Node.POSITION_NOT_SET : lineBreaks.get( lineBreaks.size() - 1 );
-        node.setLine( lineBreaks.size() + 1 );
-        node.setColumn( pos - lastLineBreakPos );
-    }
+        public void setNode( Node node )
+        {
+            m_node = node;
+        }
 
-    /**
-     * Factory method that returns a new JspDirective node.
-     */
-    private void initJspDirective()
-    {
-        // Create new JspDirective
-        JspDirective node = new JspDirective( doc );
+        /**
+         * Sets the current lifecycle stage, without resetting the current
+         * parser.
+         * 
+         * @param stage
+         */
+        public void setStage( NodeLifecycle stage )
+        {
+            // Set the new stage and set a marker at the current position
+            m_stage = stage;
+            mark();
+        }
+
+        /**
+         * <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
+         * stage is initialized by calling the new parser's
+         * {@link ParserDelegate #beginStage()} method.
+         * </p>
+         * 
+         * @param stage
+         */
+        public void setParser( ParserDelegate parser, NodeLifecycle stage )
+        {
+            // Finish the parser's current stage
+            if( parser != null && m_parser != null )
+            {
+                m_parser.endStage();
+            }
 
-        // Set parent relationship
-        node.setParent( nodeStack.peek() );
-        context.setNode( node );
-        context.setAttribute( null );
+            // Set the new stage and set a marker at the current position
+            m_stage = stage;
+            mark();
+
+            // Replace the parser and start it up
+            if( parser != null )
+            {
+                m_parser = parser;
+                m_parser.beginStage();
+            }
+        }
+
+        /**
+         * Sets the start line/column positions for a supplied node, based on
+         * the position in the ParseContext.
+         * 
+         * @param node the node to set
+         * @param the position to set
+         */
+        private void setStartPosition( Node node, int pos )
+        {
+            // Set the start, end, linebreak
+            node.setStart( pos );
+            int lastLineBreakPos = lineBreaks.size() == 0 ? Node.POSITION_NOT_SET : lineBreaks.get( lineBreaks.size() - 1 );
+            node.setLine( lineBreaks.size() + 1 );
+            node.setColumn( pos - lastLineBreakPos );
+        }
 
-        // Set the start, end, linebreak
-        updatePosition( node, context.position() - 2 );
+        /**
+         * Sets the end position for a supplied node, based on the current
+         * position in the ParseContext plus one.
+         * 
+         * @param node to set
+         */
+        private void setEndPosition( Node node )
+        {
+            ParseContext ctx = ParseContext.currentContext();
+            int pos = ctx.position() + 1;
+            if( pos > ctx.getSource().length() )
+            {
+                pos = ctx.getSource().length();
+            }
+            node.setEnd( pos );
+        }
     }
 
     /**
-     * Factory method that returns a new JspMarkup node.
+     * Constructs a new JspDocument.
      */
-    private void initJspMarkup()
+    public JspParser()
     {
-        // Create new JspMarkup
-        JspMarkup node = new JspMarkup( doc, NodeType.UNRESOLVED_JSP_TAG );
-
-        // Set parent relationship
-        node.setParent( nodeStack.peek() );
-        context.setNode( node );
-        context.setAttribute( null );
-
-        // Set the start, end, linebreak
-        updatePosition( node, context.position() - 1 );
+        super();
     }
 
     /**
-     * Factory method that constructs and returns a new Tag. When constructed,
-     * 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.
+     * Parses a JSP file, supplied as a String, into Nodes.
      * 
-     * @param type the node type
-     * @param pos the start position for the tag
+     * @param m_source the JSP file contents
      */
-    private void initTag( NodeType type, int pos )
+    public JspDocument parse( String source )
     {
-        // Create new Tag
-        Tag tag = new Tag( doc, type );
-
-        // If HTML end tag, pop the node stack first
-        if( tag.getType() == NodeType.HTML_END_TAG )
-        {
-            nodeStack.pop();
-        }
+        // Initialize the cached document, m_source, and stack variables
+        JspDocument doc = new JspDocument();
 
-        // Set parent relationship
-        tag.setParent( nodeStack.peek() );
-        context.setNode( tag );
-        context.setAttribute( null );
+        // Create new parse context and put it on the stack
+        ParseContext ctx = ParseContext.initialContext( doc, source );
 
-        // Set the start, end, linebreak
-        updatePosition( tag, pos );
-    }
+        // Parse the file, character by character
+        int pos = ctx.position();
+        while ( pos < source.length() )
+        {
+            ctx = ParseContext.currentContext();
+            char currentChar = source.charAt( pos );
 
-    private void initText( int pos )
-    {
-        // Create new Text
-        Text text = new Text( doc );
+            // Is the current character whitespace?
+            boolean isWhitespace = Character.isWhitespace( currentChar );
+            char ch = isWhitespace ? ' ' : currentChar; // For case statements
 
-        // Set parent relationship
-        text.setParent( nodeStack.peek() );
-        context.setNode( text );
-        context.setAttribute( null );
-
-        // Set the start, end, linebreak
-        updatePosition( text, pos );
-        context.setStage( NodeLifecycle.TEXT_NODE );
-    }
+            // Handle the current character
+            ParserDelegate parser = ctx.getParser();
+            parser.handle( ch );
 
-    public static abstract class ParserDelegate
-    {
-        public abstract void handle( char ch, ParseContext context );
-    }
+            // Increment the character position
+            ctx.incrementPosition();
+            pos = ctx.position();
+        }
 
-    public static class TextParser extends ParserDelegate
-    {
-        public void handle( char ch, ParseContext context )
+        // Finalize the last node and return the parsed JSP
+        Node node = ctx.getNode();
+        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 );
+            }
         }
 
+        return doc;
     }
 
 }