View Javadoc
1   package org.apache.maven.doxia.module.fo;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.io.StringWriter;
26  import java.io.Writer;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.LinkedList;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.Stack;
33  import java.util.TreeSet;
34  
35  import javax.swing.text.MutableAttributeSet;
36  import javax.swing.text.html.HTML.Attribute;
37  import javax.swing.text.html.HTML.Tag;
38  
39  import org.apache.maven.doxia.sink.SinkEventAttributes;
40  import org.apache.maven.doxia.sink.impl.AbstractXmlSink;
41  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
42  import org.apache.maven.doxia.sink.impl.SinkUtils;
43  import org.apache.maven.doxia.util.DoxiaUtils;
44  import org.apache.maven.doxia.util.HtmlTools;
45  
46  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
47  
48  /**
49   * A Doxia Sink that produces a FO model. The usage is similar to the following:
50   *
51   * <pre>
52   * FoSink sink = new FoSink( writer );
53   * sink.beginDocument();
54   * ...
55   * sink.endDocument();
56   * </pre>
57   *
58   * @author ltheussl
59   * @version $Id: FoSink.java 1726411 2016-01-23 16:34:09Z hboutemy $
60   * @since 1.1
61   */
62  public class FoSink
63      extends AbstractXmlSink
64      implements FoMarkup
65  {
66      /** For writing the result. */
67      private final PrintWriter out;
68  
69      /** Used to get the current position in numbered lists. */
70      private final Stack<NumberedListItem> listStack;
71  
72      /** Used to get attributes for a given FO element. */
73      private final FoConfiguration config;
74  
75      /** Counts the current section level. */
76      private int section = 0;
77  
78      /** Counts the current subsection level. */
79      private int subsection = 0;
80  
81      /** Counts the current subsubsection level. */
82      private int subsubsection = 0;
83  
84      /** Verbatim flag. */
85      private boolean verbatim;
86  
87      /** figure flag. */
88      private boolean inFigure;
89  
90      private final String encoding;
91  
92      private final String languageId;
93  
94      /** Stack of drawing borders on table cells. */
95      private final LinkedList<Boolean> tableGridStack;
96  
97      /** Stack of alignment int[] of table cells. */
98      private final LinkedList<int[]> cellJustifStack;
99  
100     /** Stack of justification of table cells. */
101     private final LinkedList<Boolean> isCellJustifStack;
102 
103     /** Stack of current table cell. */
104     private final LinkedList<Integer> cellCountStack;
105 
106     /** The stack of StringWriter to write the table result temporary, so we could play with the output and fix fo. */
107     private final LinkedList<StringWriter> tableContentWriterStack;
108 
109     private final LinkedList<StringWriter> tableCaptionWriterStack;
110 
111     private final LinkedList<PrettyPrintXMLWriter> tableCaptionXMLWriterStack;
112 
113     /** The stack of table caption */
114     private final LinkedList<String> tableCaptionStack;
115 
116     /** Map of warn messages with a String as key to describe the error type and a Set as value.
117      * Using to reduce warn messages. */
118     protected Map<String, Set<String>> warnMessages;
119 
120     /**
121      * Constructor, initialize the Writer.
122      *
123      * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
124      * You could use <code>newXmlWriter</code> methods from {@link org.codehaus.plexus.util.WriterFactory}.
125      */
126     protected FoSink( Writer writer )
127     {
128         this( writer, "UTF-8" );
129     }
130 
131     /**
132      * Constructor, initialize the Writer and tells which encoding is used.
133      *
134      * @param writer not null writer to write the result.
135      * @param encoding the encoding used, that should be written to the generated HTML content
136      * if not <code>null</code>.
137      */
138     protected FoSink( Writer writer, String encoding )
139     {
140         this( writer, encoding, null );
141     }
142 
143     /**
144      * Constructor, initialize the Writer and tells which encoding and languageId are used.
145      *
146      * @param writer not null writer to write the result.
147      * @param encoding the encoding used, that should be written to the generated HTML content
148      * if not <code>null</code>.
149      * @param languageId language identifier for the root element as defined by
150      * <a href="ftp://ftp.isi.edu/in-notes/bcp/bcp47.txt">IETF BCP 47</a>, Tags for the Identification of Languages;
151      * in addition, the empty string may be specified.
152      */
153     protected FoSink( Writer writer, String encoding, String languageId )
154     {
155         if ( writer == null )
156         {
157             throw new NullPointerException( "Null writer in FO Sink!" );
158         }
159 
160         this.out = new PrintWriter( writer );
161         this.encoding = encoding;
162         this.languageId = languageId;
163         this.config = new FoConfiguration();
164 
165         this.listStack = new Stack<NumberedListItem>();
166         this.tableGridStack = new LinkedList<Boolean>();
167         this.cellJustifStack = new LinkedList<int[]>();
168         this.isCellJustifStack = new LinkedList<Boolean>();
169         this.cellCountStack = new LinkedList<Integer>();
170         this.tableContentWriterStack = new LinkedList<StringWriter>();
171         this.tableCaptionWriterStack = new LinkedList<StringWriter>();
172         this.tableCaptionXMLWriterStack = new LinkedList<PrettyPrintXMLWriter>();
173         this.tableCaptionStack = new LinkedList<String>();
174 
175         setNameSpace( "fo" );
176     }
177 
178     // TODO add FOP compliance mode?
179 
180     /**
181      * Load configuration parameters from a File.
182      *
183      * @param configFile the configuration file.
184      *
185      * @throws java.io.IOException if the File cannot be read
186      *  or some error occurs when initializing the configuration parameters.
187      *
188      * @since 1.1.1
189      */
190     public void load( File configFile )
191             throws IOException
192     {
193         config.load( configFile );
194     }
195 
196     /** {@inheritDoc} */
197     public void head( SinkEventAttributes attributes )
198     {
199         init();
200 
201         startPageSequence( "0", null, null );
202     }
203 
204     /** {@inheritDoc} */
205     public void head()
206     {
207         head( null );
208     }
209 
210     /** {@inheritDoc} */
211     public void head_()
212     {
213         writeEOL();
214     }
215 
216     /** {@inheritDoc} */
217     public void title( SinkEventAttributes attributes )
218     {
219         writeStartTag( BLOCK_TAG, "doc.header.title" );
220     }
221 
222     /** {@inheritDoc} */
223     public void title()
224     {
225         title( null );
226     }
227 
228     /** {@inheritDoc} */
229     public void title_()
230     {
231         writeEndTag( BLOCK_TAG );
232         writeEOL();
233     }
234 
235     /** {@inheritDoc} */
236     public void author( SinkEventAttributes attributes )
237     {
238         writeStartTag( BLOCK_TAG, "doc.header.author" );
239     }
240 
241     /** {@inheritDoc} */
242     public void author()
243     {
244         author( null );
245     }
246 
247     /** {@inheritDoc} */
248     public void author_()
249     {
250         writeEndTag( BLOCK_TAG );
251         writeEOL();
252     }
253 
254     /** {@inheritDoc} */
255     public void date( SinkEventAttributes attributes )
256     {
257         writeStartTag( BLOCK_TAG, "doc.header.date" );
258     }
259 
260     /** {@inheritDoc} */
261     public void date()
262     {
263         date( null );
264     }
265 
266     /** {@inheritDoc} */
267     public void date_()
268     {
269         writeEndTag( BLOCK_TAG );
270         writeEOL();
271     }
272 
273     /** {@inheritDoc} */
274     public void body( SinkEventAttributes attributes )
275     {
276         // noop
277     }
278 
279     /** {@inheritDoc} */
280     public void body()
281     {
282         body( null );
283     }
284 
285     /** {@inheritDoc} */
286     public void body_()
287     {
288         writeEOL();
289         writeEndTag( FLOW_TAG );
290         writeEOL();
291         writeEndTag( PAGE_SEQUENCE_TAG );
292         writeEOL();
293         endDocument();
294     }
295 
296     // -----------------------------------------------------------------------
297     //
298     // -----------------------------------------------------------------------
299 
300     /** {@inheritDoc} */
301     public void sectionTitle()
302     {
303         // nop
304     }
305 
306     /** {@inheritDoc} */
307     public void sectionTitle_()
308     {
309         // nop
310     }
311 
312     /** {@inheritDoc} */
313     public void section( int level, SinkEventAttributes attributes )
314     {
315         if ( level == SECTION_LEVEL_1 )
316         {
317             section++;
318             subsection = 0;
319             subsubsection = 0;
320         }
321         else if ( level == SECTION_LEVEL_2 )
322         {
323             subsection++;
324             subsubsection = 0;
325         }
326         else if ( level == SECTION_LEVEL_3 )
327         {
328             subsubsection++;
329         }
330 
331         onSection();
332     }
333 
334     /** {@inheritDoc} */
335     public void section_( int level )
336     {
337         onSection_();
338     }
339 
340     /** {@inheritDoc} */
341     public void sectionTitle( int level, SinkEventAttributes attributes )
342     {
343         onSectionTitle( level );
344     }
345 
346     /** {@inheritDoc} */
347     public void sectionTitle_( int level )
348     {
349         onSectionTitle_();
350     }
351 
352     /** {@inheritDoc} */
353     public void section1()
354     {
355         section( SECTION_LEVEL_1, null );
356     }
357 
358     /** {@inheritDoc} */
359     public void sectionTitle1()
360     {
361         sectionTitle( SECTION_LEVEL_1, null );
362     }
363 
364     /** {@inheritDoc} */
365     public void sectionTitle1_()
366     {
367         sectionTitle_( SECTION_LEVEL_1 );
368     }
369 
370     /** {@inheritDoc} */
371     public void section1_()
372     {
373         section_( SECTION_LEVEL_1 );
374     }
375 
376     /** {@inheritDoc} */
377     public void section2()
378     {
379         section( SECTION_LEVEL_2, null );
380     }
381 
382     /** {@inheritDoc} */
383     public void sectionTitle2()
384     {
385         sectionTitle( SECTION_LEVEL_2, null );
386     }
387 
388     /** {@inheritDoc} */
389     public void sectionTitle2_()
390     {
391         sectionTitle_( SECTION_LEVEL_2 );
392     }
393 
394     /** {@inheritDoc} */
395     public void section2_()
396     {
397         section_( SECTION_LEVEL_2 );
398     }
399 
400     /** {@inheritDoc} */
401     public void section3()
402     {
403         section( SECTION_LEVEL_3, null );
404     }
405 
406     /** {@inheritDoc} */
407     public void sectionTitle3()
408     {
409         sectionTitle( SECTION_LEVEL_3, null );
410     }
411 
412     /** {@inheritDoc} */
413     public void sectionTitle3_()
414     {
415         sectionTitle_( SECTION_LEVEL_3 );
416     }
417 
418     /** {@inheritDoc} */
419     public void section3_()
420     {
421         section_( SECTION_LEVEL_3 );
422     }
423 
424     /** {@inheritDoc} */
425     public void section4()
426     {
427         section( SECTION_LEVEL_4, null );
428     }
429 
430     /** {@inheritDoc} */
431     public void sectionTitle4()
432     {
433         sectionTitle( SECTION_LEVEL_4, null );
434     }
435 
436     /** {@inheritDoc} */
437     public void sectionTitle4_()
438     {
439         sectionTitle_( SECTION_LEVEL_4 );
440     }
441 
442     /** {@inheritDoc} */
443     public void section4_()
444     {
445         section_( SECTION_LEVEL_4 );
446     }
447 
448     /** {@inheritDoc} */
449     public void section5()
450     {
451         section( SECTION_LEVEL_5, null );
452     }
453 
454     /** {@inheritDoc} */
455     public void sectionTitle5()
456     {
457         sectionTitle( SECTION_LEVEL_5, null );
458     }
459 
460     /** {@inheritDoc} */
461     public void sectionTitle5_()
462     {
463         sectionTitle_( SECTION_LEVEL_5 );
464     }
465 
466     /** {@inheritDoc} */
467     public void section5_()
468     {
469         section_( SECTION_LEVEL_5 );
470     }
471 
472     /** Starts a section/subsection. */
473     private void onSection()
474     {
475         writeEOL();
476         writeStartTag( BLOCK_TAG, "body.text" );
477     }
478 
479     /**
480      * Starts a section title.
481      *
482      * @param depth The section level.
483      */
484     private void onSectionTitle( int depth )
485     {
486         StringBuilder title = new StringBuilder( 16 );
487 
488         title.append( getChapterString() );
489 
490         writeEOL();
491         if ( depth == SECTION_LEVEL_1 )
492         {
493             writeStartTag( BLOCK_TAG, "body.h1" );
494             title.append( section ).append( "   " );
495         }
496         else if ( depth == SECTION_LEVEL_2 )
497         {
498             writeStartTag( BLOCK_TAG, "body.h2" );
499             title.append( section ).append( "." );
500             title.append( subsection ).append( "   " );
501         }
502         else if ( depth == SECTION_LEVEL_3 )
503         {
504             writeStartTag( BLOCK_TAG, "body.h3" );
505             title.append( section ).append( "." );
506             title.append( subsection ).append( "." );
507             title.append( subsubsection ).append( "   " );
508         }
509         else if ( depth == SECTION_LEVEL_4 )
510         {
511             writeStartTag( BLOCK_TAG, "body.h4" );
512         }
513         else
514         {
515             writeStartTag( BLOCK_TAG, "body.h5" );
516         }
517 
518         write( title.toString() );
519     }
520 
521     /** Ends a section title. */
522     private void onSectionTitle_()
523     {
524         writeEndTag( BLOCK_TAG );
525         writeEOL();
526     }
527 
528     /** Ends a section/subsection. */
529     private void onSection_()
530     {
531         writeEndTag( BLOCK_TAG );
532         writeEOL();
533     }
534 
535     /**
536      * Resets the section counter to 0.
537      * Only useful for overriding classes, like AggregateSink, the FoSink puts everything into one chapter.
538      */
539     protected void resetSectionCounter()
540     {
541         this.section = 0;
542     }
543 
544     /**
545      * Returns the current chapter number as a string.
546      * By default does nothing, gets overridden by AggregateSink.
547      *
548      * @return an empty String.
549      */
550     protected String getChapterString()
551     {
552         return "";
553     }
554 
555     // -----------------------------------------------------------------------
556     //
557     // -----------------------------------------------------------------------
558 
559     /** {@inheritDoc} */
560     public void list( SinkEventAttributes attributes )
561     {
562         writeEOL();
563         writeStartTag( LIST_BLOCK_TAG, "list" );
564     }
565 
566     /** {@inheritDoc} */
567     public void list()
568     {
569         list( null );
570     }
571 
572     /** {@inheritDoc} */
573     public void list_()
574     {
575         writeEndTag( LIST_BLOCK_TAG );
576         writeEOL();
577     }
578 
579     /** {@inheritDoc} */
580     public void listItem( SinkEventAttributes attributes )
581     {
582         writeStartTag( LIST_ITEM_TAG, "list.item" );
583         writeStartTag( LIST_ITEM_LABEL_TAG );
584         writeStartTag( BLOCK_TAG );
585         write( "&#8226;" ); // TODO customize?
586         writeEndTag( BLOCK_TAG );
587         writeEndTag( LIST_ITEM_LABEL_TAG );
588         writeEOL();
589         writeStartTag( LIST_ITEM_BODY_TAG, "list.item" );
590         writeEOL();
591         writeStartTag( BLOCK_TAG );
592     }
593 
594     /** {@inheritDoc} */
595     public void listItem()
596     {
597         listItem( null );
598     }
599 
600     /** {@inheritDoc} */
601     public void listItem_()
602     {
603         writeEndTag( BLOCK_TAG );
604         writeEOL();
605         writeEndTag( LIST_ITEM_BODY_TAG );
606         writeEOL();
607         writeEndTag( LIST_ITEM_TAG );
608         writeEOL();
609     }
610 
611     /** {@inheritDoc} */
612     public void numberedList( int numbering, SinkEventAttributes attributes )
613     {
614         this.listStack.push( new NumberedListItem( numbering ) );
615         writeEOL();
616         writeStartTag( LIST_BLOCK_TAG, "list" );
617     }
618 
619     /** {@inheritDoc} */
620     public void numberedList( int numbering )
621     {
622         numberedList( numbering, null );
623     }
624 
625     /** {@inheritDoc} */
626     public void numberedList_()
627     {
628         this.listStack.pop();
629         writeEndTag( LIST_BLOCK_TAG );
630         writeEOL();
631     }
632 
633     /** {@inheritDoc} */
634     public void numberedListItem( SinkEventAttributes attributes )
635     {
636         NumberedListItem current = this.listStack.peek();
637         current.next();
638 
639         writeStartTag( LIST_ITEM_TAG, "list.item" );
640 
641         writeEOL();
642         writeStartTag( LIST_ITEM_LABEL_TAG );
643         writeEOL();
644         writeStartTag( BLOCK_TAG );
645         write( current.getListItemSymbol() );
646         writeEndTag( BLOCK_TAG );
647         writeEOL();
648         writeEndTag( LIST_ITEM_LABEL_TAG );
649         writeEOL();
650 
651         writeStartTag( LIST_ITEM_BODY_TAG, "list.item" );
652         writeEOL();
653         writeStartTag( BLOCK_TAG );
654     }
655 
656     /** {@inheritDoc} */
657     public void numberedListItem()
658     {
659         numberedListItem( null );
660     }
661 
662     /** {@inheritDoc} */
663     public void numberedListItem_()
664     {
665         writeEndTag( BLOCK_TAG );
666         writeEOL();
667         writeEndTag( LIST_ITEM_BODY_TAG );
668         writeEOL();
669         writeEndTag( LIST_ITEM_TAG );
670         writeEOL();
671     }
672 
673     /** {@inheritDoc} */
674     public void definitionList( SinkEventAttributes attributes )
675     {
676         writeEOL();
677         writeStartTag( BLOCK_TAG, "dl" );
678     }
679 
680     /** {@inheritDoc} */
681     public void definitionList()
682     {
683         definitionList( null );
684     }
685 
686     /** {@inheritDoc} */
687     public void definitionList_()
688     {
689         writeEndTag( BLOCK_TAG );
690         writeEOL();
691     }
692 
693     /** {@inheritDoc} */
694     public void definitionListItem( SinkEventAttributes attributes )
695     {
696         // nop
697     }
698 
699     /** {@inheritDoc} */
700     public void definitionListItem()
701     {
702         definitionListItem( null );
703     }
704 
705     /** {@inheritDoc} */
706     public void definitionListItem_()
707     {
708         // nop
709     }
710 
711     /** {@inheritDoc} */
712     public void definedTerm( SinkEventAttributes attributes )
713     {
714         writeStartTag( BLOCK_TAG, "dt" );
715     }
716 
717     /** {@inheritDoc} */
718     public void definedTerm()
719     {
720         definedTerm( null );
721     }
722 
723     /** {@inheritDoc} */
724     public void definedTerm_()
725     {
726         writeEndTag( BLOCK_TAG );
727         writeEOL();
728     }
729 
730     /** {@inheritDoc} */
731     public void definition( SinkEventAttributes attributes )
732     {
733         writeEOL();
734         writeStartTag( BLOCK_TAG, "dd" );
735     }
736 
737     /** {@inheritDoc} */
738     public void definition()
739     {
740         definition( null );
741     }
742 
743     /** {@inheritDoc} */
744     public void definition_()
745     {
746         writeEndTag( BLOCK_TAG );
747         writeEOL();
748     }
749 
750     /** {@inheritDoc} */
751     public void figure( SinkEventAttributes attributes )
752     {
753         this.inFigure = true;
754         writeEOL();
755         writeStartTag( BLOCK_TAG, "figure.display" );
756     }
757 
758     /** {@inheritDoc} */
759     public void figure()
760     {
761         figure( null );
762     }
763 
764     /** {@inheritDoc} */
765     public void figure_()
766     {
767         this.inFigure = false;
768         writeEndTag( BLOCK_TAG );
769         writeEOL();
770     }
771 
772     /** {@inheritDoc} */
773     public void figureGraphics( String name )
774     {
775         figureGraphics( name, null );
776     }
777 
778     /** {@inheritDoc} */
779     public void figureGraphics( String src, SinkEventAttributes attributes )
780     {
781         MutableAttributeSet atts = config.getAttributeSet( "figure.graphics" );
782         atts.addAttribute( Attribute.SRC.toString(), src );
783 
784         // http://xmlgraphics.apache.org/fop/graphics.html#resolution
785 
786         final String[] valids = new String[] {"content-height", "content-width", "height", "width"};
787         final MutableAttributeSet filtered = SinkUtils.filterAttributes( attributes, valids );
788 
789         if ( filtered != null )
790         {
791             atts.addAttributes( filtered );
792         }
793 
794         writeln( "<fo:external-graphic" + SinkUtils.getAttributeString( atts ) + "/>" );
795     }
796 
797     /**
798      * Flags if we are inside a figure.
799      *
800      * @return True if we are between {@link #figure()} and {@link #figure_()} calls.
801      */
802     protected boolean isFigure()
803     {
804         return this.inFigure;
805     }
806 
807     /** {@inheritDoc} */
808     public void figureCaption( SinkEventAttributes attributes )
809     {
810         writeStartTag( BLOCK_TAG, "figure.caption" );
811     }
812 
813     /** {@inheritDoc} */
814     public void figureCaption()
815     {
816         figureCaption( null );
817     }
818 
819     /** {@inheritDoc} */
820     public void figureCaption_()
821     {
822         writeEndTag( BLOCK_TAG );
823         writeEOL();
824     }
825 
826     /** {@inheritDoc} */
827     public void paragraph()
828     {
829         paragraph( null );
830     }
831 
832     /** {@inheritDoc} */
833     public void paragraph( SinkEventAttributes attributes )
834     {
835         MutableAttributeSet atts = config.getAttributeSet( "normal.paragraph" );
836 
837         if ( attributes != null && attributes.isDefined( SinkEventAttributes.ALIGN ) )
838         {
839             atts.addAttribute( "text-align", attributes.getAttribute( SinkEventAttributes.ALIGN ) );
840         }
841 
842         writeEOL();
843         writeStartTag( BLOCK_TAG, atts );
844     }
845 
846     /** {@inheritDoc} */
847     public void paragraph_()
848     {
849         writeEndTag( BLOCK_TAG );
850         writeEOL();
851     }
852 
853     /** {@inheritDoc} */
854     public void verbatim( SinkEventAttributes attributes )
855     {
856         this.verbatim = true;
857 
858         boolean boxed = false;
859 
860         if ( attributes != null && attributes.isDefined( SinkEventAttributes.DECORATION ) )
861         {
862             boxed =
863                 "boxed".equals( attributes.getAttribute( SinkEventAttributes.DECORATION ).toString() );
864         }
865 
866         if ( boxed )
867         {
868             writeStartTag( BLOCK_TAG, "body.source" );
869         }
870         else
871         {
872             writeStartTag( BLOCK_TAG, "body.pre" );
873         }
874     }
875 
876     /** {@inheritDoc} */
877     public void verbatim( boolean boxed )
878     {
879         verbatim( boxed ? SinkEventAttributeSet.BOXED : null );
880     }
881 
882     /** {@inheritDoc} */
883     public void verbatim_()
884     {
885         this.verbatim = false;
886         writeEndTag( BLOCK_TAG );
887         writeEOL();
888     }
889 
890     /** {@inheritDoc} */
891     public void horizontalRule( SinkEventAttributes attributes )
892     {
893         writeEOL();
894         writeEOL();
895         writeStartTag( BLOCK_TAG );
896         writeEmptyTag( LEADER_TAG, "body.rule" );
897         writeEndTag( BLOCK_TAG );
898         writeEOL();
899     }
900 
901     /** {@inheritDoc} */
902     public void horizontalRule()
903     {
904         horizontalRule( null );
905     }
906 
907     /** {@inheritDoc} */
908     public void pageBreak()
909     {
910         writeEmptyTag( BLOCK_TAG, "break-before", "page" );
911         writeEOL();
912     }
913 
914     /** {@inheritDoc} */
915     public void table( SinkEventAttributes attributes )
916     {
917         writeEOL();
918         writeStartTag( BLOCK_TAG, "table.padding" );
919 
920         // <fo:table-and-caption> is XSL-FO 1.0 standard but still not implemented in FOP 0.95
921         //writeStartTag( TABLE_AND_CAPTION_TAG );
922 
923         this.tableContentWriterStack.addLast( new StringWriter() );
924         writeStartTag( TABLE_TAG, "table.layout" );
925     }
926 
927     /** {@inheritDoc} */
928     public void table()
929     {
930         table( null );
931     }
932 
933     /** {@inheritDoc} */
934     public void table_()
935     {
936         String content = this.tableContentWriterStack.removeLast().toString();
937 
938         StringBuilder sb = new StringBuilder();
939         int cellCount = Integer.parseInt( this.cellCountStack.removeLast().toString() );
940         for ( int i = 0; i < cellCount; i++ )
941         {
942             sb.append( "<fo:table-column column-width=\"proportional-column-width(1)\"/>" );
943             sb.append( EOL );
944         }
945 
946         int index = content.indexOf( ">" ) + 1;
947         writeln( content.substring( 0, index ) );
948         write( sb.toString() );
949         write( content.substring( index ) );
950 
951         writeEndTag( TABLE_TAG );
952         writeEOL();
953 
954         // <fo:table-and-caption> is XSL-FO 1.0 standard but still not implemented in FOP 0.95
955         //writeEndTag( TABLE_AND_CAPTION_TAG );
956 
957         writeEndTag( BLOCK_TAG );
958         writeEOL();
959 
960         if ( !this.tableCaptionStack.isEmpty() && this.tableCaptionStack.getLast() != null )
961         {
962             paragraph( SinkEventAttributeSet.CENTER );
963             write( this.tableCaptionStack.removeLast().toString() );
964             paragraph_();
965         }
966     }
967 
968     /** {@inheritDoc} */
969     public void tableRows( int[] justification, boolean grid )
970     {
971         this.tableGridStack.addLast( Boolean.valueOf( grid ) );
972         this.cellJustifStack.addLast( justification );
973         this.isCellJustifStack.addLast( Boolean.valueOf( true ) );
974         this.cellCountStack.addLast( Integer.valueOf( 0 ) );
975         writeEOL();
976         writeStartTag( TABLE_BODY_TAG );
977     }
978 
979     /** {@inheritDoc} */
980     public void tableRows_()
981     {
982         this.tableGridStack.removeLast();
983         this.cellJustifStack.removeLast();
984         this.isCellJustifStack.removeLast();
985         writeEndTag( TABLE_BODY_TAG );
986         writeEOL();
987     }
988 
989     /** {@inheritDoc} */
990     public void tableRow( SinkEventAttributes attributes )
991     {
992         // TODO spacer rows
993         writeStartTag( TABLE_ROW_TAG, "table.body.row" );
994         this.cellCountStack.removeLast();
995         this.cellCountStack.addLast( Integer.valueOf( 0 ) );
996     }
997 
998     /** {@inheritDoc} */
999     public void tableRow()
1000     {
1001         tableRow( null );
1002     }
1003 
1004     /** {@inheritDoc} */
1005     public void tableRow_()
1006     {
1007         writeEndTag( TABLE_ROW_TAG );
1008         writeEOL();
1009     }
1010 
1011     /** {@inheritDoc} */
1012     public void tableCell( SinkEventAttributes attributes )
1013     {
1014         tableCell( false, attributes );
1015     }
1016 
1017     /** {@inheritDoc} */
1018     public void tableCell()
1019     {
1020         tableCell( (SinkEventAttributes) null );
1021     }
1022 
1023     /** {@inheritDoc} */
1024     public void tableCell( String width )
1025     {
1026         // TODO: fop can't handle cell width
1027         tableCell();
1028     }
1029 
1030     /** {@inheritDoc} */
1031     public void tableHeaderCell( SinkEventAttributes attributes )
1032     {
1033         tableCell( true, attributes );
1034     }
1035 
1036     /** {@inheritDoc} */
1037     public void tableHeaderCell()
1038     {
1039         tableHeaderCell( (SinkEventAttributes) null );
1040     }
1041 
1042     /** {@inheritDoc} */
1043     public void tableHeaderCell( String width )
1044     {
1045         // TODO: fop can't handle cell width
1046         tableHeaderCell();
1047     }
1048 
1049     /**
1050      * Writes a table cell.
1051      *
1052      * @param headerRow true if this is a header cell.
1053      * @param attributes the cell attributes, could be null.
1054      */
1055     private void tableCell( boolean headerRow, SinkEventAttributes attributes )
1056     {
1057         MutableAttributeSet cellAtts = headerRow
1058                  ? config.getAttributeSet( "table.heading.cell" )
1059                  : config.getAttributeSet( "table.body.cell" );
1060 
1061         // the column-number is needed for the hack to center the table, see tableRows.
1062         int cellCount = Integer.parseInt( this.cellCountStack.getLast().toString() );
1063         cellAtts.addAttribute( "column-number", String.valueOf( cellCount + 1 ) );
1064 
1065         if ( this.tableGridStack.getLast().equals( Boolean.TRUE ) )
1066         {
1067             cellAtts.addAttributes( config.getAttributeSet( "table.body.cell.grid" ) );
1068         }
1069 
1070         MutableAttributeSet blockAtts = headerRow
1071                  ? config.getAttributeSet( "table.heading.block" )
1072                  : config.getAttributeSet( "table.body.block" );
1073 
1074         String justif = null;
1075         if ( attributes == null )
1076         {
1077             attributes = new SinkEventAttributeSet( 0 );
1078         }
1079 
1080         if ( attributes.isDefined( Attribute.ALIGN.toString() ) )
1081         {
1082             justif = attributes.getAttribute( Attribute.ALIGN.toString() ).toString();
1083         }
1084 
1085         int[] cellJustif = this.cellJustifStack.getLast();
1086         if ( justif == null && cellJustif != null && cellJustif.length > 0
1087             && this.isCellJustifStack.getLast().equals( Boolean.TRUE ) )
1088         {
1089             switch ( cellJustif[Math.min( cellCount, cellJustif.length - 1 )] )
1090             {
1091                 case JUSTIFY_LEFT:
1092                     justif = "left";
1093                     break;
1094                 case JUSTIFY_RIGHT:
1095                     justif = "right";
1096                     break;
1097                 case JUSTIFY_CENTER:
1098                 default:
1099                     justif = "center";
1100             }
1101         }
1102 
1103         if ( justif != null )
1104         {
1105             blockAtts.addAttribute( "text-align", justif );
1106         }
1107 
1108         writeStartTag( TABLE_CELL_TAG, cellAtts );
1109         writeEOL();
1110         writeStartTag( BLOCK_TAG, blockAtts );
1111         writeEOL();
1112     }
1113 
1114     /** {@inheritDoc} */
1115     public void tableCell_()
1116     {
1117         writeEndTag( BLOCK_TAG );
1118         writeEOL();
1119         writeEndTag( TABLE_CELL_TAG );
1120         writeEOL();
1121 
1122         if ( this.isCellJustifStack.getLast().equals( Boolean.TRUE ) )
1123         {
1124             int cellCount = Integer.parseInt( this.cellCountStack.removeLast().toString() );
1125             this.cellCountStack.addLast( Integer.valueOf( ++cellCount ) );
1126         }
1127     }
1128 
1129     /** {@inheritDoc} */
1130     public void tableHeaderCell_()
1131     {
1132         tableCell_();
1133     }
1134 
1135     /** {@inheritDoc} */
1136     public void tableCaption( SinkEventAttributes attributes )
1137     {
1138         StringWriter sw = new StringWriter();
1139         this.tableCaptionWriterStack.addLast( sw );
1140         this.tableCaptionXMLWriterStack.addLast( new PrettyPrintXMLWriter( sw ) );
1141 
1142         // <fo:table-caption> is XSL-FO 1.0 standard but not implemented in FOP 0.95
1143         //writeStartTag( TABLE_CAPTION_TAG );
1144 
1145         // TODO: how to implement this otherwise?
1146         // table-footer doesn't work because it has to be declared before table-body.
1147     }
1148 
1149     /** {@inheritDoc} */
1150     public void tableCaption()
1151     {
1152         tableCaption( null );
1153     }
1154 
1155     /** {@inheritDoc} */
1156     public void tableCaption_()
1157     {
1158         if ( !this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null )
1159         {
1160             this.tableCaptionStack.addLast( this.tableCaptionWriterStack.removeLast().toString() );
1161             this.tableCaptionXMLWriterStack.removeLast();
1162         }
1163         // <fo:table-caption> is XSL-FO 1.0 standard but not implemented in FOP 0.95
1164         //writeEndTag( TABLE_CAPTION_TAG );
1165     }
1166 
1167     /** {@inheritDoc} */
1168     public void anchor( String name, SinkEventAttributes attributes )
1169     {
1170         if ( name == null )
1171         {
1172             throw new NullPointerException( "Anchor name cannot be null!" );
1173         }
1174 
1175         String anchor = name;
1176 
1177         if ( !DoxiaUtils.isValidId( anchor ) )
1178         {
1179             anchor = DoxiaUtils.encodeId( name, true );
1180 
1181             String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'";
1182             logMessage( "modifiedLink", msg );
1183         }
1184 
1185         anchor = "#" + name;
1186 
1187         writeStartTag( INLINE_TAG, "id", anchor );
1188     }
1189 
1190     /** {@inheritDoc} */
1191     public void anchor( String name )
1192     {
1193         anchor( name, null );
1194     }
1195 
1196     /** {@inheritDoc} */
1197     public void anchor_()
1198     {
1199         writeEndTag( INLINE_TAG );
1200     }
1201 
1202     /** {@inheritDoc} */
1203     public void link( String name, SinkEventAttributes attributes )
1204     {
1205         if ( name == null )
1206         {
1207             throw new NullPointerException( "Link name cannot be null!" );
1208         }
1209 
1210         if ( DoxiaUtils.isExternalLink( name ) )
1211         {
1212             writeStartTag( BASIC_LINK_TAG, "external-destination", HtmlTools.escapeHTML( name ) );
1213             writeStartTag( INLINE_TAG, "href.external" );
1214         }
1215         else if ( DoxiaUtils.isInternalLink( name ) )
1216         {
1217             String anchor = name.substring( 1 );
1218 
1219             if ( !DoxiaUtils.isValidId( anchor ) )
1220             {
1221                 anchor = DoxiaUtils.encodeId( anchor, true );
1222 
1223                 String msg = "Modified invalid anchor name: '" + name + "' to '" + anchor + "'";
1224                 logMessage( "modifiedLink", msg );
1225             }
1226 
1227             anchor = "#" + anchor;
1228 
1229             writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) );
1230             writeStartTag( INLINE_TAG, "href.internal" );
1231         }
1232         else
1233         {
1234             // treat everything else as is
1235             String anchor = name;
1236 
1237             writeStartTag( BASIC_LINK_TAG, "internal-destination", HtmlTools.escapeHTML( anchor ) );
1238             writeStartTag( INLINE_TAG, "href.internal" );
1239         }
1240     }
1241 
1242     /** {@inheritDoc} */
1243     public void link( String name )
1244     {
1245         link( name, null );
1246     }
1247 
1248     /** {@inheritDoc} */
1249     public void link_()
1250     {
1251         writeEndTag( INLINE_TAG );
1252         writeEndTag( BASIC_LINK_TAG );
1253     }
1254 
1255     /** {@inheritDoc} */
1256     public void italic()
1257     {
1258         writeStartTag( INLINE_TAG, "italic" );
1259     }
1260 
1261     /** {@inheritDoc} */
1262     public void italic_()
1263     {
1264         writeEndTag( INLINE_TAG );
1265     }
1266 
1267     /** {@inheritDoc} */
1268     public void bold()
1269     {
1270         writeStartTag( INLINE_TAG, "bold" );
1271     }
1272 
1273     /** {@inheritDoc} */
1274     public void bold_()
1275     {
1276         writeEndTag( INLINE_TAG );
1277     }
1278 
1279     /** {@inheritDoc} */
1280     public void monospaced()
1281     {
1282         writeStartTag( INLINE_TAG, "monospace" );
1283     }
1284 
1285     /** {@inheritDoc} */
1286     public void monospaced_()
1287     {
1288         writeEndTag( INLINE_TAG );
1289     }
1290 
1291     /** {@inheritDoc} */
1292     public void lineBreak( SinkEventAttributes attributes )
1293     {
1294         writeEOL();
1295         writeEOL();
1296         writeSimpleTag( BLOCK_TAG );
1297     }
1298 
1299     /** {@inheritDoc} */
1300     public void lineBreak()
1301     {
1302         lineBreak( null );
1303     }
1304 
1305     /** {@inheritDoc} */
1306     public void nonBreakingSpace()
1307     {
1308         write( "&#160;" );
1309     }
1310 
1311     /** {@inheritDoc} */
1312     public void text( String text, SinkEventAttributes attributes )
1313     {
1314         content( text );
1315     }
1316 
1317     /** {@inheritDoc} */
1318     public void text( String text )
1319     {
1320         text( text, null );
1321     }
1322 
1323     /** {@inheritDoc} */
1324     public void rawText( String text )
1325     {
1326         write( text );
1327     }
1328 
1329     /** {@inheritDoc} */
1330     public void flush()
1331     {
1332         out.flush();
1333     }
1334 
1335     /** {@inheritDoc} */
1336     public void close()
1337     {
1338         out.close();
1339 
1340         if ( getLog().isWarnEnabled() && this.warnMessages != null )
1341         {
1342             for ( Map.Entry<String, Set<String>> entry : this.warnMessages.entrySet() )
1343             {
1344                 for ( String msg : entry.getValue() )
1345                 {
1346                     getLog().warn( msg );
1347                 }
1348             }
1349 
1350             this.warnMessages = null;
1351         }
1352 
1353         init();
1354     }
1355 
1356     /**
1357      * {@inheritDoc}
1358      *
1359      * Unkown events just log a warning message but are ignored otherwise.
1360      * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
1361      */
1362     public void unknown( String name, Object[] requiredParams, SinkEventAttributes attributes )
1363     {
1364         String msg = "Unknown Sink event: '" + name + "', ignoring!";
1365         logMessage( "unknownEvent", msg );
1366     }
1367 
1368     /** {@inheritDoc} */
1369     public void comment( String comment )
1370     {
1371         if ( comment != null )
1372         {
1373             final String originalComment = comment;
1374 
1375             // http://www.w3.org/TR/2000/REC-xml-20001006#sec-comments
1376             while ( comment.contains( "--" ) )
1377             {
1378                 comment = comment.replace( "--", "- -" );
1379             }
1380 
1381             if ( comment.endsWith( "-" ) )
1382             {
1383                 comment += " ";
1384             }
1385 
1386             if ( !originalComment.equals( comment ) )
1387             {
1388                 String msg = "Modified invalid comment: '" + originalComment + "' to '" + comment + "'";
1389                 logMessage( "modifyComment", msg );
1390             }
1391 
1392             final StringBuilder buffer = new StringBuilder( comment.length() + 7 );
1393 
1394             buffer.append( LESS_THAN ).append( BANG ).append( MINUS ).append( MINUS );
1395             buffer.append( comment );
1396             buffer.append( MINUS ).append( MINUS ).append( GREATER_THAN );
1397 
1398             write( buffer.toString() );
1399         }
1400     }
1401 
1402     /**
1403      * Writes the beginning of a FO document.
1404      */
1405     public void beginDocument()
1406     {
1407         write( "<?xml version=\"1.0\"" );
1408         if ( encoding != null )
1409         {
1410             write( " encoding=\"" + encoding + "\"" );
1411         }
1412         write( "?>" );
1413         writeEOL();
1414 
1415         MutableAttributeSet atts = new SinkEventAttributeSet();
1416         atts.addAttribute( "xmlns:" + getNameSpace(), FO_NAMESPACE );
1417 
1418         if ( languageId != null )
1419         {
1420             atts.addAttribute( "language", languageId );
1421         }
1422 
1423         writeStartTag( ROOT_TAG, atts );
1424 
1425         writeStartTag( LAYOUT_MASTER_SET_TAG );
1426 
1427         writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.cover-page" );
1428         writeEmptyTag( REGION_BODY_TAG, "layout.master.set.cover-page.region-body" );
1429         writeEndTag( SIMPLE_PAGE_MASTER_TAG );
1430         writeEOL();
1431 
1432         writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.toc" );
1433         writeEmptyTag( REGION_BODY_TAG, "layout.master.set.toc.region-body" );
1434         writeEmptyTag( REGION_BEFORE_TAG, "layout.master.set.toc.region-before" );
1435         writeEmptyTag( REGION_AFTER_TAG, "layout.master.set.toc.region-after" );
1436         writeEndTag( SIMPLE_PAGE_MASTER_TAG );
1437         writeEOL();
1438 
1439         writeStartTag( SIMPLE_PAGE_MASTER_TAG, "layout.master.set.body" );
1440         writeEmptyTag( REGION_BODY_TAG, "layout.master.set.body.region-body" );
1441         writeEmptyTag( REGION_BEFORE_TAG, "layout.master.set.body.region-before" );
1442         writeEmptyTag( REGION_AFTER_TAG, "layout.master.set.body.region-after" );
1443         writeEndTag( SIMPLE_PAGE_MASTER_TAG );
1444         writeEOL();
1445 
1446         writeEndTag( LAYOUT_MASTER_SET_TAG );
1447         writeEOL();
1448 
1449         pdfBookmarks();
1450     }
1451 
1452     /**
1453      * Writes the end of a FO document, flushes and closes the stream.
1454      */
1455     public void endDocument()
1456     {
1457         writeEndTag( ROOT_TAG );
1458         writeEOL();
1459 
1460         flush();
1461         close();
1462     }
1463 
1464     // ----------------------------------------------------------------------
1465     //
1466     // ----------------------------------------------------------------------
1467 
1468     /**
1469      * Returns the configuration object of this sink.
1470      *
1471      * @return The configuration object of this sink.
1472      */
1473     protected FoConfiguration getFoConfiguration()
1474     {
1475         return config;
1476     }
1477 
1478     /**
1479      * Writes a start tag, prepending EOL.
1480      *
1481      * @param tag The tag.
1482      * @param attributeId An id identifying the attribute set.
1483      */
1484     protected void writeStartTag( Tag tag, String attributeId )
1485     {
1486         writeEOL();
1487         writeStartTag( tag, config.getAttributeSet( attributeId ) );
1488     }
1489 
1490     /**
1491      * Writes a start tag, prepending EOL.
1492      *
1493      * @param tag The tag.
1494      * @param id An id to add.
1495      * @param name The name (value) of the id.
1496      */
1497     protected void writeStartTag( Tag tag, String id, String name )
1498     {
1499         writeEOL();
1500         MutableAttributeSet att = new SinkEventAttributeSet( new String[] {id, name} );
1501 
1502         writeStartTag( tag, att );
1503     }
1504 
1505     /**
1506      * Writes a start tag, prepending EOL.
1507      *
1508      * @param tag The tag.
1509      * @param id An id to add.
1510      * @param name The name (value) of the id.
1511      * @param attributeId An id identifying the attribute set.
1512      */
1513     protected void writeStartTag( Tag tag, String id, String name, String attributeId )
1514     {
1515         MutableAttributeSet att = config.getAttributeSet( attributeId );
1516 
1517         // make sure we don't add it twice
1518         if ( att.isDefined( id ) )
1519         {
1520             att.removeAttribute( id );
1521         }
1522 
1523         att.addAttribute( id, name );
1524 
1525         writeEOL();
1526         writeStartTag( tag, att );
1527     }
1528 
1529     /**
1530      * Writes an empty tag, prepending EOL.
1531      *
1532      * @param tag The tag.
1533      * @param id An id to add.
1534      * @param name The name (value) of the id.
1535      */
1536     protected void writeEmptyTag( Tag tag, String id, String name )
1537     {
1538         MutableAttributeSet att = new SinkEventAttributeSet( new String[] {id, name} );
1539 
1540         writeEOL();
1541         writeSimpleTag( tag, att );
1542     }
1543 
1544     /**
1545      * Writes a simple tag, appending EOL.
1546      *
1547      * @param tag The tag name.
1548      * @param attributeId An id identifying the attribute set.
1549      */
1550     protected void writeEmptyTag( Tag tag, String attributeId )
1551     {
1552         writeEOL();
1553         writeSimpleTag( tag, config.getAttributeSet( attributeId ) );
1554     }
1555 
1556     /**
1557      * {@inheritDoc}
1558      *
1559      * Writes a text, swallowing any exceptions.
1560      */
1561     protected void write( String text )
1562     {
1563         if ( !this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null )
1564         {
1565             this.tableCaptionXMLWriterStack.getLast().writeText( unifyEOLs( text ) );
1566         }
1567         else if ( !this.tableContentWriterStack.isEmpty() && this.tableContentWriterStack.getLast() != null )
1568         {
1569             this.tableContentWriterStack.getLast().write( unifyEOLs( text ) );
1570         }
1571         else
1572         {
1573             out.write( unifyEOLs( text ) );
1574         }
1575     }
1576 
1577     /**
1578      * Writes a text, appending EOL.
1579      *
1580      * @param text The text to write.
1581      */
1582     protected void writeln( String text )
1583     {
1584         write( text );
1585         writeEOL();
1586     }
1587 
1588     /**
1589      * Writes content, escaping special characters.
1590      *
1591      * @param text The text to write.
1592      */
1593     protected void content( String text )
1594     {
1595         write( escaped( text, verbatim ) );
1596     }
1597 
1598     /**
1599      * Escapes special characters so that the text can be included in a fo file.
1600      *
1601      * @param text The text to process.
1602      * @param verb In verbatim mode, white space and newlines are escaped.
1603      * @return The text with special characters escaped.
1604      */
1605     public static String escaped( String text, boolean verb )
1606     {
1607         int length = text.length();
1608         StringBuilder buffer = new StringBuilder( length );
1609 
1610         for ( int i = 0; i < length; ++i )
1611         {
1612             char c = text.charAt( i );
1613             switch ( c )
1614             {
1615                 case ' ':
1616                     if ( verb )
1617                     {
1618                         buffer.append( "&#160;" );
1619                     }
1620                     else
1621                     {
1622                         buffer.append( c );
1623                     }
1624                     break;
1625                 case '<':
1626                     buffer.append( "&lt;" );
1627                     break;
1628                 case '>':
1629                     buffer.append( "&gt;" );
1630                     break;
1631                 case '&':
1632                     buffer.append( "&amp;" );
1633                     break;
1634                 case '\n':
1635                     buffer.append( EOL );
1636                     if ( verb )
1637                     {
1638                         buffer.append( "<fo:block/>" + EOL );
1639                     }
1640                     break;
1641                 default:
1642                     if ( needsSymbolFont( c ) )
1643                     {
1644                         // TODO: make font configurable?
1645                         buffer.append( "<fo:inline font-family=\"Symbol\">" ).append( c ).append( "</fo:inline>" );
1646                     }
1647                     else
1648                     {
1649                         buffer.append( c );
1650                     }
1651             }
1652         }
1653 
1654         return buffer.toString();
1655     }
1656 
1657     /** {@inheritDoc} */
1658     protected void writeStartTag( Tag t, MutableAttributeSet att, boolean isSimpleTag )
1659     {
1660         if ( this.tableCaptionXMLWriterStack.isEmpty() )
1661         {
1662             super.writeStartTag ( t, att, isSimpleTag );
1663         }
1664         else
1665         {
1666             String tag = ( getNameSpace() != null ? getNameSpace() + ":" : "" ) + t.toString();
1667             this.tableCaptionXMLWriterStack.getLast().startElement( tag );
1668 
1669             if ( att != null )
1670             {
1671                 Enumeration<?> names = att.getAttributeNames();
1672                 while ( names.hasMoreElements() )
1673                 {
1674                     Object key = names.nextElement();
1675                     Object value = att.getAttribute( key );
1676 
1677                     this.tableCaptionXMLWriterStack.getLast().addAttribute( key.toString(), value.toString() );
1678                 }
1679             }
1680 
1681             if ( isSimpleTag )
1682             {
1683                 this.tableCaptionXMLWriterStack.getLast().endElement();
1684             }
1685         }
1686     }
1687 
1688     /** {@inheritDoc} */
1689     protected void writeEndTag( Tag t )
1690     {
1691         if ( this.tableCaptionXMLWriterStack.isEmpty() )
1692         {
1693             super.writeEndTag( t );
1694         }
1695         else
1696         {
1697             this.tableCaptionXMLWriterStack.getLast().endElement();
1698         }
1699     }
1700 
1701     private static final char UPPER_ALPHA = 0x391;
1702     private static final char PIV = 0x3d6;
1703     private static final char OLINE = 0x203e;
1704     private static final char DIAMS = 0x2666;
1705     private static final char EURO = 0x20ac;
1706     private static final char TRADE = 0x2122;
1707     private static final char PRIME = 0x2032;
1708     private static final char PPRIME = 0x2033;
1709 
1710     private static boolean needsSymbolFont( char c )
1711     {
1712         // greek characters and mathematical symbols, except the euro and trade symbols
1713         // symbols I couldn't get to display in any font:
1714         // zwnj (0x200C), zwj (0x200D), lrm (0x200E), rlm (0x200F), oline (0x203E),
1715         // lceil (0x2038), rceil (0x2039), lfloor (0x203A), rfloor (0x203B)
1716         return ( c >= UPPER_ALPHA && c <= PIV )
1717                 || ( c == PRIME || c == PPRIME )
1718                 || ( c >= OLINE && c <= DIAMS && c != EURO && c != TRADE );
1719     }
1720 
1721     /**
1722      * Starts a page sequence.
1723      *
1724      * @param initPageNumber The initial page number. Should be either "0" (for the first page) or "auto".
1725      * @param headerText The text to write in the header, if null, nothing is written.
1726      * @param footerText The text to write in the footer, if null, nothing is written.
1727      */
1728     protected void startPageSequence( String initPageNumber, String headerText, String footerText )
1729     {
1730         writeln( "<fo:page-sequence initial-page-number=\"" + initPageNumber + "\" master-reference=\"body\">" );
1731         regionBefore( headerText );
1732         regionAfter( footerText );
1733         writeln( "<fo:flow flow-name=\"xsl-region-body\">" );
1734         chapterHeading( null, true );
1735     }
1736 
1737     /**
1738      * Writes a 'xsl-region-before' block.
1739      *
1740      * @param headerText The text to write in the header, if null, nothing is written.
1741      */
1742     protected void regionBefore( String headerText )
1743     {
1744         // do nothing, overridden by AggregateSink
1745     }
1746 
1747     /**
1748      * Writes a 'xsl-region-after' block. By default does nothing, gets overridden by AggregateSink.
1749      *
1750      * @param footerText The text to write in the footer, if null, nothing is written.
1751      */
1752     protected void regionAfter( String footerText )
1753     {
1754         // do nothing, overridden by AggregateSink
1755     }
1756 
1757     /**
1758      * Writes a chapter heading. By default does nothing, gets overridden by AggregateSink.
1759      *
1760      * @param headerText The text to write in the header, if null, the current document title is written.
1761      * @param chapterNumber True if the chapter number should be written in front of the text.
1762      */
1763     protected void chapterHeading( String headerText, boolean chapterNumber )
1764     {
1765         // do nothing, overridden by AggregateSink
1766     }
1767 
1768     /**
1769      * Writes a fo:bookmark-tree. By default does nothing, gets overridden by AggregateSink.
1770      */
1771     protected void pdfBookmarks()
1772     {
1773         // do nothing, overridden by AggregateSink
1774     }
1775 
1776     /**
1777      * If debug mode is enabled, log the <code>msg</code> as is, otherwise add unique msg in <code>warnMessages</code>.
1778      *
1779      * @param key not null
1780      * @param msg not null
1781      * @see #close()
1782      * @since 1.1.1
1783      */
1784     protected void logMessage( String key, String msg )
1785     {
1786         msg = "[FO Sink] " + msg;
1787         if ( getLog().isDebugEnabled() )
1788         {
1789             getLog().debug( msg );
1790 
1791             return;
1792         }
1793 
1794         if ( warnMessages == null )
1795         {
1796             warnMessages = new HashMap<String, Set<String>>();
1797         }
1798 
1799         Set<String> set = warnMessages.get( key );
1800         if ( set == null )
1801         {
1802             set = new TreeSet<String>();
1803         }
1804         set.add( msg );
1805         warnMessages.put( key, set );
1806     }
1807 
1808     /** {@inheritDoc} */
1809     protected void init()
1810     {
1811         super.init();
1812 
1813         this.listStack.clear();
1814         this.tableGridStack.clear();
1815         this.cellJustifStack.clear();
1816         this.isCellJustifStack.clear();
1817         this.cellCountStack.clear();
1818         this.tableContentWriterStack.clear();
1819         this.tableCaptionWriterStack.clear();
1820         this.tableCaptionXMLWriterStack.clear();
1821         this.tableCaptionStack.clear();
1822 
1823         this.section = 0;
1824         this.subsection = 0;
1825         this.subsubsection = 0;
1826         this.verbatim = false;
1827         this.inFigure = false;
1828         this.warnMessages = null;
1829     }
1830 }