/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.doxia.module.apt;

import javax.inject.Named;
import javax.inject.Singleton;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.commons.io.IOUtils;
import org.apache.maven.doxia.macro.MacroExecutionException;
import org.apache.maven.doxia.macro.MacroRequest;
import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
import org.apache.maven.doxia.parser.AbstractTextParser;
import org.apache.maven.doxia.parser.ParseException;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.AbstractLocator;
import org.apache.maven.doxia.sink.impl.SinkAdapter;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.util.DoxiaStringUtils;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The APT parser.
 * <br>
 * Based on the <a href="http://www.xmlmind.com/aptconvert.html">APTconvert</a> project.
 *
 * @since 1.0
 */
@Singleton
@Named("apt")
public class AptParser extends AbstractTextParser implements AptMarkup {
    private static final Logger LOGGER = LoggerFactory.getLogger(AptParser.class);

    /** Title event id */
    private static final int TITLE = 0;

    /** Section 1 event id */
    private static final int SECTION1 = 1;

    /** Section 2 event id */
    private static final int SECTION2 = 2;

    /** Section 3 event id */
    private static final int SECTION3 = 3;

    /** Section 4 event id */
    private static final int SECTION4 = 4;

    /** Section 5 event id */
    private static final int SECTION5 = 5;

    /** Paragraph event id */
    private static final int PARAGRAPH = 6;

    /** Verbatim event id */
    private static final int VERBATIM = 7;

    /** Figure event id */
    private static final int FIGURE = 8;

    /** Table event id */
    private static final int TABLE = 9;

    /** List event id */
    private static final int LIST_ITEM = 10;

    /** Numbered list event id */
    private static final int NUMBERED_LIST_ITEM = 11;

    /** Definition list event id */
    private static final int DEFINITION_LIST_ITEM = 12;

    /** Horizontal rule event id */
    private static final int HORIZONTAL_RULE = 13;

    /** Page break event id */
    private static final int PG_BREAK = 14;

    /** List break event id */
    private static final int LIST_BREAK = 15;

    /** Macro event id */
    private static final int MACRO = 16;

    /** Comment event id. */
    private static final int COMMENT_BLOCK = 17;

    /** String representations of event ids */
    private static final String[] TYPE_NAMES = {
        "TITLE",
        "SECTION1",
        "SECTION2",
        "SECTION3",
        "SECTION4",
        "SECTION5",
        "PARAGRAPH",
        "VERBATIM",
        "FIGURE",
        "TABLE",
        "LIST_ITEM",
        "NUMBERED_LIST_ITEM",
        "DEFINITION_LIST_ITEM",
        "HORIZONTAL_RULE",
        "PG_BREAK",
        "LIST_BREAK",
        "MACRO",
        "COMMENT_BLOCK"
    };

    /** An array of 85 spaces. */
    protected static final char[] SPACES;

    /** Default tab width. */
    public static final int TAB_WIDTH = 8;

    // ----------------------------------------------------------------------
    // Instance fields
    // ----------------------------------------------------------------------

    /** the AptSource. */
    private AptSource source;

    /** a block of AptSource. */
    private Block block;

    /** blockFileName. */
    private String blockFileName;

    /** blockLineNumber. */
    private int blockLineNumber;

    /** sourceContent. */
    protected String sourceContent;

    /** the sink to receive the events. */
    protected Sink sink;

    /** a line of AptSource. */
    protected String line;

    private static final int NUMBER_OF_SPACES = 85;

    static {
        SPACES = new char[NUMBER_OF_SPACES];

        for (int i = 0; i < NUMBER_OF_SPACES; i++) {
            SPACES[i] = ' ';
        }
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    @Override
    public void parse(Reader source, Sink sink) throws ParseException {
        parse(source, sink, null);
    }

    private static final class AptSourceLocator extends AbstractLocator {
        private final AptSource aptSource;

        AptSourceLocator(AptSource aptSource, String reference) {
            super(reference);
            this.aptSource = aptSource;
        }

        @Override
        public int getLineNumber() {
            return aptSource.getLineNumber() > -1 ? aptSource.getLineNumber() + 1 : -1;
        }

        @Override
        public int getColumnNumber() {
            return -1;
        }
    }

    @Override
    public void parse(Reader source, Sink sink, String reference) throws ParseException {
        init();

        try {
            StringWriter contentWriter = new StringWriter();
            IOUtils.copy(source, contentWriter);
            sourceContent = contentWriter.toString();
        } catch (IOException e) {
            throw new AptParseException(e);
        }

        try {
            this.source = new AptReaderSource(new StringReader(sourceContent), reference);

            this.sink = getWrappedSink(sink);
            sink.setDocumentLocator(new AptSourceLocator(this.source, reference));

            blockFileName = null;

            blockLineNumber = -1;

            // Lookahead line.
            nextLine();

            // Lookahead block.
            nextBlock(/*first*/ true);

            // traverse comments
            while ((block != null) && (block.getType() == COMMENT_BLOCK)) {
                block.traverse();
                nextBlock(/*first*/ true);
            }

            traverseHead();

            traverseBody();
        } catch (AptParseException ape) {
            // TODO handle column number
            throw new AptParseException(null, ape, getSourceName(), getSourceLineNumber(), -1);
        } finally {
            setSecondParsing(false);
            init();
        }
    }

    /**
     * Returns the name of the Apt source document.
     *
     * @return the source name.
     */
    public String getSourceName() {
        // Use this rather than source.getName() to report errors.
        return blockFileName;
    }

    /**
     * Returns the current line number of the Apt source document.
     *
     * @return the line number.
     */
    public int getSourceLineNumber() {
        // Use this rather than source.getLineNumber() to report errors.
        return blockLineNumber;
    }

    // ----------------------------------------------------------------------
    // Protected methods
    // ----------------------------------------------------------------------

    /**
     * Parse the next line of the Apt source document.
     *
     * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
     */
    protected void nextLine() throws AptParseException {
        line = source.getNextLine();
    }

    /**
     * Parse the given text.
     *
     * @param text the text to parse.
     * @param begin offset.
     * @param end offset.
     * @param sink the sink to receive the events.
     * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
     */
    protected void doTraverseText(String text, int begin, int end, Sink sink) throws AptParseException {
        boolean anchor = false;
        boolean link = false;
        boolean italic = false;
        boolean bold = false;
        boolean monospaced = false;
        StringBuilder buffer = new StringBuilder(end - begin);

        for (int i = begin; i < end; ++i) {
            char c = text.charAt(i);
            switch (c) {
                case BACKSLASH:
                    if (i + 1 < end) {
                        char escaped = text.charAt(i + 1);
                        switch (escaped) {
                            case SPACE:
                                ++i;
                                flushTraversed(buffer, sink);
                                sink.nonBreakingSpace();
                                break;
                            case '\r':
                            case '\n':
                                ++i;
                                // Skip white space which may follow a line break.
                                while (i + 1 < end && Character.isWhitespace(text.charAt(i + 1))) {
                                    ++i;
                                }
                                flushTraversed(buffer, sink);
                                sink.lineBreak();
                                break;
                            case BACKSLASH:
                            case PIPE:
                            case COMMENT:
                            case EQUAL:
                            case MINUS:
                            case PLUS:
                            case STAR:
                            case LEFT_SQUARE_BRACKET:
                            case RIGHT_SQUARE_BRACKET:
                            case LESS_THAN:
                            case GREATER_THAN:
                            case LEFT_CURLY_BRACKET:
                            case RIGHT_CURLY_BRACKET:
                                ++i;
                                buffer.append(escaped);
                                break;
                            case 'x':
                                if (i + 3 < end && isHexChar(text.charAt(i + 2)) && isHexChar(text.charAt(i + 3))) {
                                    int value = '?';
                                    try {
                                        value = Integer.parseInt(text.substring(i + 2, i + 4), 16);
                                    } catch (NumberFormatException e) {
                                        LOGGER.debug("Not a number: {}", text.substring(i + 2, i + 4));
                                    }

                                    i += 3;
                                    buffer.append((char) value);
                                } else {
                                    buffer.append(BACKSLASH);
                                }
                                break;
                            case 'u':
                                if (i + 5 < end
                                        && isHexChar(text.charAt(i + 2))
                                        && isHexChar(text.charAt(i + 3))
                                        && isHexChar(text.charAt(i + 4))
                                        && isHexChar(text.charAt(i + 5))) {
                                    int value = '?';
                                    try {
                                        value = Integer.parseInt(text.substring(i + 2, i + 6), 16);
                                    } catch (NumberFormatException e) {
                                        LOGGER.debug("Not a number: {}", text.substring(i + 2, i + 6));
                                    }

                                    i += 5;
                                    buffer.append((char) value);
                                } else {
                                    buffer.append(BACKSLASH);
                                }
                                break;
                            default:
                                if (isOctalChar(escaped)) {
                                    int octalChars = 1;
                                    if (isOctalChar(charAt(text, end, i + 2))) {
                                        ++octalChars;
                                        if (isOctalChar(charAt(text, end, i + 3))) {
                                            ++octalChars;
                                        }
                                    }
                                    int value = '?';
                                    try {
                                        value = Integer.parseInt(text.substring(i + 1, i + 1 + octalChars), 8);
                                    } catch (NumberFormatException e) {
                                        LOGGER.debug("Not a number: {}", text.substring(i + 1, i + 1 + octalChars));
                                    }

                                    i += octalChars;
                                    buffer.append((char) value);
                                } else {
                                    buffer.append(BACKSLASH);
                                }
                        }
                    } else {
                        buffer.append(BACKSLASH);
                    }
                    break;

                case LEFT_CURLY_BRACKET: /*}*/
                    if (!anchor && !link) {
                        if (i + 1 < end && text.charAt(i + 1) == LEFT_CURLY_BRACKET /*}*/) {
                            ++i;
                            link = true;
                            flushTraversed(buffer, sink);

                            String linkAnchor = null;

                            if (i + 1 < end && text.charAt(i + 1) == LEFT_CURLY_BRACKET /*}*/) {
                                ++i;
                                StringBuilder buf = new StringBuilder();
                                i = skipTraversedLinkAnchor(text, i + 1, end, buf);
                                linkAnchor = buf.toString();
                            }

                            if (linkAnchor == null) {
                                linkAnchor = getTraversedLink(text, i + 1, end);
                            }

                            if (AptUtils.isInternalLink(linkAnchor)) {
                                linkAnchor = "#" + linkAnchor;
                            }

                            int hashIndex = linkAnchor.indexOf("#");

                            if (hashIndex != -1 && !AptUtils.isExternalLink(linkAnchor)) {
                                String hash = linkAnchor.substring(hashIndex + 1);

                                if (hash.endsWith(".html") && !hash.startsWith("./")) {
                                    LOGGER.debug("Ambiguous link '{}'. If this is a local link, prepend \"./\"!", hash);
                                }

                                // link##anchor means literal
                                if (hash.startsWith("#")) {
                                    linkAnchor = linkAnchor.substring(0, hashIndex) + hash;
                                } else if (!DoxiaUtils.isValidId(hash)) {
                                    linkAnchor = linkAnchor.substring(0, hashIndex) + "#" + DoxiaUtils.encodeId(hash);

                                    LOGGER.debug("Modified invalid link '{}' to '{}'", hash, linkAnchor);
                                }
                            }

                            sink.link(linkAnchor);
                        } else {
                            anchor = true;
                            flushTraversed(buffer, sink);

                            String linkAnchor = getTraversedAnchor(text, i + 1, end);

                            linkAnchor = DoxiaUtils.encodeId(linkAnchor);

                            sink.anchor(linkAnchor);
                        }
                    } else {
                        buffer.append(c);
                    }
                    break;

                case /*{*/ RIGHT_CURLY_BRACKET:
                    if (link && i + 1 < end && text.charAt(i + 1) == /*{*/ RIGHT_CURLY_BRACKET) {
                        ++i;
                        link = false;
                        flushTraversed(buffer, sink);
                        sink.link_();
                    } else if (anchor) {
                        anchor = false;
                        flushTraversed(buffer, sink);
                        sink.anchor_();
                    } else {
                        buffer.append(c);
                    }
                    break;

                case LESS_THAN:
                    if (!italic && !bold && !monospaced) {
                        if (i + 1 < end && text.charAt(i + 1) == LESS_THAN) {
                            if (i + 2 < end && text.charAt(i + 2) == LESS_THAN) {
                                i += 2;
                                monospaced = true;
                                flushTraversed(buffer, sink);
                                sink.monospaced();
                            } else {
                                ++i;
                                bold = true;
                                flushTraversed(buffer, sink);
                                sink.bold();
                            }
                        } else {
                            italic = true;
                            flushTraversed(buffer, sink);
                            sink.italic();
                        }
                    } else {
                        buffer.append(c);
                    }
                    break;

                case GREATER_THAN:
                    if (monospaced
                            && i + 2 < end
                            && text.charAt(i + 1) == GREATER_THAN
                            && text.charAt(i + 2) == GREATER_THAN) {
                        i += 2;
                        monospaced = false;
                        flushTraversed(buffer, sink);
                        sink.monospaced_();
                    } else if (bold && i + 1 < end && text.charAt(i + 1) == GREATER_THAN) {
                        ++i;
                        bold = false;
                        flushTraversed(buffer, sink);
                        sink.bold_();
                    } else if (italic) {
                        italic = false;
                        flushTraversed(buffer, sink);
                        sink.italic_();
                    } else {
                        buffer.append(c);
                    }
                    break;

                default:
                    if (Character.isWhitespace(c)) {
                        buffer.append(SPACE);

                        // Skip to the last char of a sequence of white spaces.
                        while (i + 1 < end && Character.isWhitespace(text.charAt(i + 1))) {
                            ++i;
                        }
                    } else {
                        buffer.append(c);
                    }
            }
        }

        if (monospaced) {
            throw new AptParseException("missing '" + MONOSPACED_END_MARKUP + "'");
        }
        if (bold) {
            throw new AptParseException("missing '" + BOLD_END_MARKUP + "'");
        }
        if (italic) {
            throw new AptParseException("missing '" + ITALIC_END_MARKUP + "'");
        }
        if (link) {
            throw new AptParseException("missing '" + LINK_END_MARKUP + "'");
        }
        if (anchor) {
            throw new AptParseException("missing '" + ANCHOR_END_MARKUP + "'");
        }

        flushTraversed(buffer, sink);
    }

    // -----------------------------------------------------------------------

    /**
     * Returns the character at position i of the given string.
     *
     * @param string the string.
     * @param length length.
     * @param i offset.
     * @return the character, or '\0' if i &gt; length.
     */
    protected static char charAt(String string, int length, int i) {
        return (i < length) ? string.charAt(i) : '\0';
    }

    /**
     * Skip spaces.
     *
     * @param string string.
     * @param length length.
     * @param i offset.
     * @return int.
     */
    protected static int skipSpace(String string, int length, int i) {
        loop:
        for (; i < length; ++i) {
            switch (string.charAt(i)) {
                case SPACE:
                case TAB:
                    break;
                default:
                    break loop;
            }
        }
        return i;
    }

    /**
     * Replace part of a string.
     *
     * @param string the string
     * @param oldSub the substring to replace
     * @param newSub the replacement string
     * @return String
     */
    protected static String replaceAll(String string, String oldSub, String newSub) {
        StringBuilder replaced = new StringBuilder();
        int oldSubLength = oldSub.length();
        int begin, end;

        begin = 0;
        while ((end = string.indexOf(oldSub, begin)) >= 0) {
            if (end > begin) {
                replaced.append(string, begin, end);
            }
            replaced.append(newSub);
            begin = end + oldSubLength;
        }
        if (begin < string.length()) {
            replaced.append(string.substring(begin));
        }

        return replaced.toString();
    }

    /**
     * {@inheritDoc}
     */
    protected void init() {
        super.init();

        this.sourceContent = null;
        this.sink = null;
        this.source = null;
        this.block = null;
        this.blockFileName = null;
        this.blockLineNumber = 0;
        this.line = null;
    }

    // ----------------------------------------------------------------------
    // Private methods
    // ----------------------------------------------------------------------

    /**
     * Parse the head of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseHead() throws AptParseException {
        sink.head();

        if (block != null && block.getType() == TITLE) {
            block.traverse();
            nextBlock();
        }

        sink.head_();
    }

    /**
     * Parse the body of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseBody() throws AptParseException {
        sink.body();

        if (block != null) {
            traverseSectionBlocks();
        }

        while (block != null) {
            traverseSection(0);
        }

        sink.body_();
    }

    /**
     * Parse a section of the Apt source document.
     *
     * @param level The section level.
     * @throws AptParseException if something goes wrong.
     */
    private void traverseSection(int level) throws AptParseException {
        if (block == null) {
            return;
        }

        int type = SECTION1 + level;

        expectedBlock(type);

        switch (level) {
            case 0:
                sink.section1();
                break;
            case 1:
                sink.section2();
                break;
            case 2:
                sink.section3();
                break;
            case 3:
                sink.section4();
                break;
            case 4:
                sink.section5();
                break;
            default:
                break;
        }

        block.traverse();

        nextBlock();

        traverseSectionBlocks();

        while (block != null) {
            if (block.getType() <= type) {
                break;
            }

            traverseSection(level + 1);
        }

        switch (level) {
            case 0:
                sink.section1_();
                break;
            case 1:
                sink.section2_();
                break;
            case 2:
                sink.section3_();
                break;
            case 3:
                sink.section4_();
                break;
            case 4:
                sink.section5_();
                break;
            default:
                break;
        }
    }

    /**
     * Parse the section blocks of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseSectionBlocks() throws AptParseException {
        loop:
        while (block != null) {
            switch (block.getType()) {
                case PARAGRAPH:
                case VERBATIM:
                case FIGURE:
                case TABLE:
                case HORIZONTAL_RULE:
                case PG_BREAK:
                case MACRO:
                case COMMENT_BLOCK:
                    block.traverse();
                    nextBlock();
                    break;

                case LIST_ITEM:
                    traverseList();
                    break;

                case NUMBERED_LIST_ITEM:
                    traverseNumberedList();
                    break;

                case DEFINITION_LIST_ITEM:
                    traverseDefinitionList();
                    break;

                case LIST_BREAK:
                    // May be this is a list break which has not been indented
                    // very precisely.
                    nextBlock();
                    break;

                default:
                    // A section block which starts a new section.
                    break loop;
            }
        }
    }

    /**
     * Parse a list of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseList() throws AptParseException {
        if (block == null) {
            return;
        }

        expectedBlock(LIST_ITEM);

        int listIndent = block.getIndent();

        sink.list();

        sink.listItem();

        block.traverse();

        nextBlock();

        loop:
        while (block != null) {
            int blockIndent = block.getIndent();

            switch (block.getType()) {
                case PARAGRAPH:
                    if (blockIndent < listIndent) {
                        break loop;
                    }
                /*FALLTHROUGH*/
                case VERBATIM:
                case MACRO:
                case FIGURE:
                case TABLE:
                case HORIZONTAL_RULE:
                case PG_BREAK:
                    block.traverse();
                    nextBlock();
                    break;

                case LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    if (blockIndent > listIndent) {
                        traverseList();
                    } else {
                        sink.listItem_();
                        sink.listItem();
                        block.traverse();
                        nextBlock();
                    }
                    break;

                case NUMBERED_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseNumberedList();
                    break;

                case DEFINITION_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseDefinitionList();
                    break;

                case LIST_BREAK:
                    if (blockIndent >= listIndent) {
                        nextBlock();
                    }
                /*FALLTHROUGH*/
                default:
                    // A block which ends the list.
                    break loop;
            }
        }

        sink.listItem_();
        sink.list_();
    }

    /**
     * Parse a numbered list of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseNumberedList() throws AptParseException {
        if (block == null) {
            return;
        }
        expectedBlock(NUMBERED_LIST_ITEM);
        int listIndent = block.getIndent();

        sink.numberedList(((NumberedListItem) block).getNumbering());
        sink.numberedListItem();
        block.traverse();
        nextBlock();

        loop:
        while (block != null) {
            int blockIndent = block.getIndent();

            switch (block.getType()) {
                case PARAGRAPH:
                    if (blockIndent < listIndent) {
                        break loop;
                    }
                /*FALLTHROUGH*/
                case VERBATIM:
                case FIGURE:
                case TABLE:
                case HORIZONTAL_RULE:
                case PG_BREAK:
                    block.traverse();
                    nextBlock();
                    break;

                case LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseList();
                    break;

                case NUMBERED_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    if (blockIndent > listIndent) {
                        traverseNumberedList();
                    } else {
                        sink.numberedListItem_();
                        sink.numberedListItem();
                        block.traverse();
                        nextBlock();
                    }
                    break;

                case DEFINITION_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseDefinitionList();
                    break;

                case LIST_BREAK:
                    if (blockIndent >= listIndent) {
                        nextBlock();
                    }
                /*FALLTHROUGH*/
                default:
                    // A block which ends the list.
                    break loop;
            }
        }

        sink.numberedListItem_();
        sink.numberedList_();
    }

    /**
     * Parse a definition list of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void traverseDefinitionList() throws AptParseException {
        if (block == null) {
            return;
        }
        expectedBlock(DEFINITION_LIST_ITEM);
        int listIndent = block.getIndent();

        sink.definitionList();
        sink.definitionListItem();
        block.traverse();
        nextBlock();

        loop:
        while (block != null) {
            int blockIndent = block.getIndent();

            switch (block.getType()) {
                case PARAGRAPH:
                    if (blockIndent < listIndent) {
                        break loop;
                    }
                /*FALLTHROUGH*/
                case VERBATIM:
                case FIGURE:
                case TABLE:
                case HORIZONTAL_RULE:
                case PG_BREAK:
                    block.traverse();
                    nextBlock();
                    break;

                case LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseList();
                    break;

                case NUMBERED_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    traverseNumberedList();
                    break;

                case DEFINITION_LIST_ITEM:
                    if (blockIndent < listIndent) {
                        break loop;
                    }

                    if (blockIndent > listIndent) {
                        traverseDefinitionList();
                    } else {
                        sink.definition_();
                        sink.definitionListItem_();
                        sink.definitionListItem();
                        block.traverse();
                        nextBlock();
                    }
                    break;

                case LIST_BREAK:
                    if (blockIndent >= listIndent) {
                        nextBlock();
                    }
                /*FALLTHROUGH*/
                default:
                    // A block which ends the list.
                    break loop;
            }
        }

        sink.definition_();
        sink.definitionListItem_();
        sink.definitionList_();
    }

    /**
     * Parse the next block of the Apt source document.
     *
     * @throws AptParseException if something goes wrong.
     */
    private void nextBlock() throws AptParseException {
        nextBlock(/*first*/ false);
    }

    /**
     * Parse the next block of the Apt source document.
     *
     * @param firstBlock True if this is the first block of the Apt source document.
     * @throws AptParseException if something goes wrong.
     */
    private void nextBlock(boolean firstBlock) throws AptParseException {
        // Skip open lines.
        int length, indent, i;

        skipLoop:
        for (; ; ) {
            if (line == null) {
                block = null;
                return;
            }

            length = line.length();
            indent = 0;
            for (i = 0; i < length; ++i) {
                switch (line.charAt(i)) {
                    case SPACE:
                        ++indent;
                        break;
                    case TAB:
                        indent += 8;
                        break;
                    default:
                        break skipLoop;
                }
            }

            if (i == length) {
                nextLine();
            }
        }

        blockFileName = source.getName();
        blockLineNumber = source.getLineNumber();
        block = null;
        switch (line.charAt(i)) {
            case STAR:
                if (indent == 0) {
                    if (charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
                        block = new Table(indent, line);
                    } else if (charAt(line, length, i + 1) == STAR) {
                        if (charAt(line, length, i + 2) == STAR) {
                            if (charAt(line, length, i + 3) == STAR) {
                                block = new Section5(indent, line);
                            } else {
                                block = new Section4(indent, line);
                            }
                        } else {
                            block = new Section3(indent, line);
                        }
                    } else {
                        block = new Section2(indent, line);
                    }
                } else {
                    block = new ListItem(indent, line);
                }
                break;
            case LEFT_SQUARE_BRACKET:
                if (charAt(line, length, i + 1) == RIGHT_SQUARE_BRACKET) {
                    block = new ListBreak(indent, line);
                } else {
                    if (indent == 0) {
                        block = new Figure(indent, line);
                    } else {
                        if (charAt(line, length, i + 1) == LEFT_SQUARE_BRACKET) {
                            int numbering;

                            switch (charAt(line, length, i + 2)) {
                                case NUMBERING_LOWER_ALPHA_CHAR:
                                    numbering = Sink.NUMBERING_LOWER_ALPHA;
                                    break;
                                case NUMBERING_UPPER_ALPHA_CHAR:
                                    numbering = Sink.NUMBERING_UPPER_ALPHA;
                                    break;
                                case NUMBERING_LOWER_ROMAN_CHAR:
                                    numbering = Sink.NUMBERING_LOWER_ROMAN;
                                    break;
                                case NUMBERING_UPPER_ROMAN_CHAR:
                                    numbering = Sink.NUMBERING_UPPER_ROMAN;
                                    break;
                                case NUMBERING:
                                default:
                                    // The first item establishes the numbering
                                    // scheme for the whole list.
                                    numbering = Sink.NUMBERING_DECIMAL;
                            }

                            block = new NumberedListItem(indent, line, numbering);
                        } else {
                            block = new DefinitionListItem(indent, line);
                        }
                    }
                }
                break;
            case MINUS:
                if (charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
                    if (indent == 0) {
                        block = new Verbatim(indent, line);
                    } else {
                        if (firstBlock) {
                            block = new Title(indent, line);
                        }
                    }
                }
                break;
            case PLUS:
                if (indent == 0 && charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
                    block = new Verbatim(indent, line);
                }
                break;
            case EQUAL:
                if (indent == 0 && charAt(line, length, i + 1) == EQUAL && charAt(line, length, i + 2) == EQUAL) {
                    block = new HorizontalRule(indent, line);
                }
                break;
            case PAGE_BREAK:
                if (indent == 0) {
                    block = new PageBreak(indent, line);
                }
                break;
            case PERCENT:
                if (indent == 0 && charAt(line, length, i + 1) == LEFT_CURLY_BRACKET) {
                    block = new MacroBlock(indent, line);
                }
                break;
            case COMMENT:
                if (charAt(line, length, i + 1) == COMMENT) {
                    block = new Comment(line.substring(i + 2));
                }
                break;
            default:
                break;
        }

        if (block == null) {
            if (indent == 0) {
                block = new Section1(indent, line);
            } else {
                block = new Paragraph(indent, line);
            }
        }
    }

    /**
     * Checks that the current block is of the expected type.
     *
     * @param type the expected type.
     * @throws AptParseException if something goes wrong.
     */
    private void expectedBlock(int type) throws AptParseException {
        int blockType = block.getType();

        if (blockType != type) {
            throw new AptParseException("expected " + TYPE_NAMES[type] + ", found " + TYPE_NAMES[blockType]);
        }
    }

    // -----------------------------------------------------------------------

    /**
     * Determine if c is an octal character.
     *
     * @param c the character.
     * @return boolean
     */
    private static boolean isOctalChar(char c) {
        return (c >= '0' && c <= '7');
    }

    /**
     * Determine if c is an hex character.
     *
     * @param c the character.
     * @return boolean
     */
    private static boolean isHexChar(char c) {
        return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
    }

    /**
     * Emits the text so far parsed into the given sink.
     *
     * @param buffer A StringBuilder that contains the text to be flushed.
     * @param sink The sink to receive the text.
     */
    private static void flushTraversed(StringBuilder buffer, Sink sink) {
        if (buffer.length() > 0) {
            sink.text(buffer.toString());
            buffer.setLength(0);
        }
    }

    /**
     * Parse the given text.
     *
     * @param text the text to parse.
     * @param begin offset.
     * @param end offset.
     * @param linkAnchor a StringBuilder.
     * @return int
     * @throws AptParseException if something goes wrong.
     */
    private static int skipTraversedLinkAnchor(String text, int begin, int end, StringBuilder linkAnchor)
            throws AptParseException {
        int i;
        loop:
        for (i = begin; i < end; ++i) {
            char c = text.charAt(i);
            switch (c) {
                case RIGHT_CURLY_BRACKET:
                    break loop;
                case BACKSLASH:
                    if (i + 1 < end) {
                        ++i;
                        linkAnchor.append(text.charAt(i));
                    } else {
                        linkAnchor.append(BACKSLASH);
                    }
                    break;
                default:
                    linkAnchor.append(c);
            }
        }
        if (i == end) {
            throw new AptParseException("missing '" + RIGHT_CURLY_BRACKET + "'");
        }

        return i;
    }

    /**
     * Parse the given text.
     *
     * @param text the text to parse.
     * @param begin offset.
     * @param end offset.
     * @return String
     * @throws AptParseException if something goes wrong.
     */
    private String getTraversedLink(String text, int begin, int end) throws AptParseException {
        char previous2 = LEFT_CURLY_BRACKET;
        char previous = LEFT_CURLY_BRACKET;
        int i;

        for (i = begin; i < end; ++i) {
            char c = text.charAt(i);
            if (c == RIGHT_CURLY_BRACKET && previous == RIGHT_CURLY_BRACKET && previous2 != BACKSLASH) {
                break;
            }

            previous2 = previous;
            previous = c;
        }
        if (i == end) {
            throw new AptParseException("missing '" + LEFT_CURLY_BRACKET + LEFT_CURLY_BRACKET + "'");
        }

        return doGetTraversedLink(text, begin, i - 1);
    }

    /**
     * Parse the given text.
     *
     * @param text the text to parse.
     * @param begin offset.
     * @param end offset.
     * @return String
     * @throws AptParseException if something goes wrong.
     */
    private String getTraversedAnchor(String text, int begin, int end) throws AptParseException {
        char previous = LEFT_CURLY_BRACKET;
        int i;

        for (i = begin; i < end; ++i) {
            char c = text.charAt(i);
            if (c == RIGHT_CURLY_BRACKET && previous != BACKSLASH) {
                break;
            }

            previous = c;
        }
        if (i == end) {
            throw new AptParseException("missing '" + RIGHT_CURLY_BRACKET + "'");
        }

        return doGetTraversedLink(text, begin, i);
    }

    /**
     * Parse the given text.
     *
     * @param text the text to parse.
     * @param begin offset.
     * @param end offset.
     * @return String
     * @throws AptParseException if something goes wrong.
     */
    private String doGetTraversedLink(String text, int begin, int end) throws AptParseException {
        final StringBuilder buffer = new StringBuilder(end - begin);

        Sink linkSink = new SinkAdapter() {
            @Override
            public void lineBreak(SinkEventAttributes attributes) {
                buffer.append(SPACE);
            }

            @Override
            public void nonBreakingSpace() {
                buffer.append(SPACE);
            }

            @Override
            public void text(String text, SinkEventAttributes attributes) {
                buffer.append(text);
            }
        };
        doTraverseText(text, begin, end, linkSink);

        return buffer.toString().trim();
    }

    // -----------------------------------------------------------------------

    /** A block of an apt source document. */
    private abstract class Block {
        /** type. */
        protected int type;

        /** indent. */
        protected int indent;

        /** text. */
        protected String text;

        /** textLength. */
        protected int textLength;

        /**
         * Constructor.
         *
         * @param type the block type.
         * @param indent indent.
         * @throws AptParseException AptParseException
         */
        Block(int type, int indent) throws AptParseException {
            this(type, indent, null);
        }

        /**
         * Constructor.
         *
         * @param type type.
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Block(int type, int indent, String firstLine) throws AptParseException {
            this.type = type;
            this.indent = indent;

            // Skip first line ---
            AptParser.this.nextLine();

            if (firstLine == null) {
                text = null;
                textLength = 0;
            } else {
                // Read block ---
                StringBuilder buffer = new StringBuilder(firstLine);

                while (AptParser.this.line != null) {
                    String l = AptParser.this.line;
                    int length = l.length();
                    int i = 0;

                    i = skipSpace(l, length, i);
                    if (i == length) {
                        // Stop after open line and skip it.
                        AptParser.this.nextLine();
                        break;
                    } else if ((AptParser.charAt(l, length, i) == COMMENT
                                    && AptParser.charAt(l, length, i + 1) == COMMENT)
                            || type == COMMENT_BLOCK) {
                        // parse comments as separate blocks line by line
                        break;
                    }

                    buffer.append(EOL);
                    buffer.append(l);

                    AptParser.this.nextLine();
                }

                text = buffer.toString();
                textLength = text.length();
            }
        }

        /**
         * Return the block type.
         *
         * @return int
         */
        public final int getType() {
            return type;
        }

        /**
         * Return the block indent.
         *
         * @return int
         */
        public final int getIndent() {
            return indent;
        }

        /**
         * Parse the block.
         *
         * @throws AptParseException if something goes wrong.
         */
        public abstract void traverse() throws AptParseException;

        /**
         * Traverse the text.
         *
         * @param begin offset.
         * @throws AptParseException if something goes wrong.
         */
        protected void traverseText(int begin) throws AptParseException {
            traverseText(begin, text.length());
        }

        /**
         * Traverse the text.
         *
         * @param begin offset.
         * @param end offset.
         * @throws AptParseException if something goes wrong.
         */
        protected void traverseText(int begin, int end) throws AptParseException {
            AptParser.this.doTraverseText(text, begin, end, AptParser.this.sink);
        }

        /**
         * Skip spaces.
         *
         * @return int.
         */
        protected int skipLeadingBullets() {
            int i = skipSpaceFrom(0);
            for (; i < textLength; ++i) {
                if (text.charAt(i) != STAR) {
                    break;
                }
            }
            return skipSpaceFrom(i);
        }

        /**
         * Skip brackets.
         *
         * @param i offset.
         * @return int.
         * @throws AptParseException if something goes wrong.
         */
        protected int skipFromLeftToRightBracket(int i) throws AptParseException {
            char previous = LEFT_SQUARE_BRACKET;
            for (++i; i < textLength; ++i) {
                char c = text.charAt(i);
                if (c == RIGHT_SQUARE_BRACKET && previous != BACKSLASH) {
                    break;
                }
                previous = c;
            }
            if (i == textLength) {
                throw new AptParseException("missing '" + RIGHT_SQUARE_BRACKET + "'");
            }

            return i;
        }

        /**
         * Skip spaces.
         *
         * @param i offset.
         * @return int.
         */
        protected final int skipSpaceFrom(int i) {
            return AptParser.skipSpace(text, textLength, i);
        }
    }

    /** A ListBreak Block. */
    private class ListBreak extends AptParser.Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        ListBreak(int indent, String firstLine) throws AptParseException {
            super(AptParser.LIST_BREAK, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            throw new AptParseException("internal error: traversing list break");
        }
    }

    /** A Title Block. */
    private class Title extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Title(int indent, String firstLine) throws AptParseException {
            super(TITLE, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            StringTokenizer lines = new StringTokenizer(text, EOL);
            int separator = -1;
            boolean firstLine = true;
            boolean title = false;
            boolean author = false;
            boolean date = false;

            loop:
            while (lines.hasMoreTokens()) {
                String line = lines.nextToken().trim();
                int lineLength = line.length();

                if (AptParser.charAt(line, lineLength, 0) == MINUS
                        && AptParser.charAt(line, lineLength, 1) == MINUS
                        && AptParser.charAt(line, lineLength, 2) == MINUS) {
                    switch (separator) {
                        case 0:
                            if (title) {
                                AptParser.this.sink.title_();
                            }
                        case 1:
                            if (author) {
                                AptParser.this.sink.author_();
                            }
                            break;
                        case 2:
                            // Note that an extra decorative line is allowed
                            // at the end of the author.
                            break loop;
                        default:
                            break;
                    }

                    ++separator;
                    firstLine = true;
                } else {
                    if (firstLine) {
                        firstLine = false;
                        switch (separator) {
                            case 0:
                                title = true;
                                AptParser.this.sink.title();
                                break;
                            case 1:
                                author = true;
                                AptParser.this.sink.author();
                                break;
                            case 2:
                                date = true;
                                AptParser.this.sink.date();
                                break;
                            default:
                                break;
                        }
                    } else {
                        if (separator == 1) {
                            AptParser.this.sink.author_();
                            AptParser.this.sink.author();
                        } else {
                            // An implicit lineBreak separates title lines.
                            AptParser.this.sink.lineBreak();
                        }
                    }

                    AptParser.this.doTraverseText(line, 0, lineLength, AptParser.this.sink);
                }
            }

            switch (separator) {
                case 0:
                    if (title) {
                        AptParser.this.sink.title_();
                    }
                case 1:
                    if (author) {
                        AptParser.this.sink.author_();
                    }
                    break;
                case 2:
                    if (date) {
                        AptParser.this.sink.date_();
                    }
                    break;
                default:
                    break;
            }
        }
    }

    /** A Section Block. */
    private abstract class Section extends Block {
        /**
         * Constructor.
         *
         * @param type type.
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section(int type, int indent, String firstLine) throws AptParseException {
            super(type, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            Title();
            traverseText(skipLeadingBullets());
            Title_();
        }

        /** Start a title. */
        public abstract void Title();

        /** End a title. */
        public abstract void Title_();
    }

    /** A Section1 Block. */
    private class Section1 extends Section {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section1(int indent, String firstLine) throws AptParseException {
            super(SECTION1, indent, firstLine);
        }

        public void Title() {
            AptParser.this.sink.sectionTitle1();
        }

        public void Title_() {
            AptParser.this.sink.sectionTitle1_();
        }
    }

    /** A Section2 Block. */
    private class Section2 extends Section {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section2(int indent, String firstLine) throws AptParseException {
            super(SECTION2, indent, firstLine);
        }

        public void Title() {
            AptParser.this.sink.sectionTitle2();
        }

        public void Title_() {
            AptParser.this.sink.sectionTitle2_();
        }
    }

    /** A Section3 Block. */
    public class Section3 extends Section {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section3(int indent, String firstLine) throws AptParseException {
            super(SECTION3, indent, firstLine);
        }

        public void Title() {
            AptParser.this.sink.sectionTitle3();
        }

        public void Title_() {
            AptParser.this.sink.sectionTitle3_();
        }
    }

    /** A Section4 Block. */
    private class Section4 extends Section {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section4(int indent, String firstLine) throws AptParseException {
            super(SECTION4, indent, firstLine);
        }

        public void Title() {
            AptParser.this.sink.sectionTitle4();
        }

        public void Title_() {
            AptParser.this.sink.sectionTitle4_();
        }
    }

    /** A Section5 Block. */
    private class Section5 extends Section {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Section5(int indent, String firstLine) throws AptParseException {
            super(SECTION5, indent, firstLine);
        }

        public void Title() {
            AptParser.this.sink.sectionTitle5();
        }

        public void Title_() {
            AptParser.this.sink.sectionTitle5_();
        }
    }

    /** A Paragraph Block. */
    private class Paragraph extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Paragraph(int indent, String firstLine) throws AptParseException {
            super(PARAGRAPH, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            AptParser.this.sink.paragraph();
            traverseText(skipSpaceFrom(0));
            AptParser.this.sink.paragraph_();
        }
    }

    /** A Comment Block. */
    private class Comment extends Block {
        /**
         * Constructor.
         *
         * @param line the comment line.
         * @throws AptParseException AptParseException
         */
        Comment(String line) throws AptParseException {
            super(COMMENT_BLOCK, 0, line);
        }

        public void traverse() throws AptParseException {
            if (isEmitComments()) {
                AptParser.this.sink.comment(text);
                // APT comments always end with a line break
                AptParser.this.sink.markupLineBreak(0);
            }
        }
    }

    /** A Verbatim Block. */
    private class Verbatim extends Block {
        /** source. */
        private boolean source;

        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Verbatim(int indent, String firstLine) throws AptParseException {
            super(VERBATIM, indent, null);

            // Read block (first line already skipped) ---

            StringBuilder buffer = new StringBuilder();
            char firstChar = firstLine.charAt(0);
            source = (firstChar == PLUS);

            while (AptParser.this.line != null) {
                String l = AptParser.this.line;
                int length = l.length();

                if (AptParser.charAt(l, length, 0) == firstChar
                        && AptParser.charAt(l, length, 1) == MINUS
                        && AptParser.charAt(l, length, 2) == MINUS) {
                    AptParser.this.nextLine();

                    break;
                }

                // Expand tabs ---

                int prevColumn, column;

                column = 0;

                for (int i = 0; i < length; ++i) {
                    char c = l.charAt(i);

                    if (c == TAB) {
                        prevColumn = column;

                        column = ((column + 1 + TAB_WIDTH - 1) / TAB_WIDTH) * TAB_WIDTH;

                        buffer.append(SPACES, 0, column - prevColumn);
                    } else {
                        ++column;
                        buffer.append(c);
                    }
                }
                buffer.append(EOL);

                AptParser.this.nextLine();
            }

            // The last '\n' is mandatory before the "---" delimeter but is
            // not part of the verbatim text.
            textLength = buffer.length();

            if (textLength > 0) {
                --textLength;

                buffer.setLength(textLength);
            }

            text = buffer.toString();
        }

        public void traverse() throws AptParseException {
            AptParser.this.sink.verbatim(source ? SinkEventAttributeSet.SOURCE : null);
            AptParser.this.sink.text(text);
            AptParser.this.sink.verbatim_();
        }
    }

    /** A Figure Block. */
    private class Figure extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Figure(int indent, String firstLine) throws AptParseException {
            super(FIGURE, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            AptParser.this.sink.figure();

            int i = skipFromLeftToRightBracket(0);
            AptParser.this.sink.figureGraphics(text.substring(1, i));

            i = skipSpaceFrom(i + 1);
            if (i < textLength) {
                AptParser.this.sink.figureCaption();
                traverseText(i);
                AptParser.this.sink.figureCaption_();
            }

            AptParser.this.sink.figure_();
        }
    }

    /** A Table Block. */
    private class Table extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        Table(int indent, String firstLine) throws AptParseException {
            super(TABLE, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            int captionIndex = -1;
            int nextLineIndex = 0;
            int init = 2;
            int[] justification = null;
            int rows = 0;
            int columns = 0;
            StringBuilder[] cells = null;
            boolean[] headers = null;
            boolean grid;

            AptParser.this.sink.table();

            while (nextLineIndex < textLength) {
                int i = text.indexOf("*--", nextLineIndex);
                if (i < 0) {
                    captionIndex = nextLineIndex;
                    break;
                }

                String line;
                i = text.indexOf('\n', nextLineIndex);
                if (i < 0) {
                    line = text.substring(nextLineIndex);
                    nextLineIndex = textLength;
                } else {
                    line = text.substring(nextLineIndex, i);
                    nextLineIndex = i + 1;
                }
                int lineLength = line.length();

                if (line.indexOf("*--") == 0) {
                    if (init == 2) {
                        init = 1;
                        justification = parseJustification(line, lineLength);
                        columns = justification.length;
                        cells = new StringBuilder[columns];
                        headers = new boolean[columns];
                        for (i = 0; i < columns; ++i) {
                            cells[i] = new StringBuilder();
                            headers[i] = false;
                        }
                    } else {
                        if (traverseRow(cells, headers, justification)) {
                            ++rows;
                        }
                        justification = parseJustification(line, lineLength);
                    }
                } else {
                    if (init == 1) {
                        init = 0;
                        grid = (AptParser.charAt(line, lineLength, 0) == PIPE);
                        AptParser.this.sink.tableRows(justification, grid);
                    }

                    line = replaceAll(line, "\\|", "\\u007C");

                    StringTokenizer cellLines = new StringTokenizer(line, "|", true);

                    i = 0;
                    boolean processedGrid = false;
                    while (cellLines.hasMoreTokens()) {
                        String cellLine = cellLines.nextToken();
                        if ("|".equals(cellLine)) {
                            if (processedGrid) {
                                headers[i] = true;
                            } else {
                                processedGrid = true;
                                headers[i] = false;
                            }
                            continue;
                        }
                        processedGrid = false;
                        cellLine = replaceAll(cellLine, "\\", "\\u00A0"); // linebreak
                        // Escaped special characters: \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\.
                        cellLine = replaceAll(cellLine, "\\u00A0~", "\\~");
                        cellLine = replaceAll(cellLine, "\\u00A0=", "\\=");
                        cellLine = replaceAll(cellLine, "\\u00A0-", "\\-");
                        cellLine = replaceAll(cellLine, "\\u00A0+", "\\+");
                        cellLine = replaceAll(cellLine, "\\u00A0*", "\\*");
                        cellLine = replaceAll(cellLine, "\\u00A0[", "\\[");
                        cellLine = replaceAll(cellLine, "\\u00A0]", "\\]");
                        cellLine = replaceAll(cellLine, "\\u00A0<", "\\<");
                        cellLine = replaceAll(cellLine, "\\u00A0>", "\\>");
                        cellLine = replaceAll(cellLine, "\\u00A0{", "\\{");
                        cellLine = replaceAll(cellLine, "\\u00A0}", "\\}");
                        cellLine = replaceAll(cellLine, "\\u00A0u", "\\u");
                        cellLine = replaceAll(cellLine, "\\u00A0\\u00A0", "\\\\");
                        cellLine = cellLine.trim();

                        StringBuilder cell = cells[i];
                        if (cellLine.length() > 0) {
                            // line break in table cells
                            if (cell.toString().trim().endsWith("\\u00A0")) {
                                cell.append("\\\n");
                            } else {
                                if (cell.length() != 0) {
                                    // Always add a space for multi line tables cells
                                    cell.append(" ");
                                }
                            }

                            cell.append(cellLine);
                        }

                        ++i;
                        if (i == columns) {
                            break;
                        }
                    }
                }
            }
            if (rows == 0) {
                throw new AptParseException("no table rows");
            }
            AptParser.this.sink.tableRows_();

            if (captionIndex >= 0) {
                AptParser.this.sink.tableCaption();
                AptParser.this.doTraverseText(text, captionIndex, textLength, AptParser.this.sink);
                AptParser.this.sink.tableCaption_();
            }

            AptParser.this.sink.table_();
        }

        /**
         * Parse a table justification line.
         *
         * @param jline the justification line.
         * @param lineLength the length of the line. Must be > 2.
         * @return int[]
         * @throws AptParseException if something goes wrong.
         */
        private int[] parseJustification(String jline, int lineLength) throws AptParseException {
            int columns = 0;

            for (int i = 2 /*Skip '*--'*/; i < lineLength; ++i) {
                switch (jline.charAt(i)) {
                    case STAR:
                    case PLUS:
                    case COLON:
                        ++columns;
                        break;
                    default:
                        break;
                }
            }

            if (columns == 0) {
                throw new AptParseException("no columns specified");
            }

            int[] justification = new int[columns];
            columns = 0;
            for (int i = 2; i < lineLength; ++i) {
                switch (jline.charAt(i)) {
                    case STAR:
                        justification[columns++] = Sink.JUSTIFY_CENTER;
                        break;
                    case PLUS:
                        justification[columns++] = Sink.JUSTIFY_LEFT;
                        break;
                    case COLON:
                        justification[columns++] = Sink.JUSTIFY_RIGHT;
                        break;
                    default:
                        break;
                }
            }

            return justification;
        }

        /**
         * Traverse a table row.
         *
         * @param cells The table cells.
         * @param headers true for header cells.
         * @param justification the justification for each cell.
         * @return boolean
         * @throws AptParseException if something goes wrong.
         */
        private boolean traverseRow(StringBuilder[] cells, boolean[] headers, int[] justification)
                throws AptParseException {
            // Skip empty row (a decorative line).
            boolean traversed = false;
            for (StringBuilder cell1 : cells) {
                if (cell1.length() > 0) {
                    traversed = true;
                    break;
                }
            }

            if (traversed) {
                AptParser.this.sink.tableRow();
                for (int i = 0; i < cells.length; ++i) {
                    StringBuilder cell = cells[i];

                    SinkEventAttributes justif;
                    switch (justification[i]) {
                        case Sink.JUSTIFY_CENTER:
                            justif = SinkEventAttributeSet.CENTER;
                            break;
                        case Sink.JUSTIFY_LEFT:
                            justif = SinkEventAttributeSet.LEFT;
                            break;
                        case Sink.JUSTIFY_RIGHT:
                            justif = SinkEventAttributeSet.RIGHT;
                            break;
                        default:
                            justif = SinkEventAttributeSet.LEFT;
                            break;
                    }
                    SinkEventAttributeSet event = new SinkEventAttributeSet();
                    event.addAttributes(justif);

                    if (headers[i]) {
                        AptParser.this.sink.tableHeaderCell(event);
                    } else {
                        AptParser.this.sink.tableCell(event);
                    }
                    if (cell.length() > 0) {
                        AptParser.this.doTraverseText(cell.toString(), 0, cell.length(), AptParser.this.sink);
                        cell.setLength(0);
                    }
                    if (headers[i]) {
                        AptParser.this.sink.tableHeaderCell_();
                        // DOXIA-404: reset header for next row
                        headers[i] = false;
                    } else {
                        AptParser.this.sink.tableCell_();
                    }
                }
                AptParser.this.sink.tableRow_();
            }

            return traversed;
        }
    }

    /** A ListItem Block. */
    private class ListItem extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        ListItem(int indent, String firstLine) throws AptParseException {
            super(LIST_ITEM, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            traverseText(skipLeadingBullets());
        }
    }

    /** A NumberedListItem Block. */
    private class NumberedListItem extends Block {
        /** numbering. */
        private int numbering;

        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @param number numbering.
         * @throws AptParseException AptParseException
         */
        NumberedListItem(int indent, String firstLine, int number) throws AptParseException {
            super(NUMBERED_LIST_ITEM, indent, firstLine);
            this.numbering = number;
        }

        /**
         * getNumbering.
         *
         * @return int
         */
        public int getNumbering() {
            return numbering;
        }

        public void traverse() throws AptParseException {
            traverseText(skipItemNumber());
        }

        /**
         * skipItemNumber.
         *
         * @return int
         * @throws AptParseException AptParseException
         */
        private int skipItemNumber() throws AptParseException {
            int i = skipSpaceFrom(0);

            char prevChar = SPACE;
            for (; i < textLength; ++i) {
                char c = text.charAt(i);
                if (c == RIGHT_SQUARE_BRACKET && prevChar == RIGHT_SQUARE_BRACKET) {
                    break;
                }
                prevChar = c;
            }

            if (i == textLength) {
                throw new AptParseException("missing '" + RIGHT_SQUARE_BRACKET + RIGHT_SQUARE_BRACKET + "'");
            }

            return skipSpaceFrom(i + 1);
        }
    }

    /** A DefinitionListItem Block. */
    private class DefinitionListItem extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        DefinitionListItem(int indent, String firstLine) throws AptParseException {
            super(DEFINITION_LIST_ITEM, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            int i = skipSpaceFrom(0);
            int j = skipFromLeftToRightBracket(i);

            AptParser.this.sink.definedTerm();
            traverseText(i + 1, j);
            AptParser.this.sink.definedTerm_();

            j = skipSpaceFrom(j + 1);
            if (j == textLength) {
                // TODO: this doesn't handle the case of a dd in a paragraph
                // throw new AptParseException("no definition");
            }

            AptParser.this.sink.definition();
            traverseText(j);
        }
    }

    /** A HorizontalRule Block. */
    private class HorizontalRule extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        HorizontalRule(int indent, String firstLine) throws AptParseException {
            super(HORIZONTAL_RULE, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            AptParser.this.sink.horizontalRule();
        }
    }

    /** A PageBreak Block. */
    private class PageBreak extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        PageBreak(int indent, String firstLine) throws AptParseException {
            super(PG_BREAK, indent, firstLine);
        }

        public void traverse() throws AptParseException {
            AptParser.this.sink.pageBreak();
        }
    }

    /** A MacroBlock Block. */
    private class MacroBlock extends Block {
        /**
         * Constructor.
         *
         * @param indent indent.
         * @param firstLine the first line.
         * @throws AptParseException AptParseException
         */
        MacroBlock(int indent, String firstLine) throws AptParseException {
            super(MACRO, indent);

            text = firstLine;
        }

        public void traverse() throws AptParseException {
            if (isSecondParsing()) {
                return;
            }

            final int start = text.indexOf('{');
            final int end = text.indexOf('}');

            String s = text.substring(start + 1, end);

            s = escapeForMacro(s);

            String[] params = DoxiaStringUtils.split(s, "|");

            String macroId = params[0];

            Map<String, Object> parameters = new LinkedHashMap<>();

            for (int i = 1; i < params.length; i++) {
                String[] param = DoxiaStringUtils.split(params[i], "=");

                if (param.length == 1) {
                    throw new AptParseException("Missing 'key=value' pair for macro parameter: " + params[i]);
                }

                String key = unescapeForMacro(param[0]);
                String value = unescapeForMacro(param[1]);

                parameters.put(key, value);
            }

            // getBasedir() does not work in multi-module builds, see DOXIA-373
            // the basedir should be injected from here, see DOXIA-224
            MacroRequest request = new MacroRequest(sourceContent, new AptParser(), parameters, getBasedir());
            try {
                AptParser.this.executeMacro(macroId, request, sink);
            } catch (MacroExecutionException e) {
                throw new AptParseException("Unable to execute macro in the APT document", e);
            } catch (MacroNotFoundException e) {
                throw new AptParseException("Unable to find macro used in the APT document", e);
            }
        }

        /**
         * escapeForMacro
         *
         * @param s String
         * @return String
         */
        private String escapeForMacro(String s) {
            if (s == null || s.length() < 1) {
                return s;
            }

            String result = s;

            // use some outrageously out-of-place chars for text
            // (these are device control one/two in unicode)
            result = DoxiaStringUtils.replace(result, "\\=", "\u0011");
            result = DoxiaStringUtils.replace(result, "\\|", "\u0012");

            return result;
        }

        /**
         * unescapeForMacro
         *
         * @param s String
         * @return String
         */
        private String unescapeForMacro(String s) {
            if (s == null || s.length() < 1) {
                return s;
            }

            String result = s;

            result = DoxiaStringUtils.replace(result, "\u0011", "=");
            result = DoxiaStringUtils.replace(result, "\u0012", "|");

            return result;
        }
    }
}
