001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.sink.impl;
020
021import javax.swing.text.MutableAttributeSet;
022import javax.swing.text.html.HTML.Tag;
023
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.io.Writer;
027import java.util.ArrayList;
028import java.util.EmptyStackException;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.Stack;
036import java.util.regex.Pattern;
037
038import org.apache.commons.lang3.StringUtils;
039import org.apache.maven.doxia.markup.HtmlMarkup;
040import org.apache.maven.doxia.markup.Markup;
041import org.apache.maven.doxia.sink.Sink;
042import org.apache.maven.doxia.sink.SinkEventAttributes;
043import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink.VerbatimMode;
044import org.apache.maven.doxia.util.DoxiaUtils;
045import org.apache.maven.doxia.util.HtmlTools;
046import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * Abstract base xhtml5 sink implementation.
052 */
053public class Xhtml5BaseSink extends AbstractXmlSink implements HtmlMarkup {
054    private static final Logger LOGGER = LoggerFactory.getLogger(Xhtml5BaseSink.class);
055
056    // ----------------------------------------------------------------------
057    // Instance fields
058    // ----------------------------------------------------------------------
059
060    /** The PrintWriter to write the result. */
061    private final PrintWriter writer;
062
063    /** Used to identify if a class string contains `hidden` */
064    private static final Pattern HIDDEN_CLASS_PATTERN = Pattern.compile("(?:.*\\s|^)hidden(?:\\s.*|$)");
065
066    /** Used to collect text events mainly for the head events. */
067    private StringBuffer textBuffer = new StringBuffer();
068
069    /** An indication on if we're inside a head. */
070    private boolean headFlag;
071
072    /** Keep track of the main and div tags for content events. */
073    protected Stack<Tag> contentStack = new Stack<>();
074
075    /** Keep track of the closing tags for inline events. */
076    protected Stack<List<Tag>> inlineStack = new Stack<>();
077
078    /** An indication on if we're inside a paragraph flag. */
079    private boolean paragraphFlag;
080
081    protected enum VerbatimMode {
082        /** not in verbatim mode */
083        OFF,
084        /** Inside {@code <pre>} */
085        ON,
086        /** Inside {@code <pre><code>} */
087        ON_WITH_CODE
088    }
089    /** An indication on if we're in verbatim mode and if so, surrounded by which tags. */
090    private VerbatimMode verbatimMode;
091
092    /** Stack of alignment int[] of table cells. */
093    private final LinkedList<int[]> cellJustifStack;
094
095    /** Stack of justification of table cells. */
096    private final LinkedList<Boolean> isCellJustifStack;
097
098    /** Stack of current table cell. */
099    private final LinkedList<Integer> cellCountStack;
100
101    /** Used to style successive table rows differently. */
102    private boolean evenTableRow = true;
103
104    /** The stack of StringWriter to write the table result temporary, so we could play with the output DOXIA-177. */
105    private final LinkedList<StringWriter> tableContentWriterStack;
106
107    private final LinkedList<StringWriter> tableCaptionWriterStack;
108
109    private final LinkedList<PrettyPrintXMLWriter> tableCaptionXMLWriterStack;
110
111    /** The stack of table caption */
112    private final LinkedList<String> tableCaptionStack;
113
114    /** used to store attributes passed to table(). */
115    protected MutableAttributeSet tableAttributes;
116
117    // ----------------------------------------------------------------------
118    // Constructor
119    // ----------------------------------------------------------------------
120
121    /**
122     * Constructor, initialize the PrintWriter.
123     *
124     * @param out The writer to write the result.
125     */
126    public Xhtml5BaseSink(Writer out) {
127        this.writer = new PrintWriter(out);
128
129        this.cellJustifStack = new LinkedList<>();
130        this.isCellJustifStack = new LinkedList<>();
131        this.cellCountStack = new LinkedList<>();
132        this.tableContentWriterStack = new LinkedList<>();
133        this.tableCaptionWriterStack = new LinkedList<>();
134        this.tableCaptionXMLWriterStack = new LinkedList<>();
135        this.tableCaptionStack = new LinkedList<>();
136
137        init();
138    }
139
140    // ----------------------------------------------------------------------
141    // Accessor methods
142    // ----------------------------------------------------------------------
143
144    /**
145     * To use mainly when playing with the head events.
146     *
147     * @return the current buffer of text events.
148     */
149    protected StringBuffer getTextBuffer() {
150        return this.textBuffer;
151    }
152
153    /**
154     * <p>Setter for the field <code>headFlag</code>.</p>
155     *
156     * @param headFlag an header flag.
157     */
158    protected void setHeadFlag(boolean headFlag) {
159        this.headFlag = headFlag;
160    }
161
162    /**
163     * <p>isHeadFlag.</p>
164     *
165     * @return the current headFlag.
166     */
167    protected boolean isHeadFlag() {
168        return this.headFlag;
169    }
170
171    /**
172     *
173     * @return the current verbatim mode.
174     */
175    protected VerbatimMode getVerbatimMode() {
176        return this.verbatimMode;
177    }
178
179    /**
180     * <p>Setter for the field <code>verbatimMode</code>.</p>
181     *
182     * @param mode a verbatim mode.
183     */
184    protected void setVerbatimMode(VerbatimMode mode) {
185        this.verbatimMode = mode;
186    }
187
188    /**
189     *
190     * @return {@code true} if inside verbatim section, {@code false} otherwise
191     */
192    protected boolean isVerbatim() {
193        return this.verbatimMode != VerbatimMode.OFF;
194    }
195
196    /**
197     * <p>Setter for the field <code>cellJustif</code>.</p>
198     *
199     * @param justif the new cell justification array.
200     */
201    protected void setCellJustif(int[] justif) {
202        this.cellJustifStack.addLast(justif);
203        this.isCellJustifStack.addLast(Boolean.TRUE);
204    }
205
206    /**
207     * <p>Getter for the field <code>cellJustif</code>.</p>
208     *
209     * @return the current cell justification array.
210     */
211    protected int[] getCellJustif() {
212        return this.cellJustifStack.getLast();
213    }
214
215    /**
216     * <p>Setter for the field <code>cellCount</code>.</p>
217     *
218     * @param count the new cell count.
219     */
220    protected void setCellCount(int count) {
221        this.cellCountStack.addLast(count);
222    }
223
224    /**
225     * <p>Getter for the field <code>cellCount</code>.</p>
226     *
227     * @return the current cell count.
228     */
229    protected int getCellCount() {
230        return this.cellCountStack.getLast();
231    }
232
233    /** {@inheritDoc} */
234    @Override
235    protected void init() {
236        super.init();
237
238        resetTextBuffer();
239
240        this.cellJustifStack.clear();
241        this.isCellJustifStack.clear();
242        this.cellCountStack.clear();
243        this.tableContentWriterStack.clear();
244        this.tableCaptionWriterStack.clear();
245        this.tableCaptionXMLWriterStack.clear();
246        this.tableCaptionStack.clear();
247        this.inlineStack.clear();
248
249        this.headFlag = false;
250        this.paragraphFlag = false;
251        this.verbatimMode = VerbatimMode.OFF;
252
253        this.evenTableRow = true;
254        this.tableAttributes = null;
255    }
256
257    /**
258     * Reset the text buffer.
259     */
260    protected void resetTextBuffer() {
261        this.textBuffer = new StringBuffer();
262    }
263
264    // ----------------------------------------------------------------------
265    // Sections
266    // ----------------------------------------------------------------------
267
268    /** {@inheritDoc} */
269    @Override
270    public void article(SinkEventAttributes attributes) {
271        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
272
273        writeStartTag(HtmlMarkup.ARTICLE, atts);
274    }
275
276    /** {@inheritDoc} */
277    @Override
278    public void article_() {
279        writeEndTag(HtmlMarkup.ARTICLE);
280    }
281
282    /** {@inheritDoc} */
283    @Override
284    public void navigation(SinkEventAttributes attributes) {
285        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
286
287        writeStartTag(HtmlMarkup.NAV, atts);
288    }
289
290    /** {@inheritDoc} */
291    @Override
292    public void navigation_() {
293        writeEndTag(HtmlMarkup.NAV);
294    }
295
296    /** {@inheritDoc} */
297    @Override
298    public void sidebar(SinkEventAttributes attributes) {
299        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
300
301        writeStartTag(HtmlMarkup.ASIDE, atts);
302    }
303
304    /** {@inheritDoc} */
305    @Override
306    public void sidebar_() {
307        writeEndTag(HtmlMarkup.ASIDE);
308    }
309
310    /** {@inheritDoc} */
311    @Override
312    public void section(int level, SinkEventAttributes attributes) {
313        onSection(level, attributes);
314    }
315
316    /** {@inheritDoc} */
317    @Override
318    public void sectionTitle(int level, SinkEventAttributes attributes) {
319        onSectionTitle(level, attributes);
320    }
321
322    /** {@inheritDoc} */
323    @Override
324    public void sectionTitle_(int level) {
325        onSectionTitle_(level);
326    }
327
328    /** {@inheritDoc} */
329    @Override
330    public void section_(int level) {
331        onSection_(level);
332    }
333
334    /**
335     * Starts a section.
336     *
337     * @param depth The level of the section.
338     * @param attributes some attributes. May be null.
339     */
340    protected void onSection(int depth, SinkEventAttributes attributes) {
341        if (depth >= SECTION_LEVEL_1 && depth <= SECTION_LEVEL_6) {
342            MutableAttributeSet att = new SinkEventAttributeSet();
343            att.addAttributes(SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES));
344
345            writeStartTag(HtmlMarkup.SECTION, att);
346        }
347    }
348
349    /**
350     * Ends a section.
351     *
352     * @param depth The level of the section.
353     * @see #SECTION
354     */
355    protected void onSection_(int depth) {
356        if (depth >= SECTION_LEVEL_1 && depth <= SECTION_LEVEL_6) {
357            writeEndTag(HtmlMarkup.SECTION);
358        }
359    }
360
361    /**
362     * Starts a section title.
363     *
364     * @param depth The level of the section title.
365     * @param attributes some attributes. May be null.
366     * @see #H1
367     * @see #H2
368     * @see #H3
369     * @see #H4
370     * @see #H5
371     * @see #H6
372     */
373    protected void onSectionTitle(int depth, SinkEventAttributes attributes) {
374        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
375
376        if (depth == SECTION_LEVEL_1) {
377            writeStartTag(HtmlMarkup.H1, atts);
378        } else if (depth == SECTION_LEVEL_2) {
379            writeStartTag(HtmlMarkup.H2, atts);
380        } else if (depth == SECTION_LEVEL_3) {
381            writeStartTag(HtmlMarkup.H3, atts);
382        } else if (depth == SECTION_LEVEL_4) {
383            writeStartTag(HtmlMarkup.H4, atts);
384        } else if (depth == SECTION_LEVEL_5) {
385            writeStartTag(HtmlMarkup.H5, atts);
386        } else if (depth == SECTION_LEVEL_6) {
387            writeStartTag(HtmlMarkup.H6, atts);
388        }
389    }
390
391    /**
392     * Ends a section title.
393     *
394     * @param depth The level of the section title.
395     * @see #H1
396     * @see #H2
397     * @see #H3
398     * @see #H4
399     * @see #H5
400     * @see #H6
401     */
402    protected void onSectionTitle_(int depth) {
403        if (depth == SECTION_LEVEL_1) {
404            writeEndTag(HtmlMarkup.H1);
405        } else if (depth == SECTION_LEVEL_2) {
406            writeEndTag(HtmlMarkup.H2);
407        } else if (depth == SECTION_LEVEL_3) {
408            writeEndTag(HtmlMarkup.H3);
409        } else if (depth == SECTION_LEVEL_4) {
410            writeEndTag(HtmlMarkup.H4);
411        } else if (depth == SECTION_LEVEL_5) {
412            writeEndTag(HtmlMarkup.H5);
413        } else if (depth == SECTION_LEVEL_6) {
414            writeEndTag(HtmlMarkup.H6);
415        }
416    }
417
418    /** {@inheritDoc} */
419    @Override
420    public void header(SinkEventAttributes attributes) {
421        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
422
423        writeStartTag(HtmlMarkup.HEADER, atts);
424    }
425
426    /** {@inheritDoc} */
427    @Override
428    public void header_() {
429        writeEndTag(HtmlMarkup.HEADER);
430    }
431
432    /** {@inheritDoc} */
433    @Override
434    public void content(SinkEventAttributes attributes) {
435        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
436
437        if (contentStack.empty()) {
438            writeStartTag(contentStack.push(HtmlMarkup.MAIN), atts);
439        } else {
440            if (atts == null) {
441                atts = new SinkEventAttributeSet(1);
442            }
443
444            String divClass = "content";
445            if (atts.isDefined(SinkEventAttributes.CLASS)) {
446                divClass += " " + atts.getAttribute(SinkEventAttributes.CLASS).toString();
447            }
448
449            atts.addAttribute(SinkEventAttributes.CLASS, divClass);
450
451            writeStartTag(contentStack.push(HtmlMarkup.DIV), atts);
452        }
453    }
454
455    /** {@inheritDoc} */
456    @Override
457    public void content_() {
458        try {
459            writeEndTag(contentStack.pop());
460        } catch (EmptyStackException ese) {
461            /* do nothing if the stack is empty */
462        }
463    }
464
465    /** {@inheritDoc} */
466    @Override
467    public void footer(SinkEventAttributes attributes) {
468        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
469
470        writeStartTag(HtmlMarkup.FOOTER, atts);
471    }
472
473    /** {@inheritDoc} */
474    @Override
475    public void footer_() {
476        writeEndTag(HtmlMarkup.FOOTER);
477    }
478
479    // -----------------------------------------------------------------------
480    //
481    // -----------------------------------------------------------------------
482
483    /**
484     * {@inheritDoc}
485     * @see javax.swing.text.html.HTML.Tag#UL
486     */
487    @Override
488    public void list(SinkEventAttributes attributes) {
489        if (paragraphFlag) {
490            // The content of element type "p" must match
491            // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong|
492            // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)".
493            paragraph_();
494        }
495
496        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
497
498        writeStartTag(HtmlMarkup.UL, atts);
499    }
500
501    /**
502     * {@inheritDoc}
503     * @see javax.swing.text.html.HTML.Tag#UL
504     */
505    @Override
506    public void list_() {
507        writeEndTag(HtmlMarkup.UL);
508    }
509
510    /**
511     * {@inheritDoc}
512     * @see javax.swing.text.html.HTML.Tag#LI
513     */
514    @Override
515    public void listItem(SinkEventAttributes attributes) {
516        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
517
518        writeStartTag(HtmlMarkup.LI, atts);
519    }
520
521    /**
522     * {@inheritDoc}
523     * @see javax.swing.text.html.HTML.Tag#LI
524     */
525    @Override
526    public void listItem_() {
527        writeEndTag(HtmlMarkup.LI);
528    }
529
530    /**
531     * The default list style depends on the numbering.
532     *
533     * {@inheritDoc}
534     * @see javax.swing.text.html.HTML.Tag#OL
535     */
536    @Override
537    public void numberedList(int numbering, SinkEventAttributes attributes) {
538        if (paragraphFlag) {
539            // The content of element type "p" must match
540            // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong|
541            // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)".
542            paragraph_();
543        }
544
545        String olStyle = "list-style-type: ";
546        switch (numbering) {
547            case NUMBERING_UPPER_ALPHA:
548                olStyle += "upper-alpha";
549                break;
550            case NUMBERING_LOWER_ALPHA:
551                olStyle += "lower-alpha";
552                break;
553            case NUMBERING_UPPER_ROMAN:
554                olStyle += "upper-roman";
555                break;
556            case NUMBERING_LOWER_ROMAN:
557                olStyle += "lower-roman";
558                break;
559            case NUMBERING_DECIMAL:
560            default:
561                olStyle += "decimal";
562        }
563        olStyle += ";";
564
565        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
566
567        if (atts == null) {
568            atts = new SinkEventAttributeSet(1);
569        }
570
571        if (atts.isDefined(SinkEventAttributes.STYLE)) {
572            olStyle += " " + atts.getAttribute(SinkEventAttributes.STYLE).toString();
573        }
574
575        atts.addAttribute(SinkEventAttributes.STYLE, olStyle);
576
577        writeStartTag(HtmlMarkup.OL, atts);
578    }
579
580    /**
581     * {@inheritDoc}
582     * @see javax.swing.text.html.HTML.Tag#OL
583     */
584    @Override
585    public void numberedList_() {
586        writeEndTag(HtmlMarkup.OL);
587    }
588
589    /**
590     * {@inheritDoc}
591     * @see javax.swing.text.html.HTML.Tag#LI
592     */
593    @Override
594    public void numberedListItem(SinkEventAttributes attributes) {
595        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
596
597        writeStartTag(HtmlMarkup.LI, atts);
598    }
599
600    /**
601     * {@inheritDoc}
602     * @see javax.swing.text.html.HTML.Tag#LI
603     */
604    @Override
605    public void numberedListItem_() {
606        writeEndTag(HtmlMarkup.LI);
607    }
608
609    /**
610     * {@inheritDoc}
611     * @see javax.swing.text.html.HTML.Tag#DL
612     */
613    @Override
614    public void definitionList(SinkEventAttributes attributes) {
615        if (paragraphFlag) {
616            // The content of element type "p" must match
617            // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong|
618            // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)".
619            paragraph_();
620        }
621
622        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
623
624        writeStartTag(HtmlMarkup.DL, atts);
625    }
626
627    /**
628     * {@inheritDoc}
629     * @see javax.swing.text.html.HTML.Tag#DL
630     */
631    @Override
632    public void definitionList_() {
633        writeEndTag(HtmlMarkup.DL);
634    }
635
636    /**
637     * {@inheritDoc}
638     * @see javax.swing.text.html.HTML.Tag#DT
639     */
640    @Override
641    public void definedTerm(SinkEventAttributes attributes) {
642        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
643
644        writeStartTag(HtmlMarkup.DT, atts);
645    }
646
647    /**
648     * {@inheritDoc}
649     * @see javax.swing.text.html.HTML.Tag#DT
650     */
651    @Override
652    public void definedTerm_() {
653        writeEndTag(HtmlMarkup.DT);
654    }
655
656    /**
657     * {@inheritDoc}
658     * @see javax.swing.text.html.HTML.Tag#DD
659     */
660    @Override
661    public void definition(SinkEventAttributes attributes) {
662        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
663
664        writeStartTag(HtmlMarkup.DD, atts);
665    }
666
667    /**
668     * {@inheritDoc}
669     * @see javax.swing.text.html.HTML.Tag#DD
670     */
671    @Override
672    public void definition_() {
673        writeEndTag(HtmlMarkup.DD);
674    }
675
676    /** {@inheritDoc} */
677    @Override
678    public void figure(SinkEventAttributes attributes) {
679        writeStartTag(HtmlMarkup.FIGURE, attributes);
680    }
681
682    /** {@inheritDoc} */
683    @Override
684    public void figure_() {
685        writeEndTag(HtmlMarkup.FIGURE);
686    }
687
688    /** {@inheritDoc} */
689    @Override
690    public void figureGraphics(String src, SinkEventAttributes attributes) {
691        MutableAttributeSet filtered = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_IMG_ATTRIBUTES);
692        if (filtered != null) {
693            filtered.removeAttribute(SinkEventAttributes.SRC.toString());
694        }
695
696        int count = (attributes == null ? 1 : attributes.getAttributeCount() + 1);
697
698        MutableAttributeSet atts = new SinkEventAttributeSet(count);
699
700        atts.addAttribute(SinkEventAttributes.SRC, HtmlTools.escapeHTML(src, true));
701        atts.addAttributes(filtered);
702
703        writeStartTag(HtmlMarkup.IMG, atts, true);
704    }
705
706    /** {@inheritDoc} */
707    @Override
708    public void figureCaption(SinkEventAttributes attributes) {
709        writeStartTag(HtmlMarkup.FIGCAPTION, attributes);
710    }
711
712    /** {@inheritDoc} */
713    @Override
714    public void figureCaption_() {
715        writeEndTag(HtmlMarkup.FIGCAPTION);
716    }
717
718    /**
719     * {@inheritDoc}
720     * @see javax.swing.text.html.HTML.Tag#P
721     */
722    @Override
723    public void paragraph(SinkEventAttributes attributes) {
724        paragraphFlag = true;
725
726        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
727
728        writeStartTag(HtmlMarkup.P, atts);
729    }
730
731    /**
732     * {@inheritDoc}
733     * @see javax.swing.text.html.HTML.Tag#P
734     */
735    @Override
736    public void paragraph_() {
737        if (paragraphFlag) {
738            writeEndTag(HtmlMarkup.P);
739            paragraphFlag = false;
740        }
741    }
742
743    /** {@inheritDoc} */
744    @Override
745    public void data(String value, SinkEventAttributes attributes) {
746        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
747
748        MutableAttributeSet att = new SinkEventAttributeSet();
749        if (value != null) {
750            att.addAttribute(SinkEventAttributes.VALUE, value);
751        }
752        att.addAttributes(atts);
753
754        writeStartTag(HtmlMarkup.DATA, att);
755    }
756
757    /** {@inheritDoc} */
758    @Override
759    public void data_() {
760        writeEndTag(HtmlMarkup.DATA);
761    }
762
763    /** {@inheritDoc} */
764    @Override
765    public void time(String datetime, SinkEventAttributes attributes) {
766        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
767
768        MutableAttributeSet att = new SinkEventAttributeSet();
769        if (datetime != null) {
770            att.addAttribute("datetime", datetime);
771        }
772        att.addAttributes(atts);
773
774        writeStartTag(HtmlMarkup.TIME, att);
775    }
776
777    /** {@inheritDoc} */
778    @Override
779    public void time_() {
780        writeEndTag(HtmlMarkup.TIME);
781    }
782
783    /**
784     * {@inheritDoc}
785     * @see javax.swing.text.html.HTML.Tag#ADDRESS
786     */
787    @Override
788    public void address(SinkEventAttributes attributes) {
789        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
790
791        writeStartTag(HtmlMarkup.ADDRESS, atts);
792    }
793
794    /**
795     * {@inheritDoc}
796     * @see javax.swing.text.html.HTML.Tag#ADDRESS
797     */
798    @Override
799    public void address_() {
800        writeEndTag(HtmlMarkup.ADDRESS);
801    }
802
803    /**
804     * {@inheritDoc}
805     * @see javax.swing.text.html.HTML.Tag#BLOCKQUOTE
806     */
807    @Override
808    public void blockquote(SinkEventAttributes attributes) {
809        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
810
811        writeStartTag(HtmlMarkup.BLOCKQUOTE, atts);
812    }
813
814    /**
815     * {@inheritDoc}
816     * @see javax.swing.text.html.HTML.Tag#BLOCKQUOTE
817     */
818    @Override
819    public void blockquote_() {
820        writeEndTag(HtmlMarkup.BLOCKQUOTE);
821    }
822
823    /**
824     * {@inheritDoc}
825     * @see javax.swing.text.html.HTML.Tag#DIV
826     */
827    @Override
828    public void division(SinkEventAttributes attributes) {
829        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
830
831        writeStartTag(HtmlMarkup.DIV, atts);
832    }
833
834    /**
835     * {@inheritDoc}
836     * @see javax.swing.text.html.HTML.Tag#DIV
837     */
838    @Override
839    public void division_() {
840        writeEndTag(HtmlMarkup.DIV);
841    }
842
843    /**
844     * Depending on whether the decoration attribute is "source" or not, this leads
845     * to either emitting {@code <pre><code>} or just {@code <pre>}.
846     * No default classes are emitted but the given attributes are always added to the {@code pre} element only.
847     *
848     * {@inheritDoc}
849     * @see javax.swing.text.html.HTML.Tag#PRE
850     * @see javax.swing.text.html.HTML.Tag#CODE
851     */
852    @Override
853    public void verbatim(SinkEventAttributes attributes) {
854        if (paragraphFlag) {
855            // The content of element type "p" must match
856            // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong|
857            // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)".
858            paragraph_();
859        }
860
861        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_VERBATIM_ATTRIBUTES);
862
863        if (atts == null) {
864            atts = new SinkEventAttributeSet();
865        }
866
867        verbatimMode = VerbatimMode.ON;
868        if (atts.isDefined(SinkEventAttributes.DECORATION)) {
869            if ("source"
870                    .equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString())) {
871                verbatimMode = VerbatimMode.ON_WITH_CODE;
872            }
873        }
874
875        atts.removeAttribute(SinkEventAttributes.DECORATION);
876
877        writeStartTag(HtmlMarkup.PRE, atts);
878        if (verbatimMode == VerbatimMode.ON_WITH_CODE) {
879            writeStartTag(HtmlMarkup.CODE);
880        }
881    }
882
883    /**
884     * {@inheritDoc}
885     * @see javax.swing.text.html.HTML.Tag#CODE
886     * @see javax.swing.text.html.HTML.Tag#PRE
887     */
888    @Override
889    public void verbatim_() {
890        if (verbatimMode == VerbatimMode.ON_WITH_CODE) {
891            writeEndTag(HtmlMarkup.CODE);
892        }
893        writeEndTag(HtmlMarkup.PRE);
894
895        verbatimMode = VerbatimMode.OFF;
896    }
897
898    /**
899     * {@inheritDoc}
900     * @see javax.swing.text.html.HTML.Tag#HR
901     */
902    @Override
903    public void horizontalRule(SinkEventAttributes attributes) {
904        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_HR_ATTRIBUTES);
905
906        writeSimpleTag(HtmlMarkup.HR, atts);
907    }
908
909    /** {@inheritDoc} */
910    @Override
911    public void table(SinkEventAttributes attributes) {
912        this.tableContentWriterStack.addLast(new StringWriter());
913
914        if (paragraphFlag) {
915            // The content of element type "p" must match
916            // "(a|br|span|bdo|object|applet|img|map|iframe|tt|i|b|u|s|strike|big|small|font|basefont|em|strong|
917            // dfn|code|q|samp|kbd|var|cite|abbr|acronym|sub|sup|input|select|textarea|label|button|ins|del|script)".
918            paragraph_();
919        }
920
921        // start table with tableRows
922        if (attributes == null) {
923            this.tableAttributes = new SinkEventAttributeSet(0);
924        } else {
925            this.tableAttributes = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_TABLE_ATTRIBUTES);
926        }
927    }
928
929    /**
930     * {@inheritDoc}
931     * @see javax.swing.text.html.HTML.Tag#TABLE
932     */
933    @Override
934    public void table_() {
935        writeEndTag(HtmlMarkup.TABLE);
936
937        if (!this.cellCountStack.isEmpty()) {
938            this.cellCountStack.removeLast().toString();
939        }
940
941        if (this.tableContentWriterStack.isEmpty()) {
942            LOGGER.warn("{}No table content", getLocationLogPrefix());
943            return;
944        }
945
946        String tableContent = this.tableContentWriterStack.removeLast().toString();
947
948        String tableCaption = null;
949        if (!this.tableCaptionStack.isEmpty() && this.tableCaptionStack.getLast() != null) {
950            tableCaption = this.tableCaptionStack.removeLast();
951        }
952
953        if (tableCaption != null) {
954            // DOXIA-177
955            StringBuilder sb = new StringBuilder();
956            sb.append(tableContent, 0, tableContent.indexOf(Markup.GREATER_THAN) + 1);
957            sb.append(tableCaption);
958            sb.append(tableContent.substring(tableContent.indexOf(Markup.GREATER_THAN) + 1));
959
960            write(sb.toString());
961        } else {
962            write(tableContent);
963        }
964    }
965
966    /**
967     * The default style class is <code>bodyTable</code>.
968     *
969     * @param grid if {@code true} the style class {@code bodyTableBorder} will be added
970     *
971     * {@inheritDoc}
972     * @see javax.swing.text.html.HTML.Tag#TABLE
973     */
974    @Override
975    public void tableRows(int[] justification, boolean grid) {
976        setCellJustif(justification);
977
978        MutableAttributeSet att = new SinkEventAttributeSet();
979
980        String tableClass = "bodyTable" + (grid ? " bodyTableBorder" : "");
981        if (this.tableAttributes.isDefined(SinkEventAttributes.CLASS.toString())) {
982            tableClass += " "
983                    + this.tableAttributes
984                            .getAttribute(SinkEventAttributes.CLASS)
985                            .toString();
986        }
987
988        att.addAttribute(SinkEventAttributes.CLASS, tableClass);
989
990        att.addAttributes(this.tableAttributes);
991        this.tableAttributes.removeAttributes(this.tableAttributes);
992
993        writeStartTag(HtmlMarkup.TABLE, att);
994
995        this.cellCountStack.addLast(0);
996    }
997
998    /** {@inheritDoc} */
999    @Override
1000    public void tableRows_() {
1001        if (!this.cellJustifStack.isEmpty()) {
1002            this.cellJustifStack.removeLast();
1003        }
1004        if (!this.isCellJustifStack.isEmpty()) {
1005            this.isCellJustifStack.removeLast();
1006        }
1007
1008        this.evenTableRow = true;
1009    }
1010
1011    /**
1012     * Rows are striped with two colors by adding the class <code>a</code> or <code>b</code>. If the provided attributes
1013     * specify the <code>hidden</code> class, the next call to tableRow will set the same striping class as this one. A
1014     * style for <code>hidden</code> or <code>table.bodyTable hidden</code> may need to be provided to actually hide
1015     * such a row. {@inheritDoc}
1016     *
1017     * @see javax.swing.text.html.HTML.Tag#TR
1018     */
1019    @Override
1020    public void tableRow(SinkEventAttributes attributes) {
1021        MutableAttributeSet attrs = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_TR_ATTRIBUTES);
1022
1023        if (attrs == null) {
1024            attrs = new SinkEventAttributeSet();
1025        }
1026
1027        String rowClass = evenTableRow ? "a" : "b";
1028        boolean hidden = false;
1029        if (attrs.isDefined(SinkEventAttributes.CLASS.toString())) {
1030            String givenRowClass = (String) attrs.getAttribute(SinkEventAttributes.CLASS.toString());
1031            if (HIDDEN_CLASS_PATTERN.matcher(givenRowClass).matches()) {
1032                hidden = true;
1033            }
1034            rowClass += " " + givenRowClass;
1035        }
1036
1037        attrs.addAttribute(SinkEventAttributes.CLASS, rowClass);
1038
1039        writeStartTag(HtmlMarkup.TR, attrs);
1040
1041        if (!hidden) {
1042            evenTableRow = !evenTableRow;
1043        }
1044
1045        if (!this.cellCountStack.isEmpty()) {
1046            this.cellCountStack.removeLast();
1047            this.cellCountStack.addLast(0);
1048        }
1049    }
1050
1051    /**
1052     * {@inheritDoc}
1053     * @see javax.swing.text.html.HTML.Tag#TR
1054     */
1055    @Override
1056    public void tableRow_() {
1057        writeEndTag(HtmlMarkup.TR);
1058    }
1059
1060    /** {@inheritDoc} */
1061    @Override
1062    public void tableCell(SinkEventAttributes attributes) {
1063        tableCell(false, attributes);
1064    }
1065
1066    /** {@inheritDoc} */
1067    @Override
1068    public void tableHeaderCell(SinkEventAttributes attributes) {
1069        tableCell(true, attributes);
1070    }
1071
1072    /**
1073     * @param headerRow true if it is an header row
1074     * @param attributes the cell attributes
1075     * @see javax.swing.text.html.HTML.Tag#TH
1076     * @see javax.swing.text.html.HTML.Tag#TD
1077     */
1078    private void tableCell(boolean headerRow, MutableAttributeSet attributes) {
1079        Tag t = (headerRow ? HtmlMarkup.TH : HtmlMarkup.TD);
1080
1081        if (!headerRow
1082                && cellCountStack != null
1083                && !cellCountStack.isEmpty()
1084                && cellJustifStack != null
1085                && !cellJustifStack.isEmpty()
1086                && getCellJustif() != null) {
1087            int cellCount = getCellCount();
1088            int[] cellJustif = getCellJustif();
1089            if (cellCount < cellJustif.length) {
1090                if (attributes == null) {
1091                    attributes = new SinkEventAttributeSet();
1092                }
1093                Map<Integer, String> hash = new HashMap<>();
1094                hash.put(Sink.JUSTIFY_CENTER, "center");
1095                hash.put(Sink.JUSTIFY_LEFT, "left");
1096                hash.put(Sink.JUSTIFY_RIGHT, "right");
1097
1098                String tdStyle = "text-align: " + hash.get(cellJustif[cellCount]) + ";";
1099                if (attributes.isDefined(SinkEventAttributes.STYLE)) {
1100                    tdStyle += " "
1101                            + attributes.getAttribute(SinkEventAttributes.STYLE).toString();
1102                }
1103
1104                attributes.addAttribute(SinkEventAttributes.STYLE, tdStyle);
1105            }
1106        }
1107
1108        if (attributes == null) {
1109            writeStartTag(t, null);
1110        } else {
1111            writeStartTag(t, SinkUtils.filterAttributes(attributes, SinkUtils.SINK_TD_ATTRIBUTES));
1112        }
1113    }
1114
1115    /** {@inheritDoc} */
1116    @Override
1117    public void tableCell_() {
1118        tableCell_(false);
1119    }
1120
1121    /** {@inheritDoc} */
1122    @Override
1123    public void tableHeaderCell_() {
1124        tableCell_(true);
1125    }
1126
1127    /**
1128     * Ends a table cell.
1129     *
1130     * @param headerRow true if it is an header row
1131     * @see javax.swing.text.html.HTML.Tag#TH
1132     * @see javax.swing.text.html.HTML.Tag#TD
1133     */
1134    private void tableCell_(boolean headerRow) {
1135        Tag t = (headerRow ? HtmlMarkup.TH : HtmlMarkup.TD);
1136
1137        writeEndTag(t);
1138
1139        if (!this.isCellJustifStack.isEmpty()
1140                && this.isCellJustifStack.getLast().equals(Boolean.TRUE)
1141                && !this.cellCountStack.isEmpty()) {
1142            int cellCount = Integer.parseInt(this.cellCountStack.removeLast().toString());
1143            this.cellCountStack.addLast(++cellCount);
1144        }
1145    }
1146
1147    /**
1148     * {@inheritDoc}
1149     * @see javax.swing.text.html.HTML.Tag#CAPTION
1150     */
1151    @Override
1152    public void tableCaption(SinkEventAttributes attributes) {
1153        StringWriter sw = new StringWriter();
1154        this.tableCaptionWriterStack.addLast(sw);
1155        this.tableCaptionXMLWriterStack.addLast(new PrettyPrintXMLWriter(sw));
1156
1157        // TODO: tableCaption should be written before tableRows (DOXIA-177)
1158        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_SECTION_ATTRIBUTES);
1159
1160        writeStartTag(HtmlMarkup.CAPTION, atts);
1161    }
1162
1163    /**
1164     * {@inheritDoc}
1165     * @see javax.swing.text.html.HTML.Tag#CAPTION
1166     */
1167    @Override
1168    public void tableCaption_() {
1169        writeEndTag(HtmlMarkup.CAPTION);
1170
1171        if (!this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null) {
1172            this.tableCaptionStack.addLast(
1173                    this.tableCaptionWriterStack.removeLast().toString());
1174            this.tableCaptionXMLWriterStack.removeLast();
1175        }
1176    }
1177
1178    /**
1179     * {@inheritDoc}
1180     * @see javax.swing.text.html.HTML.Tag#A
1181     */
1182    @Override
1183    public void anchor(String name, SinkEventAttributes attributes) {
1184        Objects.requireNonNull(name, "name cannot be null");
1185
1186        if (headFlag) {
1187            return;
1188        }
1189
1190        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BASE_ATTRIBUTES);
1191
1192        String id = name;
1193
1194        if (!DoxiaUtils.isValidId(id)) {
1195            id = DoxiaUtils.encodeId(name);
1196
1197            LOGGER.debug("{}Modified invalid anchor name '{}' to '{}'", getLocationLogPrefix(), name, id);
1198        }
1199
1200        MutableAttributeSet att = new SinkEventAttributeSet();
1201        att.addAttribute(SinkEventAttributes.ID, id);
1202        att.addAttributes(atts);
1203
1204        writeStartTag(HtmlMarkup.A, att);
1205    }
1206
1207    /**
1208     * {@inheritDoc}
1209     * @see javax.swing.text.html.HTML.Tag#A
1210     */
1211    @Override
1212    public void anchor_() {
1213        if (!headFlag) {
1214            writeEndTag(HtmlMarkup.A);
1215        }
1216    }
1217
1218    /**
1219     * The default style class for external link is <code>externalLink</code>.
1220     *
1221     * {@inheritDoc}
1222     * @see javax.swing.text.html.HTML.Tag#A
1223     **/
1224    @Override
1225    public void link(String name, SinkEventAttributes attributes) {
1226        Objects.requireNonNull(name, "name cannot be null");
1227
1228        if (headFlag) {
1229            return;
1230        }
1231
1232        MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_LINK_ATTRIBUTES);
1233
1234        if (atts == null) {
1235            atts = new SinkEventAttributeSet();
1236        }
1237
1238        if (DoxiaUtils.isExternalLink(name)) {
1239            String linkClass = "externalLink";
1240            if (atts.isDefined(SinkEventAttributes.CLASS.toString())) {
1241                String givenLinkClass = (String) atts.getAttribute(SinkEventAttributes.CLASS.toString());
1242                linkClass += " " + givenLinkClass;
1243            }
1244
1245            atts.addAttribute(SinkEventAttributes.CLASS, linkClass);
1246        }
1247
1248        atts.addAttribute(SinkEventAttributes.HREF, HtmlTools.escapeHTML(name));
1249
1250        writeStartTag(HtmlMarkup.A, atts);
1251    }
1252
1253    /**
1254     * {@inheritDoc}
1255     * @see javax.swing.text.html.HTML.Tag#A
1256     */
1257    @Override
1258    public void link_() {
1259        if (!headFlag) {
1260            writeEndTag(HtmlMarkup.A);
1261        }
1262    }
1263
1264    private void inlineSemantics(SinkEventAttributes attributes, String semantic, List<Tag> tags, Tag tag) {
1265        if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, semantic)) {
1266            SinkEventAttributes attributesNoSemantics = (SinkEventAttributes) attributes.copyAttributes();
1267            attributesNoSemantics.removeAttribute(SinkEventAttributes.SEMANTICS);
1268            writeStartTag(tag, attributesNoSemantics);
1269            tags.add(0, tag);
1270        }
1271    }
1272
1273    /** {@inheritDoc} */
1274    @Override
1275    public void inline(SinkEventAttributes attributes) {
1276        if (!headFlag) {
1277            List<Tag> tags = new ArrayList<>();
1278
1279            if (attributes != null) {
1280                inlineSemantics(attributes, "emphasis", tags, HtmlMarkup.EM);
1281                inlineSemantics(attributes, "strong", tags, HtmlMarkup.STRONG);
1282                inlineSemantics(attributes, "small", tags, HtmlMarkup.SMALL);
1283                inlineSemantics(attributes, "line-through", tags, HtmlMarkup.S);
1284                inlineSemantics(attributes, "citation", tags, HtmlMarkup.CITE);
1285                inlineSemantics(attributes, "quote", tags, HtmlMarkup.Q);
1286                inlineSemantics(attributes, "definition", tags, HtmlMarkup.DFN);
1287                inlineSemantics(attributes, "abbreviation", tags, HtmlMarkup.ABBR);
1288                inlineSemantics(attributes, "italic", tags, HtmlMarkup.I);
1289                inlineSemantics(attributes, "bold", tags, HtmlMarkup.B);
1290                inlineSemantics(attributes, "code", tags, HtmlMarkup.CODE);
1291                inlineSemantics(attributes, "variable", tags, HtmlMarkup.VAR);
1292                inlineSemantics(attributes, "sample", tags, HtmlMarkup.SAMP);
1293                inlineSemantics(attributes, "keyboard", tags, HtmlMarkup.KBD);
1294                inlineSemantics(attributes, "superscript", tags, HtmlMarkup.SUP);
1295                inlineSemantics(attributes, "subscript", tags, HtmlMarkup.SUB);
1296                inlineSemantics(attributes, "annotation", tags, HtmlMarkup.U);
1297                inlineSemantics(attributes, "highlight", tags, HtmlMarkup.MARK);
1298                inlineSemantics(attributes, "ruby", tags, HtmlMarkup.RUBY);
1299                inlineSemantics(attributes, "rubyBase", tags, HtmlMarkup.RB);
1300                inlineSemantics(attributes, "rubyText", tags, HtmlMarkup.RT);
1301                inlineSemantics(attributes, "rubyTextContainer", tags, HtmlMarkup.RTC);
1302                inlineSemantics(attributes, "rubyParentheses", tags, HtmlMarkup.RP);
1303                inlineSemantics(attributes, "bidirectionalIsolation", tags, HtmlMarkup.BDI);
1304                inlineSemantics(attributes, "bidirectionalOverride", tags, HtmlMarkup.BDO);
1305                inlineSemantics(attributes, "phrase", tags, HtmlMarkup.SPAN);
1306                inlineSemantics(attributes, "insert", tags, HtmlMarkup.INS);
1307                inlineSemantics(attributes, "delete", tags, HtmlMarkup.DEL);
1308            }
1309
1310            inlineStack.push(tags);
1311        }
1312    }
1313
1314    /** {@inheritDoc} */
1315    @Override
1316    public void inline_() {
1317        if (!headFlag) {
1318            for (Tag tag : inlineStack.pop()) {
1319                writeEndTag(tag);
1320            }
1321        }
1322    }
1323
1324    /**
1325     * {@inheritDoc}
1326     * @see javax.swing.text.html.HTML.Tag#I
1327     */
1328    @Override
1329    public void italic() {
1330        inline(SinkEventAttributeSet.Semantics.ITALIC);
1331    }
1332
1333    /**
1334     * {@inheritDoc}
1335     * @see javax.swing.text.html.HTML.Tag#I
1336     */
1337    @Override
1338    public void italic_() {
1339        inline_();
1340    }
1341
1342    /**
1343     * {@inheritDoc}
1344     * @see javax.swing.text.html.HTML.Tag#B
1345     */
1346    @Override
1347    public void bold() {
1348        inline(SinkEventAttributeSet.Semantics.BOLD);
1349    }
1350
1351    /**
1352     * {@inheritDoc}
1353     * @see javax.swing.text.html.HTML.Tag#B
1354     */
1355    @Override
1356    public void bold_() {
1357        inline_();
1358    }
1359
1360    /**
1361     * {@inheritDoc}
1362     * @see javax.swing.text.html.HTML.Tag#CODE
1363     */
1364    @Override
1365    public void monospaced() {
1366        inline(SinkEventAttributeSet.Semantics.CODE);
1367    }
1368
1369    /**
1370     * {@inheritDoc}
1371     * @see javax.swing.text.html.HTML.Tag#CODE
1372     */
1373    @Override
1374    public void monospaced_() {
1375        inline_();
1376    }
1377
1378    /**
1379     * {@inheritDoc}
1380     * @see javax.swing.text.html.HTML.Tag#BR
1381     */
1382    @Override
1383    public void lineBreak(SinkEventAttributes attributes) {
1384        if (headFlag || isVerbatim()) {
1385            getTextBuffer().append(EOL);
1386        } else {
1387            MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES);
1388
1389            writeSimpleTag(HtmlMarkup.BR, atts);
1390        }
1391    }
1392
1393    /** {@inheritDoc} */
1394    @Override
1395    public void lineBreakOpportunity(SinkEventAttributes attributes) {
1396        if (!headFlag && !isVerbatim()) {
1397            MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES);
1398
1399            writeSimpleTag(HtmlMarkup.WBR, atts);
1400        }
1401    }
1402
1403    /** {@inheritDoc} */
1404    @Override
1405    public void pageBreak() {
1406        comment(" PB ");
1407    }
1408
1409    /** {@inheritDoc} */
1410    @Override
1411    public void nonBreakingSpace() {
1412        if (headFlag) {
1413            getTextBuffer().append(' ');
1414        } else {
1415            write("&#160;");
1416        }
1417    }
1418
1419    /** {@inheritDoc} */
1420    @Override
1421    public void text(String text, SinkEventAttributes attributes) {
1422        if (attributes != null) {
1423            inline(attributes);
1424        }
1425        if (headFlag) {
1426            getTextBuffer().append(text);
1427        } else if (isVerbatim()) {
1428            verbatimContent(text);
1429        } else {
1430            content(text);
1431        }
1432        if (attributes != null) {
1433            inline_();
1434        }
1435    }
1436
1437    /** {@inheritDoc} */
1438    @Override
1439    public void rawText(String text) {
1440        if (headFlag) {
1441            getTextBuffer().append(text);
1442        } else {
1443            write(text);
1444        }
1445    }
1446
1447    /** {@inheritDoc} */
1448    @Override
1449    public void comment(String comment) {
1450        if (comment != null) {
1451            final String originalComment = comment;
1452
1453            // http://www.w3.org/TR/2000/REC-xml-20001006#sec-comments
1454            while (comment.contains("--")) {
1455                comment = comment.replace("--", "- -");
1456            }
1457
1458            if (comment.endsWith("-")) {
1459                comment += " ";
1460            }
1461
1462            if (!originalComment.equals(comment)) {
1463                LOGGER.warn(
1464                        "{}Modified invalid comment '{}' to '{}'", getLocationLogPrefix(), originalComment, comment);
1465            }
1466
1467            final StringBuilder buffer = new StringBuilder(comment.length() + 7);
1468
1469            buffer.append(LESS_THAN).append(BANG).append(MINUS).append(MINUS);
1470            buffer.append(comment);
1471            buffer.append(MINUS).append(MINUS).append(GREATER_THAN);
1472
1473            write(buffer.toString());
1474        }
1475    }
1476
1477    /**
1478     * {@inheritDoc}
1479     *
1480     * Add an unknown event.
1481     * This can be used to generate html tags for which no corresponding sink event exists.
1482     *
1483     * <p>
1484     * If {@link org.apache.maven.doxia.util.HtmlTools#getHtmlTag(String) HtmlTools.getHtmlTag(name)}
1485     * does not return null, the corresponding tag will be written.
1486     * </p>
1487     *
1488     * <p>For example, the div block</p>
1489     *
1490     * <pre>
1491     *  &lt;div class="detail" style="display:inline"&gt;text&lt;/div&gt;
1492     * </pre>
1493     *
1494     * <p>can be generated via the following event sequence:</p>
1495     *
1496     * <pre>
1497     *  SinkEventAttributeSet atts = new SinkEventAttributeSet();
1498     *  atts.addAttribute(SinkEventAttributes.CLASS, "detail");
1499     *  atts.addAttribute(SinkEventAttributes.STYLE, "display:inline");
1500     *  sink.unknown("div", new Object[]{new Integer(HtmlMarkup.TAG_TYPE_START)}, atts);
1501     *  sink.text("text");
1502     *  sink.unknown("div", new Object[]{new Integer(HtmlMarkup.TAG_TYPE_END)}, null);
1503     * </pre>
1504     *
1505     * @param name the name of the event. If this is not a valid xhtml tag name
1506     *      as defined in {@link org.apache.maven.doxia.markup.HtmlMarkup} then the event is ignored.
1507     * @param requiredParams If this is null or the first argument is not an Integer then the event is ignored.
1508     *      The first argument should indicate the type of the unknown event, its integer value should be one of
1509     *      {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_START TAG_TYPE_START},
1510     *      {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_END TAG_TYPE_END},
1511     *      {@link org.apache.maven.doxia.markup.HtmlMarkup#TAG_TYPE_SIMPLE TAG_TYPE_SIMPLE},
1512     *      {@link org.apache.maven.doxia.markup.HtmlMarkup#ENTITY_TYPE ENTITY_TYPE}, or
1513     *      {@link org.apache.maven.doxia.markup.HtmlMarkup#CDATA_TYPE CDATA_TYPE},
1514     *      otherwise the event will be ignored.
1515     * @param attributes a set of attributes for the event. May be null.
1516     *      The attributes will always be written, no validity check is performed.
1517     */
1518    @Override
1519    public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
1520        if (requiredParams == null || !(requiredParams[0] instanceof Integer)) {
1521            LOGGER.warn("{}No type information for unknown event '{}', ignoring!", getLocationLogPrefix(), name);
1522
1523            return;
1524        }
1525
1526        int tagType = (Integer) requiredParams[0];
1527
1528        if (tagType == ENTITY_TYPE) {
1529            rawText(name);
1530
1531            return;
1532        }
1533
1534        if (tagType == CDATA_TYPE) {
1535            rawText(EOL + "//<![CDATA[" + requiredParams[1] + "]]>" + EOL);
1536
1537            return;
1538        }
1539
1540        Tag tag = HtmlTools.getHtmlTag(name);
1541
1542        if (tag == null) {
1543            LOGGER.warn("[]No HTML tag found for unknown event '{}', ignoring!", getLocationLogPrefix(), name);
1544        } else {
1545            if (tagType == TAG_TYPE_SIMPLE) {
1546                writeSimpleTag(tag, escapeAttributeValues(attributes));
1547            } else if (tagType == TAG_TYPE_START) {
1548                writeStartTag(tag, escapeAttributeValues(attributes));
1549            } else if (tagType == TAG_TYPE_END) {
1550                writeEndTag(tag);
1551            } else {
1552                LOGGER.warn("{}No type information for unknown event '{}', ignoring!", getLocationLogPrefix(), name);
1553            }
1554        }
1555    }
1556
1557    private SinkEventAttributes escapeAttributeValues(SinkEventAttributes attributes) {
1558        SinkEventAttributeSet set = new SinkEventAttributeSet(attributes.getAttributeCount());
1559
1560        Enumeration<?> names = attributes.getAttributeNames();
1561
1562        while (names.hasMoreElements()) {
1563            Object name = names.nextElement();
1564
1565            set.addAttribute(name, escapeHTML(attributes.getAttribute(name).toString()));
1566        }
1567
1568        return set;
1569    }
1570
1571    /** {@inheritDoc} */
1572    @Override
1573    public void flush() {
1574        writer.flush();
1575    }
1576
1577    /** {@inheritDoc} */
1578    @Override
1579    public void close() {
1580        writer.close();
1581
1582        init();
1583    }
1584
1585    // ----------------------------------------------------------------------
1586    //
1587    // ----------------------------------------------------------------------
1588
1589    /**
1590     * Write HTML escaped text to output.
1591     *
1592     * @param text The text to write.
1593     */
1594    protected void content(String text) {
1595        // small hack due to DOXIA-314
1596        String txt = escapeHTML(text);
1597        txt = StringUtils.replace(txt, "&amp;#", "&#");
1598        write(txt);
1599    }
1600
1601    /**
1602     * Write HTML escaped text to output.
1603     *
1604     * @param text The text to write.
1605     */
1606    protected void verbatimContent(String text) {
1607        write(escapeHTML(text));
1608    }
1609
1610    /**
1611     * Forward to HtmlTools.escapeHTML(text).
1612     *
1613     * @param text the String to escape, may be null
1614     * @return the text escaped, "" if null String input
1615     * @see org.apache.maven.doxia.util.HtmlTools#escapeHTML(String)
1616     */
1617    protected static String escapeHTML(String text) {
1618        return HtmlTools.escapeHTML(text, false);
1619    }
1620
1621    /**
1622     * Forward to HtmlTools.encodeURL(text).
1623     *
1624     * @param text the String to encode, may be null.
1625     * @return the text encoded, null if null String input.
1626     * @see org.apache.maven.doxia.util.HtmlTools#encodeURL(String)
1627     */
1628    protected static String encodeURL(String text) {
1629        return HtmlTools.encodeURL(text);
1630    }
1631
1632    /** {@inheritDoc} */
1633    protected void write(String text) {
1634        if (!this.tableCaptionXMLWriterStack.isEmpty() && this.tableCaptionXMLWriterStack.getLast() != null) {
1635            this.tableCaptionXMLWriterStack.getLast().writeMarkup(unifyEOLs(text));
1636        } else if (!this.tableContentWriterStack.isEmpty() && this.tableContentWriterStack.getLast() != null) {
1637            this.tableContentWriterStack.getLast().write(unifyEOLs(text));
1638        } else {
1639            writer.write(unifyEOLs(text));
1640        }
1641    }
1642
1643    /** {@inheritDoc} */
1644    @Override
1645    protected void writeStartTag(Tag t, MutableAttributeSet att, boolean isSimpleTag) {
1646        if (this.tableCaptionXMLWriterStack.isEmpty()) {
1647            super.writeStartTag(t, att, isSimpleTag);
1648        } else {
1649            String tag = (getNameSpace() != null ? getNameSpace() + ":" : "") + t.toString();
1650            this.tableCaptionXMLWriterStack.getLast().startElement(tag);
1651
1652            if (att != null) {
1653                Enumeration<?> names = att.getAttributeNames();
1654                while (names.hasMoreElements()) {
1655                    Object key = names.nextElement();
1656                    Object value = att.getAttribute(key);
1657
1658                    this.tableCaptionXMLWriterStack.getLast().addAttribute(key.toString(), value.toString());
1659                }
1660            }
1661
1662            if (isSimpleTag) {
1663                this.tableCaptionXMLWriterStack.getLast().endElement();
1664            }
1665        }
1666    }
1667
1668    /** {@inheritDoc} */
1669    @Override
1670    protected void writeEndTag(Tag t) {
1671        if (this.tableCaptionXMLWriterStack.isEmpty()) {
1672            super.writeEndTag(t);
1673        } else {
1674            this.tableCaptionXMLWriterStack.getLast().endElement();
1675        }
1676    }
1677}