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