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