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