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