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