001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.module.apt;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.IOException;
025import java.io.Reader;
026import java.io.StringReader;
027import java.io.StringWriter;
028import java.util.LinkedHashMap;
029import java.util.Map;
030import java.util.StringTokenizer;
031
032import org.apache.maven.doxia.macro.MacroExecutionException;
033import org.apache.maven.doxia.macro.MacroRequest;
034import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
035import org.apache.maven.doxia.parser.AbstractTextParser;
036import org.apache.maven.doxia.parser.ParseException;
037import org.apache.maven.doxia.sink.Sink;
038import org.apache.maven.doxia.sink.SinkEventAttributeSet;
039import org.apache.maven.doxia.sink.SinkEventAttributes;
040import org.apache.maven.doxia.sink.impl.AbstractLocator;
041import org.apache.maven.doxia.sink.impl.SinkAdapter;
042import org.apache.maven.doxia.util.DoxiaStringUtils;
043import org.apache.maven.doxia.util.DoxiaUtils;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * The APT parser.
049 * <br>
050 * Based on the <a href="http://www.xmlmind.com/aptconvert.html">APTconvert</a> project.
051 *
052 * @since 1.0
053 */
054@Singleton
055@Named("apt")
056public class AptParser extends AbstractTextParser implements AptMarkup {
057    private static final Logger LOGGER = LoggerFactory.getLogger(AptParser.class);
058
059    /** Title event id */
060    private static final int TITLE = 0;
061
062    /** Section 1 event id */
063    private static final int SECTION1 = 1;
064
065    /** Section 2 event id */
066    private static final int SECTION2 = 2;
067
068    /** Section 3 event id */
069    private static final int SECTION3 = 3;
070
071    /** Section 4 event id */
072    private static final int SECTION4 = 4;
073
074    /** Section 5 event id */
075    private static final int SECTION5 = 5;
076
077    /** Paragraph event id */
078    private static final int PARAGRAPH = 6;
079
080    /** Verbatim event id */
081    private static final int VERBATIM = 7;
082
083    /** Figure event id */
084    private static final int FIGURE = 8;
085
086    /** Table event id */
087    private static final int TABLE = 9;
088
089    /** List event id */
090    private static final int LIST_ITEM = 10;
091
092    /** Numbered list event id */
093    private static final int NUMBERED_LIST_ITEM = 11;
094
095    /** Definition list event id */
096    private static final int DEFINITION_LIST_ITEM = 12;
097
098    /** Horizontal rule event id */
099    private static final int HORIZONTAL_RULE = 13;
100
101    /** Page break event id */
102    private static final int PG_BREAK = 14;
103
104    /** List break event id */
105    private static final int LIST_BREAK = 15;
106
107    /** Macro event id */
108    private static final int MACRO = 16;
109
110    /** Comment event id. */
111    private static final int COMMENT_BLOCK = 17;
112
113    /** String representations of event ids */
114    private static final String[] TYPE_NAMES = {
115        "TITLE",
116        "SECTION1",
117        "SECTION2",
118        "SECTION3",
119        "SECTION4",
120        "SECTION5",
121        "PARAGRAPH",
122        "VERBATIM",
123        "FIGURE",
124        "TABLE",
125        "LIST_ITEM",
126        "NUMBERED_LIST_ITEM",
127        "DEFINITION_LIST_ITEM",
128        "HORIZONTAL_RULE",
129        "PG_BREAK",
130        "LIST_BREAK",
131        "MACRO",
132        "COMMENT_BLOCK"
133    };
134
135    /** An array of 85 spaces. */
136    protected static final char[] SPACES;
137
138    /** Default tab width. */
139    public static final int TAB_WIDTH = 8;
140
141    // ----------------------------------------------------------------------
142    // Instance fields
143    // ----------------------------------------------------------------------
144
145    /** the AptSource. */
146    private AptSource source;
147
148    /** a block of AptSource. */
149    private Block block;
150
151    /** blockFileName. */
152    private String blockFileName;
153
154    /** blockLineNumber. */
155    private int blockLineNumber;
156
157    /** sourceContent. */
158    protected String sourceContent;
159
160    /** the sink to receive the events. */
161    protected Sink sink;
162
163    /** a line of AptSource. */
164    protected String line;
165
166    private static final int NUMBER_OF_SPACES = 85;
167
168    static {
169        SPACES = new char[NUMBER_OF_SPACES];
170
171        for (int i = 0; i < NUMBER_OF_SPACES; i++) {
172            SPACES[i] = ' ';
173        }
174    }
175
176    // ----------------------------------------------------------------------
177    // Public methods
178    // ----------------------------------------------------------------------
179
180    @Override
181    public void parse(Reader source, Sink sink) throws ParseException {
182        parse(source, sink, null);
183    }
184
185    private static final class AptSourceLocator extends AbstractLocator {
186        private final AptSource aptSource;
187
188        AptSourceLocator(AptSource aptSource, String reference) {
189            super(reference);
190            this.aptSource = aptSource;
191        }
192
193        @Override
194        public int getLineNumber() {
195            return aptSource.getLineNumber() > -1 ? aptSource.getLineNumber() + 1 : -1;
196        }
197
198        @Override
199        public int getColumnNumber() {
200            return -1;
201        }
202    }
203
204    @Override
205    public void parse(Reader source, Sink sink, String reference) throws ParseException {
206        init();
207
208        try {
209            StringWriter contentWriter = new StringWriter();
210            char[] buffer = new char[8192];
211            int n;
212            while ((n = source.read(buffer)) != -1) {
213                contentWriter.write(buffer, 0, n);
214            }
215            sourceContent = contentWriter.toString();
216        } catch (IOException e) {
217            throw new AptParseException(e);
218        }
219
220        try {
221            this.source = new AptReaderSource(new StringReader(sourceContent), reference);
222
223            this.sink = getWrappedSink(sink);
224            sink.setDocumentLocator(new AptSourceLocator(this.source, reference));
225
226            blockFileName = null;
227
228            blockLineNumber = -1;
229
230            // Lookahead line.
231            nextLine();
232
233            // Lookahead block.
234            nextBlock(/*first*/ true);
235
236            // traverse comments
237            while ((block != null) && (block.getType() == COMMENT_BLOCK)) {
238                block.traverse();
239                nextBlock(/*first*/ true);
240            }
241
242            traverseHead();
243
244            traverseBody();
245        } catch (AptParseException ape) {
246            // TODO handle column number
247            throw new AptParseException(null, ape, getSourceName(), getSourceLineNumber(), -1);
248        } finally {
249            setSecondParsing(false);
250            init();
251        }
252    }
253
254    /**
255     * Returns the name of the Apt source document.
256     *
257     * @return the source name.
258     */
259    public String getSourceName() {
260        // Use this rather than source.getName() to report errors.
261        return blockFileName;
262    }
263
264    /**
265     * Returns the current line number of the Apt source document.
266     *
267     * @return the line number.
268     */
269    public int getSourceLineNumber() {
270        // Use this rather than source.getLineNumber() to report errors.
271        return blockLineNumber;
272    }
273
274    // ----------------------------------------------------------------------
275    // Protected methods
276    // ----------------------------------------------------------------------
277
278    /**
279     * Parse the next line of the Apt source document.
280     *
281     * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
282     */
283    protected void nextLine() throws AptParseException {
284        line = source.getNextLine();
285    }
286
287    /**
288     * Parse the given text.
289     *
290     * @param text the text to parse.
291     * @param begin offset.
292     * @param end offset.
293     * @param sink the sink to receive the events.
294     * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
295     */
296    protected void doTraverseText(String text, int begin, int end, Sink sink) throws AptParseException {
297        boolean anchor = false;
298        boolean link = false;
299        boolean italic = false;
300        boolean bold = false;
301        boolean monospaced = false;
302        StringBuilder buffer = new StringBuilder(end - begin);
303
304        for (int i = begin; i < end; ++i) {
305            char c = text.charAt(i);
306            switch (c) {
307                case BACKSLASH:
308                    if (i + 1 < end) {
309                        char escaped = text.charAt(i + 1);
310                        switch (escaped) {
311                            case SPACE:
312                                ++i;
313                                flushTraversed(buffer, sink);
314                                sink.nonBreakingSpace();
315                                break;
316                            case '\r':
317                            case '\n':
318                                ++i;
319                                // Skip white space which may follow a line break.
320                                while (i + 1 < end && Character.isWhitespace(text.charAt(i + 1))) {
321                                    ++i;
322                                }
323                                flushTraversed(buffer, sink);
324                                sink.lineBreak();
325                                break;
326                            case BACKSLASH:
327                            case PIPE:
328                            case COMMENT:
329                            case EQUAL:
330                            case MINUS:
331                            case PLUS:
332                            case STAR:
333                            case LEFT_SQUARE_BRACKET:
334                            case RIGHT_SQUARE_BRACKET:
335                            case LESS_THAN:
336                            case GREATER_THAN:
337                            case LEFT_CURLY_BRACKET:
338                            case RIGHT_CURLY_BRACKET:
339                                ++i;
340                                buffer.append(escaped);
341                                break;
342                            case 'x':
343                                if (i + 3 < end && isHexChar(text.charAt(i + 2)) && isHexChar(text.charAt(i + 3))) {
344                                    int value = '?';
345                                    try {
346                                        value = Integer.parseInt(text.substring(i + 2, i + 4), 16);
347                                    } catch (NumberFormatException e) {
348                                        LOGGER.debug("Not a number: {}", text.substring(i + 2, i + 4));
349                                    }
350
351                                    i += 3;
352                                    buffer.append((char) value);
353                                } else {
354                                    buffer.append(BACKSLASH);
355                                }
356                                break;
357                            case 'u':
358                                if (i + 5 < end
359                                        && isHexChar(text.charAt(i + 2))
360                                        && isHexChar(text.charAt(i + 3))
361                                        && isHexChar(text.charAt(i + 4))
362                                        && isHexChar(text.charAt(i + 5))) {
363                                    int value = '?';
364                                    try {
365                                        value = Integer.parseInt(text.substring(i + 2, i + 6), 16);
366                                    } catch (NumberFormatException e) {
367                                        LOGGER.debug("Not a number: {}", text.substring(i + 2, i + 6));
368                                    }
369
370                                    i += 5;
371                                    buffer.append((char) value);
372                                } else {
373                                    buffer.append(BACKSLASH);
374                                }
375                                break;
376                            default:
377                                if (isOctalChar(escaped)) {
378                                    int octalChars = 1;
379                                    if (isOctalChar(charAt(text, end, i + 2))) {
380                                        ++octalChars;
381                                        if (isOctalChar(charAt(text, end, i + 3))) {
382                                            ++octalChars;
383                                        }
384                                    }
385                                    int value = '?';
386                                    try {
387                                        value = Integer.parseInt(text.substring(i + 1, i + 1 + octalChars), 8);
388                                    } catch (NumberFormatException e) {
389                                        LOGGER.debug("Not a number: {}", text.substring(i + 1, i + 1 + octalChars));
390                                    }
391
392                                    i += octalChars;
393                                    buffer.append((char) value);
394                                } else {
395                                    buffer.append(BACKSLASH);
396                                }
397                        }
398                    } else {
399                        buffer.append(BACKSLASH);
400                    }
401                    break;
402
403                case LEFT_CURLY_BRACKET: /*}*/
404                    if (!anchor && !link) {
405                        if (i + 1 < end && text.charAt(i + 1) == LEFT_CURLY_BRACKET /*}*/) {
406                            ++i;
407                            link = true;
408                            flushTraversed(buffer, sink);
409
410                            String linkAnchor = null;
411
412                            if (i + 1 < end && text.charAt(i + 1) == LEFT_CURLY_BRACKET /*}*/) {
413                                ++i;
414                                StringBuilder buf = new StringBuilder();
415                                i = skipTraversedLinkAnchor(text, i + 1, end, buf);
416                                linkAnchor = buf.toString();
417                            }
418
419                            if (linkAnchor == null) {
420                                linkAnchor = getTraversedLink(text, i + 1, end);
421                            }
422
423                            if (AptUtils.isInternalLink(linkAnchor)) {
424                                linkAnchor = "#" + linkAnchor;
425                            }
426
427                            int hashIndex = linkAnchor.indexOf("#");
428
429                            if (hashIndex != -1 && !AptUtils.isExternalLink(linkAnchor)) {
430                                String hash = linkAnchor.substring(hashIndex + 1);
431
432                                if (hash.endsWith(".html") && !hash.startsWith("./")) {
433                                    LOGGER.debug("Ambiguous link '{}'. If this is a local link, prepend \"./\"!", hash);
434                                }
435
436                                // link##anchor means literal
437                                if (hash.startsWith("#")) {
438                                    linkAnchor = linkAnchor.substring(0, hashIndex) + hash;
439                                } else if (!DoxiaUtils.isValidId(hash)) {
440                                    linkAnchor = linkAnchor.substring(0, hashIndex) + "#" + DoxiaUtils.encodeId(hash);
441
442                                    LOGGER.debug("Modified invalid link '{}' to '{}'", hash, linkAnchor);
443                                }
444                            }
445
446                            sink.link(linkAnchor);
447                        } else {
448                            anchor = true;
449                            flushTraversed(buffer, sink);
450
451                            String linkAnchor = getTraversedAnchor(text, i + 1, end);
452
453                            linkAnchor = DoxiaUtils.encodeId(linkAnchor);
454
455                            sink.anchor(linkAnchor);
456                        }
457                    } else {
458                        buffer.append(c);
459                    }
460                    break;
461
462                case /*{*/ RIGHT_CURLY_BRACKET:
463                    if (link && i + 1 < end && text.charAt(i + 1) == /*{*/ RIGHT_CURLY_BRACKET) {
464                        ++i;
465                        link = false;
466                        flushTraversed(buffer, sink);
467                        sink.link_();
468                    } else if (anchor) {
469                        anchor = false;
470                        flushTraversed(buffer, sink);
471                        sink.anchor_();
472                    } else {
473                        buffer.append(c);
474                    }
475                    break;
476
477                case LESS_THAN:
478                    if (!italic && !bold && !monospaced) {
479                        if (i + 1 < end && text.charAt(i + 1) == LESS_THAN) {
480                            if (i + 2 < end && text.charAt(i + 2) == LESS_THAN) {
481                                i += 2;
482                                monospaced = true;
483                                flushTraversed(buffer, sink);
484                                sink.monospaced();
485                            } else {
486                                ++i;
487                                bold = true;
488                                flushTraversed(buffer, sink);
489                                sink.bold();
490                            }
491                        } else {
492                            italic = true;
493                            flushTraversed(buffer, sink);
494                            sink.italic();
495                        }
496                    } else {
497                        buffer.append(c);
498                    }
499                    break;
500
501                case GREATER_THAN:
502                    if (monospaced
503                            && i + 2 < end
504                            && text.charAt(i + 1) == GREATER_THAN
505                            && text.charAt(i + 2) == GREATER_THAN) {
506                        i += 2;
507                        monospaced = false;
508                        flushTraversed(buffer, sink);
509                        sink.monospaced_();
510                    } else if (bold && i + 1 < end && text.charAt(i + 1) == GREATER_THAN) {
511                        ++i;
512                        bold = false;
513                        flushTraversed(buffer, sink);
514                        sink.bold_();
515                    } else if (italic) {
516                        italic = false;
517                        flushTraversed(buffer, sink);
518                        sink.italic_();
519                    } else {
520                        buffer.append(c);
521                    }
522                    break;
523
524                default:
525                    if (Character.isWhitespace(c)) {
526                        buffer.append(SPACE);
527
528                        // Skip to the last char of a sequence of white spaces.
529                        while (i + 1 < end && Character.isWhitespace(text.charAt(i + 1))) {
530                            ++i;
531                        }
532                    } else {
533                        buffer.append(c);
534                    }
535            }
536        }
537
538        if (monospaced) {
539            throw new AptParseException("missing '" + MONOSPACED_END_MARKUP + "'");
540        }
541        if (bold) {
542            throw new AptParseException("missing '" + BOLD_END_MARKUP + "'");
543        }
544        if (italic) {
545            throw new AptParseException("missing '" + ITALIC_END_MARKUP + "'");
546        }
547        if (link) {
548            throw new AptParseException("missing '" + LINK_END_MARKUP + "'");
549        }
550        if (anchor) {
551            throw new AptParseException("missing '" + ANCHOR_END_MARKUP + "'");
552        }
553
554        flushTraversed(buffer, sink);
555    }
556
557    // -----------------------------------------------------------------------
558
559    /**
560     * Returns the character at position i of the given string.
561     *
562     * @param string the string.
563     * @param length length.
564     * @param i offset.
565     * @return the character, or '\0' if i &gt; length.
566     */
567    protected static char charAt(String string, int length, int i) {
568        return (i < length) ? string.charAt(i) : '\0';
569    }
570
571    /**
572     * Skip spaces.
573     *
574     * @param string string.
575     * @param length length.
576     * @param i offset.
577     * @return int.
578     */
579    protected static int skipSpace(String string, int length, int i) {
580        loop:
581        for (; i < length; ++i) {
582            switch (string.charAt(i)) {
583                case SPACE:
584                case TAB:
585                    break;
586                default:
587                    break loop;
588            }
589        }
590        return i;
591    }
592
593    /**
594     * Replace part of a string.
595     *
596     * @param string the string
597     * @param oldSub the substring to replace
598     * @param newSub the replacement string
599     * @return String
600     */
601    protected static String replaceAll(String string, String oldSub, String newSub) {
602        StringBuilder replaced = new StringBuilder();
603        int oldSubLength = oldSub.length();
604        int begin, end;
605
606        begin = 0;
607        while ((end = string.indexOf(oldSub, begin)) >= 0) {
608            if (end > begin) {
609                replaced.append(string, begin, end);
610            }
611            replaced.append(newSub);
612            begin = end + oldSubLength;
613        }
614        if (begin < string.length()) {
615            replaced.append(string.substring(begin));
616        }
617
618        return replaced.toString();
619    }
620
621    /**
622     * {@inheritDoc}
623     */
624    protected void init() {
625        super.init();
626
627        this.sourceContent = null;
628        this.sink = null;
629        this.source = null;
630        this.block = null;
631        this.blockFileName = null;
632        this.blockLineNumber = 0;
633        this.line = null;
634    }
635
636    // ----------------------------------------------------------------------
637    // Private methods
638    // ----------------------------------------------------------------------
639
640    /**
641     * Parse the head of the Apt source document.
642     *
643     * @throws AptParseException if something goes wrong.
644     */
645    private void traverseHead() throws AptParseException {
646        sink.head();
647
648        if (block != null && block.getType() == TITLE) {
649            block.traverse();
650            nextBlock();
651        }
652
653        sink.head_();
654    }
655
656    /**
657     * Parse the body of the Apt source document.
658     *
659     * @throws AptParseException if something goes wrong.
660     */
661    private void traverseBody() throws AptParseException {
662        sink.body();
663
664        if (block != null) {
665            traverseSectionBlocks();
666        }
667
668        while (block != null) {
669            traverseSection(0);
670        }
671
672        sink.body_();
673    }
674
675    /**
676     * Parse a section of the Apt source document.
677     *
678     * @param level The section level.
679     * @throws AptParseException if something goes wrong.
680     */
681    private void traverseSection(int level) throws AptParseException {
682        if (block == null) {
683            return;
684        }
685
686        int type = SECTION1 + level;
687
688        expectedBlock(type);
689
690        switch (level) {
691            case 0:
692                sink.section1();
693                break;
694            case 1:
695                sink.section2();
696                break;
697            case 2:
698                sink.section3();
699                break;
700            case 3:
701                sink.section4();
702                break;
703            case 4:
704                sink.section5();
705                break;
706            default:
707                break;
708        }
709
710        block.traverse();
711
712        nextBlock();
713
714        traverseSectionBlocks();
715
716        while (block != null) {
717            if (block.getType() <= type) {
718                break;
719            }
720
721            traverseSection(level + 1);
722        }
723
724        switch (level) {
725            case 0:
726                sink.section1_();
727                break;
728            case 1:
729                sink.section2_();
730                break;
731            case 2:
732                sink.section3_();
733                break;
734            case 3:
735                sink.section4_();
736                break;
737            case 4:
738                sink.section5_();
739                break;
740            default:
741                break;
742        }
743    }
744
745    /**
746     * Parse the section blocks of the Apt source document.
747     *
748     * @throws AptParseException if something goes wrong.
749     */
750    private void traverseSectionBlocks() throws AptParseException {
751        loop:
752        while (block != null) {
753            switch (block.getType()) {
754                case PARAGRAPH:
755                case VERBATIM:
756                case FIGURE:
757                case TABLE:
758                case HORIZONTAL_RULE:
759                case PG_BREAK:
760                case MACRO:
761                case COMMENT_BLOCK:
762                    block.traverse();
763                    nextBlock();
764                    break;
765
766                case LIST_ITEM:
767                    traverseList();
768                    break;
769
770                case NUMBERED_LIST_ITEM:
771                    traverseNumberedList();
772                    break;
773
774                case DEFINITION_LIST_ITEM:
775                    traverseDefinitionList();
776                    break;
777
778                case LIST_BREAK:
779                    // May be this is a list break which has not been indented
780                    // very precisely.
781                    nextBlock();
782                    break;
783
784                default:
785                    // A section block which starts a new section.
786                    break loop;
787            }
788        }
789    }
790
791    /**
792     * Parse a list of the Apt source document.
793     *
794     * @throws AptParseException if something goes wrong.
795     */
796    private void traverseList() throws AptParseException {
797        if (block == null) {
798            return;
799        }
800
801        expectedBlock(LIST_ITEM);
802
803        int listIndent = block.getIndent();
804
805        sink.list();
806
807        sink.listItem();
808
809        block.traverse();
810
811        nextBlock();
812
813        loop:
814        while (block != null) {
815            int blockIndent = block.getIndent();
816
817            switch (block.getType()) {
818                case PARAGRAPH:
819                    if (blockIndent < listIndent) {
820                        break loop;
821                    }
822                /*FALLTHROUGH*/
823                case VERBATIM:
824                case MACRO:
825                case FIGURE:
826                case TABLE:
827                case HORIZONTAL_RULE:
828                case PG_BREAK:
829                    block.traverse();
830                    nextBlock();
831                    break;
832
833                case LIST_ITEM:
834                    if (blockIndent < listIndent) {
835                        break loop;
836                    }
837
838                    if (blockIndent > listIndent) {
839                        traverseList();
840                    } else {
841                        sink.listItem_();
842                        sink.listItem();
843                        block.traverse();
844                        nextBlock();
845                    }
846                    break;
847
848                case NUMBERED_LIST_ITEM:
849                    if (blockIndent < listIndent) {
850                        break loop;
851                    }
852
853                    traverseNumberedList();
854                    break;
855
856                case DEFINITION_LIST_ITEM:
857                    if (blockIndent < listIndent) {
858                        break loop;
859                    }
860
861                    traverseDefinitionList();
862                    break;
863
864                case LIST_BREAK:
865                    if (blockIndent >= listIndent) {
866                        nextBlock();
867                    }
868                /*FALLTHROUGH*/
869                default:
870                    // A block which ends the list.
871                    break loop;
872            }
873        }
874
875        sink.listItem_();
876        sink.list_();
877    }
878
879    /**
880     * Parse a numbered list of the Apt source document.
881     *
882     * @throws AptParseException if something goes wrong.
883     */
884    private void traverseNumberedList() throws AptParseException {
885        if (block == null) {
886            return;
887        }
888        expectedBlock(NUMBERED_LIST_ITEM);
889        int listIndent = block.getIndent();
890
891        sink.numberedList(((NumberedListItem) block).getNumbering());
892        sink.numberedListItem();
893        block.traverse();
894        nextBlock();
895
896        loop:
897        while (block != null) {
898            int blockIndent = block.getIndent();
899
900            switch (block.getType()) {
901                case PARAGRAPH:
902                    if (blockIndent < listIndent) {
903                        break loop;
904                    }
905                /*FALLTHROUGH*/
906                case VERBATIM:
907                case FIGURE:
908                case TABLE:
909                case HORIZONTAL_RULE:
910                case PG_BREAK:
911                    block.traverse();
912                    nextBlock();
913                    break;
914
915                case LIST_ITEM:
916                    if (blockIndent < listIndent) {
917                        break loop;
918                    }
919
920                    traverseList();
921                    break;
922
923                case NUMBERED_LIST_ITEM:
924                    if (blockIndent < listIndent) {
925                        break loop;
926                    }
927
928                    if (blockIndent > listIndent) {
929                        traverseNumberedList();
930                    } else {
931                        sink.numberedListItem_();
932                        sink.numberedListItem();
933                        block.traverse();
934                        nextBlock();
935                    }
936                    break;
937
938                case DEFINITION_LIST_ITEM:
939                    if (blockIndent < listIndent) {
940                        break loop;
941                    }
942
943                    traverseDefinitionList();
944                    break;
945
946                case LIST_BREAK:
947                    if (blockIndent >= listIndent) {
948                        nextBlock();
949                    }
950                /*FALLTHROUGH*/
951                default:
952                    // A block which ends the list.
953                    break loop;
954            }
955        }
956
957        sink.numberedListItem_();
958        sink.numberedList_();
959    }
960
961    /**
962     * Parse a definition list of the Apt source document.
963     *
964     * @throws AptParseException if something goes wrong.
965     */
966    private void traverseDefinitionList() throws AptParseException {
967        if (block == null) {
968            return;
969        }
970        expectedBlock(DEFINITION_LIST_ITEM);
971        int listIndent = block.getIndent();
972
973        sink.definitionList();
974        sink.definitionListItem();
975        block.traverse();
976        nextBlock();
977
978        loop:
979        while (block != null) {
980            int blockIndent = block.getIndent();
981
982            switch (block.getType()) {
983                case PARAGRAPH:
984                    if (blockIndent < listIndent) {
985                        break loop;
986                    }
987                /*FALLTHROUGH*/
988                case VERBATIM:
989                case FIGURE:
990                case TABLE:
991                case HORIZONTAL_RULE:
992                case PG_BREAK:
993                    block.traverse();
994                    nextBlock();
995                    break;
996
997                case LIST_ITEM:
998                    if (blockIndent < listIndent) {
999                        break loop;
1000                    }
1001
1002                    traverseList();
1003                    break;
1004
1005                case NUMBERED_LIST_ITEM:
1006                    if (blockIndent < listIndent) {
1007                        break loop;
1008                    }
1009
1010                    traverseNumberedList();
1011                    break;
1012
1013                case DEFINITION_LIST_ITEM:
1014                    if (blockIndent < listIndent) {
1015                        break loop;
1016                    }
1017
1018                    if (blockIndent > listIndent) {
1019                        traverseDefinitionList();
1020                    } else {
1021                        sink.definition_();
1022                        sink.definitionListItem_();
1023                        sink.definitionListItem();
1024                        block.traverse();
1025                        nextBlock();
1026                    }
1027                    break;
1028
1029                case LIST_BREAK:
1030                    if (blockIndent >= listIndent) {
1031                        nextBlock();
1032                    }
1033                /*FALLTHROUGH*/
1034                default:
1035                    // A block which ends the list.
1036                    break loop;
1037            }
1038        }
1039
1040        sink.definition_();
1041        sink.definitionListItem_();
1042        sink.definitionList_();
1043    }
1044
1045    /**
1046     * Parse the next block of the Apt source document.
1047     *
1048     * @throws AptParseException if something goes wrong.
1049     */
1050    private void nextBlock() throws AptParseException {
1051        nextBlock(/*first*/ false);
1052    }
1053
1054    /**
1055     * Parse the next block of the Apt source document.
1056     *
1057     * @param firstBlock True if this is the first block of the Apt source document.
1058     * @throws AptParseException if something goes wrong.
1059     */
1060    private void nextBlock(boolean firstBlock) throws AptParseException {
1061        // Skip open lines.
1062        int length, indent, i;
1063
1064        skipLoop:
1065        for (; ; ) {
1066            if (line == null) {
1067                block = null;
1068                return;
1069            }
1070
1071            length = line.length();
1072            indent = 0;
1073            for (i = 0; i < length; ++i) {
1074                switch (line.charAt(i)) {
1075                    case SPACE:
1076                        ++indent;
1077                        break;
1078                    case TAB:
1079                        indent += 8;
1080                        break;
1081                    default:
1082                        break skipLoop;
1083                }
1084            }
1085
1086            if (i == length) {
1087                nextLine();
1088            }
1089        }
1090
1091        blockFileName = source.getName();
1092        blockLineNumber = source.getLineNumber();
1093        block = null;
1094        switch (line.charAt(i)) {
1095            case STAR:
1096                if (indent == 0) {
1097                    if (charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
1098                        block = new Table(indent, line);
1099                    } else if (charAt(line, length, i + 1) == STAR) {
1100                        if (charAt(line, length, i + 2) == STAR) {
1101                            if (charAt(line, length, i + 3) == STAR) {
1102                                block = new Section5(indent, line);
1103                            } else {
1104                                block = new Section4(indent, line);
1105                            }
1106                        } else {
1107                            block = new Section3(indent, line);
1108                        }
1109                    } else {
1110                        block = new Section2(indent, line);
1111                    }
1112                } else {
1113                    block = new ListItem(indent, line);
1114                }
1115                break;
1116            case LEFT_SQUARE_BRACKET:
1117                if (charAt(line, length, i + 1) == RIGHT_SQUARE_BRACKET) {
1118                    block = new ListBreak(indent, line);
1119                } else {
1120                    if (indent == 0) {
1121                        block = new Figure(indent, line);
1122                    } else {
1123                        if (charAt(line, length, i + 1) == LEFT_SQUARE_BRACKET) {
1124                            int numbering;
1125
1126                            switch (charAt(line, length, i + 2)) {
1127                                case NUMBERING_LOWER_ALPHA_CHAR:
1128                                    numbering = Sink.NUMBERING_LOWER_ALPHA;
1129                                    break;
1130                                case NUMBERING_UPPER_ALPHA_CHAR:
1131                                    numbering = Sink.NUMBERING_UPPER_ALPHA;
1132                                    break;
1133                                case NUMBERING_LOWER_ROMAN_CHAR:
1134                                    numbering = Sink.NUMBERING_LOWER_ROMAN;
1135                                    break;
1136                                case NUMBERING_UPPER_ROMAN_CHAR:
1137                                    numbering = Sink.NUMBERING_UPPER_ROMAN;
1138                                    break;
1139                                case NUMBERING:
1140                                default:
1141                                    // The first item establishes the numbering
1142                                    // scheme for the whole list.
1143                                    numbering = Sink.NUMBERING_DECIMAL;
1144                            }
1145
1146                            block = new NumberedListItem(indent, line, numbering);
1147                        } else {
1148                            block = new DefinitionListItem(indent, line);
1149                        }
1150                    }
1151                }
1152                break;
1153            case MINUS:
1154                if (charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
1155                    if (indent == 0) {
1156                        block = new Verbatim(indent, line);
1157                    } else {
1158                        if (firstBlock) {
1159                            block = new Title(indent, line);
1160                        }
1161                    }
1162                }
1163                break;
1164            case PLUS:
1165                if (indent == 0 && charAt(line, length, i + 1) == MINUS && charAt(line, length, i + 2) == MINUS) {
1166                    block = new Verbatim(indent, line);
1167                }
1168                break;
1169            case EQUAL:
1170                if (indent == 0 && charAt(line, length, i + 1) == EQUAL && charAt(line, length, i + 2) == EQUAL) {
1171                    block = new HorizontalRule(indent, line);
1172                }
1173                break;
1174            case PAGE_BREAK:
1175                if (indent == 0) {
1176                    block = new PageBreak(indent, line);
1177                }
1178                break;
1179            case PERCENT:
1180                if (indent == 0 && charAt(line, length, i + 1) == LEFT_CURLY_BRACKET) {
1181                    block = new MacroBlock(indent, line);
1182                }
1183                break;
1184            case COMMENT:
1185                if (charAt(line, length, i + 1) == COMMENT) {
1186                    block = new Comment(line.substring(i + 2));
1187                }
1188                break;
1189            default:
1190                break;
1191        }
1192
1193        if (block == null) {
1194            if (indent == 0) {
1195                block = new Section1(indent, line);
1196            } else {
1197                block = new Paragraph(indent, line);
1198            }
1199        }
1200    }
1201
1202    /**
1203     * Checks that the current block is of the expected type.
1204     *
1205     * @param type the expected type.
1206     * @throws AptParseException if something goes wrong.
1207     */
1208    private void expectedBlock(int type) throws AptParseException {
1209        int blockType = block.getType();
1210
1211        if (blockType != type) {
1212            throw new AptParseException("expected " + TYPE_NAMES[type] + ", found " + TYPE_NAMES[blockType]);
1213        }
1214    }
1215
1216    // -----------------------------------------------------------------------
1217
1218    /**
1219     * Determine if c is an octal character.
1220     *
1221     * @param c the character.
1222     * @return boolean
1223     */
1224    private static boolean isOctalChar(char c) {
1225        return (c >= '0' && c <= '7');
1226    }
1227
1228    /**
1229     * Determine if c is an hex character.
1230     *
1231     * @param c the character.
1232     * @return boolean
1233     */
1234    private static boolean isHexChar(char c) {
1235        return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'));
1236    }
1237
1238    /**
1239     * Emits the text so far parsed into the given sink.
1240     *
1241     * @param buffer A StringBuilder that contains the text to be flushed.
1242     * @param sink The sink to receive the text.
1243     */
1244    private static void flushTraversed(StringBuilder buffer, Sink sink) {
1245        if (buffer.length() > 0) {
1246            sink.text(buffer.toString());
1247            buffer.setLength(0);
1248        }
1249    }
1250
1251    /**
1252     * Parse the given text.
1253     *
1254     * @param text the text to parse.
1255     * @param begin offset.
1256     * @param end offset.
1257     * @param linkAnchor a StringBuilder.
1258     * @return int
1259     * @throws AptParseException if something goes wrong.
1260     */
1261    private static int skipTraversedLinkAnchor(String text, int begin, int end, StringBuilder linkAnchor)
1262            throws AptParseException {
1263        int i;
1264        loop:
1265        for (i = begin; i < end; ++i) {
1266            char c = text.charAt(i);
1267            switch (c) {
1268                case RIGHT_CURLY_BRACKET:
1269                    break loop;
1270                case BACKSLASH:
1271                    if (i + 1 < end) {
1272                        ++i;
1273                        linkAnchor.append(text.charAt(i));
1274                    } else {
1275                        linkAnchor.append(BACKSLASH);
1276                    }
1277                    break;
1278                default:
1279                    linkAnchor.append(c);
1280            }
1281        }
1282        if (i == end) {
1283            throw new AptParseException("missing '" + RIGHT_CURLY_BRACKET + "'");
1284        }
1285
1286        return i;
1287    }
1288
1289    /**
1290     * Parse the given text.
1291     *
1292     * @param text the text to parse.
1293     * @param begin offset.
1294     * @param end offset.
1295     * @return String
1296     * @throws AptParseException if something goes wrong.
1297     */
1298    private String getTraversedLink(String text, int begin, int end) throws AptParseException {
1299        char previous2 = LEFT_CURLY_BRACKET;
1300        char previous = LEFT_CURLY_BRACKET;
1301        int i;
1302
1303        for (i = begin; i < end; ++i) {
1304            char c = text.charAt(i);
1305            if (c == RIGHT_CURLY_BRACKET && previous == RIGHT_CURLY_BRACKET && previous2 != BACKSLASH) {
1306                break;
1307            }
1308
1309            previous2 = previous;
1310            previous = c;
1311        }
1312        if (i == end) {
1313            throw new AptParseException("missing '" + LEFT_CURLY_BRACKET + LEFT_CURLY_BRACKET + "'");
1314        }
1315
1316        return doGetTraversedLink(text, begin, i - 1);
1317    }
1318
1319    /**
1320     * Parse the given text.
1321     *
1322     * @param text the text to parse.
1323     * @param begin offset.
1324     * @param end offset.
1325     * @return String
1326     * @throws AptParseException if something goes wrong.
1327     */
1328    private String getTraversedAnchor(String text, int begin, int end) throws AptParseException {
1329        char previous = LEFT_CURLY_BRACKET;
1330        int i;
1331
1332        for (i = begin; i < end; ++i) {
1333            char c = text.charAt(i);
1334            if (c == RIGHT_CURLY_BRACKET && previous != BACKSLASH) {
1335                break;
1336            }
1337
1338            previous = c;
1339        }
1340        if (i == end) {
1341            throw new AptParseException("missing '" + RIGHT_CURLY_BRACKET + "'");
1342        }
1343
1344        return doGetTraversedLink(text, begin, i);
1345    }
1346
1347    /**
1348     * Parse the given text.
1349     *
1350     * @param text the text to parse.
1351     * @param begin offset.
1352     * @param end offset.
1353     * @return String
1354     * @throws AptParseException if something goes wrong.
1355     */
1356    private String doGetTraversedLink(String text, int begin, int end) throws AptParseException {
1357        final StringBuilder buffer = new StringBuilder(end - begin);
1358
1359        Sink linkSink = new SinkAdapter() {
1360            @Override
1361            public void lineBreak(SinkEventAttributes attributes) {
1362                buffer.append(SPACE);
1363            }
1364
1365            @Override
1366            public void nonBreakingSpace() {
1367                buffer.append(SPACE);
1368            }
1369
1370            @Override
1371            public void text(String text, SinkEventAttributes attributes) {
1372                buffer.append(text);
1373            }
1374        };
1375        doTraverseText(text, begin, end, linkSink);
1376
1377        return buffer.toString().trim();
1378    }
1379
1380    // -----------------------------------------------------------------------
1381
1382    /** A block of an apt source document. */
1383    private abstract class Block {
1384        /** type. */
1385        protected int type;
1386
1387        /** indent. */
1388        protected int indent;
1389
1390        /** text. */
1391        protected String text;
1392
1393        /** textLength. */
1394        protected int textLength;
1395
1396        /**
1397         * Constructor.
1398         *
1399         * @param type the block type.
1400         * @param indent indent.
1401         * @throws AptParseException AptParseException
1402         */
1403        Block(int type, int indent) throws AptParseException {
1404            this(type, indent, null);
1405        }
1406
1407        /**
1408         * Constructor.
1409         *
1410         * @param type type.
1411         * @param indent indent.
1412         * @param firstLine the first line.
1413         * @throws AptParseException AptParseException
1414         */
1415        Block(int type, int indent, String firstLine) throws AptParseException {
1416            this.type = type;
1417            this.indent = indent;
1418
1419            // Skip first line ---
1420            AptParser.this.nextLine();
1421
1422            if (firstLine == null) {
1423                text = null;
1424                textLength = 0;
1425            } else {
1426                // Read block ---
1427                StringBuilder buffer = new StringBuilder(firstLine);
1428
1429                while (AptParser.this.line != null) {
1430                    String l = AptParser.this.line;
1431                    int length = l.length();
1432                    int i = 0;
1433
1434                    i = skipSpace(l, length, i);
1435                    if (i == length) {
1436                        // Stop after open line and skip it.
1437                        AptParser.this.nextLine();
1438                        break;
1439                    } else if ((AptParser.charAt(l, length, i) == COMMENT
1440                                    && AptParser.charAt(l, length, i + 1) == COMMENT)
1441                            || type == COMMENT_BLOCK) {
1442                        // parse comments as separate blocks line by line
1443                        break;
1444                    }
1445
1446                    buffer.append(EOL);
1447                    buffer.append(l);
1448
1449                    AptParser.this.nextLine();
1450                }
1451
1452                text = buffer.toString();
1453                textLength = text.length();
1454            }
1455        }
1456
1457        /**
1458         * Return the block type.
1459         *
1460         * @return int
1461         */
1462        public final int getType() {
1463            return type;
1464        }
1465
1466        /**
1467         * Return the block indent.
1468         *
1469         * @return int
1470         */
1471        public final int getIndent() {
1472            return indent;
1473        }
1474
1475        /**
1476         * Parse the block.
1477         *
1478         * @throws AptParseException if something goes wrong.
1479         */
1480        public abstract void traverse() throws AptParseException;
1481
1482        /**
1483         * Traverse the text.
1484         *
1485         * @param begin offset.
1486         * @throws AptParseException if something goes wrong.
1487         */
1488        protected void traverseText(int begin) throws AptParseException {
1489            traverseText(begin, text.length());
1490        }
1491
1492        /**
1493         * Traverse the text.
1494         *
1495         * @param begin offset.
1496         * @param end offset.
1497         * @throws AptParseException if something goes wrong.
1498         */
1499        protected void traverseText(int begin, int end) throws AptParseException {
1500            AptParser.this.doTraverseText(text, begin, end, AptParser.this.sink);
1501        }
1502
1503        /**
1504         * Skip spaces.
1505         *
1506         * @return int.
1507         */
1508        protected int skipLeadingBullets() {
1509            int i = skipSpaceFrom(0);
1510            for (; i < textLength; ++i) {
1511                if (text.charAt(i) != STAR) {
1512                    break;
1513                }
1514            }
1515            return skipSpaceFrom(i);
1516        }
1517
1518        /**
1519         * Skip brackets.
1520         *
1521         * @param i offset.
1522         * @return int.
1523         * @throws AptParseException if something goes wrong.
1524         */
1525        protected int skipFromLeftToRightBracket(int i) throws AptParseException {
1526            char previous = LEFT_SQUARE_BRACKET;
1527            for (++i; i < textLength; ++i) {
1528                char c = text.charAt(i);
1529                if (c == RIGHT_SQUARE_BRACKET && previous != BACKSLASH) {
1530                    break;
1531                }
1532                previous = c;
1533            }
1534            if (i == textLength) {
1535                throw new AptParseException("missing '" + RIGHT_SQUARE_BRACKET + "'");
1536            }
1537
1538            return i;
1539        }
1540
1541        /**
1542         * Skip spaces.
1543         *
1544         * @param i offset.
1545         * @return int.
1546         */
1547        protected final int skipSpaceFrom(int i) {
1548            return AptParser.skipSpace(text, textLength, i);
1549        }
1550    }
1551
1552    /** A ListBreak Block. */
1553    private class ListBreak extends AptParser.Block {
1554        /**
1555         * Constructor.
1556         *
1557         * @param indent indent.
1558         * @param firstLine the first line.
1559         * @throws AptParseException AptParseException
1560         */
1561        ListBreak(int indent, String firstLine) throws AptParseException {
1562            super(AptParser.LIST_BREAK, indent, firstLine);
1563        }
1564
1565        public void traverse() throws AptParseException {
1566            throw new AptParseException("internal error: traversing list break");
1567        }
1568    }
1569
1570    /** A Title Block. */
1571    private class Title extends Block {
1572        /**
1573         * Constructor.
1574         *
1575         * @param indent indent.
1576         * @param firstLine the first line.
1577         * @throws AptParseException AptParseException
1578         */
1579        Title(int indent, String firstLine) throws AptParseException {
1580            super(TITLE, indent, firstLine);
1581        }
1582
1583        public void traverse() throws AptParseException {
1584            StringTokenizer lines = new StringTokenizer(text, EOL);
1585            int separator = -1;
1586            boolean firstLine = true;
1587            boolean title = false;
1588            boolean author = false;
1589            boolean date = false;
1590
1591            loop:
1592            while (lines.hasMoreTokens()) {
1593                String line = lines.nextToken().trim();
1594                int lineLength = line.length();
1595
1596                if (AptParser.charAt(line, lineLength, 0) == MINUS
1597                        && AptParser.charAt(line, lineLength, 1) == MINUS
1598                        && AptParser.charAt(line, lineLength, 2) == MINUS) {
1599                    switch (separator) {
1600                        case 0:
1601                            if (title) {
1602                                AptParser.this.sink.title_();
1603                            }
1604                        case 1:
1605                            if (author) {
1606                                AptParser.this.sink.author_();
1607                            }
1608                            break;
1609                        case 2:
1610                            // Note that an extra decorative line is allowed
1611                            // at the end of the author.
1612                            break loop;
1613                        default:
1614                            break;
1615                    }
1616
1617                    ++separator;
1618                    firstLine = true;
1619                } else {
1620                    if (firstLine) {
1621                        firstLine = false;
1622                        switch (separator) {
1623                            case 0:
1624                                title = true;
1625                                AptParser.this.sink.title();
1626                                break;
1627                            case 1:
1628                                author = true;
1629                                AptParser.this.sink.author();
1630                                break;
1631                            case 2:
1632                                date = true;
1633                                AptParser.this.sink.date();
1634                                break;
1635                            default:
1636                                break;
1637                        }
1638                    } else {
1639                        if (separator == 1) {
1640                            AptParser.this.sink.author_();
1641                            AptParser.this.sink.author();
1642                        } else {
1643                            // An implicit lineBreak separates title lines.
1644                            AptParser.this.sink.lineBreak();
1645                        }
1646                    }
1647
1648                    AptParser.this.doTraverseText(line, 0, lineLength, AptParser.this.sink);
1649                }
1650            }
1651
1652            switch (separator) {
1653                case 0:
1654                    if (title) {
1655                        AptParser.this.sink.title_();
1656                    }
1657                case 1:
1658                    if (author) {
1659                        AptParser.this.sink.author_();
1660                    }
1661                    break;
1662                case 2:
1663                    if (date) {
1664                        AptParser.this.sink.date_();
1665                    }
1666                    break;
1667                default:
1668                    break;
1669            }
1670        }
1671    }
1672
1673    /** A Section Block. */
1674    private abstract class Section extends Block {
1675        /**
1676         * Constructor.
1677         *
1678         * @param type type.
1679         * @param indent indent.
1680         * @param firstLine the first line.
1681         * @throws AptParseException AptParseException
1682         */
1683        Section(int type, int indent, String firstLine) throws AptParseException {
1684            super(type, indent, firstLine);
1685        }
1686
1687        public void traverse() throws AptParseException {
1688            Title();
1689            traverseText(skipLeadingBullets());
1690            Title_();
1691        }
1692
1693        /** Start a title. */
1694        public abstract void Title();
1695
1696        /** End a title. */
1697        public abstract void Title_();
1698    }
1699
1700    /** A Section1 Block. */
1701    private class Section1 extends Section {
1702        /**
1703         * Constructor.
1704         *
1705         * @param indent indent.
1706         * @param firstLine the first line.
1707         * @throws AptParseException AptParseException
1708         */
1709        Section1(int indent, String firstLine) throws AptParseException {
1710            super(SECTION1, indent, firstLine);
1711        }
1712
1713        public void Title() {
1714            AptParser.this.sink.sectionTitle1();
1715        }
1716
1717        public void Title_() {
1718            AptParser.this.sink.sectionTitle1_();
1719        }
1720    }
1721
1722    /** A Section2 Block. */
1723    private class Section2 extends Section {
1724        /**
1725         * Constructor.
1726         *
1727         * @param indent indent.
1728         * @param firstLine the first line.
1729         * @throws AptParseException AptParseException
1730         */
1731        Section2(int indent, String firstLine) throws AptParseException {
1732            super(SECTION2, indent, firstLine);
1733        }
1734
1735        public void Title() {
1736            AptParser.this.sink.sectionTitle2();
1737        }
1738
1739        public void Title_() {
1740            AptParser.this.sink.sectionTitle2_();
1741        }
1742    }
1743
1744    /** A Section3 Block. */
1745    public class Section3 extends Section {
1746        /**
1747         * Constructor.
1748         *
1749         * @param indent indent.
1750         * @param firstLine the first line.
1751         * @throws AptParseException AptParseException
1752         */
1753        Section3(int indent, String firstLine) throws AptParseException {
1754            super(SECTION3, indent, firstLine);
1755        }
1756
1757        public void Title() {
1758            AptParser.this.sink.sectionTitle3();
1759        }
1760
1761        public void Title_() {
1762            AptParser.this.sink.sectionTitle3_();
1763        }
1764    }
1765
1766    /** A Section4 Block. */
1767    private class Section4 extends Section {
1768        /**
1769         * Constructor.
1770         *
1771         * @param indent indent.
1772         * @param firstLine the first line.
1773         * @throws AptParseException AptParseException
1774         */
1775        Section4(int indent, String firstLine) throws AptParseException {
1776            super(SECTION4, indent, firstLine);
1777        }
1778
1779        public void Title() {
1780            AptParser.this.sink.sectionTitle4();
1781        }
1782
1783        public void Title_() {
1784            AptParser.this.sink.sectionTitle4_();
1785        }
1786    }
1787
1788    /** A Section5 Block. */
1789    private class Section5 extends Section {
1790        /**
1791         * Constructor.
1792         *
1793         * @param indent indent.
1794         * @param firstLine the first line.
1795         * @throws AptParseException AptParseException
1796         */
1797        Section5(int indent, String firstLine) throws AptParseException {
1798            super(SECTION5, indent, firstLine);
1799        }
1800
1801        public void Title() {
1802            AptParser.this.sink.sectionTitle5();
1803        }
1804
1805        public void Title_() {
1806            AptParser.this.sink.sectionTitle5_();
1807        }
1808    }
1809
1810    /** A Paragraph Block. */
1811    private class Paragraph extends Block {
1812        /**
1813         * Constructor.
1814         *
1815         * @param indent indent.
1816         * @param firstLine the first line.
1817         * @throws AptParseException AptParseException
1818         */
1819        Paragraph(int indent, String firstLine) throws AptParseException {
1820            super(PARAGRAPH, indent, firstLine);
1821        }
1822
1823        public void traverse() throws AptParseException {
1824            AptParser.this.sink.paragraph();
1825            traverseText(skipSpaceFrom(0));
1826            AptParser.this.sink.paragraph_();
1827        }
1828    }
1829
1830    /** A Comment Block. */
1831    private class Comment extends Block {
1832        /**
1833         * Constructor.
1834         *
1835         * @param line the comment line.
1836         * @throws AptParseException AptParseException
1837         */
1838        Comment(String line) throws AptParseException {
1839            super(COMMENT_BLOCK, 0, line);
1840        }
1841
1842        public void traverse() throws AptParseException {
1843            if (isEmitComments()) {
1844                AptParser.this.sink.comment(text);
1845                // APT comments always end with a line break
1846                AptParser.this.sink.markupLineBreak(0);
1847            }
1848        }
1849    }
1850
1851    /** A Verbatim Block. */
1852    private class Verbatim extends Block {
1853        /** source. */
1854        private boolean source;
1855
1856        /**
1857         * Constructor.
1858         *
1859         * @param indent indent.
1860         * @param firstLine the first line.
1861         * @throws AptParseException AptParseException
1862         */
1863        Verbatim(int indent, String firstLine) throws AptParseException {
1864            super(VERBATIM, indent, null);
1865
1866            // Read block (first line already skipped) ---
1867
1868            StringBuilder buffer = new StringBuilder();
1869            char firstChar = firstLine.charAt(0);
1870            source = (firstChar == PLUS);
1871
1872            while (AptParser.this.line != null) {
1873                String l = AptParser.this.line;
1874                int length = l.length();
1875
1876                if (AptParser.charAt(l, length, 0) == firstChar
1877                        && AptParser.charAt(l, length, 1) == MINUS
1878                        && AptParser.charAt(l, length, 2) == MINUS) {
1879                    AptParser.this.nextLine();
1880
1881                    break;
1882                }
1883
1884                // Expand tabs ---
1885
1886                int prevColumn, column;
1887
1888                column = 0;
1889
1890                for (int i = 0; i < length; ++i) {
1891                    char c = l.charAt(i);
1892
1893                    if (c == TAB) {
1894                        prevColumn = column;
1895
1896                        column = ((column + 1 + TAB_WIDTH - 1) / TAB_WIDTH) * TAB_WIDTH;
1897
1898                        buffer.append(SPACES, 0, column - prevColumn);
1899                    } else {
1900                        ++column;
1901                        buffer.append(c);
1902                    }
1903                }
1904                buffer.append(EOL);
1905
1906                AptParser.this.nextLine();
1907            }
1908
1909            // The last '\n' is mandatory before the "---" delimeter but is
1910            // not part of the verbatim text.
1911            textLength = buffer.length();
1912
1913            if (textLength > 0) {
1914                --textLength;
1915
1916                buffer.setLength(textLength);
1917            }
1918
1919            text = buffer.toString();
1920        }
1921
1922        public void traverse() throws AptParseException {
1923            AptParser.this.sink.verbatim(source ? SinkEventAttributeSet.SOURCE : null);
1924            AptParser.this.sink.text(text);
1925            AptParser.this.sink.verbatim_();
1926        }
1927    }
1928
1929    /** A Figure Block. */
1930    private class Figure extends Block {
1931        /**
1932         * Constructor.
1933         *
1934         * @param indent indent.
1935         * @param firstLine the first line.
1936         * @throws AptParseException AptParseException
1937         */
1938        Figure(int indent, String firstLine) throws AptParseException {
1939            super(FIGURE, indent, firstLine);
1940        }
1941
1942        public void traverse() throws AptParseException {
1943            AptParser.this.sink.figure();
1944
1945            int i = skipFromLeftToRightBracket(0);
1946            AptParser.this.sink.figureGraphics(text.substring(1, i));
1947
1948            i = skipSpaceFrom(i + 1);
1949            if (i < textLength) {
1950                AptParser.this.sink.figureCaption();
1951                traverseText(i);
1952                AptParser.this.sink.figureCaption_();
1953            }
1954
1955            AptParser.this.sink.figure_();
1956        }
1957    }
1958
1959    /** A Table Block. */
1960    private class Table extends Block {
1961        /**
1962         * Constructor.
1963         *
1964         * @param indent indent.
1965         * @param firstLine the first line.
1966         * @throws AptParseException AptParseException
1967         */
1968        Table(int indent, String firstLine) throws AptParseException {
1969            super(TABLE, indent, firstLine);
1970        }
1971
1972        public void traverse() throws AptParseException {
1973            int captionIndex = -1;
1974            int nextLineIndex = 0;
1975            int init = 2;
1976            int[] justification = null;
1977            int rows = 0;
1978            int columns = 0;
1979            StringBuilder[] cells = null;
1980            boolean[] headers = null;
1981            boolean grid;
1982
1983            AptParser.this.sink.table();
1984
1985            while (nextLineIndex < textLength) {
1986                int i = text.indexOf("*--", nextLineIndex);
1987                if (i < 0) {
1988                    captionIndex = nextLineIndex;
1989                    break;
1990                }
1991
1992                String line;
1993                i = text.indexOf('\n', nextLineIndex);
1994                if (i < 0) {
1995                    line = text.substring(nextLineIndex);
1996                    nextLineIndex = textLength;
1997                } else {
1998                    line = text.substring(nextLineIndex, i);
1999                    nextLineIndex = i + 1;
2000                }
2001                int lineLength = line.length();
2002
2003                if (line.indexOf("*--") == 0) {
2004                    if (init == 2) {
2005                        init = 1;
2006                        justification = parseJustification(line, lineLength);
2007                        columns = justification.length;
2008                        cells = new StringBuilder[columns];
2009                        headers = new boolean[columns];
2010                        for (i = 0; i < columns; ++i) {
2011                            cells[i] = new StringBuilder();
2012                            headers[i] = false;
2013                        }
2014                    } else {
2015                        if (traverseRow(cells, headers, justification)) {
2016                            ++rows;
2017                        }
2018                        justification = parseJustification(line, lineLength);
2019                    }
2020                } else {
2021                    if (init == 1) {
2022                        init = 0;
2023                        grid = (AptParser.charAt(line, lineLength, 0) == PIPE);
2024                        AptParser.this.sink.tableRows(justification, grid);
2025                    }
2026
2027                    line = replaceAll(line, "\\|", "\\u007C");
2028
2029                    StringTokenizer cellLines = new StringTokenizer(line, "|", true);
2030
2031                    i = 0;
2032                    boolean processedGrid = false;
2033                    while (cellLines.hasMoreTokens()) {
2034                        String cellLine = cellLines.nextToken();
2035                        if ("|".equals(cellLine)) {
2036                            if (processedGrid) {
2037                                headers[i] = true;
2038                            } else {
2039                                processedGrid = true;
2040                                headers[i] = false;
2041                            }
2042                            continue;
2043                        }
2044                        processedGrid = false;
2045                        cellLine = replaceAll(cellLine, "\\", "\\u00A0"); // linebreak
2046                        // Escaped special characters: \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\.
2047                        cellLine = replaceAll(cellLine, "\\u00A0~", "\\~");
2048                        cellLine = replaceAll(cellLine, "\\u00A0=", "\\=");
2049                        cellLine = replaceAll(cellLine, "\\u00A0-", "\\-");
2050                        cellLine = replaceAll(cellLine, "\\u00A0+", "\\+");
2051                        cellLine = replaceAll(cellLine, "\\u00A0*", "\\*");
2052                        cellLine = replaceAll(cellLine, "\\u00A0[", "\\[");
2053                        cellLine = replaceAll(cellLine, "\\u00A0]", "\\]");
2054                        cellLine = replaceAll(cellLine, "\\u00A0<", "\\<");
2055                        cellLine = replaceAll(cellLine, "\\u00A0>", "\\>");
2056                        cellLine = replaceAll(cellLine, "\\u00A0{", "\\{");
2057                        cellLine = replaceAll(cellLine, "\\u00A0}", "\\}");
2058                        cellLine = replaceAll(cellLine, "\\u00A0u", "\\u");
2059                        cellLine = replaceAll(cellLine, "\\u00A0\\u00A0", "\\\\");
2060                        cellLine = cellLine.trim();
2061
2062                        StringBuilder cell = cells[i];
2063                        if (cellLine.length() > 0) {
2064                            // line break in table cells
2065                            if (cell.toString().trim().endsWith("\\u00A0")) {
2066                                cell.append("\\\n");
2067                            } else {
2068                                if (cell.length() != 0) {
2069                                    // Always add a space for multi line tables cells
2070                                    cell.append(" ");
2071                                }
2072                            }
2073
2074                            cell.append(cellLine);
2075                        }
2076
2077                        ++i;
2078                        if (i == columns) {
2079                            break;
2080                        }
2081                    }
2082                }
2083            }
2084            if (rows == 0) {
2085                throw new AptParseException("no table rows");
2086            }
2087            AptParser.this.sink.tableRows_();
2088
2089            if (captionIndex >= 0) {
2090                AptParser.this.sink.tableCaption();
2091                AptParser.this.doTraverseText(text, captionIndex, textLength, AptParser.this.sink);
2092                AptParser.this.sink.tableCaption_();
2093            }
2094
2095            AptParser.this.sink.table_();
2096        }
2097
2098        /**
2099         * Parse a table justification line.
2100         *
2101         * @param jline the justification line.
2102         * @param lineLength the length of the line. Must be > 2.
2103         * @return int[]
2104         * @throws AptParseException if something goes wrong.
2105         */
2106        private int[] parseJustification(String jline, int lineLength) throws AptParseException {
2107            int columns = 0;
2108
2109            for (int i = 2 /*Skip '*--'*/; i < lineLength; ++i) {
2110                switch (jline.charAt(i)) {
2111                    case STAR:
2112                    case PLUS:
2113                    case COLON:
2114                        ++columns;
2115                        break;
2116                    default:
2117                        break;
2118                }
2119            }
2120
2121            if (columns == 0) {
2122                throw new AptParseException("no columns specified");
2123            }
2124
2125            int[] justification = new int[columns];
2126            columns = 0;
2127            for (int i = 2; i < lineLength; ++i) {
2128                switch (jline.charAt(i)) {
2129                    case STAR:
2130                        justification[columns++] = Sink.JUSTIFY_CENTER;
2131                        break;
2132                    case PLUS:
2133                        justification[columns++] = Sink.JUSTIFY_LEFT;
2134                        break;
2135                    case COLON:
2136                        justification[columns++] = Sink.JUSTIFY_RIGHT;
2137                        break;
2138                    default:
2139                        break;
2140                }
2141            }
2142
2143            return justification;
2144        }
2145
2146        /**
2147         * Traverse a table row.
2148         *
2149         * @param cells The table cells.
2150         * @param headers true for header cells.
2151         * @param justification the justification for each cell.
2152         * @return boolean
2153         * @throws AptParseException if something goes wrong.
2154         */
2155        private boolean traverseRow(StringBuilder[] cells, boolean[] headers, int[] justification)
2156                throws AptParseException {
2157            // Skip empty row (a decorative line).
2158            boolean traversed = false;
2159            for (StringBuilder cell1 : cells) {
2160                if (cell1.length() > 0) {
2161                    traversed = true;
2162                    break;
2163                }
2164            }
2165
2166            if (traversed) {
2167                AptParser.this.sink.tableRow();
2168                for (int i = 0; i < cells.length; ++i) {
2169                    StringBuilder cell = cells[i];
2170
2171                    SinkEventAttributes justif;
2172                    switch (justification[i]) {
2173                        case Sink.JUSTIFY_CENTER:
2174                            justif = SinkEventAttributeSet.CENTER;
2175                            break;
2176                        case Sink.JUSTIFY_LEFT:
2177                            justif = SinkEventAttributeSet.LEFT;
2178                            break;
2179                        case Sink.JUSTIFY_RIGHT:
2180                            justif = SinkEventAttributeSet.RIGHT;
2181                            break;
2182                        default:
2183                            justif = SinkEventAttributeSet.LEFT;
2184                            break;
2185                    }
2186                    SinkEventAttributeSet event = new SinkEventAttributeSet();
2187                    event.addAttributes(justif);
2188
2189                    if (headers[i]) {
2190                        AptParser.this.sink.tableHeaderCell(event);
2191                    } else {
2192                        AptParser.this.sink.tableCell(event);
2193                    }
2194                    if (cell.length() > 0) {
2195                        AptParser.this.doTraverseText(cell.toString(), 0, cell.length(), AptParser.this.sink);
2196                        cell.setLength(0);
2197                    }
2198                    if (headers[i]) {
2199                        AptParser.this.sink.tableHeaderCell_();
2200                        // DOXIA-404: reset header for next row
2201                        headers[i] = false;
2202                    } else {
2203                        AptParser.this.sink.tableCell_();
2204                    }
2205                }
2206                AptParser.this.sink.tableRow_();
2207            }
2208
2209            return traversed;
2210        }
2211    }
2212
2213    /** A ListItem Block. */
2214    private class ListItem extends Block {
2215        /**
2216         * Constructor.
2217         *
2218         * @param indent indent.
2219         * @param firstLine the first line.
2220         * @throws AptParseException AptParseException
2221         */
2222        ListItem(int indent, String firstLine) throws AptParseException {
2223            super(LIST_ITEM, indent, firstLine);
2224        }
2225
2226        public void traverse() throws AptParseException {
2227            traverseText(skipLeadingBullets());
2228        }
2229    }
2230
2231    /** A NumberedListItem Block. */
2232    private class NumberedListItem extends Block {
2233        /** numbering. */
2234        private int numbering;
2235
2236        /**
2237         * Constructor.
2238         *
2239         * @param indent indent.
2240         * @param firstLine the first line.
2241         * @param number numbering.
2242         * @throws AptParseException AptParseException
2243         */
2244        NumberedListItem(int indent, String firstLine, int number) throws AptParseException {
2245            super(NUMBERED_LIST_ITEM, indent, firstLine);
2246            this.numbering = number;
2247        }
2248
2249        /**
2250         * getNumbering.
2251         *
2252         * @return int
2253         */
2254        public int getNumbering() {
2255            return numbering;
2256        }
2257
2258        public void traverse() throws AptParseException {
2259            traverseText(skipItemNumber());
2260        }
2261
2262        /**
2263         * skipItemNumber.
2264         *
2265         * @return int
2266         * @throws AptParseException AptParseException
2267         */
2268        private int skipItemNumber() throws AptParseException {
2269            int i = skipSpaceFrom(0);
2270
2271            char prevChar = SPACE;
2272            for (; i < textLength; ++i) {
2273                char c = text.charAt(i);
2274                if (c == RIGHT_SQUARE_BRACKET && prevChar == RIGHT_SQUARE_BRACKET) {
2275                    break;
2276                }
2277                prevChar = c;
2278            }
2279
2280            if (i == textLength) {
2281                throw new AptParseException("missing '" + RIGHT_SQUARE_BRACKET + RIGHT_SQUARE_BRACKET + "'");
2282            }
2283
2284            return skipSpaceFrom(i + 1);
2285        }
2286    }
2287
2288    /** A DefinitionListItem Block. */
2289    private class DefinitionListItem extends Block {
2290        /**
2291         * Constructor.
2292         *
2293         * @param indent indent.
2294         * @param firstLine the first line.
2295         * @throws AptParseException AptParseException
2296         */
2297        DefinitionListItem(int indent, String firstLine) throws AptParseException {
2298            super(DEFINITION_LIST_ITEM, indent, firstLine);
2299        }
2300
2301        public void traverse() throws AptParseException {
2302            int i = skipSpaceFrom(0);
2303            int j = skipFromLeftToRightBracket(i);
2304
2305            AptParser.this.sink.definedTerm();
2306            traverseText(i + 1, j);
2307            AptParser.this.sink.definedTerm_();
2308
2309            j = skipSpaceFrom(j + 1);
2310            if (j == textLength) {
2311                // TODO: this doesn't handle the case of a dd in a paragraph
2312                // throw new AptParseException("no definition");
2313            }
2314
2315            AptParser.this.sink.definition();
2316            traverseText(j);
2317        }
2318    }
2319
2320    /** A HorizontalRule Block. */
2321    private class HorizontalRule extends Block {
2322        /**
2323         * Constructor.
2324         *
2325         * @param indent indent.
2326         * @param firstLine the first line.
2327         * @throws AptParseException AptParseException
2328         */
2329        HorizontalRule(int indent, String firstLine) throws AptParseException {
2330            super(HORIZONTAL_RULE, indent, firstLine);
2331        }
2332
2333        public void traverse() throws AptParseException {
2334            AptParser.this.sink.horizontalRule();
2335        }
2336    }
2337
2338    /** A PageBreak Block. */
2339    private class PageBreak extends Block {
2340        /**
2341         * Constructor.
2342         *
2343         * @param indent indent.
2344         * @param firstLine the first line.
2345         * @throws AptParseException AptParseException
2346         */
2347        PageBreak(int indent, String firstLine) throws AptParseException {
2348            super(PG_BREAK, indent, firstLine);
2349        }
2350
2351        public void traverse() throws AptParseException {
2352            AptParser.this.sink.pageBreak();
2353        }
2354    }
2355
2356    /** A MacroBlock Block. */
2357    private class MacroBlock extends Block {
2358        /**
2359         * Constructor.
2360         *
2361         * @param indent indent.
2362         * @param firstLine the first line.
2363         * @throws AptParseException AptParseException
2364         */
2365        MacroBlock(int indent, String firstLine) throws AptParseException {
2366            super(MACRO, indent);
2367
2368            text = firstLine;
2369        }
2370
2371        public void traverse() throws AptParseException {
2372            if (isSecondParsing()) {
2373                return;
2374            }
2375
2376            final int start = text.indexOf('{');
2377            // the macro block always ends with the closing brace, so look for the last one:
2378            // parameter values may contain braces themselves (e.g. unresolved Velocity references
2379            // such as ${project.build.directory} when the raw source of a *.apt.vm file is parsed)
2380            final int end = text.lastIndexOf('}');
2381
2382            String s = text.substring(start + 1, end);
2383
2384            s = escapeForMacro(s);
2385
2386            String[] params = DoxiaStringUtils.split(s, "|");
2387
2388            String macroId = params[0];
2389
2390            Map<String, Object> parameters = new LinkedHashMap<>();
2391
2392            for (int i = 1; i < params.length; i++) {
2393                String[] param = DoxiaStringUtils.split(params[i], "=");
2394
2395                if (param.length == 1) {
2396                    throw new AptParseException("Missing 'key=value' pair for macro parameter: " + params[i]);
2397                }
2398
2399                String key = unescapeForMacro(param[0]);
2400                String value = unescapeForMacro(param[1]);
2401
2402                parameters.put(key, value);
2403            }
2404
2405            // getBasedir() does not work in multi-module builds, see DOXIA-373
2406            // the basedir should be injected from here, see DOXIA-224
2407            MacroRequest request = new MacroRequest(sourceContent, new AptParser(), parameters, getBasedir());
2408            try {
2409                AptParser.this.executeMacro(macroId, request, sink);
2410            } catch (MacroExecutionException e) {
2411                throw new AptParseException("Unable to execute macro in the APT document", e);
2412            } catch (MacroNotFoundException e) {
2413                throw new AptParseException("Unable to find macro used in the APT document", e);
2414            }
2415        }
2416
2417        /**
2418         * escapeForMacro
2419         *
2420         * @param s String
2421         * @return String
2422         */
2423        private String escapeForMacro(String s) {
2424            if (s == null || s.length() < 1) {
2425                return s;
2426            }
2427
2428            String result = s;
2429
2430            // use some outrageously out-of-place chars for text
2431            // (these are device control one/two in unicode)
2432            result = DoxiaStringUtils.replace(result, "\\=", "\u0011");
2433            result = DoxiaStringUtils.replace(result, "\\|", "\u0012");
2434
2435            return result;
2436        }
2437
2438        /**
2439         * unescapeForMacro
2440         *
2441         * @param s String
2442         * @return String
2443         */
2444        private String unescapeForMacro(String s) {
2445            if (s == null || s.length() < 1) {
2446                return s;
2447            }
2448
2449            String result = s;
2450
2451            result = DoxiaStringUtils.replace(result, "\u0011", "=");
2452            result = DoxiaStringUtils.replace(result, "\u0012", "|");
2453
2454            return result;
2455        }
2456    }
2457}