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