blob: 2a448a3d6fd2af1daef20ccbd92b2efe3ddfe381 [file] [log] [blame]
package org.apache.maven.doxia.module.fo;
/*
* 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 java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML.Attribute;
import javax.swing.text.html.HTML.Tag;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.AbstractXmlSink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.sink.impl.SinkUtils;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.apache.maven.doxia.util.HtmlTools;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
/**
* A Doxia Sink that produces a FO model. The usage is similar to the following:
*
* <pre>
* FoSink sink = new FoSink( writer );
* sink.beginDocument();
* ...
* sink.endDocument();
* </pre>
*
* @author ltheussl
* @version $Id$
* @since 1.1
*/
public class FoSink
extends AbstractXmlSink
implements FoMarkup
{
/** For writing the result. */
private final PrintWriter out;
/** Used to get the current position in numbered lists. */
private final Stack<NumberedListItem> listStack;
/** Used to get attributes for a given FO element. */
private final FoConfiguration config;
/** Counts the current section level. */
private int section = 0;
/** Counts the current subsection level. */
private int subsection = 0;
/** Counts the current subsubsection level. */
private int subsubsection = 0;
/** Verbatim flag. */
private boolean verbatim;
/** figure flag. */
private boolean inFigure;
private final String encoding;
private final String languageId;
/** Stack of drawing borders on table cells. */
private final LinkedList<Boolean> tableGridStack;
/** Stack of alignment int[] of table cells. */
private final LinkedList<int[]> cellJustifStack;
/** Stack of justification of table cells. */
private final LinkedList<Boolean> isCellJustifStack;
/** Stack of current table cell. */
private final LinkedList<Integer> cellCountStack;
/** The stack of StringWriter to write the table result temporary, so we could play with the output and fix fo. */
private final LinkedList<StringWriter> tableContentWriterStack;
private final LinkedList<StringWriter> tableCaptionWriterStack;
private final LinkedList<PrettyPrintXMLWriter> tableCaptionXMLWriterStack;
/** The stack of table caption */
private final LinkedList<String> tableCaptionStack;
/** Keep track of the closing tags for inline events. */
protected Stack<List<Tag>> inlineStack = new Stack<List<Tag>>();
/** 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;
/**
* Constructor, initialize the Writer.
*
* @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
* You could use <code>newXmlWriter</code> methods from {@link org.codehaus.plexus.util.WriterFactory}.
*/
protected FoSink( Writer writer )
{
this( writer, "UTF-8" );
}
/**
* Constructor, initialize the Writer and tells which encoding is used.
*
* @param writer not null writer to write the result.
* @param encoding the encoding used, that should be written to the generated HTML content
* if not <code>null</code>.
*/
protected FoSink( Writer writer, String encoding )
{
this( writer, encoding, null );
}
/**
* Constructor, initialize the Writer and tells which encoding and languageId are used.
*
* @param writer not null writer to write the result.
* @param encoding the encoding used, that should be written to the generated HTML content
* if not <code>null</code>.
* @param languageId language identifier for the root element as defined by
* <a href="ftp://ftp.isi.edu/in-notes/bcp/bcp47.txt">IETF BCP 47</a>, Tags for the Identification of Languages;
* in addition, the empty string may be specified.
*/
protected FoSink( Writer writer, String encoding, String languageId )
{
if ( writer == null )
{
throw new NullPointerException( "Null writer in FO Sink!" );
}
this.out = new PrintWriter( writer );
this.encoding = encoding;
this.languageId = languageId;
this.config = new FoConfiguration();
this.listStack = new Stack<NumberedListItem>();
this.tableGridStack = new LinkedList<Boolean>();
this.cellJustifStack = new LinkedList<int[]>();
this.isCellJustifStack = new LinkedList<Boolean>();
this.cellCountStack = new LinkedList<Integer>();
this.tableContentWriterStack = new LinkedList<StringWriter>();
this.tableCaptionWriterStack = new LinkedList<StringWriter>();
this.tableCaptionXMLWriterStack = new LinkedList<PrettyPrintXMLWriter>();
this.tableCaptionStack = new LinkedList<String>();
setNameSpace( "fo" );
}
// TODO add FOP compliance mode?
/**
* Load configuration parameters from a File.
*
* @param configFile the configuration file.
*
* @throws java.io.IOException if the File cannot be read
* or some error occurs when initializing the configuration parameters.
*
* @since 1.1.1
*/
public void load( File configFile )
throws IOException
{
config.load( configFile );
}
/** {@inheritDoc} */
public void head( SinkEventAttributes attributes )
{
init();
startPageSequence( "0", null, null );
}
/** {@inheritDoc} */
public void head()
{
head( null );
}
/** {@inheritDoc} */
public void head_()
{
writeEOL();
}
/** {@inheritDoc} */
public void title( SinkEventAttributes attributes )
{
writeStartTag( BLOCK_TAG, "doc.header.title" );
}
/** {@inheritDoc} */
public void title()
{
title( null );
}
/** {@inheritDoc} */
public void title_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void author( SinkEventAttributes attributes )
{
writeStartTag( BLOCK_TAG, "doc.header.author" );
}
/** {@inheritDoc} */
public void author()
{
author( null );
}
/** {@inheritDoc} */
public void author_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void date( SinkEventAttributes attributes )
{
writeStartTag( BLOCK_TAG, "doc.header.date" );
}
/** {@inheritDoc} */
public void date()
{
date( null );
}
/** {@inheritDoc} */
public void date_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void body( SinkEventAttributes attributes )
{
// noop
}
/** {@inheritDoc} */
public void body()
{
body( null );
}
/** {@inheritDoc} */
public void body_()
{
writeEOL();
writeEndTag( FLOW_TAG );
writeEOL();
writeEndTag( PAGE_SEQUENCE_TAG );
writeEOL();
endDocument();
}
// -----------------------------------------------------------------------
//
// -----------------------------------------------------------------------
/** {@inheritDoc} */
public void sectionTitle()
{
// nop
}
/** {@inheritDoc} */
public void sectionTitle_()
{
// nop
}
/** {@inheritDoc} */
public void section( int level, SinkEventAttributes attributes )
{
if ( level == SECTION_LEVEL_1 )
{
section++;
subsection = 0;
subsubsection = 0;
}
else if ( level == SECTION_LEVEL_2 )
{
subsection++;
subsubsection = 0;
}
else if ( level == SECTION_LEVEL_3 )
{
subsubsection++;
}
onSection();
}
/** {@inheritDoc} */
public void section_( int level )
{
onSection_();
}
/** {@inheritDoc} */
public void sectionTitle( int level, SinkEventAttributes attributes )
{
onSectionTitle( level );
}
/** {@inheritDoc} */
public void sectionTitle_( int level )
{
onSectionTitle_();
}
/** {@inheritDoc} */
public void section1()
{
section( SECTION_LEVEL_1, null );
}
/** {@inheritDoc} */
public void sectionTitle1()
{
sectionTitle( SECTION_LEVEL_1, null );
}
/** {@inheritDoc} */
public void sectionTitle1_()
{
sectionTitle_( SECTION_LEVEL_1 );
}
/** {@inheritDoc} */
public void section1_()
{
section_( SECTION_LEVEL_1 );
}
/** {@inheritDoc} */
public void section2()
{
section( SECTION_LEVEL_2, null );
}
/** {@inheritDoc} */
public void sectionTitle2()
{
sectionTitle( SECTION_LEVEL_2, null );
}
/** {@inheritDoc} */
public void sectionTitle2_()
{
sectionTitle_( SECTION_LEVEL_2 );
}
/** {@inheritDoc} */
public void section2_()
{
section_( SECTION_LEVEL_2 );
}
/** {@inheritDoc} */
public void section3()
{
section( SECTION_LEVEL_3, null );
}
/** {@inheritDoc} */
public void sectionTitle3()
{
sectionTitle( SECTION_LEVEL_3, null );
}
/** {@inheritDoc} */
public void sectionTitle3_()
{
sectionTitle_( SECTION_LEVEL_3 );
}
/** {@inheritDoc} */
public void section3_()
{
section_( SECTION_LEVEL_3 );
}
/** {@inheritDoc} */
public void section4()
{
section( SECTION_LEVEL_4, null );
}
/** {@inheritDoc} */
public void sectionTitle4()
{
sectionTitle( SECTION_LEVEL_4, null );
}
/** {@inheritDoc} */
public void sectionTitle4_()
{
sectionTitle_( SECTION_LEVEL_4 );
}
/** {@inheritDoc} */
public void section4_()
{
section_( SECTION_LEVEL_4 );
}
/** {@inheritDoc} */
public void section5()
{
section( SECTION_LEVEL_5, null );
}
/** {@inheritDoc} */
public void sectionTitle5()
{
sectionTitle( SECTION_LEVEL_5, null );
}
/** {@inheritDoc} */
public void sectionTitle5_()
{
sectionTitle_( SECTION_LEVEL_5 );
}
/** {@inheritDoc} */
public void section5_()
{
section_( SECTION_LEVEL_5 );
}
/** Starts a section/subsection. */
private void onSection()
{
writeEOL();
writeStartTag( BLOCK_TAG, "body.text" );
}
/**
* Starts a section title.
*
* @param depth The section level.
*/
private void onSectionTitle( int depth )
{
StringBuilder title = new StringBuilder( 16 );
title.append( getChapterString() );
writeEOL();
if ( depth == SECTION_LEVEL_1 )
{
writeStartTag( BLOCK_TAG, "body.h1" );
title.append( section ).append( " " );
}
else if ( depth == SECTION_LEVEL_2 )
{
writeStartTag( BLOCK_TAG, "body.h2" );
title.append( section ).append( "." );
title.append( subsection ).append( " " );
}
else if ( depth == SECTION_LEVEL_3 )
{
writeStartTag( BLOCK_TAG, "body.h3" );
title.append( section ).append( "." );
title.append( subsection ).append( "." );
title.append( subsubsection ).append( " " );
}
else if ( depth == SECTION_LEVEL_4 )
{
writeStartTag( BLOCK_TAG, "body.h4" );
}
else
{
writeStartTag( BLOCK_TAG, "body.h5" );
}
write( title.toString() );
}
/** Ends a section title. */
private void onSectionTitle_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** Ends a section/subsection. */
private void onSection_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/**
* Resets the section counter to 0.
* Only useful for overriding classes, like AggregateSink, the FoSink puts everything into one chapter.
*/
protected void resetSectionCounter()
{
this.section = 0;
}
/**
* Returns the current chapter number as a string.
* By default does nothing, gets overridden by AggregateSink.
*
* @return an empty String.
*/
protected String getChapterString()
{
return "";
}
// -----------------------------------------------------------------------
//
// -----------------------------------------------------------------------
/** {@inheritDoc} */
public void list( SinkEventAttributes attributes )
{
writeEOL();
writeStartTag( LIST_BLOCK_TAG, "list" );
}
/** {@inheritDoc} */
public void list()
{
list( null );
}
/** {@inheritDoc} */
public void list_()
{
writeEndTag( LIST_BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void listItem( SinkEventAttributes attributes )
{
writeStartTag( LIST_ITEM_TAG, "list.item" );
writeStartTag( LIST_ITEM_LABEL_TAG );
writeStartTag( BLOCK_TAG );
write( "&#8226;" ); // TODO customize?
writeEndTag( BLOCK_TAG );
writeEndTag( LIST_ITEM_LABEL_TAG );
writeEOL();
writeStartTag( LIST_ITEM_BODY_TAG, "list.item" );
writeEOL();
writeStartTag( BLOCK_TAG );
}
/** {@inheritDoc} */
public void listItem()
{
listItem( null );
}
/** {@inheritDoc} */
public void listItem_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
writeEndTag( LIST_ITEM_BODY_TAG );
writeEOL();
writeEndTag( LIST_ITEM_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void numberedList( int numbering, SinkEventAttributes attributes )
{
this.listStack.push( new NumberedListItem( numbering ) );
writeEOL();
writeStartTag( LIST_BLOCK_TAG, "list" );
}
/** {@inheritDoc} */
public void numberedList( int numbering )
{
numberedList( numbering, null );
}
/** {@inheritDoc} */
public void numberedList_()
{
this.listStack.pop();
writeEndTag( LIST_BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void numberedListItem( SinkEventAttributes attributes )
{
NumberedListItem current = this.listStack.peek();
current.next();
writeStartTag( LIST_ITEM_TAG, "list.item" );
writeEOL();
writeStartTag( LIST_ITEM_LABEL_TAG );
writeEOL();
writeStartTag( BLOCK_TAG );
write( current.getListItemSymbol() );
writeEndTag( BLOCK_TAG );
writeEOL();
writeEndTag( LIST_ITEM_LABEL_TAG );
writeEOL();
writeStartTag( LIST_ITEM_BODY_TAG, "list.item" );
writeEOL();
writeStartTag( BLOCK_TAG );
}
/** {@inheritDoc} */
public void numberedListItem()
{
numberedListItem( null );
}
/** {@inheritDoc} */
public void numberedListItem_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
writeEndTag( LIST_ITEM_BODY_TAG );
writeEOL();
writeEndTag( LIST_ITEM_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void definitionList( SinkEventAttributes attributes )
{
writeEOL();
writeStartTag( BLOCK_TAG, "dl" );
}
/** {@inheritDoc} */
public void definitionList()
{
definitionList( null );
}
/** {@inheritDoc} */
public void definitionList_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void definitionListItem( SinkEventAttributes attributes )
{
// nop
}
/** {@inheritDoc} */
public void definitionListItem()
{
definitionListItem( null );
}
/** {@inheritDoc} */
public void definitionListItem_()
{
// nop
}
/** {@inheritDoc} */
public void definedTerm( SinkEventAttributes attributes )
{
writeStartTag( BLOCK_TAG, "dt" );
}
/** {@inheritDoc} */
public void definedTerm()
{
definedTerm( null );
}
/** {@inheritDoc} */
public void definedTerm_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void definition( SinkEventAttributes attributes )
{
writeEOL();
writeStartTag( BLOCK_TAG, "dd" );
}
/** {@inheritDoc} */
public void definition()
{
definition( null );
}
/** {@inheritDoc} */
public void definition_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void figure( SinkEventAttributes attributes )
{
this.inFigure = true;
writeEOL();
writeStartTag( BLOCK_TAG, "figure.display" );
}
/** {@inheritDoc} */
public void figure()
{
figure( null );
}
/** {@inheritDoc} */
public void figure_()
{
this.inFigure = false;
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void figureGraphics( String name )
{
figureGraphics( name, null );
}
/** {@inheritDoc} */
public void figureGraphics( String src, SinkEventAttributes attributes )
{
MutableAttributeSet atts = config.getAttributeSet( "figure.graphics" );
atts.addAttribute( Attribute.SRC.toString(), src );
// http://xmlgraphics.apache.org/fop/graphics.html#resolution
final String[] valids = new String[] {"content-height", "content-width", "height", "width"};
final MutableAttributeSet filtered = SinkUtils.filterAttributes( attributes, valids );
if ( filtered != null )
{
atts.addAttributes( filtered );
}
writeln( "<fo:external-graphic" + SinkUtils.getAttributeString( atts ) + "/>" );
}
/**
* Flags if we are inside a figure.
*
* @return True if we are between {@link #figure()} and {@link #figure_()} calls.
*/
protected boolean isFigure()
{
return this.inFigure;
}
/** {@inheritDoc} */
public void figureCaption( SinkEventAttributes attributes )
{
writeStartTag( BLOCK_TAG, "figure.caption" );
}
/** {@inheritDoc} */
public void figureCaption()
{
figureCaption( null );
}
/** {@inheritDoc} */
public void figureCaption_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void paragraph()
{
paragraph( null );
}
/** {@inheritDoc} */
public void paragraph( SinkEventAttributes attributes )
{
MutableAttributeSet atts = config.getAttributeSet( "normal.paragraph" );
if ( attributes != null && attributes.isDefined( SinkEventAttributes.ALIGN ) )
{
atts.addAttribute( "text-align", attributes.getAttribute( SinkEventAttributes.ALIGN ) );
}
writeEOL();
writeStartTag( BLOCK_TAG, atts );
}
/** {@inheritDoc} */
public void paragraph_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void verbatim( SinkEventAttributes attributes )
{
this.verbatim = true;
boolean boxed = false;
if ( attributes != null && attributes.isDefined( SinkEventAttributes.DECORATION ) )
{
boxed =
"boxed".equals( attributes.getAttribute( SinkEventAttributes.DECORATION ).toString() );
}
if ( boxed )
{
writeStartTag( BLOCK_TAG, "body.source" );
}
else
{
writeStartTag( BLOCK_TAG, "body.pre" );
}
}
/** {@inheritDoc} */
public void verbatim( boolean boxed )
{
verbatim( boxed ? SinkEventAttributeSet.BOXED : null );
}
/** {@inheritDoc} */
public void verbatim_()
{
this.verbatim = false;
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void horizontalRule( SinkEventAttributes attributes )
{
writeEOL();
writeEOL();
writeStartTag( BLOCK_TAG );
writeEmptyTag( LEADER_TAG, "body.rule" );
writeEndTag( BLOCK_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void horizontalRule()
{
horizontalRule( null );
}
/** {@inheritDoc} */
public void pageBreak()
{
writeEmptyTag( BLOCK_TAG, "break-before", "page" );
writeEOL();
}
/** {@inheritDoc} */
public void table( SinkEventAttributes attributes )
{
writeEOL();
writeStartTag( BLOCK_TAG, "table.padding" );
// <fo:table-and-caption> is XSL-FO 1.0 standard but still not implemented in FOP 0.95
//writeStartTag( TABLE_AND_CAPTION_TAG );
this.tableContentWriterStack.addLast( new StringWriter() );
writeStartTag( TABLE_TAG, "table.layout" );
}
/** {@inheritDoc} */
public void table()
{
table( null );
}
/** {@inheritDoc} */
public void table_()
{
String content = this.tableContentWriterStack.removeLast().toString();
StringBuilder sb = new StringBuilder();
int cellCount = Integer.parseInt( this.cellCountStack.removeLast().toString() );
for ( int i = 0; i < cellCount; i++ )
{
sb.append( "<fo:table-column column-width=\"proportional-column-width(1)\"/>" );
sb.append( EOL );
}
int index = content.indexOf( ">" ) + 1;
writeln( content.substring( 0, index ) );
write( sb.toString() );
write( content.substring( index ) );
writeEndTag( TABLE_TAG );
writeEOL();
// <fo:table-and-caption> is XSL-FO 1.0 standard but still not implemented in FOP 0.95
//writeEndTag( TABLE_AND_CAPTION_TAG );
writeEndTag( BLOCK_TAG );
writeEOL();
if ( !this.tableCaptionStack.isEmpty() && this.tableCaptionStack.getLast() != null )
{
paragraph( SinkEventAttributeSet.CENTER );
write( this.tableCaptionStack.removeLast().toString() );
paragraph_();
}
}
/** {@inheritDoc} */
public void tableRows( int[] justification, boolean grid )
{
this.tableGridStack.addLast( Boolean.valueOf( grid ) );
this.cellJustifStack.addLast( justification );
this.isCellJustifStack.addLast( Boolean.valueOf( true ) );
this.cellCountStack.addLast( Integer.valueOf( 0 ) );
writeEOL();
writeStartTag( TABLE_BODY_TAG );
}
/** {@inheritDoc} */
public void tableRows_()
{
this.tableGridStack.removeLast();
this.cellJustifStack.removeLast();
this.isCellJustifStack.removeLast();
writeEndTag( TABLE_BODY_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void tableRow( SinkEventAttributes attributes )
{
// TODO spacer rows
writeStartTag( TABLE_ROW_TAG, "table.body.row" );
this.cellCountStack.removeLast();
this.cellCountStack.addLast( Integer.valueOf( 0 ) );
}
/** {@inheritDoc} */
public void tableRow()
{
tableRow( null );
}
/** {@inheritDoc} */
public void tableRow_()
{
writeEndTag( TABLE_ROW_TAG );
writeEOL();
}
/** {@inheritDoc} */
public void tableCell( SinkEventAttributes attributes )
{
tableCell( false, attributes );
}
/** {@inheritDoc} */
public void tableCell()
{
tableCell( (SinkEventAttributes) null );
}
/** {@inheritDoc} */
public void tableCell( String width )
{
// TODO: fop can't handle cell width
tableCell();
}
/** {@inheritDoc} */
public void tableHeaderCell( SinkEventAttributes attributes )
{
tableCell( true, attributes );
}
/** {@inheritDoc} */
public void tableHeaderCell()
{
tableHeaderCell( (SinkEventAttributes) null );
}
/** {@inheritDoc} */
public void tableHeaderCell( String width )
{
// TODO: fop can't handle cell width
tableHeaderCell();
}
/**
* Writes a table cell.
*
* @param headerRow true if this is a header cell.
* @param attributes the cell attributes, could be null.
*/
private void tableCell( boolean headerRow, SinkEventAttributes attributes )
{
MutableAttributeSet cellAtts = headerRow
? config.getAttributeSet( "table.heading.cell" )
: config.getAttributeSet( "table.body.cell" );
// the column-number is needed for the hack to center the table, see tableRows.
int cellCount = Integer.parseInt( this.cellCountStack.getLast().toString() );
cellAtts.addAttribute( "column-number", String.valueOf( cellCount + 1 ) );
if ( this.tableGridStack.getLast().equals( Boolean.TRUE ) )
{
cellAtts.addAttributes( config.getAttributeSet( "table.body.cell.grid" ) );
}
MutableAttributeSet blockAtts = headerRow
? config.getAttributeSet( "table.heading.block" )
: config.getAttributeSet( "table.body.block" );
String justif = null;
if ( attributes == null )
{
attributes = new SinkEventAttributeSet( 0 );
}
if ( attributes.isDefined( Attribute.ALIGN.toString() ) )
{
justif = attributes.getAttribute( Attribute.ALIGN.toString() ).toString();
}
int[] cellJustif = this.cellJustifStack.getLast();
if ( justif == null && cellJustif != null && cellJustif.length > 0
&& this.isCellJustifStack.getLast().equals( Boolean.TRUE ) )
{
switch ( cellJustif[Math.min( cellCount, cellJustif.length - 1 )] )
{
case JUSTIFY_LEFT:
justif = "left";
break;
case JUSTIFY_RIGHT:
justif = "right";
break;
case JUSTIFY_CENTER:
default:
justif = "center";
}
}
if ( justif != null )
{
blockAtts.addAttribute( "text-align", justif );
}
writeStartTag( TABLE_CELL_TAG, cellAtts );
writeEOL();
writeStartTag( BLOCK_TAG, blockAtts );
writeEOL();
}
/** {@inheritDoc} */
public void tableCell_()
{
writeEndTag( BLOCK_TAG );
writeEOL();
writeEndTag( TABLE_CELL_TAG );
writeEOL();
if ( this.isCellJustifStack.getLast().equals( Boolean.TRUE ) )
{
int cellCount = Integer.parseInt( this.cellCountStack.removeLast().toString() );
this.cellCountStack.addLast( Integer.valueOf( ++cellCount ) );
}
}
/** {@inheritDoc} */
public void tableHeaderCell_()
{
tableCell_();
}
/** {@inheritDoc} */
public void tableCaption( SinkEventAttributes attributes )
{
StringWriter sw = new StringWriter();
this.tableCaptionWriterStack.addLast( sw );
this.tableCaptionXMLWriterStack.addLast( new PrettyPrintXMLWriter( sw ) );
// <fo:table-caption> is XSL-FO 1.0 standard but not implemented in FOP 0.95
//writeStartTag( TABLE_CAPTION_TAG );
// TODO: how to implement this otherwise?
// table-footer doesn't work because it has to be declared before table-body.
}
/** {@inheritDoc} */
public void tableCaption()
{
tableCaption( null );
}
/** {@inheritDoc} */
public void tableCaption_()
{
if ( !this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null )
{
this.tableCaptionStack.addLast( this.tableCaptionWriterStack.removeLast().toString() );
this.tableCaptionXMLWriterStack.removeLast();
}
// <fo:table-caption> is XSL-FO 1.0 standard but not implemented in FOP 0.95
//writeEndTag( TABLE_CAPTION_TAG );
}
/** {@inheritDoc} */
public void anchor( String name, SinkEventAttributes attributes )
{
if ( name == null )
{
throw new NullPointerException( "Anchor name cannot be null!" );
}
String anchor = name;
if ( !DoxiaUtils.isValidId( anchor ) )
{
anchor = DoxiaUtils.encodeId( name, true );
String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'";
logMessage( "modifiedLink", msg );
}
anchor = "#" + name;
writeStartTag( INLINE_TAG, "id", anchor );
}
/** {@inheritDoc} */
public void anchor( String name )
{
anchor( name, null );
}
/** {@inheritDoc} */
public void anchor_()
{
writeEndTag( INLINE_TAG );
}
/** {@inheritDoc} */
public void link( String name, SinkEventAttributes attributes )
{
if ( name == null )
{
throw new NullPointerException( "Link name cannot be null!" );
}
if ( DoxiaUtils.isExternalLink( name ) )
{
writeStartTag( BASIC_LINK_TAG, "external-destination", HtmlTools.escapeHTML( name ) );
writeStartTag( INLINE_TAG, "href.external" );
}
else if ( DoxiaUtils.isInternalLink( name ) )
{
String anchor = name.substring( 1 );
if ( !DoxiaUtils.isValidId( anchor ) )
{
anchor = DoxiaUtils.encodeId( anchor, true );
String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'";
logMessage( "modifiedLink", msg );
}
anchor = "#" + anchor;
writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) );
writeStartTag( INLINE_TAG, "href.internal" );
}
else
{
// treat everything else as is
String anchor = name;
writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) );
writeStartTag( INLINE_TAG, "href.internal" );
}
}
/** {@inheritDoc} */
public void link( String name )
{
link( name, null );
}
/** {@inheritDoc} */
public void link_()
{
writeEndTag( INLINE_TAG );
writeEndTag( BASIC_LINK_TAG );
}
/** {@inheritDoc} */
public void inline()
{
inline( null );
}
/** {@inheritDoc} */
public void inline( SinkEventAttributes attributes )
{
List<Tag> tags = new ArrayList<Tag>();
if ( attributes != null )
{
if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "italic" ) )
{
writeStartTag( INLINE_TAG, "italic" );
tags.add( 0, INLINE_TAG );
}
if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "bold" ) )
{
writeStartTag( INLINE_TAG, "bold" );
tags.add( 0, INLINE_TAG );
}
if ( attributes.containsAttribute( SinkEventAttributes.SEMANTICS, "code" ) )
{
writeStartTag( INLINE_TAG, "monospace" );
tags.add( 0, INLINE_TAG );
}
}
inlineStack.push( tags );
}
/** {@inheritDoc} */
public void inline_()
{
for ( Tag tag: inlineStack.pop() )
{
writeEndTag( tag );
}
}
/** {@inheritDoc} */
public void italic()
{
inline( SinkEventAttributeSet.Semantics.ITALIC );
}
/** {@inheritDoc} */
public void italic_()
{
inline_();
}
/** {@inheritDoc} */
public void bold()
{
inline( SinkEventAttributeSet.Semantics.BOLD );
}
/** {@inheritDoc} */
public void bold_()
{
inline_();
}
/** {@inheritDoc} */
public void monospaced()
{
inline( SinkEventAttributeSet.Semantics.CODE );
}
/** {@inheritDoc} */
public void monospaced_()
{
inline_();
}
/** {@inheritDoc} */
public void lineBreak( SinkEventAttributes attributes )
{
writeEOL();
writeEOL();
writeSimpleTag( BLOCK_TAG );
}
/** {@inheritDoc} */
public void lineBreak()
{
lineBreak( null );
}
/** {@inheritDoc} */
public void nonBreakingSpace()
{
write( "&#160;" );
}
/** {@inheritDoc} */
public void text( String text, SinkEventAttributes attributes )
{
content( text );
}
/** {@inheritDoc} */
public void text( String text )
{
text( text, null );
}
/** {@inheritDoc} */
public void rawText( String text )
{
write( text );
}
/** {@inheritDoc} */
public void flush()
{
out.flush();
}
/** {@inheritDoc} */
public void close()
{
out.close();
if ( getLog().isWarnEnabled() && this.warnMessages != null )
{
for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
{
for ( String msg : entry.getValue() )
{
getLog().warn( msg );
}
}
this.warnMessages = null;
}
init();
}
/**
* {@inheritDoc}
*
* Unkown events just log a warning message but are ignored otherwise.
* @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
*/
public void unknown( String name, Object[] requiredParams, SinkEventAttributes attributes )
{
String msg = "Unknown Sink event: '" + name + "', ignoring!";
logMessage( "unknownEvent", msg );
}
/** {@inheritDoc} */
public void comment( String comment )
{
if ( comment != null )
{
final String originalComment = comment;
// http://www.w3.org/TR/2000/REC-xml-20001006#sec-comments
while ( comment.contains( "--" ) )
{
comment = comment.replace( "--", "- -" );
}
if ( comment.endsWith( "-" ) )
{
comment += " ";
}
if ( !originalComment.equals( comment ) )
{
String msg = "Modified invalid comment: '" + originalComment + "' to '" + comment + "'";
logMessage( "modifyComment", msg );
}
final StringBuilder buffer = new StringBuilder( comment.length() + 7 );
buffer.append( LESS_THAN ).append( BANG ).append( MINUS ).append( MINUS );
buffer.append( comment );
buffer.append( MINUS ).append( MINUS ).append( GREATER_THAN );
write( buffer.toString() );
}
}
/**
* Writes the beginning of a FO document.
*/
public void beginDocument()
{
write( "<?xml version=\"1.0\"" );
if ( encoding != null )
{
write( " encoding=\"" + encoding + "\"" );
}
write( "?>" );
writeEOL();
MutableAttributeSet atts = new SinkEventAttributeSet();
atts.addAttribute( "xmlns:" + getNameSpace(), FO_NAMESPACE );
if ( languageId != null )
{
atts.addAttribute( "language", languageId );
}
writeStartTag( ROOT_TAG, atts );
writeStartTag( LAYOUT_MASTER_SET_TAG );
writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.cover-page" );
writeEmptyTag( REGION_BODY_TAG, "layout.master.set.cover-page.region-body" );
writeEndTag( SIMPLE_PAGE_MASTER_TAG );
writeEOL();
writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.toc" );
writeEmptyTag( REGION_BODY_TAG, "layout.master.set.toc.region-body" );
writeEmptyTag( REGION_BEFORE_TAG, "layout.master.set.toc.region-before" );
writeEmptyTag( REGION_AFTER_TAG, "layout.master.set.toc.region-after" );
writeEndTag( SIMPLE_PAGE_MASTER_TAG );
writeEOL();
writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.body" );
writeEmptyTag( REGION_BODY_TAG, "layout.master.set.body.region-body" );
writeEmptyTag( REGION_BEFORE_TAG, "layout.master.set.body.region-before" );
writeEmptyTag( REGION_AFTER_TAG, "layout.master.set.body.region-after" );
writeEndTag( SIMPLE_PAGE_MASTER_TAG );
writeEOL();
writeEndTag( LAYOUT_MASTER_SET_TAG );
writeEOL();
pdfBookmarks();
}
/**
* Writes the end of a FO document, flushes and closes the stream.
*/
public void endDocument()
{
writeEndTag( ROOT_TAG );
writeEOL();
flush();
close();
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
/**
* Returns the configuration object of this sink.
*
* @return The configuration object of this sink.
*/
protected FoConfiguration getFoConfiguration()
{
return config;
}
/**
* Writes a start tag, prepending EOL.
*
* @param tag The tag.
* @param attributeId An id identifying the attribute set.
*/
protected void writeStartTag( Tag tag, String attributeId )
{
writeEOL();
writeStartTag( tag, config.getAttributeSet( attributeId ) );
}
/**
* Writes a start tag, prepending EOL.
*
* @param tag The tag.
* @param id An id to add.
* @param name The name (value) of the id.
*/
protected void writeStartTag( Tag tag, String id, String name )
{
writeEOL();
MutableAttributeSet att = new SinkEventAttributeSet( new String[] {id, name} );
writeStartTag( tag, att );
}
/**
* Writes a start tag, prepending EOL.
*
* @param tag The tag.
* @param id An id to add.
* @param name The name (value) of the id.
* @param attributeId An id identifying the attribute set.
*/
protected void writeStartTag( Tag tag, String id, String name, String attributeId )
{
MutableAttributeSet att = config.getAttributeSet( attributeId );
// make sure we don't add it twice
if ( att.isDefined( id ) )
{
att.removeAttribute( id );
}
att.addAttribute( id, name );
writeEOL();
writeStartTag( tag, att );
}
/**
* Writes an empty tag, prepending EOL.
*
* @param tag The tag.
* @param id An id to add.
* @param name The name (value) of the id.
*/
protected void writeEmptyTag( Tag tag, String id, String name )
{
MutableAttributeSet att = new SinkEventAttributeSet( new String[] {id, name} );
writeEOL();
writeSimpleTag( tag, att );
}
/**
* Writes a simple tag, appending EOL.
*
* @param tag The tag name.
* @param attributeId An id identifying the attribute set.
*/
protected void writeEmptyTag( Tag tag, String attributeId )
{
writeEOL();
writeSimpleTag( tag, config.getAttributeSet( attributeId ) );
}
/**
* {@inheritDoc}
*
* Writes a text, swallowing any exceptions.
*/
protected void write( String text )
{
if ( !this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null )
{
this.tableCaptionXMLWriterStack.getLast().writeText( unifyEOLs( text ) );
}
else if ( !this.tableContentWriterStack.isEmpty() && this.tableContentWriterStack.getLast() != null )
{
this.tableContentWriterStack.getLast().write( unifyEOLs( text ) );
}
else
{
out.write( unifyEOLs( text ) );
}
}
/**
* Writes a text, appending EOL.
*
* @param text The text to write.
*/
protected void writeln( String text )
{
write( text );
writeEOL();
}
/**
* Writes content, escaping special characters.
*
* @param text The text to write.
*/
protected void content( String text )
{
write( escaped( text, verbatim ) );
}
/**
* Escapes special characters so that the text can be included in a fo file.
*
* @param text The text to process.
* @param verb In verbatim mode, white space and newlines are escaped.
* @return The text with special characters escaped.
*/
public static String escaped( String text, boolean verb )
{
int length = text.length();
StringBuilder buffer = new StringBuilder( length );
for ( int i = 0; i < length; ++i )
{
char c = text.charAt( i );
switch ( c )
{
case ' ':
if ( verb )
{
buffer.append( "&#160;" );
}
else
{
buffer.append( c );
}
break;
case '<':
buffer.append( "&lt;" );
break;
case '>':
buffer.append( "&gt;" );
break;
case '&':
buffer.append( "&amp;" );
break;
case '\n':
buffer.append( EOL );
if ( verb )
{
buffer.append( "<fo:block/>" + EOL );
}
break;
default:
if ( needsSymbolFont( c ) )
{
// TODO: make font configurable?
buffer.append( "<fo:inline font-family=\"Symbol\">" ).append( c ).append( "</fo:inline>" );
}
else
{
buffer.append( c );
}
}
}
return buffer.toString();
}
/** {@inheritDoc} */
protected void writeStartTag( Tag t, MutableAttributeSet att, boolean isSimpleTag )
{
if ( this.tableCaptionXMLWriterStack.isEmpty() )
{
super.writeStartTag ( t, att, isSimpleTag );
}
else
{
String tag = ( getNameSpace() != null ? getNameSpace() + ":" : "" ) + t.toString();
this.tableCaptionXMLWriterStack.getLast().startElement( tag );
if ( att != null )
{
Enumeration<?> names = att.getAttributeNames();
while ( names.hasMoreElements() )
{
Object key = names.nextElement();
Object value = att.getAttribute( key );
this.tableCaptionXMLWriterStack.getLast().addAttribute( key.toString(), value.toString() );
}
}
if ( isSimpleTag )
{
this.tableCaptionXMLWriterStack.getLast().endElement();
}
}
}
/** {@inheritDoc} */
protected void writeEndTag( Tag t )
{
if ( this.tableCaptionXMLWriterStack.isEmpty() )
{
super.writeEndTag( t );
}
else
{
this.tableCaptionXMLWriterStack.getLast().endElement();
}
}
private static final char UPPER_ALPHA = 0x391;
private static final char PIV = 0x3d6;
private static final char OLINE = 0x203e;
private static final char DIAMS = 0x2666;
private static final char EURO = 0x20ac;
private static final char TRADE = 0x2122;
private static final char PRIME = 0x2032;
private static final char PPRIME = 0x2033;
private static boolean needsSymbolFont( char c )
{
// greek characters and mathematical symbols, except the euro and trade symbols
// symbols I couldn't get to display in any font:
// zwnj (0x200C), zwj (0x200D), lrm (0x200E), rlm (0x200F), oline (0x203E),
// lceil (0x2038), rceil (0x2039), lfloor (0x203A), rfloor (0x203B)
return ( c >= UPPER_ALPHA && c <= PIV )
|| ( c == PRIME || c == PPRIME )
|| ( c >= OLINE && c <= DIAMS && c != EURO && c != TRADE );
}
/**
* Starts a page sequence.
*
* @param initPageNumber The initial page number. Should be either "0" (for the first page) or "auto".
* @param headerText The text to write in the header, if null, nothing is written.
* @param footerText The text to write in the footer, if null, nothing is written.
*/
protected void startPageSequence( String initPageNumber, String headerText, String footerText )
{
writeln( "<fo:page-sequence initial-page-number=\"" + initPageNumber + "\" master-reference=\"body\">" );
regionBefore( headerText );
regionAfter( footerText );
writeln( "<fo:flow flow-name=\"xsl-region-body\">" );
chapterHeading( null, true );
}
/**
* Writes a 'xsl-region-before' block.
*
* @param headerText The text to write in the header, if null, nothing is written.
*/
protected void regionBefore( String headerText )
{
// do nothing, overridden by AggregateSink
}
/**
* Writes a 'xsl-region-after' block. By default does nothing, gets overridden by AggregateSink.
*
* @param footerText The text to write in the footer, if null, nothing is written.
*/
protected void regionAfter( String footerText )
{
// do nothing, overridden by AggregateSink
}
/**
* Writes a chapter heading. By default does nothing, gets overridden by AggregateSink.
*
* @param headerText The text to write in the header, if null, the current document title is written.
* @param chapterNumber True if the chapter number should be written in front of the text.
*/
protected void chapterHeading( String headerText, boolean chapterNumber )
{
// do nothing, overridden by AggregateSink
}
/**
* Writes a fo:bookmark-tree. By default does nothing, gets overridden by AggregateSink.
*/
protected void pdfBookmarks()
{
// do nothing, overridden by AggregateSink
}
/**
* 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 #close()
* @since 1.1.1
*/
protected void logMessage( String key, String msg )
{
msg = "[FO Sink] " + msg;
if ( getLog().isDebugEnabled() )
{
getLog().debug( msg );
return;
}
if ( warnMessages == null )
{
warnMessages = new HashMap<String, Set<String>>();
}
Set<String> set = warnMessages.get( key );
if ( set == null )
{
set = new TreeSet<String>();
}
set.add( msg );
warnMessages.put( key, set );
}
/** {@inheritDoc} */
protected void init()
{
super.init();
this.listStack.clear();
this.tableGridStack.clear();
this.cellJustifStack.clear();
this.isCellJustifStack.clear();
this.cellCountStack.clear();
this.tableContentWriterStack.clear();
this.tableCaptionWriterStack.clear();
this.tableCaptionXMLWriterStack.clear();
this.tableCaptionStack.clear();
this.section = 0;
this.subsection = 0;
this.subsubsection = 0;
this.verbatim = false;
this.inFigure = false;
this.warnMessages = null;
}
}