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