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