View Javadoc
1   package org.apache.maven.doxia.module.apt;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.doxia.macro.MacroExecutionException;
23  import org.apache.maven.doxia.macro.MacroRequest;
24  import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
25  import org.apache.maven.doxia.parser.AbstractTextParser;
26  import org.apache.maven.doxia.parser.ParseException;
27  import org.apache.maven.doxia.parser.Parser;
28  import org.apache.maven.doxia.sink.Sink;
29  import org.apache.maven.doxia.sink.SinkAdapter;
30  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
31  import org.apache.maven.doxia.sink.SinkEventAttributes;
32  import org.apache.maven.doxia.util.DoxiaUtils;
33  
34  import org.codehaus.plexus.component.annotations.Component;
35  import org.codehaus.plexus.util.IOUtil;
36  import org.codehaus.plexus.util.StringUtils;
37  
38  import java.io.IOException;
39  import java.io.Reader;
40  import java.io.StringReader;
41  import java.io.StringWriter;
42  import java.util.HashMap;
43  import java.util.Map;
44  import java.util.Set;
45  import java.util.StringTokenizer;
46  import java.util.TreeSet;
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   * @version $Id: AptParser.java 1465336 2013-04-07 07:39:00Z hboutemy $
54   * @since 1.0
55   */
56  @Component( role = Parser.class, hint = "apt" )
57  public class AptParser
58      extends AbstractTextParser
59      implements AptMarkup
60  {
61      /** Title event id */
62      private static final int TITLE = 0;
63  
64      /** Section 1 event id */
65      private static final int SECTION1 = 1;
66  
67      /** Section 2 event id */
68      private static final int SECTION2 = 2;
69  
70      /** Section 3 event id */
71      private static final int SECTION3 = 3;
72  
73      /** Section 4 event id */
74      private static final int SECTION4 = 4;
75  
76      /** Section 5 event id */
77      private static final int SECTION5 = 5;
78  
79      /** Paragraph event id */
80      private static final int PARAGRAPH = 6;
81  
82      /** Verbatim event id */
83      private static final int VERBATIM = 7;
84  
85      /** Figure event id */
86      private static final int FIGURE = 8;
87  
88      /** Table event id */
89      private static final int TABLE = 9;
90  
91      /** List event id */
92      private static final int LIST_ITEM = 10;
93  
94      /** Numbered list event id */
95      private static final int NUMBERED_LIST_ITEM = 11;
96  
97      /** Definition list event id */
98      private static final int DEFINITION_LIST_ITEM = 12;
99  
100     /** Horizontal rule event id */
101     private static final int HORIZONTAL_RULE = 13;
102 
103     /** Page break event id */
104     private static final int PG_BREAK = 14;
105 
106     /** List break event id */
107     private static final int LIST_BREAK = 15;
108 
109     /** Macro event id */
110     private static final int MACRO = 16;
111 
112     /** Comment event id. */
113     private static final int COMMENT_BLOCK = 17;
114 
115     /** String representations of event ids */
116     private static final String TYPE_NAMES[] = {
117         "TITLE",
118         "SECTION1",
119         "SECTION2",
120         "SECTION3",
121         "SECTION4",
122         "SECTION5",
123         "PARAGRAPH",
124         "VERBATIM",
125         "FIGURE",
126         "TABLE",
127         "LIST_ITEM",
128         "NUMBERED_LIST_ITEM",
129         "DEFINITION_LIST_ITEM",
130         "HORIZONTAL_RULE",
131         "PG_BREAK",
132         "LIST_BREAK",
133         "MACRO",
134         "COMMENT_BLOCK" };
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     /** Map of warn messages with a String as key to describe the error type and a Set as value.
168      * Using to reduce warn messages. */
169     protected Map<String, Set<String>> warnMessages;
170 
171     private static final int NUMBER_OF_SPACES = 85;
172 
173     static
174     {
175         SPACES = new char[NUMBER_OF_SPACES];
176 
177         for ( int i = 0; i < NUMBER_OF_SPACES; i++ )
178         {
179             SPACES[i] = ' ';
180         }
181     }
182 
183     // ----------------------------------------------------------------------
184     // Public methods
185     // ----------------------------------------------------------------------
186 
187     /** {@inheritDoc} */
188     public void parse( Reader source, Sink sink )
189         throws ParseException
190     {
191         init();
192 
193         try
194         {
195             StringWriter contentWriter = new StringWriter();
196             IOUtil.copy( source, contentWriter );
197             sourceContent = contentWriter.toString();
198         }
199         catch ( IOException e )
200         {
201             throw new AptParseException( "IOException: " + e.getMessage(), e );
202         }
203 
204         try
205         {
206             this.source = new AptReaderSource( new StringReader( sourceContent ) );
207 
208             this.sink = sink;
209             sink.enableLogging( getLog() );
210 
211             blockFileName = null;
212 
213             blockLineNumber = -1;
214 
215             // Lookahead line.
216             nextLine();
217 
218             // Lookahead block.
219             nextBlock( /*first*/true );
220 
221             // traverse comments
222             while ( ( block != null ) && ( block.getType() == COMMENT_BLOCK ) )
223             {
224                 block.traverse();
225                 nextBlock( /*first*/true );
226             }
227 
228             traverseHead();
229 
230             traverseBody();
231         }
232         catch ( AptParseException ape )
233         {
234             // TODO handle column number
235             throw new AptParseException( ape.getMessage(), ape, getSourceName(), getSourceLineNumber(), -1 );
236         }
237         finally
238         {
239             logWarnings();
240 
241             setSecondParsing( false );
242             init();
243         }
244     }
245 
246     /**
247      * Returns the name of the Apt source document.
248      *
249      * @return the source name.
250      */
251     public String getSourceName()
252     {
253         // Use this rather than source.getName() to report errors.
254         return blockFileName;
255     }
256 
257     /**
258      * Returns the current line number of the Apt source document.
259      *
260      * @return the line number.
261      */
262     public int getSourceLineNumber()
263     {
264         // Use this rather than source.getLineNumber() to report errors.
265         return blockLineNumber;
266     }
267 
268     // ----------------------------------------------------------------------
269     // Protected methods
270     // ----------------------------------------------------------------------
271 
272     /**
273      * Parse the next line of the Apt source document.
274      *
275      * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
276      */
277     protected void nextLine()
278         throws AptParseException
279     {
280         line = source.getNextLine();
281     }
282 
283     /**
284      * Parse the given text.
285      *
286      * @param text the text to parse.
287      * @param begin offset.
288      * @param end offset.
289      * @param sink the sink to receive the events.
290      * @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
291      */
292     protected void doTraverseText( String text, int begin, int end, Sink sink )
293         throws AptParseException
294     {
295         boolean anchor = false;
296         boolean link = false;
297         boolean italic = false;
298         boolean bold = false;
299         boolean monospaced = false;
300         StringBuilder buffer = new StringBuilder( end - begin );
301 
302         for ( int i = begin; i < end; ++i )
303         {
304             char c = text.charAt( i );
305             switch ( c )
306             {
307                 case BACKSLASH:
308                     if ( i + 1 < end )
309                     {
310                         char escaped = text.charAt( i + 1 );
311                         switch ( escaped )
312                         {
313                             case SPACE:
314                                 ++i;
315                                 flushTraversed( buffer, sink );
316                                 sink.nonBreakingSpace();
317                                 break;
318                             case '\r':
319                             case '\n':
320                                 ++i;
321                                 // Skip white space which may follow a line break.
322                                 while ( i + 1 < end && Character.isWhitespace( text.charAt( i + 1 ) ) )
323                                 {
324                                     ++i;
325                                 }
326                                 flushTraversed( buffer, sink );
327                                 sink.lineBreak();
328                                 break;
329                             case BACKSLASH:
330                             case PIPE:
331                             case COMMENT:
332                             case EQUAL:
333                             case MINUS:
334                             case PLUS:
335                             case STAR:
336                             case LEFT_SQUARE_BRACKET:
337                             case RIGHT_SQUARE_BRACKET:
338                             case LESS_THAN:
339                             case GREATER_THAN:
340                             case LEFT_CURLY_BRACKET:
341                             case RIGHT_CURLY_BRACKET:
342                                 ++i;
343                                 buffer.append( escaped );
344                                 break;
345                             case 'x':
346                                 if ( i + 3 < end && isHexChar( text.charAt( i + 2 ) )
347                                     && isHexChar( text.charAt( i + 3 ) ) )
348                                 {
349                                     int value = '?';
350                                     try
351                                     {
352                                         value = Integer.parseInt( text.substring( i + 2, i + 4 ), 16 );
353                                     }
354                                     catch ( NumberFormatException e )
355                                     {
356                                         if ( getLog().isDebugEnabled() )
357                                         {
358                                             getLog().debug( "Not a number: " + text.substring( i + 2, i + 4 ) );
359                                         }
360                                     }
361 
362                                     i += 3;
363                                     buffer.append( (char) value );
364                                 }
365                                 else
366                                 {
367                                     buffer.append( BACKSLASH );
368                                 }
369                                 break;
370                             case 'u':
371                                 if ( i + 5 < end && isHexChar( text.charAt( i + 2 ) )
372                                     && isHexChar( text.charAt( i + 3 ) ) && isHexChar( text.charAt( i + 4 ) )
373                                     && isHexChar( text.charAt( i + 5 ) ) )
374                                 {
375                                     int value = '?';
376                                     try
377                                     {
378                                         value = Integer.parseInt( text.substring( i + 2, i + 6 ), 16 );
379                                     }
380                                     catch ( NumberFormatException e )
381                                     {
382                                         if ( getLog().isDebugEnabled() )
383                                         {
384                                             getLog().debug( "Not a number: " + text.substring( i + 2, i + 6 ) );
385                                         }
386                                     }
387 
388                                     i += 5;
389                                     buffer.append( (char) value );
390                                 }
391                                 else
392                                 {
393                                     buffer.append( BACKSLASH );
394                                 }
395                                 break;
396                             default:
397                                 if ( isOctalChar( escaped ) )
398                                 {
399                                     int octalChars = 1;
400                                     if ( isOctalChar( charAt( text, end, i + 2 ) ) )
401                                     {
402                                         ++octalChars;
403                                         if ( isOctalChar( charAt( text, end, i + 3 ) ) )
404                                         {
405                                             ++octalChars;
406                                         }
407                                     }
408                                     int value = '?';
409                                     try
410                                     {
411                                         value = Integer.parseInt( text.substring( i + 1, i + 1 + octalChars ), 8 );
412                                     }
413                                     catch ( NumberFormatException e )
414                                     {
415                                         if ( getLog().isDebugEnabled() )
416                                         {
417                                             getLog().debug(
418                                                             "Not a number: "
419                                                                 + text.substring( i + 1, i + 1 + octalChars ) );
420                                         }
421                                     }
422 
423                                     i += octalChars;
424                                     buffer.append( (char) value );
425                                 }
426                                 else
427                                 {
428                                     buffer.append( BACKSLASH );
429                                 }
430                         }
431                     }
432                     else
433                     {
434                         buffer.append( BACKSLASH );
435                     }
436                     break;
437 
438                 case LEFT_CURLY_BRACKET: /*}*/
439                     if ( !anchor && !link )
440                     {
441                         if ( i + 1 < end && text.charAt( i + 1 ) == LEFT_CURLY_BRACKET /*}*/ )
442                         {
443                             ++i;
444                             link = true;
445                             flushTraversed( buffer, sink );
446 
447                             String linkAnchor = null;
448 
449                             if ( i + 1 < end && text.charAt( i + 1 ) == LEFT_CURLY_BRACKET /*}*/ )
450                             {
451                                 ++i;
452                                 StringBuilder buf = new StringBuilder();
453                                 i = skipTraversedLinkAnchor( text, i + 1, end, buf );
454                                 linkAnchor = buf.toString();
455                             }
456 
457                             if ( linkAnchor == null )
458                             {
459                                 linkAnchor = getTraversedLink( text, i + 1, end );
460                             }
461 
462                             if ( AptUtils.isInternalLink( linkAnchor ) )
463                             {
464                                 linkAnchor = "#" + linkAnchor;
465                             }
466 
467                             int hashIndex = linkAnchor.indexOf( "#" );
468 
469                             if ( hashIndex != -1 && !AptUtils.isExternalLink( linkAnchor ) )
470                             {
471                                 String hash = linkAnchor.substring( hashIndex + 1 );
472 
473                                 if ( hash.endsWith( ".html" ) && !hash.startsWith( "./" ) )
474                                 {
475                                     String msg = "Ambiguous link: '" + hash
476                                             + "'. If this is a local link, prepend \"./\"!";
477                                     logMessage( "ambiguousLink", msg );
478                                 }
479 
480                                 // link##anchor means literal
481                                 if ( hash.startsWith( "#" ) )
482                                 {
483                                     linkAnchor = linkAnchor.substring( 0, hashIndex ) + hash;
484                                 }
485                                 else if ( !DoxiaUtils.isValidId( hash ) )
486                                 {
487                                     linkAnchor =
488                                         linkAnchor.substring( 0, hashIndex ) + "#"
489                                             + DoxiaUtils.encodeId( hash, true );
490 
491                                     String msg = "Modified invalid link: '" + hash + "' to '" + linkAnchor + "'";
492                                     logMessage( "modifiedLink", msg );
493                                 }
494                             }
495 
496                             sink.link( linkAnchor );
497                         }
498                         else
499                         {
500                             anchor = true;
501                             flushTraversed( buffer, sink );
502 
503                             String linkAnchor = getTraversedAnchor( text, i + 1, end );
504 
505                             linkAnchor = AptUtils.encodeAnchor( linkAnchor );
506 
507                             sink.anchor( linkAnchor );
508                         }
509                     }
510                     else
511                     {
512                         buffer.append( c );
513                     }
514                     break;
515 
516                 case /*{*/RIGHT_CURLY_BRACKET:
517                     if ( link && i + 1 < end && text.charAt( i + 1 ) == /*{*/RIGHT_CURLY_BRACKET )
518                     {
519                         ++i;
520                         link = false;
521                         flushTraversed( buffer, sink );
522                         sink.link_();
523                     }
524                     else if ( anchor )
525                     {
526                         anchor = false;
527                         flushTraversed( buffer, sink );
528                         sink.anchor_();
529                     }
530                     else
531                     {
532                         buffer.append( c );
533                     }
534                     break;
535 
536                 case LESS_THAN:
537                     if ( !italic && !bold && !monospaced )
538                     {
539                         if ( i + 1 < end && text.charAt( i + 1 ) == LESS_THAN )
540                         {
541                             if ( i + 2 < end && text.charAt( i + 2 ) == LESS_THAN )
542                             {
543                                 i += 2;
544                                 monospaced = true;
545                                 flushTraversed( buffer, sink );
546                                 sink.monospaced();
547                             }
548                             else
549                             {
550                                 ++i;
551                                 bold = true;
552                                 flushTraversed( buffer, sink );
553                                 sink.bold();
554                             }
555                         }
556                         else
557                         {
558                             italic = true;
559                             flushTraversed( buffer, sink );
560                             sink.italic();
561                         }
562                     }
563                     else
564                     {
565                         buffer.append( c );
566                     }
567                     break;
568 
569                 case GREATER_THAN:
570                     if ( monospaced && i + 2 < end && text.charAt( i + 1 ) == GREATER_THAN
571                         && text.charAt( i + 2 ) == GREATER_THAN )
572                     {
573                         i += 2;
574                         monospaced = false;
575                         flushTraversed( buffer, sink );
576                         sink.monospaced_();
577                     }
578                     else if ( bold && i + 1 < end && text.charAt( i + 1 ) == GREATER_THAN )
579                     {
580                         ++i;
581                         bold = false;
582                         flushTraversed( buffer, sink );
583                         sink.bold_();
584                     }
585                     else if ( italic )
586                     {
587                         italic = false;
588                         flushTraversed( buffer, sink );
589                         sink.italic_();
590                     }
591                     else
592                     {
593                         buffer.append( c );
594                     }
595                     break;
596 
597                 default:
598                     if ( Character.isWhitespace( c ) )
599                     {
600                         buffer.append( SPACE );
601 
602                         // Skip to the last char of a sequence of white spaces.
603                         while ( i + 1 < end && Character.isWhitespace( text.charAt( i + 1 ) ) )
604                         {
605                             ++i;
606                         }
607                     }
608                     else
609                     {
610                         buffer.append( c );
611                     }
612             }
613         }
614 
615         if ( monospaced )
616         {
617             throw new AptParseException( "missing '" + MONOSPACED_END_MARKUP + "'" );
618         }
619         if ( bold )
620         {
621             throw new AptParseException( "missing '" + BOLD_END_MARKUP + "'" );
622         }
623         if ( italic )
624         {
625             throw new AptParseException( "missing '" + ITALIC_END_MARKUP + "'" );
626         }
627         if ( link )
628         {
629             throw new AptParseException( "missing '" + LINK_END_MARKUP + "'" );
630         }
631         if ( anchor )
632         {
633             throw new AptParseException( "missing '" + ANCHOR_END_MARKUP + "'" );
634         }
635 
636         flushTraversed( buffer, sink );
637     }
638 
639     // -----------------------------------------------------------------------
640 
641     /**
642      * Returns the character at position i of the given string.
643      *
644      * @param string the string.
645      * @param length length.
646      * @param i offset.
647      * @return the character, or '\0' if i > length.
648      */
649     protected static char charAt( String string, int length, int i )
650     {
651         return ( i < length ) ? string.charAt( i ) : '\0';
652     }
653 
654     /**
655      * Skip spaces.
656      *
657      * @param string string.
658      * @param length length.
659      * @param i offset.
660      * @return int.
661      */
662     protected static int skipSpace( String string, int length, int i )
663     {
664         loop: for ( ; i < length; ++i )
665         {
666             switch ( string.charAt( i ) )
667             {
668                 case SPACE:
669                 case TAB:
670                     break;
671                 default:
672                     break loop;
673             }
674         }
675         return i;
676     }
677 
678     /**
679      * Replace part of a string.
680      *
681      * @param string the string
682      * @param oldSub the substring to replace
683      * @param newSub the replacement string
684      * @return String
685      */
686     protected static String replaceAll( String string, String oldSub, String newSub )
687     {
688         StringBuilder replaced = new StringBuilder();
689         int oldSubLength = oldSub.length();
690         int begin, end;
691 
692         begin = 0;
693         while ( ( end = string.indexOf( oldSub, begin ) ) >= 0 )
694         {
695             if ( end > begin )
696             {
697                 replaced.append( string.substring( begin, end ) );
698             }
699             replaced.append( newSub );
700             begin = end + oldSubLength;
701         }
702         if ( begin < string.length() )
703         {
704             replaced.append( string.substring( begin ) );
705         }
706 
707         return replaced.toString();
708     }
709 
710     /** {@inheritDoc} */
711     protected void init()
712     {
713         super.init();
714 
715         this.sourceContent = null;
716         this.sink = null;
717         this.source = null;
718         this.block = null;
719         this.blockFileName = null;
720         this.blockLineNumber = 0;
721         this.line = null;
722         this.warnMessages = null;
723     }
724 
725     // ----------------------------------------------------------------------
726     // Private methods
727     // ----------------------------------------------------------------------
728 
729     /**
730      * Parse the head of the Apt source document.
731      *
732      * @throws AptParseException if something goes wrong.
733      */
734     private void traverseHead()
735         throws AptParseException
736     {
737         sink.head();
738 
739         if ( block != null && block.getType() == TITLE )
740         {
741             block.traverse();
742             nextBlock();
743         }
744 
745         sink.head_();
746     }
747 
748     /**
749      * Parse the body of the Apt source document.
750      *
751      * @throws AptParseException if something goes wrong.
752      */
753     private void traverseBody()
754         throws AptParseException
755     {
756         sink.body();
757 
758         if ( block != null )
759         {
760             traverseSectionBlocks();
761         }
762 
763         while ( block != null )
764         {
765             traverseSection( 0 );
766         }
767 
768         sink.body_();
769     }
770 
771     /**
772      * Parse a section of the Apt source document.
773      *
774      * @param level The section level.
775      * @throws AptParseException if something goes wrong.
776      */
777     private void traverseSection( int level )
778         throws AptParseException
779     {
780         if ( block == null )
781         {
782             return;
783         }
784 
785         int type = SECTION1 + level;
786 
787         expectedBlock( type );
788 
789         switch ( level )
790         {
791             case 0:
792                 sink.section1();
793                 break;
794             case 1:
795                 sink.section2();
796                 break;
797             case 2:
798                 sink.section3();
799                 break;
800             case 3:
801                 sink.section4();
802                 break;
803             case 4:
804                 sink.section5();
805                 break;
806             default:
807                 break;
808         }
809 
810         block.traverse();
811 
812         nextBlock();
813 
814         traverseSectionBlocks();
815 
816         while ( block != null )
817         {
818             if ( block.getType() <= type )
819             {
820                 break;
821             }
822 
823             traverseSection( level + 1 );
824         }
825 
826         switch ( level )
827         {
828             case 0:
829                 sink.section1_();
830                 break;
831             case 1:
832                 sink.section2_();
833                 break;
834             case 2:
835                 sink.section3_();
836                 break;
837             case 3:
838                 sink.section4_();
839                 break;
840             case 4:
841                 sink.section5_();
842                 break;
843             default:
844                 break;
845         }
846     }
847 
848     /**
849      * Parse the section blocks of the Apt source document.
850      *
851      * @throws AptParseException if something goes wrong.
852      */
853     private void traverseSectionBlocks()
854         throws AptParseException
855     {
856         loop: while ( block != null )
857         {
858             switch ( block.getType() )
859             {
860                 case PARAGRAPH:
861                 case VERBATIM:
862                 case FIGURE:
863                 case TABLE:
864                 case HORIZONTAL_RULE:
865                 case PG_BREAK:
866                 case MACRO:
867                 case COMMENT_BLOCK:
868                     block.traverse();
869                     nextBlock();
870                     break;
871 
872                 case LIST_ITEM:
873                     traverseList();
874                     break;
875 
876                 case NUMBERED_LIST_ITEM:
877                     traverseNumberedList();
878                     break;
879 
880                 case DEFINITION_LIST_ITEM:
881                     traverseDefinitionList();
882                     break;
883 
884                 case LIST_BREAK:
885                     // May be this is a list break which has not been indented
886                     // very precisely.
887                     nextBlock();
888                     break;
889 
890                 default:
891                     // A section block which starts a new section.
892                     break loop;
893             }
894         }
895     }
896 
897     /**
898      * Parse a list of the Apt source document.
899      *
900      * @throws AptParseException if something goes wrong.
901      */
902     private void traverseList()
903         throws AptParseException
904     {
905         if ( block == null )
906         {
907             return;
908         }
909 
910         expectedBlock( LIST_ITEM );
911 
912         int listIndent = block.getIndent();
913 
914         sink.list();
915 
916         sink.listItem();
917 
918         block.traverse();
919 
920         nextBlock();
921 
922         loop: while ( block != null )
923         {
924             int blockIndent = block.getIndent();
925 
926             switch ( block.getType() )
927             {
928                 case PARAGRAPH:
929                     if ( blockIndent < listIndent )
930                     {
931                         break loop;
932                     }
933                     /*FALLTHROUGH*/
934                 case VERBATIM:
935                 case MACRO:
936                 case FIGURE:
937                 case TABLE:
938                 case HORIZONTAL_RULE:
939                 case PG_BREAK:
940                     block.traverse();
941                     nextBlock();
942                     break;
943 
944                 case LIST_ITEM:
945                     if ( blockIndent < listIndent )
946                     {
947                         break loop;
948                     }
949 
950                     if ( blockIndent > listIndent )
951                     {
952                         traverseList();
953                     }
954                     else
955                     {
956                         sink.listItem_();
957                         sink.listItem();
958                         block.traverse();
959                         nextBlock();
960                     }
961                     break;
962 
963                 case NUMBERED_LIST_ITEM:
964                     if ( blockIndent < listIndent )
965                     {
966                         break loop;
967                     }
968 
969                     traverseNumberedList();
970                     break;
971 
972                 case DEFINITION_LIST_ITEM:
973                     if ( blockIndent < listIndent )
974                     {
975                         break loop;
976                     }
977 
978                     traverseDefinitionList();
979                     break;
980 
981                 case LIST_BREAK:
982                     if ( blockIndent >= listIndent )
983                     {
984                         nextBlock();
985                     }
986                     /*FALLTHROUGH*/
987                 default:
988                     // A block which ends the list.
989                     break loop;
990             }
991         }
992 
993         sink.listItem_();
994         sink.list_();
995     }
996 
997     /**
998      * Parse a numbered list of the Apt source document.
999      *
1000      * @throws AptParseException if something goes wrong.
1001      */
1002     private void traverseNumberedList()
1003         throws AptParseException
1004     {
1005         if ( block == null )
1006         {
1007             return;
1008         }
1009         expectedBlock( NUMBERED_LIST_ITEM );
1010         int listIndent = block.getIndent();
1011 
1012         sink.numberedList( ( (NumberedListItem) block ).getNumbering() );
1013         sink.numberedListItem();
1014         block.traverse();
1015         nextBlock();
1016 
1017         loop: while ( block != null )
1018         {
1019             int blockIndent = block.getIndent();
1020 
1021             switch ( block.getType() )
1022             {
1023                 case PARAGRAPH:
1024                     if ( blockIndent < listIndent )
1025                     {
1026                         break loop;
1027                     }
1028                     /*FALLTHROUGH*/
1029                 case VERBATIM:
1030                 case FIGURE:
1031                 case TABLE:
1032                 case HORIZONTAL_RULE:
1033                 case PG_BREAK:
1034                     block.traverse();
1035                     nextBlock();
1036                     break;
1037 
1038                 case LIST_ITEM:
1039                     if ( blockIndent < listIndent )
1040                     {
1041                         break loop;
1042                     }
1043 
1044                     traverseList();
1045                     break;
1046 
1047                 case NUMBERED_LIST_ITEM:
1048                     if ( blockIndent < listIndent )
1049                     {
1050                         break loop;
1051                     }
1052 
1053                     if ( blockIndent > listIndent )
1054                     {
1055                         traverseNumberedList();
1056                     }
1057                     else
1058                     {
1059                         sink.numberedListItem_();
1060                         sink.numberedListItem();
1061                         block.traverse();
1062                         nextBlock();
1063                     }
1064                     break;
1065 
1066                 case DEFINITION_LIST_ITEM:
1067                     if ( blockIndent < listIndent )
1068                     {
1069                         break loop;
1070                     }
1071 
1072                     traverseDefinitionList();
1073                     break;
1074 
1075                 case LIST_BREAK:
1076                     if ( blockIndent >= listIndent )
1077                     {
1078                         nextBlock();
1079                     }
1080                     /*FALLTHROUGH*/
1081                 default:
1082                     // A block which ends the list.
1083                     break loop;
1084             }
1085         }
1086 
1087         sink.numberedListItem_();
1088         sink.numberedList_();
1089     }
1090 
1091     /**
1092      * Parse a definition list of the Apt source document.
1093      *
1094      * @throws AptParseException if something goes wrong.
1095      */
1096     private void traverseDefinitionList()
1097         throws AptParseException
1098     {
1099         if ( block == null )
1100         {
1101             return;
1102         }
1103         expectedBlock( DEFINITION_LIST_ITEM );
1104         int listIndent = block.getIndent();
1105 
1106         sink.definitionList();
1107         sink.definitionListItem();
1108         block.traverse();
1109         nextBlock();
1110 
1111         loop: while ( block != null )
1112         {
1113             int blockIndent = block.getIndent();
1114 
1115             switch ( block.getType() )
1116             {
1117                 case PARAGRAPH:
1118                     if ( blockIndent < listIndent )
1119                     {
1120                         break loop;
1121                     }
1122                     /*FALLTHROUGH*/
1123                 case VERBATIM:
1124                 case FIGURE:
1125                 case TABLE:
1126                 case HORIZONTAL_RULE:
1127                 case PG_BREAK:
1128                     block.traverse();
1129                     nextBlock();
1130                     break;
1131 
1132                 case LIST_ITEM:
1133                     if ( blockIndent < listIndent )
1134                     {
1135                         break loop;
1136                     }
1137 
1138                     traverseList();
1139                     break;
1140 
1141                 case NUMBERED_LIST_ITEM:
1142                     if ( blockIndent < listIndent )
1143                     {
1144                         break loop;
1145                     }
1146 
1147                     traverseNumberedList();
1148                     break;
1149 
1150                 case DEFINITION_LIST_ITEM:
1151                     if ( blockIndent < listIndent )
1152                     {
1153                         break loop;
1154                     }
1155 
1156                     if ( blockIndent > listIndent )
1157                     {
1158                         traverseDefinitionList();
1159                     }
1160                     else
1161                     {
1162                         sink.definition_();
1163                         sink.definitionListItem_();
1164                         sink.definitionListItem();
1165                         block.traverse();
1166                         nextBlock();
1167                     }
1168                     break;
1169 
1170                 case LIST_BREAK:
1171                     if ( blockIndent >= listIndent )
1172                     {
1173                         nextBlock();
1174                     }
1175                     /*FALLTHROUGH*/
1176                 default:
1177                     // A block which ends the list.
1178                     break loop;
1179             }
1180         }
1181 
1182         sink.definition_();
1183         sink.definitionListItem_();
1184         sink.definitionList_();
1185     }
1186 
1187     /**
1188      * Parse the next block of the Apt source document.
1189      *
1190      * @throws AptParseException if something goes wrong.
1191      */
1192     private void nextBlock()
1193         throws AptParseException
1194     {
1195         nextBlock( /*first*/false );
1196     }
1197 
1198     /**
1199      * Parse the next block of the Apt source document.
1200      *
1201      * @param firstBlock True if this is the first block of the Apt source document.
1202      * @throws AptParseException if something goes wrong.
1203      */
1204     private void nextBlock( boolean firstBlock )
1205         throws AptParseException
1206     {
1207         // Skip open lines.
1208         int length, indent, i;
1209 
1210         skipLoop: for ( ;; )
1211         {
1212             if ( line == null )
1213             {
1214                 block = null;
1215                 return;
1216             }
1217 
1218             length = line.length();
1219             indent = 0;
1220             for ( i = 0; i < length; ++i )
1221             {
1222                 switch ( line.charAt( i ) )
1223                 {
1224                     case SPACE:
1225                         ++indent;
1226                         break;
1227                     case TAB:
1228                         indent += 8;
1229                         break;
1230                     default:
1231                         break skipLoop;
1232                 }
1233             }
1234 
1235             if ( i == length )
1236             {
1237                 nextLine();
1238             }
1239         }
1240 
1241         blockFileName = source.getName();
1242         blockLineNumber = source.getLineNumber();
1243         block = null;
1244         switch ( line.charAt( i ) )
1245         {
1246             case STAR:
1247                 if ( indent == 0 )
1248                 {
1249                     if ( charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
1250                     {
1251                         block = new Table( indent, line );
1252                     }
1253                     else if ( charAt( line, length, i + 1 ) == STAR )
1254                     {
1255                         if ( charAt( line, length, i + 2 ) == STAR )
1256                         {
1257                             if ( charAt( line, length, i + 3 ) == STAR )
1258                             {
1259                                 block = new Section5( indent, line );
1260                             }
1261                             else
1262                             {
1263                                 block = new Section4( indent, line );
1264                             }
1265                         }
1266                         else
1267                         {
1268                             block = new Section3( indent, line );
1269                         }
1270                     }
1271                     else
1272                     {
1273                         block = new Section2( indent, line );
1274                     }
1275                 }
1276                 else
1277                 {
1278                     block = new ListItem( indent, line );
1279                 }
1280                 break;
1281             case LEFT_SQUARE_BRACKET:
1282                 if ( charAt( line, length, i + 1 ) == RIGHT_SQUARE_BRACKET )
1283                 {
1284                     block = new ListBreak( indent, line );
1285                 }
1286                 else
1287                 {
1288                     if ( indent == 0 )
1289                     {
1290                         block = new Figure( indent, line );
1291                     }
1292                     else
1293                     {
1294                         if ( charAt( line, length, i + 1 ) == LEFT_SQUARE_BRACKET )
1295                         {
1296                             int numbering;
1297 
1298                             switch ( charAt( line, length, i + 2 ) )
1299                             {
1300                                 case NUMBERING_LOWER_ALPHA_CHAR:
1301                                     numbering = Sink.NUMBERING_LOWER_ALPHA;
1302                                     break;
1303                                 case NUMBERING_UPPER_ALPHA_CHAR:
1304                                     numbering = Sink.NUMBERING_UPPER_ALPHA;
1305                                     break;
1306                                 case NUMBERING_LOWER_ROMAN_CHAR:
1307                                     numbering = Sink.NUMBERING_LOWER_ROMAN;
1308                                     break;
1309                                 case NUMBERING_UPPER_ROMAN_CHAR:
1310                                     numbering = Sink.NUMBERING_UPPER_ROMAN;
1311                                     break;
1312                                 case NUMBERING:
1313                                 default:
1314                                     // The first item establishes the numbering
1315                                     // scheme for the whole list.
1316                                     numbering = Sink.NUMBERING_DECIMAL;
1317                             }
1318 
1319                             block = new NumberedListItem( indent, line, numbering );
1320                         }
1321                         else
1322                         {
1323                             block = new DefinitionListItem( indent, line );
1324                         }
1325                     }
1326                 }
1327                 break;
1328             case MINUS:
1329                 if ( charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
1330                 {
1331                     if ( indent == 0 )
1332                     {
1333                         block = new Verbatim( indent, line );
1334                     }
1335                     else
1336                     {
1337                         if ( firstBlock )
1338                         {
1339                             block = new Title( indent, line );
1340                         }
1341                     }
1342                 }
1343                 break;
1344             case PLUS:
1345                 if ( indent == 0 && charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
1346                 {
1347                     block = new Verbatim( indent, line );
1348                 }
1349                 break;
1350             case EQUAL:
1351                 if ( indent == 0 && charAt( line, length, i + 1 ) == EQUAL && charAt( line, length, i + 2 ) == EQUAL )
1352                 {
1353                     block = new HorizontalRule( indent, line );
1354                 }
1355                 break;
1356             case PAGE_BREAK:
1357                 if ( indent == 0 )
1358                 {
1359                     block = new PageBreak( indent, line );
1360                 }
1361                 break;
1362             case PERCENT:
1363                 if ( indent == 0 && charAt( line, length, i + 1 ) == LEFT_CURLY_BRACKET )
1364                 {
1365                     block = new MacroBlock( indent, line );
1366                 }
1367                 break;
1368             case COMMENT:
1369                 if ( charAt( line, length, i + 1 ) == COMMENT )
1370                 {
1371                     block = new Comment( line.substring( i + 2 ).trim() );
1372                 }
1373                 break;
1374             default:
1375                 break;
1376         }
1377 
1378         if ( block == null )
1379         {
1380             if ( indent == 0 )
1381             {
1382                 block = new Section1( indent, line );
1383             }
1384             else
1385             {
1386                 block = new Paragraph( indent, line );
1387             }
1388         }
1389     }
1390 
1391     /**
1392      * Checks that the current block is of the expected type.
1393      *
1394      * @param type the expected type.
1395      * @throws AptParseException if something goes wrong.
1396      */
1397     private void expectedBlock( int type )
1398         throws AptParseException
1399     {
1400         int blockType = block.getType();
1401 
1402         if ( blockType != type )
1403         {
1404             throw new AptParseException( "expected " + TYPE_NAMES[type] + ", found " + TYPE_NAMES[blockType] );
1405         }
1406     }
1407 
1408     // -----------------------------------------------------------------------
1409 
1410     /**
1411      * Determine if c is an octal character.
1412      *
1413      * @param c the character.
1414      * @return boolean
1415      */
1416     private static boolean isOctalChar( char c )
1417     {
1418         return ( c >= '0' && c <= '7' );
1419     }
1420 
1421     /**
1422      * Determine if c is an hex character.
1423      *
1424      * @param c the character.
1425      * @return boolean
1426      */
1427     private static boolean isHexChar( char c )
1428     {
1429         return ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'f' ) || ( c >= 'A' && c <= 'F' ) );
1430     }
1431 
1432     /**
1433      * Emits the text so far parsed into the given sink.
1434      *
1435      * @param buffer A StringBuilder that contains the text to be flushed.
1436      * @param sink The sink to receive the text.
1437      */
1438     private static void flushTraversed( StringBuilder buffer, Sink sink )
1439     {
1440         if ( buffer.length() > 0 )
1441         {
1442             sink.text( buffer.toString() );
1443             buffer.setLength( 0 );
1444         }
1445     }
1446 
1447     /**
1448      * Parse the given text.
1449      *
1450      * @param text the text to parse.
1451      * @param begin offset.
1452      * @param end offset.
1453      * @param linkAnchor a StringBuilder.
1454      * @return int
1455      * @throws AptParseException if something goes wrong.
1456      */
1457     private static int skipTraversedLinkAnchor( String text, int begin, int end, StringBuilder linkAnchor )
1458         throws AptParseException
1459     {
1460         int i;
1461         loop: for ( i = begin; i < end; ++i )
1462         {
1463             char c = text.charAt( i );
1464             switch ( c )
1465             {
1466                 case RIGHT_CURLY_BRACKET:
1467                     break loop;
1468                 case BACKSLASH:
1469                     if ( i + 1 < end )
1470                     {
1471                         ++i;
1472                         linkAnchor.append( text.charAt( i ) );
1473                     }
1474                     else
1475                     {
1476                         linkAnchor.append( BACKSLASH );
1477                     }
1478                     break;
1479                 default:
1480                     linkAnchor.append( c );
1481             }
1482         }
1483         if ( i == end )
1484         {
1485             throw new AptParseException( "missing '" + RIGHT_CURLY_BRACKET + "'" );
1486         }
1487 
1488         return i;
1489     }
1490 
1491     /**
1492      * Parse the given text.
1493      *
1494      * @param text the text to parse.
1495      * @param begin offset.
1496      * @param end offset.
1497      * @return String
1498      * @throws AptParseException if something goes wrong.
1499      */
1500     private String getTraversedLink( String text, int begin, int end )
1501         throws AptParseException
1502     {
1503         char previous2 = LEFT_CURLY_BRACKET;
1504         char previous = LEFT_CURLY_BRACKET;
1505         int i;
1506 
1507         for ( i = begin; i < end; ++i )
1508         {
1509             char c = text.charAt( i );
1510             if ( c == RIGHT_CURLY_BRACKET && previous == RIGHT_CURLY_BRACKET && previous2 != BACKSLASH )
1511             {
1512                 break;
1513             }
1514 
1515             previous2 = previous;
1516             previous = c;
1517         }
1518         if ( i == end )
1519         {
1520             throw new AptParseException( "missing '" + LEFT_CURLY_BRACKET + LEFT_CURLY_BRACKET + "'" );
1521         }
1522 
1523         return doGetTraversedLink( text, begin, i - 1 );
1524     }
1525 
1526     /**
1527      * Parse the given text.
1528      *
1529      * @param text the text to parse.
1530      * @param begin offset.
1531      * @param end offset.
1532      * @return String
1533      * @throws AptParseException if something goes wrong.
1534      */
1535     private String getTraversedAnchor( String text, int begin, int end )
1536         throws AptParseException
1537     {
1538         char previous = LEFT_CURLY_BRACKET;
1539         int i;
1540 
1541         for ( i = begin; i < end; ++i )
1542         {
1543             char c = text.charAt( i );
1544             if ( c == RIGHT_CURLY_BRACKET && previous != BACKSLASH )
1545             {
1546                 break;
1547             }
1548 
1549             previous = c;
1550         }
1551         if ( i == end )
1552         {
1553             throw new AptParseException( "missing '" + RIGHT_CURLY_BRACKET + "'" );
1554         }
1555 
1556         return doGetTraversedLink( text, begin, i );
1557     }
1558 
1559     /**
1560      * Parse the given text.
1561      *
1562      * @param text the text to parse.
1563      * @param begin offset.
1564      * @param end offset.
1565      * @return String
1566      * @throws AptParseException if something goes wrong.
1567      */
1568     private String doGetTraversedLink( String text, int begin, int end )
1569         throws AptParseException
1570     {
1571         final StringBuilder buffer = new StringBuilder( end - begin );
1572 
1573         Sink linkSink = new SinkAdapter()
1574         {
1575             /** {@inheritDoc} */
1576             public void lineBreak()
1577             {
1578                 buffer.append( SPACE );
1579             }
1580 
1581             /** {@inheritDoc} */
1582             public void nonBreakingSpace()
1583             {
1584                 buffer.append( SPACE );
1585             }
1586 
1587             /** {@inheritDoc} */
1588             public void text( String text )
1589             {
1590                 buffer.append( text );
1591             }
1592         };
1593         doTraverseText( text, begin, end, linkSink );
1594 
1595         return buffer.toString().trim();
1596     }
1597 
1598     /**
1599      * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
1600      *
1601      * @param key not null
1602      * @param msg not null
1603      * @see #parse(Reader, Sink)
1604      * @since 1.1.1
1605      */
1606     private void logMessage( String key, String msg )
1607     {
1608         msg = "[APT Parser] " + msg;
1609         if ( getLog().isDebugEnabled() )
1610         {
1611             getLog().debug( msg );
1612 
1613             return;
1614         }
1615 
1616         if ( warnMessages == null )
1617         {
1618             warnMessages = new HashMap<String, Set<String>>();
1619         }
1620 
1621         Set<String> set = warnMessages.get( key );
1622         if ( set == null )
1623         {
1624             set = new TreeSet<String>();
1625         }
1626         set.add( msg );
1627         warnMessages.put( key, set );
1628     }
1629 
1630     /**
1631      * @since 1.1.2
1632      */
1633     private void logWarnings()
1634     {
1635         if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() )
1636         {
1637             for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
1638             {
1639                 for ( String msg : entry.getValue() )
1640                 {
1641                     getLog().warn( msg );
1642                 }
1643             }
1644 
1645             this.warnMessages = null;
1646         }
1647     }
1648 
1649     // -----------------------------------------------------------------------
1650 
1651     /** A block of an apt source document. */
1652     private abstract class Block
1653     {
1654         /** type. */
1655         protected int type;
1656 
1657         /** indent. */
1658         protected int indent;
1659 
1660         /** text. */
1661         protected String text;
1662 
1663         /** textLength. */
1664         protected int textLength;
1665 
1666         /**
1667          * Constructor.
1668          *
1669          * @param type the block type.
1670          * @param indent indent.
1671          * @throws AptParseException AptParseException
1672          */
1673         public Block( int type, int indent )
1674             throws AptParseException
1675         {
1676             this( type, indent, null );
1677         }
1678 
1679         /**
1680          * Constructor.
1681          *
1682          * @param type type.
1683          * @param indent indent.
1684          * @param firstLine the first line.
1685          * @throws AptParseException AptParseException
1686          */
1687         public Block( int type, int indent, String firstLine )
1688             throws AptParseException
1689         {
1690             this.type = type;
1691             this.indent = indent;
1692 
1693             // Skip first line ---
1694             AptParser.this.nextLine();
1695 
1696             if ( firstLine == null )
1697             {
1698                 text = null;
1699                 textLength = 0;
1700             }
1701             else
1702             {
1703                 // Read block ---
1704                 StringBuilder buffer = new StringBuilder( firstLine );
1705 
1706                 while ( AptParser.this.line != null )
1707                 {
1708                     String l = AptParser.this.line;
1709                     int length = l.length();
1710                     int i = 0;
1711 
1712                     i = skipSpace( l, length, i );
1713                     if ( i == length )
1714                     {
1715                         // Stop after open line and skip it.
1716                         AptParser.this.nextLine();
1717                         break;
1718                     }
1719                     else if ( ( AptParser.charAt( l, length, i ) == COMMENT
1720                             && AptParser.charAt( l, length, i + 1 ) == COMMENT )
1721                             || type == COMMENT_BLOCK )
1722                     {
1723                         // parse comments as separate blocks line by line
1724                         break;
1725                     }
1726 
1727                     buffer.append( EOL );
1728                     buffer.append( l );
1729 
1730                     AptParser.this.nextLine();
1731                 }
1732 
1733                 text = buffer.toString();
1734                 textLength = text.length();
1735             }
1736         }
1737 
1738         /**
1739          * Return the block type.
1740          *
1741          * @return int
1742          */
1743         public final int getType()
1744         {
1745             return type;
1746         }
1747 
1748         /**
1749          * Return the block indent.
1750          *
1751          * @return int
1752          */
1753         public final int getIndent()
1754         {
1755             return indent;
1756         }
1757 
1758         /**
1759          * Parse the block.
1760          *
1761          * @throws AptParseException if something goes wrong.
1762          */
1763         public abstract void traverse()
1764             throws AptParseException;
1765 
1766         /**
1767          * Traverse the text.
1768          *
1769          * @param begin offset.
1770          * @throws AptParseException if something goes wrong.
1771          */
1772         protected void traverseText( int begin )
1773             throws AptParseException
1774         {
1775             traverseText( begin, text.length() );
1776         }
1777 
1778         /**
1779          * Traverse the text.
1780          *
1781          * @param begin offset.
1782          * @param end offset.
1783          * @throws AptParseException if something goes wrong.
1784          */
1785         protected void traverseText( int begin, int end )
1786             throws AptParseException
1787         {
1788             AptParser.this.doTraverseText( text, begin, end, AptParser.this.sink );
1789         }
1790 
1791         /**
1792          * Skip spaces.
1793          *
1794          * @return int.
1795          */
1796         protected int skipLeadingBullets()
1797         {
1798             int i = skipSpaceFrom( 0 );
1799             for ( ; i < textLength; ++i )
1800             {
1801                 if ( text.charAt( i ) != STAR )
1802                 {
1803                     break;
1804                 }
1805             }
1806             return skipSpaceFrom( i );
1807         }
1808 
1809         /**
1810          * Skip brackets.
1811          *
1812          * @param i offset.
1813          * @return int.
1814          * @throws AptParseException if something goes wrong.
1815          */
1816         protected int skipFromLeftToRightBracket( int i )
1817             throws AptParseException
1818         {
1819             char previous = LEFT_SQUARE_BRACKET;
1820             for ( ++i; i < textLength; ++i )
1821             {
1822                 char c = text.charAt( i );
1823                 if ( c == RIGHT_SQUARE_BRACKET && previous != BACKSLASH )
1824                 {
1825                     break;
1826                 }
1827                 previous = c;
1828             }
1829             if ( i == textLength )
1830             {
1831                 throw new AptParseException( "missing '" + RIGHT_SQUARE_BRACKET + "'" );
1832             }
1833 
1834             return i;
1835         }
1836 
1837         /**
1838          * Skip spaces.
1839          *
1840          * @param i offset.
1841          * @return int.
1842          */
1843         protected final int skipSpaceFrom( int i )
1844         {
1845             return AptParser.skipSpace( text, textLength, i );
1846         }
1847     }
1848 
1849     /** A ListBreak Block. */
1850     private class ListBreak
1851         extends AptParser.Block
1852     {
1853         /**
1854          * Constructor.
1855          *
1856          * @param indent indent.
1857          * @param firstLine the first line.
1858          * @throws AptParseException AptParseException
1859          */
1860         public ListBreak( int indent, String firstLine )
1861             throws AptParseException
1862         {
1863             super( AptParser.LIST_BREAK, indent, firstLine );
1864         }
1865 
1866         /** {@inheritDoc} */
1867         public void traverse()
1868             throws AptParseException
1869         {
1870             throw new AptParseException( "internal error: traversing list break" );
1871         }
1872     }
1873 
1874     /** A Title Block. */
1875     private class Title
1876         extends Block
1877     {
1878         /**
1879          * Constructor.
1880          *
1881          * @param indent indent.
1882          * @param firstLine the first line.
1883          * @throws AptParseException AptParseException
1884          */
1885         public Title( int indent, String firstLine )
1886             throws AptParseException
1887         {
1888             super( TITLE, indent, firstLine );
1889         }
1890 
1891         /** {@inheritDoc} */
1892         public void traverse()
1893             throws AptParseException
1894         {
1895             StringTokenizer lines = new StringTokenizer( text, EOL );
1896             int separator = -1;
1897             boolean firstLine = true;
1898             boolean title = false;
1899             boolean author = false;
1900             boolean date = false;
1901 
1902             loop: while ( lines.hasMoreTokens() )
1903             {
1904                 String line = lines.nextToken().trim();
1905                 int lineLength = line.length();
1906 
1907                 if ( AptParser.charAt( line, lineLength, 0 ) == MINUS
1908                     && AptParser.charAt( line, lineLength, 1 ) == MINUS
1909                     && AptParser.charAt( line, lineLength, 2 ) == MINUS )
1910                 {
1911                     switch ( separator )
1912                     {
1913                         case 0:
1914                             if ( title )
1915                             {
1916                                 AptParser.this.sink.title_();
1917                             }
1918                             else
1919                             {
1920                                 throw new AptParseException( "missing title" );
1921                             }
1922                             break;
1923                         case 1:
1924                             if ( author )
1925                             {
1926                                 AptParser.this.sink.author_();
1927                             }
1928                             break;
1929                         case 2:
1930                             // Note that an extra decorative line is allowed
1931                             // at the end of the author.
1932                             break loop;
1933                         default:
1934                             break;
1935                     }
1936 
1937                     ++separator;
1938                     firstLine = true;
1939                 }
1940                 else
1941                 {
1942                     if ( firstLine )
1943                     {
1944                         firstLine = false;
1945                         switch ( separator )
1946                         {
1947                             case 0:
1948                                 title = true;
1949                                 AptParser.this.sink.title();
1950                                 break;
1951                             case 1:
1952                                 author = true;
1953                                 AptParser.this.sink.author();
1954                                 break;
1955                             case 2:
1956                                 date = true;
1957                                 AptParser.this.sink.date();
1958                                 break;
1959                             default:
1960                                 break;
1961                         }
1962                     }
1963                     else
1964                     {
1965                         // An implicit lineBreak separates title lines.
1966                         AptParser.this.sink.lineBreak();
1967                     }
1968 
1969                     AptParser.this.doTraverseText( line, 0, lineLength, AptParser.this.sink );
1970                 }
1971             }
1972 
1973             switch ( separator )
1974             {
1975                 case 0:
1976                     if ( title )
1977                     {
1978                         AptParser.this.sink.title_();
1979                     }
1980                     else
1981                     {
1982                         throw new AptParseException( "missing title" );
1983                     }
1984                     break;
1985                 case 1:
1986                     if ( author )
1987                     {
1988                         AptParser.this.sink.author_();
1989                     }
1990                     break;
1991                 case 2:
1992                     if ( date )
1993                     {
1994                         AptParser.this.sink.date_();
1995                     }
1996                     break;
1997                 default:
1998                     break;
1999             }
2000         }
2001     }
2002 
2003     /** A Section Block. */
2004     private abstract class Section
2005         extends Block
2006     {
2007         /**
2008          * Constructor.
2009          *
2010          * @param type type.
2011          * @param indent indent.
2012          * @param firstLine the first line.
2013          * @throws AptParseException AptParseException
2014          */
2015         public Section( int type, int indent, String firstLine )
2016             throws AptParseException
2017         {
2018             super( type, indent, firstLine );
2019         }
2020 
2021         /** {@inheritDoc} */
2022         public void traverse()
2023             throws AptParseException
2024         {
2025             Title();
2026             traverseText( skipLeadingBullets() );
2027             Title_();
2028         }
2029 
2030         /** Start a title. */
2031         public abstract void Title();
2032 
2033         /** End a title. */
2034         public abstract void Title_();
2035     }
2036 
2037     /** A Section1 Block. */
2038     private class Section1
2039         extends Section
2040     {
2041         /**
2042          * Constructor.
2043          *
2044          * @param indent indent.
2045          * @param firstLine the first line.
2046          * @throws AptParseException AptParseException
2047          */
2048         public Section1( int indent, String firstLine )
2049             throws AptParseException
2050         {
2051             super( SECTION1, indent, firstLine );
2052         }
2053 
2054         /** {@inheritDoc} */
2055         public void Title()
2056         {
2057             AptParser.this.sink.sectionTitle1();
2058         }
2059 
2060         /** {@inheritDoc} */
2061         public void Title_()
2062         {
2063             AptParser.this.sink.sectionTitle1_();
2064         }
2065     }
2066 
2067     /** A Section2 Block. */
2068     private class Section2
2069         extends Section
2070     {
2071         /**
2072          * Constructor.
2073          *
2074          * @param indent indent.
2075          * @param firstLine the first line.
2076          * @throws AptParseException AptParseException
2077          */
2078         public Section2( int indent, String firstLine )
2079             throws AptParseException
2080         {
2081             super( SECTION2, indent, firstLine );
2082         }
2083 
2084         /** {@inheritDoc} */
2085         public void Title()
2086         {
2087             AptParser.this.sink.sectionTitle2();
2088         }
2089 
2090         /** {@inheritDoc} */
2091         public void Title_()
2092         {
2093             AptParser.this.sink.sectionTitle2_();
2094         }
2095     }
2096 
2097     /** A Section3 Block. */
2098     private class Section3
2099         extends Section
2100     {
2101         /**
2102          * Constructor.
2103          *
2104          * @param indent indent.
2105          * @param firstLine the first line.
2106          * @throws AptParseException AptParseException
2107          */
2108         public Section3( int indent, String firstLine )
2109             throws AptParseException
2110         {
2111             super( SECTION3, indent, firstLine );
2112         }
2113 
2114         /** {@inheritDoc} */
2115         public void Title()
2116         {
2117             AptParser.this.sink.sectionTitle3();
2118         }
2119 
2120         /** {@inheritDoc} */
2121         public void Title_()
2122         {
2123             AptParser.this.sink.sectionTitle3_();
2124         }
2125     }
2126 
2127     /** A Section4 Block. */
2128     private class Section4
2129         extends Section
2130     {
2131         /**
2132          * Constructor.
2133          *
2134          * @param indent indent.
2135          * @param firstLine the first line.
2136          * @throws AptParseException AptParseException
2137          */
2138         public Section4( int indent, String firstLine )
2139             throws AptParseException
2140         {
2141             super( SECTION4, indent, firstLine );
2142         }
2143 
2144         /** {@inheritDoc} */
2145         public void Title()
2146         {
2147             AptParser.this.sink.sectionTitle4();
2148         }
2149 
2150         /** {@inheritDoc} */
2151         public void Title_()
2152         {
2153             AptParser.this.sink.sectionTitle4_();
2154         }
2155     }
2156 
2157     /** A Section5 Block. */
2158     private class Section5
2159         extends Section
2160     {
2161         /**
2162          * Constructor.
2163          *
2164          * @param indent indent.
2165          * @param firstLine the first line.
2166          * @throws AptParseException AptParseException
2167          */
2168         public Section5( int indent, String firstLine )
2169             throws AptParseException
2170         {
2171             super( SECTION5, indent, firstLine );
2172         }
2173 
2174         /** {@inheritDoc} */
2175         public void Title()
2176         {
2177             AptParser.this.sink.sectionTitle5();
2178         }
2179 
2180         /** {@inheritDoc} */
2181         public void Title_()
2182         {
2183             AptParser.this.sink.sectionTitle5_();
2184         }
2185     }
2186 
2187     /** A Paragraph Block. */
2188     private class Paragraph
2189         extends Block
2190     {
2191         /**
2192          * Constructor.
2193          *
2194          * @param indent indent.
2195          * @param firstLine the first line.
2196          * @throws AptParseException AptParseException
2197          */
2198         public Paragraph( int indent, String firstLine )
2199             throws AptParseException
2200         {
2201             super( PARAGRAPH, indent, firstLine );
2202         }
2203 
2204         /** {@inheritDoc} */
2205         public void traverse()
2206             throws AptParseException
2207         {
2208             AptParser.this.sink.paragraph();
2209             traverseText( skipSpaceFrom( 0 ) );
2210             AptParser.this.sink.paragraph_();
2211         }
2212     }
2213 
2214     /** A Comment Block. */
2215     private class Comment
2216         extends Block
2217     {
2218         /**
2219          * Constructor.
2220          *
2221          * @param line the comment line.
2222          * @throws AptParseException AptParseException
2223          */
2224         public Comment( String line )
2225             throws AptParseException
2226         {
2227             super( COMMENT_BLOCK, 0, line );
2228         }
2229 
2230         /** {@inheritDoc} */
2231         public void traverse()
2232             throws AptParseException
2233         {
2234             AptParser.this.sink.comment( text );
2235         }
2236     }
2237 
2238     /** A Verbatim Block. */
2239     private class Verbatim
2240         extends Block
2241     {
2242         /** boxed. */
2243         private boolean boxed;
2244 
2245         /**
2246          * Constructor.
2247          *
2248          * @param indent indent.
2249          * @param firstLine the first line.
2250          * @throws AptParseException AptParseException
2251          */
2252         public Verbatim( int indent, String firstLine )
2253             throws AptParseException
2254         {
2255             super( VERBATIM, indent, null );
2256 
2257             // Read block (first line already skipped) ---
2258 
2259             StringBuilder buffer = new StringBuilder();
2260             char firstChar = firstLine.charAt( 0 );
2261             boxed = ( firstChar == PLUS );
2262 
2263             while ( AptParser.this.line != null )
2264             {
2265                 String l = AptParser.this.line;
2266                 int length = l.length();
2267 
2268                 if ( AptParser.charAt( l, length, 0 ) == firstChar && AptParser.charAt( l, length, 1 ) == MINUS
2269                     && AptParser.charAt( l, length, 2 ) == MINUS )
2270                 {
2271                     AptParser.this.nextLine();
2272 
2273                     break;
2274                 }
2275 
2276                 // Expand tabs ---
2277 
2278                 int prevColumn, column;
2279 
2280                 column = 0;
2281 
2282                 for ( int i = 0; i < length; ++i )
2283                 {
2284                     char c = l.charAt( i );
2285 
2286                     if ( c == TAB )
2287                     {
2288                         prevColumn = column;
2289 
2290                         column = ( ( column + 1 + TAB_WIDTH - 1 ) / TAB_WIDTH ) * TAB_WIDTH;
2291 
2292                         buffer.append( SPACES, 0, column - prevColumn );
2293                     }
2294                     else
2295                     {
2296                         ++column;
2297                         buffer.append( c );
2298                     }
2299                 }
2300                 buffer.append( EOL );
2301 
2302                 AptParser.this.nextLine();
2303             }
2304 
2305             // The last '\n' is mandatory before the "---" delimeter but is
2306             // not part of the verbatim text.
2307             textLength = buffer.length();
2308 
2309             if ( textLength > 0 )
2310             {
2311                 --textLength;
2312 
2313                 buffer.setLength( textLength );
2314             }
2315 
2316             text = buffer.toString();
2317         }
2318 
2319         /** {@inheritDoc} */
2320         public void traverse()
2321             throws AptParseException
2322         {
2323             AptParser.this.sink.verbatim( boxed ? SinkEventAttributeSet.BOXED : null );
2324             AptParser.this.sink.text( text );
2325             AptParser.this.sink.verbatim_();
2326         }
2327     }
2328 
2329     /** A Figure Block. */
2330     private class Figure
2331         extends Block
2332     {
2333         /**
2334          * Constructor.
2335          *
2336          * @param indent indent.
2337          * @param firstLine the first line.
2338          * @throws AptParseException AptParseException
2339          */
2340         public Figure( int indent, String firstLine )
2341             throws AptParseException
2342         {
2343             super( FIGURE, indent, firstLine );
2344         }
2345 
2346         /** {@inheritDoc} */
2347         public void traverse()
2348             throws AptParseException
2349         {
2350             AptParser.this.sink.figure();
2351 
2352             int i = skipFromLeftToRightBracket( 0 );
2353             AptParser.this.sink.figureGraphics( text.substring( 1, i ) );
2354 
2355             i = skipSpaceFrom( i + 1 );
2356             if ( i < textLength )
2357             {
2358                 AptParser.this.sink.figureCaption();
2359                 traverseText( i );
2360                 AptParser.this.sink.figureCaption_();
2361             }
2362 
2363             AptParser.this.sink.figure_();
2364         }
2365     }
2366 
2367     /** A Table Block. */
2368     private class Table
2369         extends Block
2370     {
2371         /**
2372          * Constructor.
2373          *
2374          * @param indent indent.
2375          * @param firstLine the first line.
2376          * @throws AptParseException AptParseException
2377          */
2378         public Table( int indent, String firstLine )
2379             throws AptParseException
2380         {
2381             super( TABLE, indent, firstLine );
2382         }
2383 
2384         /** {@inheritDoc} */
2385         public void traverse()
2386             throws AptParseException
2387         {
2388             int captionIndex = -1;
2389             int nextLineIndex = 0;
2390             int init = 2;
2391             int[] justification = null;
2392             int rows = 0;
2393             int columns = 0;
2394             StringBuilder[] cells = null;
2395             boolean[] headers = null;
2396             boolean grid;
2397 
2398             AptParser.this.sink.table();
2399 
2400             while ( nextLineIndex < textLength )
2401             {
2402                 int i = text.indexOf( "*--", nextLineIndex );
2403                 if ( i < 0 )
2404                 {
2405                     captionIndex = nextLineIndex;
2406                     break;
2407                 }
2408 
2409                 String line;
2410                 i = text.indexOf( '\n', nextLineIndex );
2411                 if ( i < 0 )
2412                 {
2413                     line = text.substring( nextLineIndex );
2414                     nextLineIndex = textLength;
2415                 }
2416                 else
2417                 {
2418                     line = text.substring( nextLineIndex, i );
2419                     nextLineIndex = i + 1;
2420                 }
2421                 int lineLength = line.length();
2422 
2423                 if ( line.indexOf( "*--" ) == 0 )
2424                 {
2425                     if ( init == 2 )
2426                     {
2427                         init = 1;
2428                         justification = parseJustification( line, lineLength );
2429                         columns = justification.length;
2430                         cells = new StringBuilder[columns];
2431                         headers = new boolean[columns];
2432                         for ( i = 0; i < columns; ++i )
2433                         {
2434                             cells[i] = new StringBuilder();
2435                             headers[i] = false;
2436                         }
2437                     }
2438                     else
2439                     {
2440                         if ( traverseRow( cells, headers, justification ) )
2441                         {
2442                             ++rows;
2443                         }
2444                         justification = parseJustification( line, lineLength );
2445                     }
2446                 }
2447                 else
2448                 {
2449                     if ( init == 1 )
2450                     {
2451                         init = 0;
2452                         grid = ( AptParser.charAt( line, lineLength, 0 ) == PIPE );
2453                         AptParser.this.sink.tableRows( justification, grid );
2454                     }
2455 
2456                     line = replaceAll( line, "\\|", "\\u007C" );
2457 
2458                     StringTokenizer cellLines = new StringTokenizer( line, "|", true );
2459 
2460                     i = 0;
2461                     boolean processedGrid = false;
2462                     while ( cellLines.hasMoreTokens() )
2463                     {
2464                         String cellLine = cellLines.nextToken();
2465                         if ( "|".equals( cellLine ) )
2466                         {
2467                             if ( processedGrid )
2468                             {
2469                                 headers[i] = true;
2470                             }
2471                             else
2472                             {
2473                                 processedGrid = true;
2474                                 headers[i] = false;
2475                             }
2476                             continue;
2477                         }
2478                         processedGrid = false;
2479                         cellLine = replaceAll( cellLine, "\\", "\\u00A0" ); // linebreak
2480                         // Escaped special characters: \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\.
2481                         cellLine = replaceAll( cellLine, "\\u00A0~", "\\~" );
2482                         cellLine = replaceAll( cellLine, "\\u00A0=", "\\=" );
2483                         cellLine = replaceAll( cellLine, "\\u00A0-", "\\-" );
2484                         cellLine = replaceAll( cellLine, "\\u00A0+", "\\+" );
2485                         cellLine = replaceAll( cellLine, "\\u00A0*", "\\*" );
2486                         cellLine = replaceAll( cellLine, "\\u00A0[", "\\[" );
2487                         cellLine = replaceAll( cellLine, "\\u00A0]", "\\]" );
2488                         cellLine = replaceAll( cellLine, "\\u00A0<", "\\<" );
2489                         cellLine = replaceAll( cellLine, "\\u00A0>", "\\>" );
2490                         cellLine = replaceAll( cellLine, "\\u00A0{", "\\{" );
2491                         cellLine = replaceAll( cellLine, "\\u00A0}", "\\}" );
2492                         cellLine = replaceAll( cellLine, "\\u00A0u", "\\u" );
2493                         cellLine = replaceAll( cellLine, "\\u00A0\\u00A0", "\\\\" );
2494                         cellLine = cellLine.trim();
2495 
2496                         StringBuilder cell = cells[i];
2497                         if ( cellLine.length() > 0 )
2498                         {
2499                             // line break in table cells
2500                             if ( cell.toString().trim().endsWith( "\\u00A0" ) )
2501                             {
2502                                 cell.append( "\\\n" );
2503                             }
2504                             else
2505                             {
2506                                 if ( cell.length() != 0 )
2507                                 {
2508                                     // Always add a space for multi line tables cells
2509                                     cell.append( " " );
2510                                 }
2511                             }
2512 
2513                             cell.append( cellLine );
2514                         }
2515 
2516                         ++i;
2517                         if ( i == columns )
2518                         {
2519                             break;
2520                         }
2521                     }
2522                 }
2523             }
2524             if ( rows == 0 )
2525             {
2526                 throw new AptParseException( "no table rows" );
2527             }
2528             AptParser.this.sink.tableRows_();
2529 
2530             if ( captionIndex >= 0 )
2531             {
2532                 AptParser.this.sink.tableCaption();
2533                 AptParser.this.doTraverseText( text, captionIndex, textLength, AptParser.this.sink );
2534                 AptParser.this.sink.tableCaption_();
2535             }
2536 
2537             AptParser.this.sink.table_();
2538         }
2539 
2540         /**
2541          * Parse a table justification line.
2542          *
2543          * @param jline the justification line.
2544          * @param lineLength the length of the line. Must be > 2.
2545          * @return int[]
2546          * @throws AptParseException if something goes wrong.
2547          */
2548         private int[] parseJustification( String jline, int lineLength )
2549             throws AptParseException
2550         {
2551             int columns = 0;
2552 
2553             for ( int i = 2 /*Skip '*--'*/; i < lineLength; ++i )
2554             {
2555                 switch ( jline.charAt( i ) )
2556                 {
2557                     case STAR:
2558                     case PLUS:
2559                     case COLON:
2560                         ++columns;
2561                         break;
2562                     default:
2563                         break;
2564                 }
2565             }
2566 
2567             if ( columns == 0 )
2568             {
2569                 throw new AptParseException( "no columns specified" );
2570             }
2571 
2572             int[] justification = new int[columns];
2573             columns = 0;
2574             for ( int i = 2; i < lineLength; ++i )
2575             {
2576                 switch ( jline.charAt( i ) )
2577                 {
2578                     case STAR:
2579                         justification[columns++] = Sink.JUSTIFY_CENTER;
2580                         break;
2581                     case PLUS:
2582                         justification[columns++] = Sink.JUSTIFY_LEFT;
2583                         break;
2584                     case COLON:
2585                         justification[columns++] = Sink.JUSTIFY_RIGHT;
2586                         break;
2587                     default:
2588                         break;
2589                 }
2590             }
2591 
2592             return justification;
2593         }
2594 
2595         /**
2596          * Traverse a table row.
2597          *
2598          * @param cells The table cells.
2599          * @param headers true for header cells.
2600          * @param justification the justification for each cell.
2601          * @return boolean
2602          * @throws AptParseException if something goes wrong.
2603          */
2604         private boolean traverseRow( StringBuilder[] cells, boolean[] headers, int[] justification )
2605             throws AptParseException
2606         {
2607             // Skip empty row (a decorative line).
2608             boolean traversed = false;
2609             for ( int i = 0; i < cells.length; ++i )
2610             {
2611                 if ( cells[i].length() > 0 )
2612                 {
2613                     traversed = true;
2614                     break;
2615                 }
2616             }
2617 
2618             if ( traversed )
2619             {
2620                 AptParser.this.sink.tableRow();
2621                 for ( int i = 0; i < cells.length; ++i )
2622                 {
2623                     StringBuilder cell = cells[i];
2624 
2625                     SinkEventAttributes justif;
2626                     switch ( justification[i] )
2627                     {
2628                         case Sink.JUSTIFY_CENTER:
2629                             justif = SinkEventAttributeSet.CENTER;
2630                             break;
2631                         case Sink.JUSTIFY_LEFT:
2632                             justif = SinkEventAttributeSet.LEFT;
2633                             break;
2634                         case Sink.JUSTIFY_RIGHT:
2635                             justif = SinkEventAttributeSet.RIGHT;
2636                             break;
2637                         default:
2638                             justif = SinkEventAttributeSet.LEFT;
2639                             break;
2640                     }
2641                     SinkEventAttributeSet event = new SinkEventAttributeSet();
2642                     event.addAttributes( justif );
2643 
2644                     if ( headers[i] )
2645                     {
2646                         AptParser.this.sink.tableHeaderCell( event );
2647                     }
2648                     else
2649                     {
2650                         AptParser.this.sink.tableCell( event );
2651                     }
2652                     if ( cell.length() > 0 )
2653                     {
2654                         AptParser.this.doTraverseText( cell.toString(), 0, cell.length(), AptParser.this.sink );
2655                         cell.setLength( 0 );
2656                     }
2657                     if ( headers[i] )
2658                     {
2659                         AptParser.this.sink.tableHeaderCell_();
2660                         // DOXIA-404: reset header for next row
2661                         headers[i] = false;
2662                     }
2663                     else
2664                     {
2665                         AptParser.this.sink.tableCell_();
2666                     }
2667                 }
2668                 AptParser.this.sink.tableRow_();
2669             }
2670 
2671             return traversed;
2672         }
2673     }
2674 
2675     /** A ListItem Block. */
2676     private class ListItem
2677         extends Block
2678     {
2679         /**
2680          * Constructor.
2681          *
2682          * @param indent indent.
2683          * @param firstLine the first line.
2684          * @throws AptParseException AptParseException
2685          */
2686         public ListItem( int indent, String firstLine )
2687             throws AptParseException
2688         {
2689             super( LIST_ITEM, indent, firstLine );
2690         }
2691 
2692         /** {@inheritDoc} */
2693         public void traverse()
2694             throws AptParseException
2695         {
2696             traverseText( skipLeadingBullets() );
2697         }
2698     }
2699 
2700     /** A NumberedListItem Block. */
2701     private class NumberedListItem
2702         extends Block
2703     {
2704         /** numbering. */
2705         private int numbering;
2706 
2707         /**
2708          * Constructor.
2709          *
2710          * @param indent indent.
2711          * @param firstLine the first line.
2712          * @param number numbering.
2713          * @throws AptParseException AptParseException
2714          */
2715         public NumberedListItem( int indent, String firstLine, int number )
2716             throws AptParseException
2717         {
2718             super( NUMBERED_LIST_ITEM, indent, firstLine );
2719             this.numbering = number;
2720         }
2721 
2722         /**
2723          * getNumbering.
2724          *
2725          * @return int
2726          */
2727         public int getNumbering()
2728         {
2729             return numbering;
2730         }
2731 
2732         /** {@inheritDoc} */
2733         public void traverse()
2734             throws AptParseException
2735         {
2736             traverseText( skipItemNumber() );
2737         }
2738 
2739         /**
2740          * skipItemNumber.
2741          *
2742          * @return int
2743          * @throws AptParseException AptParseException
2744          */
2745         private int skipItemNumber()
2746             throws AptParseException
2747         {
2748             int i = skipSpaceFrom( 0 );
2749 
2750             char prevChar = SPACE;
2751             for ( ; i < textLength; ++i )
2752             {
2753                 char c = text.charAt( i );
2754                 if ( c == RIGHT_SQUARE_BRACKET && prevChar == RIGHT_SQUARE_BRACKET )
2755                 {
2756                     break;
2757                 }
2758                 prevChar = c;
2759             }
2760 
2761             if ( i == textLength )
2762             {
2763                 throw new AptParseException( "missing '" + RIGHT_SQUARE_BRACKET + RIGHT_SQUARE_BRACKET + "'" );
2764             }
2765 
2766             return skipSpaceFrom( i + 1 );
2767         }
2768     }
2769 
2770     /** A DefinitionListItem Block. */
2771     private class DefinitionListItem
2772         extends Block
2773     {
2774         /**
2775          * Constructor.
2776          *
2777          * @param indent indent.
2778          * @param firstLine the first line.
2779          * @throws AptParseException AptParseException
2780          */
2781         public DefinitionListItem( int indent, String firstLine )
2782             throws AptParseException
2783         {
2784             super( DEFINITION_LIST_ITEM, indent, firstLine );
2785         }
2786 
2787         /** {@inheritDoc} */
2788         public void traverse()
2789             throws AptParseException
2790         {
2791             int i = skipSpaceFrom( 0 );
2792             int j = skipFromLeftToRightBracket( i );
2793 
2794             AptParser.this.sink.definedTerm();
2795             traverseText( i + 1, j );
2796             AptParser.this.sink.definedTerm_();
2797 
2798             j = skipSpaceFrom( j + 1 );
2799             if ( j == textLength )
2800             {
2801                 // TODO: this doesn't handle the case of a dd in a paragraph
2802                 //throw new AptParseException( "no definition" );
2803             }
2804 
2805             AptParser.this.sink.definition();
2806             traverseText( j );
2807         }
2808     }
2809 
2810     /** A HorizontalRule Block. */
2811     private class HorizontalRule
2812         extends Block
2813     {
2814         /**
2815          * Constructor.
2816          *
2817          * @param indent indent.
2818          * @param firstLine the first line.
2819          * @throws AptParseException AptParseException
2820          */
2821         public HorizontalRule( int indent, String firstLine )
2822             throws AptParseException
2823         {
2824             super( HORIZONTAL_RULE, indent, firstLine );
2825         }
2826 
2827         /** {@inheritDoc} */
2828         public void traverse()
2829             throws AptParseException
2830         {
2831             AptParser.this.sink.horizontalRule();
2832         }
2833     }
2834 
2835     /** A PageBreak Block. */
2836     private class PageBreak
2837         extends Block
2838     {
2839         /**
2840          * Constructor.
2841          *
2842          * @param indent indent.
2843          * @param firstLine the first line.
2844          * @throws AptParseException AptParseException
2845          */
2846         public PageBreak( int indent, String firstLine )
2847             throws AptParseException
2848         {
2849             super( PG_BREAK, indent, firstLine );
2850         }
2851 
2852         /** {@inheritDoc} */
2853         public void traverse()
2854             throws AptParseException
2855         {
2856             AptParser.this.sink.pageBreak();
2857         }
2858     }
2859 
2860     /** A MacroBlock Block. */
2861     private class MacroBlock
2862         extends Block
2863     {
2864         /**
2865          * Constructor.
2866          *
2867          * @param indent indent.
2868          * @param firstLine the first line.
2869          * @throws AptParseException AptParseException
2870          */
2871         public MacroBlock( int indent, String firstLine )
2872             throws AptParseException
2873         {
2874             super( MACRO, indent );
2875 
2876             text = firstLine;
2877         }
2878 
2879         /** {@inheritDoc} */
2880         public void traverse()
2881             throws AptParseException
2882         {
2883             if ( isSecondParsing() )
2884             {
2885                 return;
2886             }
2887 
2888             final int start = text.indexOf( '{' );
2889             final int end = text.indexOf( '}' );
2890 
2891             String s = text.substring( start + 1, end );
2892 
2893             s = escapeForMacro( s );
2894 
2895             String[] params = StringUtils.split( s, "|" );
2896 
2897             String macroId = params[0];
2898 
2899             Map<String, Object> parameters = new HashMap<String, Object>();
2900 
2901             for ( int i = 1; i < params.length; i++ )
2902             {
2903                 String[] param = StringUtils.split( params[i], "=" );
2904 
2905                 if ( param.length == 1 )
2906                 {
2907                     throw new AptParseException( "Missing 'key=value' pair for macro parameter: " + params[i] );
2908                 }
2909 
2910                 String key = unescapeForMacro( param[0] );
2911                 String value = unescapeForMacro( param[1] );
2912 
2913                 parameters.put( key, value );
2914             }
2915 
2916             parameters.put( "sourceContent", sourceContent );
2917 
2918             AptParser aptParser = new AptParser();
2919             aptParser.setSecondParsing( true );
2920             aptParser.enableLogging( getLog() );
2921             parameters.put( "parser", aptParser );
2922 
2923             // getBasedir() does not work in multi-module builds, see DOXIA-373
2924             // the basedir should be injected from here, see DOXIA-224
2925             MacroRequest request = new MacroRequest( parameters, getBasedir() );
2926             try
2927             {
2928                 AptParser.this.executeMacro( macroId, request, sink );
2929             }
2930             catch ( MacroExecutionException e )
2931             {
2932                 throw new AptParseException( "Unable to execute macro in the APT document", e );
2933             }
2934             catch ( MacroNotFoundException e )
2935             {
2936                 throw new AptParseException( "Unable to find macro used in the APT document", e );
2937             }
2938         }
2939 
2940         /**
2941          * escapeForMacro
2942          *
2943          * @param s String
2944          * @return String
2945          */
2946         private String escapeForMacro( String s )
2947         {
2948             if ( s == null || s.length() < 1 )
2949             {
2950                 return s;
2951             }
2952 
2953             String result = s;
2954 
2955             // use some outrageously out-of-place chars for text
2956             // (these are device control one/two in unicode)
2957             result = StringUtils.replace( result, "\\=", "\u0011" );
2958             result = StringUtils.replace( result, "\\|", "\u0012" );
2959 
2960             return result;
2961         }
2962 
2963         /**
2964          * unescapeForMacro
2965          *
2966          * @param s String
2967          * @return String
2968          */
2969         private String unescapeForMacro( String s )
2970         {
2971             if ( s == null || s.length() < 1 )
2972             {
2973                 return s;
2974             }
2975 
2976             String result = s;
2977 
2978             result = StringUtils.replace( result, "\u0011", "=" );
2979             result = StringUtils.replace( result, "\u0012", "|" );
2980 
2981             return result;
2982         }
2983     }
2984 }