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