blob: 21cf716659b9d937691bfd959fa5b4f975c8ae50 [file] [log] [blame]
package org.apache.maven.doxia.module.apt;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.doxia.macro.MacroExecutionException;
import org.apache.maven.doxia.macro.MacroRequest;
import org.apache.maven.doxia.macro.manager.MacroNotFoundException;
import org.apache.maven.doxia.parser.AbstractTextParser;
import org.apache.maven.doxia.parser.ParseException;
import org.apache.maven.doxia.parser.Parser;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.SinkAdapter;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
/**
* The APT parser.
* <br>
* Based on the <a href="http://www.xmlmind.com/aptconvert.html">APTconvert</a> project.
*
* @version $Id$
* @since 1.0
*/
@Component( role = Parser.class, hint = "apt" )
public class AptParser
extends AbstractTextParser
implements AptMarkup
{
/** Title event id */
private static final int TITLE = 0;
/** Section 1 event id */
private static final int SECTION1 = 1;
/** Section 2 event id */
private static final int SECTION2 = 2;
/** Section 3 event id */
private static final int SECTION3 = 3;
/** Section 4 event id */
private static final int SECTION4 = 4;
/** Section 5 event id */
private static final int SECTION5 = 5;
/** Paragraph event id */
private static final int PARAGRAPH = 6;
/** Verbatim event id */
private static final int VERBATIM = 7;
/** Figure event id */
private static final int FIGURE = 8;
/** Table event id */
private static final int TABLE = 9;
/** List event id */
private static final int LIST_ITEM = 10;
/** Numbered list event id */
private static final int NUMBERED_LIST_ITEM = 11;
/** Definition list event id */
private static final int DEFINITION_LIST_ITEM = 12;
/** Horizontal rule event id */
private static final int HORIZONTAL_RULE = 13;
/** Page break event id */
private static final int PG_BREAK = 14;
/** List break event id */
private static final int LIST_BREAK = 15;
/** Macro event id */
private static final int MACRO = 16;
/** Comment event id. */
private static final int COMMENT_BLOCK = 17;
/** String representations of event ids */
private static final String[] TYPE_NAMES = {
"TITLE",
"SECTION1",
"SECTION2",
"SECTION3",
"SECTION4",
"SECTION5",
"PARAGRAPH",
"VERBATIM",
"FIGURE",
"TABLE",
"LIST_ITEM",
"NUMBERED_LIST_ITEM",
"DEFINITION_LIST_ITEM",
"HORIZONTAL_RULE",
"PG_BREAK",
"LIST_BREAK",
"MACRO",
"COMMENT_BLOCK" };
/** An array of 85 spaces. */
protected static final char[] SPACES;
/** Default tab width. */
public static final int TAB_WIDTH = 8;
// ----------------------------------------------------------------------
// Instance fields
// ----------------------------------------------------------------------
/** the AptSource. */
private AptSource source;
/** a block of AptSource. */
private Block block;
/** blockFileName. */
private String blockFileName;
/** blockLineNumber. */
private int blockLineNumber;
/** sourceContent. */
protected String sourceContent;
/** the sink to receive the events. */
protected Sink sink;
/** a line of AptSource. */
protected String line;
/** Map of warn messages with a String as key to describe the error type and a Set as value.
* Using to reduce warn messages. */
protected Map<String, Set<String>> warnMessages;
private static final int NUMBER_OF_SPACES = 85;
static
{
SPACES = new char[NUMBER_OF_SPACES];
for ( int i = 0; i < NUMBER_OF_SPACES; i++ )
{
SPACES[i] = ' ';
}
}
// ----------------------------------------------------------------------
// Public methods
// ----------------------------------------------------------------------
@Override
public void parse( Reader source, Sink sink )
throws ParseException
{
parse( source, sink, "" );
}
@Override
public void parse( Reader source, Sink sink, String reference )
throws ParseException
{
init();
try
{
StringWriter contentWriter = new StringWriter();
IOUtil.copy( source, contentWriter );
sourceContent = contentWriter.toString();
}
catch ( IOException e )
{
throw new AptParseException( "IOException: " + e.getMessage(), e );
}
try
{
this.source = new AptReaderSource( new StringReader( sourceContent ), reference );
this.sink = sink;
sink.enableLogging( getLog() );
blockFileName = null;
blockLineNumber = -1;
// Lookahead line.
nextLine();
// Lookahead block.
nextBlock( /*first*/true );
// traverse comments
while ( ( block != null ) && ( block.getType() == COMMENT_BLOCK ) )
{
block.traverse();
nextBlock( /*first*/true );
}
traverseHead();
traverseBody();
}
catch ( AptParseException ape )
{
// TODO handle column number
throw new AptParseException( ape.getMessage(), ape, getSourceName(), getSourceLineNumber(), -1 );
}
finally
{
logWarnings();
setSecondParsing( false );
init();
}
}
/**
* Returns the name of the Apt source document.
*
* @return the source name.
*/
public String getSourceName()
{
// Use this rather than source.getName() to report errors.
return blockFileName;
}
/**
* Returns the current line number of the Apt source document.
*
* @return the line number.
*/
public int getSourceLineNumber()
{
// Use this rather than source.getLineNumber() to report errors.
return blockLineNumber;
}
// ----------------------------------------------------------------------
// Protected methods
// ----------------------------------------------------------------------
/**
* Parse the next line of the Apt source document.
*
* @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
*/
protected void nextLine()
throws AptParseException
{
line = source.getNextLine();
}
/**
* Parse the given text.
*
* @param text the text to parse.
* @param begin offset.
* @param end offset.
* @param sink the sink to receive the events.
* @throws org.apache.maven.doxia.module.apt.AptParseException if something goes wrong.
*/
protected void doTraverseText( String text, int begin, int end, Sink sink )
throws AptParseException
{
boolean anchor = false;
boolean link = false;
boolean italic = false;
boolean bold = false;
boolean monospaced = false;
StringBuilder buffer = new StringBuilder( end - begin );
for ( int i = begin; i < end; ++i )
{
char c = text.charAt( i );
switch ( c )
{
case BACKSLASH:
if ( i + 1 < end )
{
char escaped = text.charAt( i + 1 );
switch ( escaped )
{
case SPACE:
++i;
flushTraversed( buffer, sink );
sink.nonBreakingSpace();
break;
case '\r':
case '\n':
++i;
// Skip white space which may follow a line break.
while ( i + 1 < end && Character.isWhitespace( text.charAt( i + 1 ) ) )
{
++i;
}
flushTraversed( buffer, sink );
sink.lineBreak();
break;
case BACKSLASH:
case PIPE:
case COMMENT:
case EQUAL:
case MINUS:
case PLUS:
case STAR:
case LEFT_SQUARE_BRACKET:
case RIGHT_SQUARE_BRACKET:
case LESS_THAN:
case GREATER_THAN:
case LEFT_CURLY_BRACKET:
case RIGHT_CURLY_BRACKET:
++i;
buffer.append( escaped );
break;
case 'x':
if ( i + 3 < end && isHexChar( text.charAt( i + 2 ) )
&& isHexChar( text.charAt( i + 3 ) ) )
{
int value = '?';
try
{
value = Integer.parseInt( text.substring( i + 2, i + 4 ), 16 );
}
catch ( NumberFormatException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Not a number: " + text.substring( i + 2, i + 4 ) );
}
}
i += 3;
buffer.append( (char) value );
}
else
{
buffer.append( BACKSLASH );
}
break;
case 'u':
if ( i + 5 < end && isHexChar( text.charAt( i + 2 ) )
&& isHexChar( text.charAt( i + 3 ) ) && isHexChar( text.charAt( i + 4 ) )
&& isHexChar( text.charAt( i + 5 ) ) )
{
int value = '?';
try
{
value = Integer.parseInt( text.substring( i + 2, i + 6 ), 16 );
}
catch ( NumberFormatException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Not a number: " + text.substring( i + 2, i + 6 ) );
}
}
i += 5;
buffer.append( (char) value );
}
else
{
buffer.append( BACKSLASH );
}
break;
default:
if ( isOctalChar( escaped ) )
{
int octalChars = 1;
if ( isOctalChar( charAt( text, end, i + 2 ) ) )
{
++octalChars;
if ( isOctalChar( charAt( text, end, i + 3 ) ) )
{
++octalChars;
}
}
int value = '?';
try
{
value = Integer.parseInt( text.substring( i + 1, i + 1 + octalChars ), 8 );
}
catch ( NumberFormatException e )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug(
"Not a number: "
+ text.substring( i + 1, i + 1 + octalChars ) );
}
}
i += octalChars;
buffer.append( (char) value );
}
else
{
buffer.append( BACKSLASH );
}
}
}
else
{
buffer.append( BACKSLASH );
}
break;
case LEFT_CURLY_BRACKET: /*}*/
if ( !anchor && !link )
{
if ( i + 1 < end && text.charAt( i + 1 ) == LEFT_CURLY_BRACKET /*}*/ )
{
++i;
link = true;
flushTraversed( buffer, sink );
String linkAnchor = null;
if ( i + 1 < end && text.charAt( i + 1 ) == LEFT_CURLY_BRACKET /*}*/ )
{
++i;
StringBuilder buf = new StringBuilder();
i = skipTraversedLinkAnchor( text, i + 1, end, buf );
linkAnchor = buf.toString();
}
if ( linkAnchor == null )
{
linkAnchor = getTraversedLink( text, i + 1, end );
}
if ( AptUtils.isInternalLink( linkAnchor ) )
{
linkAnchor = "#" + linkAnchor;
}
int hashIndex = linkAnchor.indexOf( "#" );
if ( hashIndex != -1 && !AptUtils.isExternalLink( linkAnchor ) )
{
String hash = linkAnchor.substring( hashIndex + 1 );
if ( hash.endsWith( ".html" ) && !hash.startsWith( "./" ) )
{
String msg = "Ambiguous link: '" + hash
+ "'. If this is a local link, prepend \"./\"!";
logMessage( "ambiguousLink", msg );
}
// link##anchor means literal
if ( hash.startsWith( "#" ) )
{
linkAnchor = linkAnchor.substring( 0, hashIndex ) + hash;
}
else if ( !DoxiaUtils.isValidId( hash ) )
{
linkAnchor =
linkAnchor.substring( 0, hashIndex ) + "#"
+ DoxiaUtils.encodeId( hash, true );
String msg = "Modified invalid link: '" + hash + "' to '" + linkAnchor + "'";
logMessage( "modifiedLink", msg );
}
}
sink.link( linkAnchor );
}
else
{
anchor = true;
flushTraversed( buffer, sink );
String linkAnchor = getTraversedAnchor( text, i + 1, end );
linkAnchor = AptUtils.encodeAnchor( linkAnchor );
sink.anchor( linkAnchor );
}
}
else
{
buffer.append( c );
}
break;
case /*{*/RIGHT_CURLY_BRACKET:
if ( link && i + 1 < end && text.charAt( i + 1 ) == /*{*/RIGHT_CURLY_BRACKET )
{
++i;
link = false;
flushTraversed( buffer, sink );
sink.link_();
}
else if ( anchor )
{
anchor = false;
flushTraversed( buffer, sink );
sink.anchor_();
}
else
{
buffer.append( c );
}
break;
case LESS_THAN:
if ( !italic && !bold && !monospaced )
{
if ( i + 1 < end && text.charAt( i + 1 ) == LESS_THAN )
{
if ( i + 2 < end && text.charAt( i + 2 ) == LESS_THAN )
{
i += 2;
monospaced = true;
flushTraversed( buffer, sink );
sink.monospaced();
}
else
{
++i;
bold = true;
flushTraversed( buffer, sink );
sink.bold();
}
}
else
{
italic = true;
flushTraversed( buffer, sink );
sink.italic();
}
}
else
{
buffer.append( c );
}
break;
case GREATER_THAN:
if ( monospaced && i + 2 < end && text.charAt( i + 1 ) == GREATER_THAN
&& text.charAt( i + 2 ) == GREATER_THAN )
{
i += 2;
monospaced = false;
flushTraversed( buffer, sink );
sink.monospaced_();
}
else if ( bold && i + 1 < end && text.charAt( i + 1 ) == GREATER_THAN )
{
++i;
bold = false;
flushTraversed( buffer, sink );
sink.bold_();
}
else if ( italic )
{
italic = false;
flushTraversed( buffer, sink );
sink.italic_();
}
else
{
buffer.append( c );
}
break;
default:
if ( Character.isWhitespace( c ) )
{
buffer.append( SPACE );
// Skip to the last char of a sequence of white spaces.
while ( i + 1 < end && Character.isWhitespace( text.charAt( i + 1 ) ) )
{
++i;
}
}
else
{
buffer.append( c );
}
}
}
if ( monospaced )
{
throw new AptParseException( "missing '" + MONOSPACED_END_MARKUP + "'" );
}
if ( bold )
{
throw new AptParseException( "missing '" + BOLD_END_MARKUP + "'" );
}
if ( italic )
{
throw new AptParseException( "missing '" + ITALIC_END_MARKUP + "'" );
}
if ( link )
{
throw new AptParseException( "missing '" + LINK_END_MARKUP + "'" );
}
if ( anchor )
{
throw new AptParseException( "missing '" + ANCHOR_END_MARKUP + "'" );
}
flushTraversed( buffer, sink );
}
// -----------------------------------------------------------------------
/**
* Returns the character at position i of the given string.
*
* @param string the string.
* @param length length.
* @param i offset.
* @return the character, or '\0' if i &gt; length.
*/
protected static char charAt( String string, int length, int i )
{
return ( i < length ) ? string.charAt( i ) : '\0';
}
/**
* Skip spaces.
*
* @param string string.
* @param length length.
* @param i offset.
* @return int.
*/
protected static int skipSpace( String string, int length, int i )
{
loop: for ( ; i < length; ++i )
{
switch ( string.charAt( i ) )
{
case SPACE:
case TAB:
break;
default:
break loop;
}
}
return i;
}
/**
* Replace part of a string.
*
* @param string the string
* @param oldSub the substring to replace
* @param newSub the replacement string
* @return String
*/
protected static String replaceAll( String string, String oldSub, String newSub )
{
StringBuilder replaced = new StringBuilder();
int oldSubLength = oldSub.length();
int begin, end;
begin = 0;
while ( ( end = string.indexOf( oldSub, begin ) ) >= 0 )
{
if ( end > begin )
{
replaced.append( string, begin, end );
}
replaced.append( newSub );
begin = end + oldSubLength;
}
if ( begin < string.length() )
{
replaced.append( string.substring( begin ) );
}
return replaced.toString();
}
/** {@inheritDoc} */
protected void init()
{
super.init();
this.sourceContent = null;
this.sink = null;
this.source = null;
this.block = null;
this.blockFileName = null;
this.blockLineNumber = 0;
this.line = null;
this.warnMessages = null;
}
// ----------------------------------------------------------------------
// Private methods
// ----------------------------------------------------------------------
/**
* Parse the head of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseHead()
throws AptParseException
{
sink.head();
if ( block != null && block.getType() == TITLE )
{
block.traverse();
nextBlock();
}
sink.head_();
}
/**
* Parse the body of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseBody()
throws AptParseException
{
sink.body();
if ( block != null )
{
traverseSectionBlocks();
}
while ( block != null )
{
traverseSection( 0 );
}
sink.body_();
}
/**
* Parse a section of the Apt source document.
*
* @param level The section level.
* @throws AptParseException if something goes wrong.
*/
private void traverseSection( int level )
throws AptParseException
{
if ( block == null )
{
return;
}
int type = SECTION1 + level;
expectedBlock( type );
switch ( level )
{
case 0:
sink.section1();
break;
case 1:
sink.section2();
break;
case 2:
sink.section3();
break;
case 3:
sink.section4();
break;
case 4:
sink.section5();
break;
default:
break;
}
block.traverse();
nextBlock();
traverseSectionBlocks();
while ( block != null )
{
if ( block.getType() <= type )
{
break;
}
traverseSection( level + 1 );
}
switch ( level )
{
case 0:
sink.section1_();
break;
case 1:
sink.section2_();
break;
case 2:
sink.section3_();
break;
case 3:
sink.section4_();
break;
case 4:
sink.section5_();
break;
default:
break;
}
}
/**
* Parse the section blocks of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseSectionBlocks()
throws AptParseException
{
loop: while ( block != null )
{
switch ( block.getType() )
{
case PARAGRAPH:
case VERBATIM:
case FIGURE:
case TABLE:
case HORIZONTAL_RULE:
case PG_BREAK:
case MACRO:
case COMMENT_BLOCK:
block.traverse();
nextBlock();
break;
case LIST_ITEM:
traverseList();
break;
case NUMBERED_LIST_ITEM:
traverseNumberedList();
break;
case DEFINITION_LIST_ITEM:
traverseDefinitionList();
break;
case LIST_BREAK:
// May be this is a list break which has not been indented
// very precisely.
nextBlock();
break;
default:
// A section block which starts a new section.
break loop;
}
}
}
/**
* Parse a list of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseList()
throws AptParseException
{
if ( block == null )
{
return;
}
expectedBlock( LIST_ITEM );
int listIndent = block.getIndent();
sink.list();
sink.listItem();
block.traverse();
nextBlock();
loop: while ( block != null )
{
int blockIndent = block.getIndent();
switch ( block.getType() )
{
case PARAGRAPH:
if ( blockIndent < listIndent )
{
break loop;
}
/*FALLTHROUGH*/
case VERBATIM:
case MACRO:
case FIGURE:
case TABLE:
case HORIZONTAL_RULE:
case PG_BREAK:
block.traverse();
nextBlock();
break;
case LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
if ( blockIndent > listIndent )
{
traverseList();
}
else
{
sink.listItem_();
sink.listItem();
block.traverse();
nextBlock();
}
break;
case NUMBERED_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseNumberedList();
break;
case DEFINITION_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseDefinitionList();
break;
case LIST_BREAK:
if ( blockIndent >= listIndent )
{
nextBlock();
}
/*FALLTHROUGH*/
default:
// A block which ends the list.
break loop;
}
}
sink.listItem_();
sink.list_();
}
/**
* Parse a numbered list of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseNumberedList()
throws AptParseException
{
if ( block == null )
{
return;
}
expectedBlock( NUMBERED_LIST_ITEM );
int listIndent = block.getIndent();
sink.numberedList( ( (NumberedListItem) block ).getNumbering() );
sink.numberedListItem();
block.traverse();
nextBlock();
loop: while ( block != null )
{
int blockIndent = block.getIndent();
switch ( block.getType() )
{
case PARAGRAPH:
if ( blockIndent < listIndent )
{
break loop;
}
/*FALLTHROUGH*/
case VERBATIM:
case FIGURE:
case TABLE:
case HORIZONTAL_RULE:
case PG_BREAK:
block.traverse();
nextBlock();
break;
case LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseList();
break;
case NUMBERED_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
if ( blockIndent > listIndent )
{
traverseNumberedList();
}
else
{
sink.numberedListItem_();
sink.numberedListItem();
block.traverse();
nextBlock();
}
break;
case DEFINITION_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseDefinitionList();
break;
case LIST_BREAK:
if ( blockIndent >= listIndent )
{
nextBlock();
}
/*FALLTHROUGH*/
default:
// A block which ends the list.
break loop;
}
}
sink.numberedListItem_();
sink.numberedList_();
}
/**
* Parse a definition list of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void traverseDefinitionList()
throws AptParseException
{
if ( block == null )
{
return;
}
expectedBlock( DEFINITION_LIST_ITEM );
int listIndent = block.getIndent();
sink.definitionList();
sink.definitionListItem();
block.traverse();
nextBlock();
loop: while ( block != null )
{
int blockIndent = block.getIndent();
switch ( block.getType() )
{
case PARAGRAPH:
if ( blockIndent < listIndent )
{
break loop;
}
/*FALLTHROUGH*/
case VERBATIM:
case FIGURE:
case TABLE:
case HORIZONTAL_RULE:
case PG_BREAK:
block.traverse();
nextBlock();
break;
case LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseList();
break;
case NUMBERED_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
traverseNumberedList();
break;
case DEFINITION_LIST_ITEM:
if ( blockIndent < listIndent )
{
break loop;
}
if ( blockIndent > listIndent )
{
traverseDefinitionList();
}
else
{
sink.definition_();
sink.definitionListItem_();
sink.definitionListItem();
block.traverse();
nextBlock();
}
break;
case LIST_BREAK:
if ( blockIndent >= listIndent )
{
nextBlock();
}
/*FALLTHROUGH*/
default:
// A block which ends the list.
break loop;
}
}
sink.definition_();
sink.definitionListItem_();
sink.definitionList_();
}
/**
* Parse the next block of the Apt source document.
*
* @throws AptParseException if something goes wrong.
*/
private void nextBlock()
throws AptParseException
{
nextBlock( /*first*/false );
}
/**
* Parse the next block of the Apt source document.
*
* @param firstBlock True if this is the first block of the Apt source document.
* @throws AptParseException if something goes wrong.
*/
private void nextBlock( boolean firstBlock )
throws AptParseException
{
// Skip open lines.
int length, indent, i;
skipLoop: for ( ;; )
{
if ( line == null )
{
block = null;
return;
}
length = line.length();
indent = 0;
for ( i = 0; i < length; ++i )
{
switch ( line.charAt( i ) )
{
case SPACE:
++indent;
break;
case TAB:
indent += 8;
break;
default:
break skipLoop;
}
}
if ( i == length )
{
nextLine();
}
}
blockFileName = source.getName();
blockLineNumber = source.getLineNumber();
block = null;
switch ( line.charAt( i ) )
{
case STAR:
if ( indent == 0 )
{
if ( charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
{
block = new Table( indent, line );
}
else if ( charAt( line, length, i + 1 ) == STAR )
{
if ( charAt( line, length, i + 2 ) == STAR )
{
if ( charAt( line, length, i + 3 ) == STAR )
{
block = new Section5( indent, line );
}
else
{
block = new Section4( indent, line );
}
}
else
{
block = new Section3( indent, line );
}
}
else
{
block = new Section2( indent, line );
}
}
else
{
block = new ListItem( indent, line );
}
break;
case LEFT_SQUARE_BRACKET:
if ( charAt( line, length, i + 1 ) == RIGHT_SQUARE_BRACKET )
{
block = new ListBreak( indent, line );
}
else
{
if ( indent == 0 )
{
block = new Figure( indent, line );
}
else
{
if ( charAt( line, length, i + 1 ) == LEFT_SQUARE_BRACKET )
{
int numbering;
switch ( charAt( line, length, i + 2 ) )
{
case NUMBERING_LOWER_ALPHA_CHAR:
numbering = Sink.NUMBERING_LOWER_ALPHA;
break;
case NUMBERING_UPPER_ALPHA_CHAR:
numbering = Sink.NUMBERING_UPPER_ALPHA;
break;
case NUMBERING_LOWER_ROMAN_CHAR:
numbering = Sink.NUMBERING_LOWER_ROMAN;
break;
case NUMBERING_UPPER_ROMAN_CHAR:
numbering = Sink.NUMBERING_UPPER_ROMAN;
break;
case NUMBERING:
default:
// The first item establishes the numbering
// scheme for the whole list.
numbering = Sink.NUMBERING_DECIMAL;
}
block = new NumberedListItem( indent, line, numbering );
}
else
{
block = new DefinitionListItem( indent, line );
}
}
}
break;
case MINUS:
if ( charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
{
if ( indent == 0 )
{
block = new Verbatim( indent, line );
}
else
{
if ( firstBlock )
{
block = new Title( indent, line );
}
}
}
break;
case PLUS:
if ( indent == 0 && charAt( line, length, i + 1 ) == MINUS && charAt( line, length, i + 2 ) == MINUS )
{
block = new Verbatim( indent, line );
}
break;
case EQUAL:
if ( indent == 0 && charAt( line, length, i + 1 ) == EQUAL && charAt( line, length, i + 2 ) == EQUAL )
{
block = new HorizontalRule( indent, line );
}
break;
case PAGE_BREAK:
if ( indent == 0 )
{
block = new PageBreak( indent, line );
}
break;
case PERCENT:
if ( indent == 0 && charAt( line, length, i + 1 ) == LEFT_CURLY_BRACKET )
{
block = new MacroBlock( indent, line );
}
break;
case COMMENT:
if ( charAt( line, length, i + 1 ) == COMMENT )
{
block = new Comment( line.substring( i + 2 ) );
}
break;
default:
break;
}
if ( block == null )
{
if ( indent == 0 )
{
block = new Section1( indent, line );
}
else
{
block = new Paragraph( indent, line );
}
}
}
/**
* Checks that the current block is of the expected type.
*
* @param type the expected type.
* @throws AptParseException if something goes wrong.
*/
private void expectedBlock( int type )
throws AptParseException
{
int blockType = block.getType();
if ( blockType != type )
{
throw new AptParseException( "expected " + TYPE_NAMES[type] + ", found " + TYPE_NAMES[blockType] );
}
}
// -----------------------------------------------------------------------
/**
* Determine if c is an octal character.
*
* @param c the character.
* @return boolean
*/
private static boolean isOctalChar( char c )
{
return ( c >= '0' && c <= '7' );
}
/**
* Determine if c is an hex character.
*
* @param c the character.
* @return boolean
*/
private static boolean isHexChar( char c )
{
return ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'f' ) || ( c >= 'A' && c <= 'F' ) );
}
/**
* Emits the text so far parsed into the given sink.
*
* @param buffer A StringBuilder that contains the text to be flushed.
* @param sink The sink to receive the text.
*/
private static void flushTraversed( StringBuilder buffer, Sink sink )
{
if ( buffer.length() > 0 )
{
sink.text( buffer.toString() );
buffer.setLength( 0 );
}
}
/**
* Parse the given text.
*
* @param text the text to parse.
* @param begin offset.
* @param end offset.
* @param linkAnchor a StringBuilder.
* @return int
* @throws AptParseException if something goes wrong.
*/
private static int skipTraversedLinkAnchor( String text, int begin, int end, StringBuilder linkAnchor )
throws AptParseException
{
int i;
loop: for ( i = begin; i < end; ++i )
{
char c = text.charAt( i );
switch ( c )
{
case RIGHT_CURLY_BRACKET:
break loop;
case BACKSLASH:
if ( i + 1 < end )
{
++i;
linkAnchor.append( text.charAt( i ) );
}
else
{
linkAnchor.append( BACKSLASH );
}
break;
default:
linkAnchor.append( c );
}
}
if ( i == end )
{
throw new AptParseException( "missing '" + RIGHT_CURLY_BRACKET + "'" );
}
return i;
}
/**
* Parse the given text.
*
* @param text the text to parse.
* @param begin offset.
* @param end offset.
* @return String
* @throws AptParseException if something goes wrong.
*/
private String getTraversedLink( String text, int begin, int end )
throws AptParseException
{
char previous2 = LEFT_CURLY_BRACKET;
char previous = LEFT_CURLY_BRACKET;
int i;
for ( i = begin; i < end; ++i )
{
char c = text.charAt( i );
if ( c == RIGHT_CURLY_BRACKET && previous == RIGHT_CURLY_BRACKET && previous2 != BACKSLASH )
{
break;
}
previous2 = previous;
previous = c;
}
if ( i == end )
{
throw new AptParseException( "missing '" + LEFT_CURLY_BRACKET + LEFT_CURLY_BRACKET + "'" );
}
return doGetTraversedLink( text, begin, i - 1 );
}
/**
* Parse the given text.
*
* @param text the text to parse.
* @param begin offset.
* @param end offset.
* @return String
* @throws AptParseException if something goes wrong.
*/
private String getTraversedAnchor( String text, int begin, int end )
throws AptParseException
{
char previous = LEFT_CURLY_BRACKET;
int i;
for ( i = begin; i < end; ++i )
{
char c = text.charAt( i );
if ( c == RIGHT_CURLY_BRACKET && previous != BACKSLASH )
{
break;
}
previous = c;
}
if ( i == end )
{
throw new AptParseException( "missing '" + RIGHT_CURLY_BRACKET + "'" );
}
return doGetTraversedLink( text, begin, i );
}
/**
* Parse the given text.
*
* @param text the text to parse.
* @param begin offset.
* @param end offset.
* @return String
* @throws AptParseException if something goes wrong.
*/
private String doGetTraversedLink( String text, int begin, int end )
throws AptParseException
{
final StringBuilder buffer = new StringBuilder( end - begin );
Sink linkSink = new SinkAdapter()
{
/** {@inheritDoc} */
public void lineBreak()
{
buffer.append( SPACE );
}
/** {@inheritDoc} */
public void nonBreakingSpace()
{
buffer.append( SPACE );
}
/** {@inheritDoc} */
public void text( String text )
{
buffer.append( text );
}
};
doTraverseText( text, begin, end, linkSink );
return buffer.toString().trim();
}
/**
* If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
*
* @param key not null
* @param msg not null
* @see #parse(Reader, Sink)
* @since 1.1.1
*/
private void logMessage( String key, String msg )
{
msg = "[APT Parser] " + msg;
if ( getLog().isDebugEnabled() )
{
getLog().debug( msg );
return;
}
if ( warnMessages == null )
{
warnMessages = new HashMap<>();
}
Set<String> set = warnMessages.get( key );
if ( set == null )
{
set = new TreeSet<>();
}
set.add( msg );
warnMessages.put( key, set );
}
/**
* @since 1.1.2
*/
private void logWarnings()
{
if ( getLog().isWarnEnabled() && this.warnMessages != null && !isSecondParsing() )
{
for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
{
for ( String msg : entry.getValue() )
{
getLog().warn( msg );
}
}
this.warnMessages = null;
}
}
// -----------------------------------------------------------------------
/** A block of an apt source document. */
private abstract class Block
{
/** type. */
protected int type;
/** indent. */
protected int indent;
/** text. */
protected String text;
/** textLength. */
protected int textLength;
/**
* Constructor.
*
* @param type the block type.
* @param indent indent.
* @throws AptParseException AptParseException
*/
Block( int type, int indent )
throws AptParseException
{
this( type, indent, null );
}
/**
* Constructor.
*
* @param type type.
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Block( int type, int indent, String firstLine )
throws AptParseException
{
this.type = type;
this.indent = indent;
// Skip first line ---
AptParser.this.nextLine();
if ( firstLine == null )
{
text = null;
textLength = 0;
}
else
{
// Read block ---
StringBuilder buffer = new StringBuilder( firstLine );
while ( AptParser.this.line != null )
{
String l = AptParser.this.line;
int length = l.length();
int i = 0;
i = skipSpace( l, length, i );
if ( i == length )
{
// Stop after open line and skip it.
AptParser.this.nextLine();
break;
}
else if ( ( AptParser.charAt( l, length, i ) == COMMENT
&& AptParser.charAt( l, length, i + 1 ) == COMMENT )
|| type == COMMENT_BLOCK )
{
// parse comments as separate blocks line by line
break;
}
buffer.append( EOL );
buffer.append( l );
AptParser.this.nextLine();
}
text = buffer.toString();
textLength = text.length();
}
}
/**
* Return the block type.
*
* @return int
*/
public final int getType()
{
return type;
}
/**
* Return the block indent.
*
* @return int
*/
public final int getIndent()
{
return indent;
}
/**
* Parse the block.
*
* @throws AptParseException if something goes wrong.
*/
public abstract void traverse()
throws AptParseException;
/**
* Traverse the text.
*
* @param begin offset.
* @throws AptParseException if something goes wrong.
*/
protected void traverseText( int begin )
throws AptParseException
{
traverseText( begin, text.length() );
}
/**
* Traverse the text.
*
* @param begin offset.
* @param end offset.
* @throws AptParseException if something goes wrong.
*/
protected void traverseText( int begin, int end )
throws AptParseException
{
AptParser.this.doTraverseText( text, begin, end, AptParser.this.sink );
}
/**
* Skip spaces.
*
* @return int.
*/
protected int skipLeadingBullets()
{
int i = skipSpaceFrom( 0 );
for ( ; i < textLength; ++i )
{
if ( text.charAt( i ) != STAR )
{
break;
}
}
return skipSpaceFrom( i );
}
/**
* Skip brackets.
*
* @param i offset.
* @return int.
* @throws AptParseException if something goes wrong.
*/
protected int skipFromLeftToRightBracket( int i )
throws AptParseException
{
char previous = LEFT_SQUARE_BRACKET;
for ( ++i; i < textLength; ++i )
{
char c = text.charAt( i );
if ( c == RIGHT_SQUARE_BRACKET && previous != BACKSLASH )
{
break;
}
previous = c;
}
if ( i == textLength )
{
throw new AptParseException( "missing '" + RIGHT_SQUARE_BRACKET + "'" );
}
return i;
}
/**
* Skip spaces.
*
* @param i offset.
* @return int.
*/
protected final int skipSpaceFrom( int i )
{
return AptParser.skipSpace( text, textLength, i );
}
}
/** A ListBreak Block. */
private class ListBreak
extends AptParser.Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
ListBreak( int indent, String firstLine )
throws AptParseException
{
super( AptParser.LIST_BREAK, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
throw new AptParseException( "internal error: traversing list break" );
}
}
/** A Title Block. */
private class Title
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Title( int indent, String firstLine )
throws AptParseException
{
super( TITLE, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
StringTokenizer lines = new StringTokenizer( text, EOL );
int separator = -1;
boolean firstLine = true;
boolean title = false;
boolean author = false;
boolean date = false;
loop: while ( lines.hasMoreTokens() )
{
String line = lines.nextToken().trim();
int lineLength = line.length();
if ( AptParser.charAt( line, lineLength, 0 ) == MINUS
&& AptParser.charAt( line, lineLength, 1 ) == MINUS
&& AptParser.charAt( line, lineLength, 2 ) == MINUS )
{
switch ( separator )
{
case 0:
if ( title )
{
AptParser.this.sink.title_();
}
else
{
throw new AptParseException( "missing title" );
}
break;
case 1:
if ( author )
{
AptParser.this.sink.author_();
}
break;
case 2:
// Note that an extra decorative line is allowed
// at the end of the author.
break loop;
default:
break;
}
++separator;
firstLine = true;
}
else
{
if ( firstLine )
{
firstLine = false;
switch ( separator )
{
case 0:
title = true;
AptParser.this.sink.title();
break;
case 1:
author = true;
AptParser.this.sink.author();
break;
case 2:
date = true;
AptParser.this.sink.date();
break;
default:
break;
}
}
else
{
// An implicit lineBreak separates title lines.
AptParser.this.sink.lineBreak();
}
AptParser.this.doTraverseText( line, 0, lineLength, AptParser.this.sink );
}
}
switch ( separator )
{
case 0:
if ( title )
{
AptParser.this.sink.title_();
}
else
{
throw new AptParseException( "missing title" );
}
break;
case 1:
if ( author )
{
AptParser.this.sink.author_();
}
break;
case 2:
if ( date )
{
AptParser.this.sink.date_();
}
break;
default:
break;
}
}
}
/** A Section Block. */
private abstract class Section
extends Block
{
/**
* Constructor.
*
* @param type type.
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section( int type, int indent, String firstLine )
throws AptParseException
{
super( type, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
Title();
traverseText( skipLeadingBullets() );
Title_();
}
/** Start a title. */
public abstract void Title();
/** End a title. */
public abstract void Title_();
}
/** A Section1 Block. */
private class Section1
extends Section
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section1( int indent, String firstLine )
throws AptParseException
{
super( SECTION1, indent, firstLine );
}
/** {@inheritDoc} */
public void Title()
{
AptParser.this.sink.sectionTitle1();
}
/** {@inheritDoc} */
public void Title_()
{
AptParser.this.sink.sectionTitle1_();
}
}
/** A Section2 Block. */
private class Section2
extends Section
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section2( int indent, String firstLine )
throws AptParseException
{
super( SECTION2, indent, firstLine );
}
/** {@inheritDoc} */
public void Title()
{
AptParser.this.sink.sectionTitle2();
}
/** {@inheritDoc} */
public void Title_()
{
AptParser.this.sink.sectionTitle2_();
}
}
/** A Section3 Block. */
public class Section3
extends Section
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section3( int indent, String firstLine )
throws AptParseException
{
super( SECTION3, indent, firstLine );
}
/** {@inheritDoc} */
public void Title()
{
AptParser.this.sink.sectionTitle3();
}
/** {@inheritDoc} */
public void Title_()
{
AptParser.this.sink.sectionTitle3_();
}
}
/** A Section4 Block. */
private class Section4
extends Section
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section4( int indent, String firstLine )
throws AptParseException
{
super( SECTION4, indent, firstLine );
}
/** {@inheritDoc} */
public void Title()
{
AptParser.this.sink.sectionTitle4();
}
/** {@inheritDoc} */
public void Title_()
{
AptParser.this.sink.sectionTitle4_();
}
}
/** A Section5 Block. */
private class Section5
extends Section
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Section5( int indent, String firstLine )
throws AptParseException
{
super( SECTION5, indent, firstLine );
}
/** {@inheritDoc} */
public void Title()
{
AptParser.this.sink.sectionTitle5();
}
/** {@inheritDoc} */
public void Title_()
{
AptParser.this.sink.sectionTitle5_();
}
}
/** A Paragraph Block. */
private class Paragraph
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Paragraph( int indent, String firstLine )
throws AptParseException
{
super( PARAGRAPH, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
AptParser.this.sink.paragraph();
traverseText( skipSpaceFrom( 0 ) );
AptParser.this.sink.paragraph_();
}
}
/** A Comment Block. */
private class Comment
extends Block
{
/**
* Constructor.
*
* @param line the comment line.
* @throws AptParseException AptParseException
*/
Comment( String line )
throws AptParseException
{
super( COMMENT_BLOCK, 0, line );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
if ( isEmitComments() )
{
AptParser.this.sink.comment( text );
}
}
}
/** A Verbatim Block. */
private class Verbatim
extends Block
{
/** boxed. */
private boolean boxed;
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Verbatim( int indent, String firstLine )
throws AptParseException
{
super( VERBATIM, indent, null );
// Read block (first line already skipped) ---
StringBuilder buffer = new StringBuilder();
char firstChar = firstLine.charAt( 0 );
boxed = ( firstChar == PLUS );
while ( AptParser.this.line != null )
{
String l = AptParser.this.line;
int length = l.length();
if ( AptParser.charAt( l, length, 0 ) == firstChar && AptParser.charAt( l, length, 1 ) == MINUS
&& AptParser.charAt( l, length, 2 ) == MINUS )
{
AptParser.this.nextLine();
break;
}
// Expand tabs ---
int prevColumn, column;
column = 0;
for ( int i = 0; i < length; ++i )
{
char c = l.charAt( i );
if ( c == TAB )
{
prevColumn = column;
column = ( ( column + 1 + TAB_WIDTH - 1 ) / TAB_WIDTH ) * TAB_WIDTH;
buffer.append( SPACES, 0, column - prevColumn );
}
else
{
++column;
buffer.append( c );
}
}
buffer.append( EOL );
AptParser.this.nextLine();
}
// The last '\n' is mandatory before the "---" delimeter but is
// not part of the verbatim text.
textLength = buffer.length();
if ( textLength > 0 )
{
--textLength;
buffer.setLength( textLength );
}
text = buffer.toString();
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
AptParser.this.sink.verbatim( boxed ? SinkEventAttributeSet.BOXED : null );
AptParser.this.sink.text( text );
AptParser.this.sink.verbatim_();
}
}
/** A Figure Block. */
private class Figure
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Figure( int indent, String firstLine )
throws AptParseException
{
super( FIGURE, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
AptParser.this.sink.figure();
int i = skipFromLeftToRightBracket( 0 );
AptParser.this.sink.figureGraphics( text.substring( 1, i ) );
i = skipSpaceFrom( i + 1 );
if ( i < textLength )
{
AptParser.this.sink.figureCaption();
traverseText( i );
AptParser.this.sink.figureCaption_();
}
AptParser.this.sink.figure_();
}
}
/** A Table Block. */
private class Table
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
Table( int indent, String firstLine )
throws AptParseException
{
super( TABLE, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
int captionIndex = -1;
int nextLineIndex = 0;
int init = 2;
int[] justification = null;
int rows = 0;
int columns = 0;
StringBuilder[] cells = null;
boolean[] headers = null;
boolean grid;
AptParser.this.sink.table();
while ( nextLineIndex < textLength )
{
int i = text.indexOf( "*--", nextLineIndex );
if ( i < 0 )
{
captionIndex = nextLineIndex;
break;
}
String line;
i = text.indexOf( '\n', nextLineIndex );
if ( i < 0 )
{
line = text.substring( nextLineIndex );
nextLineIndex = textLength;
}
else
{
line = text.substring( nextLineIndex, i );
nextLineIndex = i + 1;
}
int lineLength = line.length();
if ( line.indexOf( "*--" ) == 0 )
{
if ( init == 2 )
{
init = 1;
justification = parseJustification( line, lineLength );
columns = justification.length;
cells = new StringBuilder[columns];
headers = new boolean[columns];
for ( i = 0; i < columns; ++i )
{
cells[i] = new StringBuilder();
headers[i] = false;
}
}
else
{
if ( traverseRow( cells, headers, justification ) )
{
++rows;
}
justification = parseJustification( line, lineLength );
}
}
else
{
if ( init == 1 )
{
init = 0;
grid = ( AptParser.charAt( line, lineLength, 0 ) == PIPE );
AptParser.this.sink.tableRows( justification, grid );
}
line = replaceAll( line, "\\|", "\\u007C" );
StringTokenizer cellLines = new StringTokenizer( line, "|", true );
i = 0;
boolean processedGrid = false;
while ( cellLines.hasMoreTokens() )
{
String cellLine = cellLines.nextToken();
if ( "|".equals( cellLine ) )
{
if ( processedGrid )
{
headers[i] = true;
}
else
{
processedGrid = true;
headers[i] = false;
}
continue;
}
processedGrid = false;
cellLine = replaceAll( cellLine, "\\", "\\u00A0" ); // linebreak
// Escaped special characters: \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\.
cellLine = replaceAll( cellLine, "\\u00A0~", "\\~" );
cellLine = replaceAll( cellLine, "\\u00A0=", "\\=" );
cellLine = replaceAll( cellLine, "\\u00A0-", "\\-" );
cellLine = replaceAll( cellLine, "\\u00A0+", "\\+" );
cellLine = replaceAll( cellLine, "\\u00A0*", "\\*" );
cellLine = replaceAll( cellLine, "\\u00A0[", "\\[" );
cellLine = replaceAll( cellLine, "\\u00A0]", "\\]" );
cellLine = replaceAll( cellLine, "\\u00A0<", "\\<" );
cellLine = replaceAll( cellLine, "\\u00A0>", "\\>" );
cellLine = replaceAll( cellLine, "\\u00A0{", "\\{" );
cellLine = replaceAll( cellLine, "\\u00A0}", "\\}" );
cellLine = replaceAll( cellLine, "\\u00A0u", "\\u" );
cellLine = replaceAll( cellLine, "\\u00A0\\u00A0", "\\\\" );
cellLine = cellLine.trim();
StringBuilder cell = cells[i];
if ( cellLine.length() > 0 )
{
// line break in table cells
if ( cell.toString().trim().endsWith( "\\u00A0" ) )
{
cell.append( "\\\n" );
}
else
{
if ( cell.length() != 0 )
{
// Always add a space for multi line tables cells
cell.append( " " );
}
}
cell.append( cellLine );
}
++i;
if ( i == columns )
{
break;
}
}
}
}
if ( rows == 0 )
{
throw new AptParseException( "no table rows" );
}
AptParser.this.sink.tableRows_();
if ( captionIndex >= 0 )
{
AptParser.this.sink.tableCaption();
AptParser.this.doTraverseText( text, captionIndex, textLength, AptParser.this.sink );
AptParser.this.sink.tableCaption_();
}
AptParser.this.sink.table_();
}
/**
* Parse a table justification line.
*
* @param jline the justification line.
* @param lineLength the length of the line. Must be > 2.
* @return int[]
* @throws AptParseException if something goes wrong.
*/
private int[] parseJustification( String jline, int lineLength )
throws AptParseException
{
int columns = 0;
for ( int i = 2 /*Skip '*--'*/; i < lineLength; ++i )
{
switch ( jline.charAt( i ) )
{
case STAR:
case PLUS:
case COLON:
++columns;
break;
default:
break;
}
}
if ( columns == 0 )
{
throw new AptParseException( "no columns specified" );
}
int[] justification = new int[columns];
columns = 0;
for ( int i = 2; i < lineLength; ++i )
{
switch ( jline.charAt( i ) )
{
case STAR:
justification[columns++] = Sink.JUSTIFY_CENTER;
break;
case PLUS:
justification[columns++] = Sink.JUSTIFY_LEFT;
break;
case COLON:
justification[columns++] = Sink.JUSTIFY_RIGHT;
break;
default:
break;
}
}
return justification;
}
/**
* Traverse a table row.
*
* @param cells The table cells.
* @param headers true for header cells.
* @param justification the justification for each cell.
* @return boolean
* @throws AptParseException if something goes wrong.
*/
private boolean traverseRow( StringBuilder[] cells, boolean[] headers, int[] justification )
throws AptParseException
{
// Skip empty row (a decorative line).
boolean traversed = false;
for ( StringBuilder cell1 : cells )
{
if ( cell1.length() > 0 )
{
traversed = true;
break;
}
}
if ( traversed )
{
AptParser.this.sink.tableRow();
for ( int i = 0; i < cells.length; ++i )
{
StringBuilder cell = cells[i];
SinkEventAttributes justif;
switch ( justification[i] )
{
case Sink.JUSTIFY_CENTER:
justif = SinkEventAttributeSet.CENTER;
break;
case Sink.JUSTIFY_LEFT:
justif = SinkEventAttributeSet.LEFT;
break;
case Sink.JUSTIFY_RIGHT:
justif = SinkEventAttributeSet.RIGHT;
break;
default:
justif = SinkEventAttributeSet.LEFT;
break;
}
SinkEventAttributeSet event = new SinkEventAttributeSet();
event.addAttributes( justif );
if ( headers[i] )
{
AptParser.this.sink.tableHeaderCell( event );
}
else
{
AptParser.this.sink.tableCell( event );
}
if ( cell.length() > 0 )
{
AptParser.this.doTraverseText( cell.toString(), 0, cell.length(), AptParser.this.sink );
cell.setLength( 0 );
}
if ( headers[i] )
{
AptParser.this.sink.tableHeaderCell_();
// DOXIA-404: reset header for next row
headers[i] = false;
}
else
{
AptParser.this.sink.tableCell_();
}
}
AptParser.this.sink.tableRow_();
}
return traversed;
}
}
/** A ListItem Block. */
private class ListItem
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
ListItem( int indent, String firstLine )
throws AptParseException
{
super( LIST_ITEM, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
traverseText( skipLeadingBullets() );
}
}
/** A NumberedListItem Block. */
private class NumberedListItem
extends Block
{
/** numbering. */
private int numbering;
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @param number numbering.
* @throws AptParseException AptParseException
*/
NumberedListItem( int indent, String firstLine, int number )
throws AptParseException
{
super( NUMBERED_LIST_ITEM, indent, firstLine );
this.numbering = number;
}
/**
* getNumbering.
*
* @return int
*/
public int getNumbering()
{
return numbering;
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
traverseText( skipItemNumber() );
}
/**
* skipItemNumber.
*
* @return int
* @throws AptParseException AptParseException
*/
private int skipItemNumber()
throws AptParseException
{
int i = skipSpaceFrom( 0 );
char prevChar = SPACE;
for ( ; i < textLength; ++i )
{
char c = text.charAt( i );
if ( c == RIGHT_SQUARE_BRACKET && prevChar == RIGHT_SQUARE_BRACKET )
{
break;
}
prevChar = c;
}
if ( i == textLength )
{
throw new AptParseException( "missing '" + RIGHT_SQUARE_BRACKET + RIGHT_SQUARE_BRACKET + "'" );
}
return skipSpaceFrom( i + 1 );
}
}
/** A DefinitionListItem Block. */
private class DefinitionListItem
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
DefinitionListItem( int indent, String firstLine )
throws AptParseException
{
super( DEFINITION_LIST_ITEM, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
int i = skipSpaceFrom( 0 );
int j = skipFromLeftToRightBracket( i );
AptParser.this.sink.definedTerm();
traverseText( i + 1, j );
AptParser.this.sink.definedTerm_();
j = skipSpaceFrom( j + 1 );
if ( j == textLength )
{
// TODO: this doesn't handle the case of a dd in a paragraph
//throw new AptParseException( "no definition" );
}
AptParser.this.sink.definition();
traverseText( j );
}
}
/** A HorizontalRule Block. */
private class HorizontalRule
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
HorizontalRule( int indent, String firstLine )
throws AptParseException
{
super( HORIZONTAL_RULE, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
AptParser.this.sink.horizontalRule();
}
}
/** A PageBreak Block. */
private class PageBreak
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
PageBreak( int indent, String firstLine )
throws AptParseException
{
super( PG_BREAK, indent, firstLine );
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
AptParser.this.sink.pageBreak();
}
}
/** A MacroBlock Block. */
private class MacroBlock
extends Block
{
/**
* Constructor.
*
* @param indent indent.
* @param firstLine the first line.
* @throws AptParseException AptParseException
*/
MacroBlock( int indent, String firstLine )
throws AptParseException
{
super( MACRO, indent );
text = firstLine;
}
/** {@inheritDoc} */
public void traverse()
throws AptParseException
{
if ( isSecondParsing() )
{
return;
}
final int start = text.indexOf( '{' );
final int end = text.indexOf( '}' );
String s = text.substring( start + 1, end );
s = escapeForMacro( s );
String[] params = StringUtils.split( s, "|" );
String macroId = params[0];
Map<String, Object> parameters = new HashMap<>();
for ( int i = 1; i < params.length; i++ )
{
String[] param = StringUtils.split( params[i], "=" );
if ( param.length == 1 )
{
throw new AptParseException( "Missing 'key=value' pair for macro parameter: " + params[i] );
}
String key = unescapeForMacro( param[0] );
String value = unescapeForMacro( param[1] );
parameters.put( key, value );
}
// getBasedir() does not work in multi-module builds, see DOXIA-373
// the basedir should be injected from here, see DOXIA-224
MacroRequest request = new MacroRequest( sourceContent, new AptParser(), parameters, getBasedir() );
try
{
AptParser.this.executeMacro( macroId, request, sink );
}
catch ( MacroExecutionException e )
{
throw new AptParseException( "Unable to execute macro in the APT document", e );
}
catch ( MacroNotFoundException e )
{
throw new AptParseException( "Unable to find macro used in the APT document", e );
}
}
/**
* escapeForMacro
*
* @param s String
* @return String
*/
private String escapeForMacro( String s )
{
if ( s == null || s.length() < 1 )
{
return s;
}
String result = s;
// use some outrageously out-of-place chars for text
// (these are device control one/two in unicode)
result = StringUtils.replace( result, "\\=", "\u0011" );
result = StringUtils.replace( result, "\\|", "\u0012" );
return result;
}
/**
* unescapeForMacro
*
* @param s String
* @return String
*/
private String unescapeForMacro( String s )
{
if ( s == null || s.length() < 1 )
{
return s;
}
String result = s;
result = StringUtils.replace( result, "\u0011", "=" );
result = StringUtils.replace( result, "\u0012", "|" );
return result;
}
}
}