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