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