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