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