/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.transcoder.svg2svg;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.LinkedList;
import java.util.List;

import org.apache.batik.transcoder.ErrorHandler;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.xml.LexicalUnits;
import org.apache.batik.xml.XMLException;
import org.apache.batik.xml.XMLScanner;

/**
 * This class represents an SVG source files pretty-printer.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id: PrettyPrinter.java,v 1.7 2003/08/08 11:39:26 vhardy Exp $
 */
public class PrettyPrinter {

    // The doctype options.
    public final static int DOCTYPE_CHANGE = 0;
    public final static int DOCTYPE_REMOVE = 1;
    public final static int DOCTYPE_KEEP_UNCHANGED = 2;

    /**
     * The document scanner.
     */
    protected XMLScanner scanner;

    /**
     * The output manager.
     */
    protected OutputManager output;

    /**
     * The writer used to output the document.
     */
    protected Writer writer;

    /**
     * The error handler.
     */
    protected ErrorHandler errorHandler = SVGTranscoder.DEFAULT_ERROR_HANDLER;

    /**
     * The newline characters.
     */
    protected String newline = "\n";

    /**
     * Whether the output must be formatted.
     */
    protected boolean format = true;

    /**
     * The tabulation width.
     */
    protected int tabulationWidth = 4;

    /**
     * The document width.
     */
    protected int documentWidth = 80;

    /**
     * The doctype option.
     */
    protected int doctypeOption = DOCTYPE_KEEP_UNCHANGED;

    /**
     * The public id.
     */
    protected String publicId;

    /**
     * The system id.
     */
    protected String systemId;

    /**
     * The XML declaration.
     */
    protected String xmlDeclaration;

    /**
     * The type of the current lexical unit.
     */
    protected int type;

    /**
     * Sets the XML declaration text.
     */
    public void setXMLDeclaration(String s) {
        xmlDeclaration = s;
    }

    /**
     * Sets the doctype option.
     */
    public void setDoctypeOption(int i) {
        doctypeOption = i;
    }

    /**
     * Sets the public ID.
     */
    public void setPublicId(String s) {
        publicId = s;
    }

    /**
     * Sets the system ID.
     */
    public void setSystemId(String s) {
        systemId = s;
    }

    /**
     * Sets the newline characters.
     */
    public void setNewline(String s) {
        newline = s;
    }

    /**
     * Returns the newline characters.
     */
    public String getNewline() {
        return newline;
    }

    /**
     * Sets the format attribute.
     */
    public void setFormat(boolean b) {
        format = b;
    }

    /**
     * Returns whether the output must be formatted.
     */
    public boolean getFormat() {
        return format;
    }

    /**
     * Sets the tabulation width.
     */
    public void setTabulationWidth(int i) {
        tabulationWidth = Math.max(i, 0);
    }

    /**
     * Returns whether the tabulation width.
     */
    public int getTabulationWidth() {
        return tabulationWidth;
    }

    /**
     * Sets the document width.
     */
    public void setDocumentWidth(int i) {
        documentWidth = Math.max(i, 0);
    }

    /**
     * Returns whether the document width.
     */
    public int getDocumentWidth() {
        return documentWidth;
    }

    /**
     * Prints an SVG document from the given reader to the given writer.
     */
    public void print(Reader r, Writer w) throws TranscoderException,
                                                 IOException {
        try {
            scanner = new XMLScanner(r);
            output = new OutputManager(this, w);
            writer = w;
            type = scanner.next();

            printXMLDecl();

            misc1: for (;;) {
                switch (type) {
                case LexicalUnits.S:
                    output.printTopSpaces(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.COMMENT:
                    output.printComment(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.PI_START:
                    printPI();
                    break;
                default:
                    break misc1;
                }
            }

            printDoctype();

            misc2: for (;;) {
                scanner.clearBuffer();
                switch (type) {
                case LexicalUnits.S:
                    output.printTopSpaces(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.COMMENT:
                    output.printComment(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.PI_START:
                    printPI();
                    break;
                default:
                    break misc2;
                }
            }

            if (type != LexicalUnits.START_TAG) {
                throw fatalError("element", null);
            }
            
            printElement();

            misc3: for (;;) {
                switch (type) {
                case LexicalUnits.S:
                    output.printTopSpaces(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.COMMENT:
                    output.printComment(getCurrentValue());
                    scanner.clearBuffer();
                    type = scanner.next();
                    break;
                case LexicalUnits.PI_START:
                    printPI();
                    break;
                default:
                    break misc3;
                }
            }
        } catch (XMLException e) {
            errorHandler.fatalError(new TranscoderException(e.getMessage()));
        }
    }

    /**
     * Prints the XML declaration.
     */
    protected void printXMLDecl()
        throws TranscoderException,
               XMLException,
               IOException {
        if (xmlDeclaration == null) {
            if (type == LexicalUnits.XML_DECL_START) {
                if (scanner.next() != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                char[] space1 = getCurrentValue();
                
                if (scanner.next() != LexicalUnits.VERSION_IDENTIFIER) {
                    throw fatalError("token", new Object[] { "version" });
                }
                type = scanner.next();
            
                char[] space2 = null;
                if (type == LexicalUnits.S) {
                    space2 = getCurrentValue();
                    type = scanner.next();
                }
                if (type != LexicalUnits.EQ) {
                    throw fatalError("token", new Object[] { "=" });
                }
                type = scanner.next();
                
                char[] space3 = null;
                if (type == LexicalUnits.S) {
                    space3 = getCurrentValue();
                    type = scanner.next();
                }
                
                if (type != LexicalUnits.STRING) {
                    throw fatalError("string", null);
                }

                char[] version = getCurrentValue();
                char versionDelim = scanner.getStringDelimiter();

                char[] space4 = null;
                char[] space5 = null;
                char[] space6 = null;
                char[] encoding = null;
                char encodingDelim = 0;
                char[] space7 = null;
                char[] space8 = null;
                char[] space9 = null;
                char[] standalone = null;
                char standaloneDelim = 0;
                char[] space10 = null;
                
                type = scanner.next();
                if (type == LexicalUnits.S) {
                    space4 = getCurrentValue();
                    type = scanner.next();
                    
                    if (type == LexicalUnits.ENCODING_IDENTIFIER) {
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space5 = getCurrentValue();
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.EQ) {
                            throw fatalError("token", new Object[] { "=" });
                        }
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space6 = getCurrentValue();
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        encoding = getCurrentValue();
                        encodingDelim = scanner.getStringDelimiter();

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space7 = getCurrentValue();
                            type = scanner.next();
                        }
                    }
            
                    if (type == LexicalUnits.STANDALONE_IDENTIFIER) {
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space8 = getCurrentValue();
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.EQ) {
                            throw fatalError("token", new Object[] { "=" });
                        }
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space9 = getCurrentValue();
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }
                        
                        standalone = getCurrentValue();
                        standaloneDelim = scanner.getStringDelimiter();
                        
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space10 = getCurrentValue();
                            type = scanner.next();
                        }
                    }
                }
                if (type != LexicalUnits.PI_END) {
                    throw fatalError("pi.end", null);
                }

                output.printXMLDecl(space1, space2, space3,
                                    version, versionDelim,
                                    space4, space5, space6,
                                    encoding, encodingDelim,
                                    space7, space8, space9,
                                    standalone, standaloneDelim,
                                    space10);

                type = scanner.next();
            }
        } else {
            output.printString(xmlDeclaration);
            output.printNewline();

            if (type == LexicalUnits.XML_DECL_START) {
                // Skip the XML declaraction.
                if (scanner.next() != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                
                if (scanner.next() != LexicalUnits.VERSION_IDENTIFIER) {
                    throw fatalError("token", new Object[] { "version" });
                }
                type = scanner.next();
            
                if (type == LexicalUnits.S) {
                    type = scanner.next();
                }
                if (type != LexicalUnits.EQ) {
                    throw fatalError("token", new Object[] { "=" });
                }
                type = scanner.next();
                
                if (type == LexicalUnits.S) {
                    type = scanner.next();
                }
                
                if (type != LexicalUnits.STRING) {
                    throw fatalError("string", null);
                }

                type = scanner.next();
                if (type == LexicalUnits.S) {
                    type = scanner.next();
                    
                    if (type == LexicalUnits.ENCODING_IDENTIFIER) {
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.EQ) {
                            throw fatalError("token", new Object[] { "=" });
                        }
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                    }
            
                    if (type == LexicalUnits.STANDALONE_IDENTIFIER) {
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.EQ) {
                            throw fatalError("token", new Object[] { "=" });
                        }
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }
                        
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                    }
                }
                if (type != LexicalUnits.PI_END) {
                    throw fatalError("pi.end", null);
                }

                type = scanner.next();
            }
        }
    }

    /**
     * Prints a processing instruction.
     */
    protected void printPI()
        throws TranscoderException,
               XMLException,
               IOException {
        char[] target = getCurrentValue();

        type = scanner.next();
        char[] space = {};
        if (type == LexicalUnits.S) {
            space = getCurrentValue();
            type = scanner.next();
        }
        if (type != LexicalUnits.PI_DATA) {
            throw fatalError("pi.data", null);
        }
        char[] data = getCurrentValue();

        type = scanner.next();
        if (type != LexicalUnits.PI_END) {
            throw fatalError("pi.end", null);
        }

        output.printPI(target, space, data);

        type = scanner.next();
    }

    /**
     * Prints the doctype.
     */
    protected void printDoctype()
        throws TranscoderException,
               XMLException,
               IOException {
        switch (doctypeOption) {
        default:
            if (type == LexicalUnits.DOCTYPE_START) {
                type = scanner.next();

                if (type != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                char[] space1 = getCurrentValue();
                type = scanner.next();
                
                if (type != LexicalUnits.NAME) {
                    throw fatalError("name", null);
                }

                char[] root = getCurrentValue();
                char[] space2 = null;
                String externalId = null;
                char[] space3 = null;
                char[] string1 = null;
                char string1Delim = 0;
                char[] space4 = null;
                char[] string2 = null;
                char string2Delim = 0;
                char[] space5 = null;

                type = scanner.next();
                if (type == LexicalUnits.S) {
                    space2 = getCurrentValue();
                    type = scanner.next();
                    
                    switch (type) {
                    case LexicalUnits.PUBLIC_IDENTIFIER:
                        externalId = "PUBLIC";
                        
                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        space3 = getCurrentValue();
                        type = scanner.next();
                        
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }
                    
                        string1 = getCurrentValue();
                        string1Delim = scanner.getStringDelimiter();

                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        space4 = getCurrentValue();
                        type = scanner.next();
                        
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        string2 = getCurrentValue();
                        string2Delim = scanner.getStringDelimiter();

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space5 = getCurrentValue();
                            type = scanner.next();
                        }
                        break;
                    case LexicalUnits.SYSTEM_IDENTIFIER:
                        externalId = "SYSTEM";
                        
                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        space3 = getCurrentValue();
                        type = scanner.next();

                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        string1 = getCurrentValue();
                        string1Delim = scanner.getStringDelimiter();

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            space4 = getCurrentValue();
                            type = scanner.next();
                        }
                    }
                }

                if (doctypeOption == DOCTYPE_CHANGE) {
                    if (publicId != null) {
                        externalId = "PUBLIC";
                        string1 = publicId.toCharArray();
                        string1Delim = '"';
                        if (systemId != null) {
                            string2 = systemId.toCharArray();
                            string2Delim = '"';
                        }
                    } else if (systemId != null) {
                        externalId = "SYSTEM";
                        string1 = systemId.toCharArray();
                        string1Delim = '"';
                        string2 = null;
                    }
                }
                output.printDoctypeStart(space1, root, space2,
                                         externalId, space3,
                                         string1, string1Delim,
                                         space4,
                                         string2, string2Delim,
                                         space5);

                if (type == LexicalUnits.LSQUARE_BRACKET) {
                    output.printCharacter('[');
                    type = scanner.next();
                    
                    dtd: for (;;) {
                        switch (type) {
                        case LexicalUnits.S:
                            output.printSpaces(getCurrentValue(), true);
                            scanner.clearBuffer();
                            type = scanner.next();
                            break;
                        case LexicalUnits.COMMENT:
                            output.printComment(getCurrentValue());
                            scanner.clearBuffer();
                            type = scanner.next();
                            break;
                        case LexicalUnits.PI_START:
                            printPI();
                            break;
                        case LexicalUnits.PARAMETER_ENTITY_REFERENCE:
                            output.printParameterEntityReference(getCurrentValue());
                            scanner.clearBuffer();
                            type = scanner.next();
                            break;
                        case LexicalUnits.ELEMENT_DECLARATION_START:
                            scanner.clearBuffer();
                            printElementDeclaration();
                            break;
                        case LexicalUnits.ATTLIST_START:
                            scanner.clearBuffer();
                            printAttlist();
                            break;
                        case LexicalUnits.NOTATION_START:
                            scanner.clearBuffer();
                            printNotation();
                            break;
                        case LexicalUnits.ENTITY_START:
                            scanner.clearBuffer();
                            printEntityDeclaration();
                            break;
                        case LexicalUnits.RSQUARE_BRACKET:
                            output.printCharacter(']');
                            scanner.clearBuffer();
                            type = scanner.next();
                            break dtd;
                        default:
                            throw fatalError("xml", null);
                        }
                    }
                }
                char[] endSpace = null;
                if (type == LexicalUnits.S) {
                    endSpace = getCurrentValue();
                    type = scanner.next();
                }

                if (type != LexicalUnits.END_CHAR) {
                    throw fatalError("end", null);
                }
                type = scanner.next();
                output.printDoctypeEnd(endSpace);
            } else {
                if (doctypeOption == DOCTYPE_CHANGE) {
                    String externalId = "PUBLIC";
                    char[] string1 = SVGConstants.SVG_PUBLIC_ID.toCharArray();
                    char[] string2 = SVGConstants.SVG_SYSTEM_ID.toCharArray();
                    if (publicId != null) {
                        string1 = publicId.toCharArray();
                        if (systemId != null) {
                            string2 = systemId.toCharArray();
                        }
                    } else if (systemId != null) {
                        externalId = "SYSTEM";
                        string1 = systemId.toCharArray();
                        string2 = null;
                    }
                    output.printDoctypeStart(new char[] { ' ' },
                                             new char[] { 's', 'v', 'g' },
                                             new char[] { ' ' },
                                             externalId,
                                             new char[] { ' ' },
                                             string1, '"',
                                             new char[] { ' ' },
                                             string2, '"',
                                             null);
                    output.printDoctypeEnd(null);
                }
            }

            break;

        case DOCTYPE_REMOVE:
            if (type == LexicalUnits.DOCTYPE_START) {
                type = scanner.next();

                if (type != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                type = scanner.next();
                
                if (type != LexicalUnits.NAME) {
                    throw fatalError("name", null);
                }

                type = scanner.next();
                if (type == LexicalUnits.S) {
                    type = scanner.next();
                    
                    switch (type) {
                    case LexicalUnits.PUBLIC_IDENTIFIER:
                        
                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        type = scanner.next();
                        
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }
                    
                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        type = scanner.next();
                        
                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                        break;
                    case LexicalUnits.SYSTEM_IDENTIFIER:
                        
                        type = scanner.next();
                        if (type != LexicalUnits.S) {
                            throw fatalError("space", null);
                        }
                        type = scanner.next();

                        if (type != LexicalUnits.STRING) {
                            throw fatalError("string", null);
                        }

                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            type = scanner.next();
                        }
                    }
                }

                if (type == LexicalUnits.LSQUARE_BRACKET) {
                    do {
                        type = scanner.next();
                    } while (type != LexicalUnits.RSQUARE_BRACKET);
                }
                if (type == LexicalUnits.S) {
                    type = scanner.next();
                }

                if (type != LexicalUnits.END_CHAR) {
                    throw fatalError("end", null);
                }
            }
            type = scanner.next();
        }
    }

    /**
     * Prints an element.
     */
    protected void printElement()
        throws TranscoderException,
               XMLException,
               IOException {
        char[] name = getCurrentValue();
        List attributes = new LinkedList();
        char[] space = null;

        type = scanner.next();
        while (type == LexicalUnits.S) {
            space = getCurrentValue();

            type = scanner.next();
            if (type == LexicalUnits.NAME) {
                char[] attName = getCurrentValue();
                char[] space1 = null;

                type = scanner.next();
                if (type == LexicalUnits.S) {
                    space1 = getCurrentValue();
                    type = scanner.next();
                }
                if (type != LexicalUnits.EQ) {
                    throw fatalError("token", new Object[] { "=" });
                }
                type = scanner.next();

                char[] space2 = null;
                if (type == LexicalUnits.S) {
                    space2 = getCurrentValue();
                    type = scanner.next();
                }
                if (type != LexicalUnits.STRING &&
                    type != LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT) {
                    throw fatalError("string", null);
                }

                char valueDelim = scanner.getStringDelimiter();
                boolean hasEntityRef = false;

                StringBuffer sb = new StringBuffer();
                sb.append(getCurrentValue());
                loop: for (;;) {
                    scanner.clearBuffer();
                    type = scanner.next();
                    switch (type) {
                    case LexicalUnits.STRING:
                    case LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT:
                    case LexicalUnits.LAST_ATTRIBUTE_FRAGMENT:
                    case LexicalUnits.ATTRIBUTE_FRAGMENT:
                        sb.append(getCurrentValue());
                        break;
                    case LexicalUnits.CHARACTER_REFERENCE:
                        hasEntityRef = true;
                        sb.append("&#");
                        sb.append(getCurrentValue());
                        sb.append(";");
                        break;
                    case LexicalUnits.ENTITY_REFERENCE:
                        hasEntityRef = true;
                        sb.append("&");
                        sb.append(getCurrentValue());
                        sb.append(";");
                        break;
                    default:
                        break loop;
                    }
                }

                attributes.add(new OutputManager.AttributeInfo(space,
                                                               attName,
                                                               space1, space2,
                                                               new String(sb),
                                                               valueDelim,
                                                               hasEntityRef));
                space = null;
            }
        }
        output.printElementStart(name, attributes, space);
        
        switch (type) {
        default:
            throw fatalError("xml", null);
        case LexicalUnits.EMPTY_ELEMENT_END:
            output.printElementEnd(null, null);
            break;
        case LexicalUnits.END_CHAR:
            output.printCharacter('>');
            type = scanner.next();
            printContent();
            if (type != LexicalUnits.END_TAG) {
                throw fatalError("end.tag", null);
            }
            name = getCurrentValue();

            type = scanner.next();
            space = null;
            if (type == LexicalUnits.S) {
                space = getCurrentValue();
                type = scanner.next();
            }

            output.printElementEnd(name, space);

            if (type != LexicalUnits.END_CHAR) {
                throw fatalError("end", null);
            }
        }

        type = scanner.next();
    }

    /**
     * Prints the content of an element.
     */
    protected void printContent()
        throws TranscoderException,
               XMLException,
               IOException {
        content: for (;;) {
            switch (type) {
            case LexicalUnits.COMMENT:
                output.printComment(getCurrentValue());
                scanner.clearBuffer();
                type = scanner.next();
                break;
            case LexicalUnits.PI_START:
                printPI();
                break;
            case LexicalUnits.CHARACTER_DATA:
                output.printCharacterData(getCurrentValue());
                scanner.clearBuffer();
                type = scanner.next();
                break;
            case LexicalUnits.CDATA_START:
                type = scanner.next();
                if (type != LexicalUnits.CHARACTER_DATA) {
                    throw fatalError("character.data", null);
                }
                output.printCDATASection(getCurrentValue());
                if (scanner.next() != LexicalUnits.SECTION_END) {
                    throw fatalError("section.end", null);
                }
                scanner.clearBuffer();
                type = scanner.next();
                break;
            case LexicalUnits.START_TAG:
                printElement();
                break;
            case LexicalUnits.CHARACTER_REFERENCE:
                output.printCharacterEntityReference(getCurrentValue());
                scanner.clearBuffer();
                type = scanner.next();
                break;
            case LexicalUnits.ENTITY_REFERENCE:
                output.printEntityReference(getCurrentValue());
                scanner.clearBuffer();
                type = scanner.next();
                break;
            default:
                break content;
            }
        }
    }

    /**
     * Prints a notation declaration.
     */
    protected void printNotation()
        throws TranscoderException,
               XMLException,
               IOException {
        int t = scanner.next();
        if (t != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        char[] space1 = getCurrentValue();
        t = scanner.next();

        if (t != LexicalUnits.NAME) {
            throw fatalError("name", null);
        }
        char[] name = getCurrentValue();
        t = scanner.next();

        if (t != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        char[] space2 = getCurrentValue();
        t = scanner.next();

        String externalId = null;
        char[] space3 = null;
        char[] string1 = null;
        char string1Delim = 0;
        char[] space4 = null;
        char[] string2 = null;
        char string2Delim = 0;

        switch (t) {
        default:
            throw fatalError("notation.definition", null);
        case LexicalUnits.PUBLIC_IDENTIFIER:
            externalId = "PUBLIC";

            t = scanner.next();
            if (t != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            space3 = getCurrentValue();
            t = scanner.next();

            if (t != LexicalUnits.STRING) {
                throw fatalError("string", null);
            }
            string1 = getCurrentValue();
            string1Delim = scanner.getStringDelimiter();
            t = scanner.next();
            
            if (t == LexicalUnits.S) {
                space4 = getCurrentValue();
                t = scanner.next();

                if (t == LexicalUnits.STRING) {
                    string2 = getCurrentValue();
                    string2Delim = scanner.getStringDelimiter();
                    t = scanner.next();
                }
            }

            break;
        case LexicalUnits.SYSTEM_IDENTIFIER:
            externalId = "SYSTEM";

            t = scanner.next();
            if (t != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            space3 = getCurrentValue();
            t = scanner.next();

            if (t != LexicalUnits.STRING) {
                throw fatalError("string", null);
            }
            string1 = getCurrentValue();
            string1Delim = scanner.getStringDelimiter();
            t = scanner.next();
        }

        char[] space5 = null;
        if (t == LexicalUnits.S) {
            space5 = getCurrentValue();
            t = scanner.next();
        }
        if (t != LexicalUnits.END_CHAR) {
            throw fatalError("end", null);
        }
        output.printNotation(space1, name, space2, externalId, space3,
                             string1, string1Delim, space4,
                             string2, string2Delim, space5);

        scanner.next();
    }

    /**
     * Prints an ATTLIST declaration.
     */
    protected void printAttlist()
        throws TranscoderException,
               XMLException,
               IOException {
        type = scanner.next();
        if (type != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        char[] space = getCurrentValue();
        type = scanner.next();

        if (type != LexicalUnits.NAME) {
            throw fatalError("name", null);
        }
        char[] name = getCurrentValue();
        type = scanner.next();

        output.printAttlistStart(space, name);

        while (type == LexicalUnits.S) {
            space = getCurrentValue();
            type = scanner.next();
            
            if (type != LexicalUnits.NAME) {
                break;
            }
            name = getCurrentValue();
            type = scanner.next();

            if (type != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            char[] space2 = getCurrentValue();
            type = scanner.next();

            output.printAttName(space, name, space2);

            switch (type) {
            case LexicalUnits.CDATA_IDENTIFIER:
            case LexicalUnits.ID_IDENTIFIER:
            case LexicalUnits.IDREF_IDENTIFIER:
            case LexicalUnits.IDREFS_IDENTIFIER:
            case LexicalUnits.ENTITY_IDENTIFIER:
            case LexicalUnits.ENTITIES_IDENTIFIER:
            case LexicalUnits.NMTOKEN_IDENTIFIER:
            case LexicalUnits.NMTOKENS_IDENTIFIER:
                output.printCharacters(getCurrentValue());
                type = scanner.next();
                break;
            case LexicalUnits.NOTATION_IDENTIFIER:
                output.printCharacters(getCurrentValue());
                type = scanner.next();

                if (type != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                output.printSpaces(getCurrentValue(), false);
                type = scanner.next();
            
                if (type != LexicalUnits.LEFT_BRACE) {
                    throw fatalError("left.brace", null);
                }
                type = scanner.next();

                List names = new LinkedList();
                space = null;

                if (type == LexicalUnits.S) {
                    space = getCurrentValue();
                    type = scanner.next();
                }

                if (type != LexicalUnits.NAME) {
                    throw fatalError("name", null);
                }
                name = getCurrentValue();
                type = scanner.next();

                space2 = null;
                if (type == LexicalUnits.S) {
                    space2 = getCurrentValue();
                    type = scanner.next();
                }

                names.add(new OutputManager.NameInfo(space, name, space2));

                loop: for (;;) {
                    switch (type) {
                    default:
                        break loop;
                    case LexicalUnits.PIPE:
                        type = scanner.next();
                        
                        space = null;
                        if (type == LexicalUnits.S) {
                            space = getCurrentValue();
                            type = scanner.next();
                        }

                        if (type != LexicalUnits.NAME) {
                            throw fatalError("name", null);
                        }
                        name = getCurrentValue();
                        type = scanner.next();

                        space2 = null;
                        if (type == LexicalUnits.S) {
                            space2 = getCurrentValue();
                            type = scanner.next();
                        }
                        
                        names.add(new OutputManager.NameInfo(space, name, space2));
                    }
                }
                if (type != LexicalUnits.RIGHT_BRACE) {
                    throw fatalError("right.brace", null);
                }

                output.printEnumeration(names);
                type = scanner.next();
                break;
            case LexicalUnits.LEFT_BRACE:
                type = scanner.next();

                names = new LinkedList();
                space = null;

                if (type == LexicalUnits.S) {
                    space = getCurrentValue();
                    type = scanner.next();
                }

                if (type != LexicalUnits.NMTOKEN) {
                    throw fatalError("nmtoken", null);
                }
                name = getCurrentValue();
                type = scanner.next();

                space2 = null;
                if (type == LexicalUnits.S) {
                    space2 = getCurrentValue();
                    type = scanner.next();
                }

                names.add(new OutputManager.NameInfo(space, name, space2));

                loop: for (;;) {
                    switch (type) {
                    default:
                        break loop;
                    case LexicalUnits.PIPE:
                        type = scanner.next();
                        
                        space = null;
                        if (type == LexicalUnits.S) {
                            space = getCurrentValue();
                            type = scanner.next();
                        }

                        if (type != LexicalUnits.NMTOKEN) {
                            throw fatalError("nmtoken", null);
                        }
                        name = getCurrentValue();
                        type = scanner.next();

                        space2 = null;
                        if (type == LexicalUnits.S) {
                            space2 = getCurrentValue();
                            type = scanner.next();
                        }
                        
                        names.add(new OutputManager.NameInfo(space, name, space2));
                    }
                }
                if (type != LexicalUnits.RIGHT_BRACE) {
                    throw fatalError("right.brace", null);
                }

                output.printEnumeration(names);
                type = scanner.next();
                
            }

            if (type == LexicalUnits.S) {
                output.printSpaces(getCurrentValue(), true);
                type = scanner.next();
            }

            switch (type) {
            default:
                throw fatalError("default.decl", null);                
            case LexicalUnits.REQUIRED_IDENTIFIER:
            case LexicalUnits.IMPLIED_IDENTIFIER:
                output.printCharacters(getCurrentValue());
                type = scanner.next();
                break;
            case LexicalUnits.FIXED_IDENTIFIER:
                output.printCharacters(getCurrentValue());
                type = scanner.next();
                
                if (type != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                output.printSpaces(getCurrentValue(), false);
                type = scanner.next();

                if (type != LexicalUnits.STRING &&
                    type != LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT) {
                    throw fatalError("space", null);
                }
            case LexicalUnits.STRING:
            case LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT:
                output.printCharacter(scanner.getStringDelimiter());
                output.printCharacters(getCurrentValue());
                loop: for (;;) {
                    type = scanner.next();
                    switch (type) {
                    case LexicalUnits.STRING:
                    case LexicalUnits.ATTRIBUTE_FRAGMENT:
                    case LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT:
                    case LexicalUnits.LAST_ATTRIBUTE_FRAGMENT:
                        output.printCharacters(getCurrentValue());
                        break;
                    case LexicalUnits.CHARACTER_REFERENCE:
                        output.printString("&#");
                        output.printCharacters(getCurrentValue());
                        output.printCharacter(';');
                        break;
                    case LexicalUnits.ENTITY_REFERENCE:
                        output.printCharacter('&');
                        output.printCharacters(getCurrentValue());
                        output.printCharacter(';');
                        break;
                    default:
                        break loop;
                    }
                }
                output.printCharacter(scanner.getStringDelimiter());
            }
            space = null;
        }

        if (type != LexicalUnits.END_CHAR) {
            throw fatalError("end", null);
        }
        output.printAttlistEnd(space);
        type = scanner.next();
    }

    /**
     * Prints an entity declaration.
     */
    protected void printEntityDeclaration()
        throws TranscoderException,
               XMLException,
               IOException {
        writer.write("<!ENTITY");

        type = scanner.next();
        if (type != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        writer.write(getCurrentValue());
        type = scanner.next();

        boolean pe = false;

        switch (type) {
        default:
            throw fatalError("xml", null);
        case LexicalUnits.NAME:
            writer.write(getCurrentValue());
            type = scanner.next();
            break;
        case LexicalUnits.PERCENT:
            pe = true;
            writer.write('%');
            type = scanner.next();

            if (type != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            writer.write(getCurrentValue());
            type = scanner.next();

            if (type != LexicalUnits.NAME) {
                throw fatalError("name", null);
            }
            writer.write(getCurrentValue());
            type = scanner.next();
        }

        if (type != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        writer.write(getCurrentValue());
        type = scanner.next();

        switch (type) {
        case LexicalUnits.STRING:
        case LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT:
            char sd = scanner.getStringDelimiter();
            writer.write(sd);
            loop: for (;;) {
                switch (type) {
                case LexicalUnits.STRING:
                case LexicalUnits.ATTRIBUTE_FRAGMENT:
                case LexicalUnits.FIRST_ATTRIBUTE_FRAGMENT:
                case LexicalUnits.LAST_ATTRIBUTE_FRAGMENT:
                    writer.write(getCurrentValue());
                    break;
                case LexicalUnits.ENTITY_REFERENCE:
                    writer.write('&');
                    writer.write(getCurrentValue());
                    writer.write(';');
                    break;
                case LexicalUnits.PARAMETER_ENTITY_REFERENCE:
                    writer.write('&');
                    writer.write(getCurrentValue());
                    writer.write(';');
                    break;
                default:
                    break loop;
                }
                type = scanner.next();
            }
            writer.write(sd);

            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }
            
            if (type != LexicalUnits.END_CHAR) {
                throw fatalError("end", null);
            }
            writer.write(">");
            type = scanner.next();
            return;
        case LexicalUnits.PUBLIC_IDENTIFIER:
            writer.write("PUBLIC");
            type = scanner.next();
            if (type != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            type = scanner.next();
            if (type != LexicalUnits.STRING) {
                throw fatalError("string", null);
            }

            writer.write(" \"");
            writer.write(getCurrentValue());
            writer.write("\" \"");

            type = scanner.next();
            if (type != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            type = scanner.next();
            if (type != LexicalUnits.STRING) {
                throw fatalError("string", null);
            }

            writer.write(getCurrentValue());
            writer.write('"');
            break;
            
        case LexicalUnits.SYSTEM_IDENTIFIER:
            writer.write("SYSTEM");
            type = scanner.next();
            if (type != LexicalUnits.S) {
                throw fatalError("space", null);
            }
            type = scanner.next();
            if (type != LexicalUnits.STRING) {
                throw fatalError("string", null);
            }
            writer.write(" \"");
            writer.write(getCurrentValue());
            writer.write('"');
        }

        type = scanner.next();
        if (type == LexicalUnits.S) {
            writer.write(getCurrentValue());
            type = scanner.next();
            if (!pe && type == LexicalUnits.NDATA_IDENTIFIER) {
                writer.write("NDATA");
                type = scanner.next();
                if (type != LexicalUnits.S) {
                    throw fatalError("space", null);
                }
                writer.write(getCurrentValue());
                type = scanner.next();
                if (type != LexicalUnits.NAME) {
                    throw fatalError("name", null);
                }
                writer.write(getCurrentValue());
                type = scanner.next();
            }
            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }
        }
        
        if (type != LexicalUnits.END_CHAR) {
            throw fatalError("end", null);
        }
        writer.write('>');
        type = scanner.next();
    }

    /**
     * Prints an element declaration.
     */
    protected void printElementDeclaration()
        throws TranscoderException,
               XMLException,
               IOException {
        writer.write("<!ELEMENT");

        type = scanner.next();
        if (type != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        writer.write(getCurrentValue());
        type = scanner.next();
        switch (type) {
        default:
            throw fatalError("name", null);
        case LexicalUnits.NAME:
            writer.write(getCurrentValue());
        }

        type = scanner.next();
        if (type != LexicalUnits.S) {
            throw fatalError("space", null);
        }
        writer.write(getCurrentValue());

        switch (type = scanner.next()) {
        case LexicalUnits.EMPTY_IDENTIFIER:
            writer.write("EMPTY");
            type = scanner.next();
            break;
        case LexicalUnits.ANY_IDENTIFIER:
            writer.write("ANY");
            type = scanner.next();
            break;
        case LexicalUnits.LEFT_BRACE:
            writer.write('(');
            type = scanner.next();
            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }
            mixed: switch (type) {
            case LexicalUnits.PCDATA_IDENTIFIER:
                writer.write("#PCDATA");
                type = scanner.next();

                for (;;) {
                    switch (type) {
                    case LexicalUnits.S:
                        writer.write(getCurrentValue());
                        type = scanner.next();
                        break;
                    case LexicalUnits.PIPE:
                        writer.write('|');
                        type = scanner.next();
                        if (type == LexicalUnits.S) {
                            writer.write(getCurrentValue());
                            type = scanner.next();
                        }
                        if (type != LexicalUnits.NAME) {
                            throw fatalError("name", null);
                        }
                        writer.write(getCurrentValue());
                        type = scanner.next();
                        break;
                    case LexicalUnits.RIGHT_BRACE:
                        writer.write(')');
                        type = scanner.next();
                        break mixed;
                    }
                }

            case LexicalUnits.NAME:
            case LexicalUnits.LEFT_BRACE:
                printChildren();
                if (type != LexicalUnits.RIGHT_BRACE) {
                    throw fatalError("right.brace", null);
                }
                writer.write(')');
                type = scanner.next();
                if (type == LexicalUnits.S) {
                    writer.write(getCurrentValue());
                    type = scanner.next();
                }
                switch (type) {
                case LexicalUnits.QUESTION:
                    writer.write('?');
                    type = scanner.next();
                    break;
                case LexicalUnits.STAR:
                    writer.write('*');
                    type = scanner.next();
                    break;
                case LexicalUnits.PLUS:
                    writer.write('+');
                    type = scanner.next();
                }
            }
        }
        
        if (type == LexicalUnits.S) {
            writer.write(getCurrentValue());
            type = scanner.next();
        }

        if (type != LexicalUnits.END_CHAR) {
            throw fatalError("end", null);
        }
        writer.write('>');
        scanner.next();
    }

    /**
     * Prints the children of an element declaration.
     */
    protected void printChildren()
        throws TranscoderException,
               XMLException,
               IOException {
        int op = 0;
        loop: for (;;) {
            switch (type) {
            default:
                throw new RuntimeException("Invalid XML");
            case LexicalUnits.NAME:
                writer.write(getCurrentValue());
                type = scanner.next();
                break;
            case LexicalUnits.LEFT_BRACE:
                writer.write('(');
                type = scanner.next();
                if (type == LexicalUnits.S) {
                    writer.write(getCurrentValue());
                    type = scanner.next();
                }
                printChildren();
                if (type != LexicalUnits.RIGHT_BRACE) {
                    throw fatalError("right.brace", null);
                }
                writer.write(')');
                type = scanner.next();
            }

            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }

            switch (type) {
            case LexicalUnits.RIGHT_BRACE:
                break loop;
            case LexicalUnits.STAR:
                writer.write('*');
                type = scanner.next();
                break;
            case LexicalUnits.QUESTION:
                writer.write('?');
                type = scanner.next();
                break;
            case LexicalUnits.PLUS:
                writer.write('+');
                type = scanner.next();
                break;
            }

            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }

            switch (type) {
            case LexicalUnits.PIPE:
                if (op != 0 && op != type) {
                    throw new RuntimeException("Invalid XML");
                }
                writer.write('|');
                op = type;
                type = scanner.next();
                break;
            case LexicalUnits.COMMA:
                if (op != 0 && op != type) {
                    throw new RuntimeException("Invalid XML");
                }
                writer.write(',');
                op = type;
                type = scanner.next();
            }

            if (type == LexicalUnits.S) {
                writer.write(getCurrentValue());
                type = scanner.next();
            }
        }
    }

    /**
     * Returns the current lexical unit value.
     */
    protected char[] getCurrentValue() {
        int off = scanner.getStart() + scanner.getStartOffset();
        int len = scanner.getEnd() + scanner.getEndOffset() - off;
        char[] result = new char[len];
        char[] buffer = scanner.getBuffer();
        for (int i = 0; i < len; i++) {
            result[i] = buffer[off + i];
        }
        return result;
    }

    /**
     * Creates a transcoder exception.
     */
    protected TranscoderException fatalError(String key, Object[] params)
        throws TranscoderException {
        TranscoderException result = new TranscoderException(key);
        errorHandler.fatalError(result);
        return result;
    }
}
