View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.module.apt;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.IOException;
25  import java.io.Reader;
26  import java.io.StringReader;
27  import java.io.StringWriter;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.StringTokenizer;
31  
32  import org.apache.commons.io.IOUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.maven.doxia.macro.MacroExecutionException;
35  import org.apache.maven.doxia.macro.MacroRequest;
36  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
37  import org.apache.maven.doxia.parser.AbstractTextParser;
38  import org.apache.maven.doxia.parser.ParseException;
39  import org.apache.maven.doxia.sink.Sink;
40  import org.apache.maven.doxia.sink.SinkEventAttributes;
41  import org.apache.maven.doxia.sink.impl.AbstractLocator;
42  import org.apache.maven.doxia.sink.impl.SinkAdapter;
43  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
44  import org.apache.maven.doxia.util.DoxiaUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  /**
49   * The APT parser.
50   * <br>
51   * Based on the <a href="http://www.xmlmind.com/aptconvert.html">APTconvert</a> project.
52   *
53   * @since 1.0
54   */
55  @Singleton
56  @Named("apt")
57  public class AptParser extends AbstractTextParser implements AptMarkup {
58      private static final Logger LOGGER = LoggerFactory.getLogger(AptParser.class);
59  
60      /** Title event id */
61      private static final int TITLE = 0;
62  
63      /** Section 1 event id */
64      private static final int SECTION1 = 1;
65  
66      /** Section 2 event id */
67      private static final int SECTION2 = 2;
68  
69      /** Section 3 event id */
70      private static final int SECTION3 = 3;
71  
72      /** Section 4 event id */
73      private static final int SECTION4 = 4;
74  
75      /** Section 5 event id */
76      private static final int SECTION5 = 5;
77  
78      /** Paragraph event id */
79      private static final int PARAGRAPH = 6;
80  
81      /** Verbatim event id */
82      private static final int VERBATIM = 7;
83  
84      /** Figure event id */
85      private static final int FIGURE = 8;
86  
87      /** Table event id */
88      private static final int TABLE = 9;
89  
90      /** List event id */
91      private static final int LIST_ITEM = 10;
92  
93      /** Numbered list event id */
94      private static final int NUMBERED_LIST_ITEM = 11;
95  
96      /** Definition list event id */
97      private static final int DEFINITION_LIST_ITEM = 12;
98  
99      /** 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 }