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