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.module.markdown;
020
021import javax.swing.text.AttributeSet;
022import javax.swing.text.MutableAttributeSet;
023
024import java.io.Writer;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Queue;
032import java.util.stream.Collectors;
033
034import org.apache.maven.doxia.sink.Sink;
035import org.apache.maven.doxia.sink.SinkEventAttributeSet;
036import org.apache.maven.doxia.sink.SinkEventAttributes;
037import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink;
038import org.apache.maven.doxia.util.DoxiaStringUtils;
039import org.apache.maven.doxia.util.DoxiaUtils;
040import org.apache.maven.doxia.util.HtmlTools;
041import org.jsoup.parser.Tag;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045/**
046 * Markdown generator implementation.
047 * <br>
048 * <b>Note</b>: The encoding used is UTF-8.
049 * Extends the Xhtml5 sink as in some context HTML needs to be emitted.
050 */
051public class MarkdownSink extends Xhtml5BaseSink implements MarkdownMarkup {
052    private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
053
054    // ----------------------------------------------------------------------
055    // Instance fields
056    // ----------------------------------------------------------------------
057
058    /** author. */
059    private Collection<String> authors;
060
061    /** title. */
062    private String title;
063
064    /** date. */
065    private String date;
066
067    /** linkName. */
068    private String linkName;
069
070    /** tableHeaderCellFlag, set to {@code true} for table rows containing at least one table header cell */
071    private boolean tableHeaderCellFlag;
072
073    /** number of cells in a table. */
074    private int cellCount;
075
076    /** justification of table cells per column. */
077    private List<Integer> cellJustif;
078
079    /** is header row */
080    private boolean isFirstTableRow;
081
082    /** The inner decorated writer to buffer the text of contexts requiring buffering. Writing to this and {@code bufferingWriter} has the same effect. */
083    private final BufferingStackWriter bufferingStackWriter;
084
085    /** The outer decorated writer taking care of remembering the last two written lines. Writing to this and {@code writer} has the same effect. */
086    private final LastTwoLinesAwareWriter lineAwareWriter;
087
088    private static final String USE_XHTML_SINK = "XhtmlSink";
089
090    /** Keep track of end markup for inline events. Special value  {@link #USE_XHTML_SINK} is used to indicate usage of the Xhtml5BaseSink.inline_()*/
091    protected Queue<Queue<String>> inlineStack;
092
093    /** The context of the surrounding elements as stack (LIFO) */
094    protected Queue<ElementContext> elementContextStack;
095
096    private String figureSrc;
097
098    /** flag if the current verbatim block added a HTML context or not */
099    private boolean isVerbatimHtmlContext;
100
101    @FunctionalInterface
102    interface TextEscapeFunction {
103        String escape(ElementContext context, LastTwoLinesAwareWriter writer, String text);
104    }
105    /** Most important contextual metadata (of elements). This contains information about necessary escaping rules, potential prefixes and newlines */
106    enum ElementContext {
107        ROOT_WITH_BUFFERING(
108                Type.GENERIC_CONTAINER,
109                true,
110                ElementContext::escapeMarkdown,
111                true), // only needs buffering until head()_ is called to make sure to emit metadata first
112        ROOT_WITHOUT_BUFFERING(
113                Type.GENERIC_CONTAINER,
114                true,
115                null,
116                false), // used after head()_/body() to prevent unnecessary buffering
117        HEAD(Type.GENERIC_CONTAINER, false, null, true),
118        BODY(Type.GENERIC_CONTAINER, true, ElementContext::escapeMarkdown),
119        // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here
120        FIGURE(Type.INLINE, false, ElementContext::escapeMarkdown, true),
121        HEADING(Type.LEAF_BLOCK, false, ElementContext::escapeMarkdown),
122        CODE_BLOCK(Type.LEAF_BLOCK, false, null),
123        CODE_SPAN(Type.INLINE, false, null, true),
124        TABLE(Type.CONTAINER_BLOCK, false, null, false, "", true),
125        TABLE_CAPTION(Type.INLINE, false, ElementContext::escapeMarkdown),
126        TABLE_ROW(Type.INLINE, false, null, true), // special handling of newlines
127        TABLE_CELL(
128                Type.INLINE,
129                false,
130                ElementContext::escapeForTableCell,
131                false), // special type, as allows containing inlines, but not starting on a separate line
132        // same parameters as BODY but paragraphs inside list items are handled differently
133        LIST_ITEM(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, INDENT),
134        BLOCKQUOTE(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP),
135        HTML_BLOCK(Type.GENERIC_CONTAINER, true, ElementContext::escapeHtml, false, "", false);
136
137        /**
138         * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a>
139         */
140        enum Type {
141            /**
142             * Container with no special meaning for (nested) child element contexts
143             */
144            GENERIC_CONTAINER,
145            /**
146             * Is supposed to start on a new line, and must have a prefix (for nested blocks)
147             */
148            CONTAINER_BLOCK,
149            /**
150             * Is supposed to start on a new line, must not contain any other block element context (neither leaf nor container)
151             */
152            LEAF_BLOCK,
153            /**
154             * Are not allowed to contain any other element context (i.e. leaf contexts), except for some other inlines (depends on the actual type)
155             */
156            INLINE
157        }
158        /**
159         * {@code true} if block element, otherwise {@code false} for inline elements
160         */
161        final Type type;
162
163        /**
164         * The function to call to escape the given text. The function is supposed to return the escaped text or return just the given text if no escaping is necessary in this context
165         */
166        final TextEscapeFunction escapeFunction;
167
168        /**
169         * if {@code true} requires buffering any text appearing inside this context
170         */
171        final boolean requiresBuffering;
172
173        /**
174         * prefix to be used for each line of (nested) block elements inside the current container context (only not empty for {@link #type} being {@link Type#CONTAINER_BLOCK})
175         */
176        final String prefix;
177
178        /**
179         * Only relevant for block element, if set to {@code true} the element requires to be surrounded by blank lines.
180         */
181        final boolean requiresSurroundingByBlankLines;
182
183        /**
184         * If markup linebreaks (i.e. insignificant linebreaks in the source) are allowed in this context.
185         * This is relevant for markdown as in some contexts (e.g. list items) linebreaks are always significant (while for HTML they wouldn't be)
186         */
187        final boolean allowsMarkupLinebreaks;
188
189        ElementContext(Type type, boolean allowsMarkupLinebreaks, TextEscapeFunction escapeFunction) {
190            this(type, allowsMarkupLinebreaks, escapeFunction, false);
191        }
192
193        ElementContext(
194                Type type,
195                boolean allowsMarkupLinebreaks,
196                TextEscapeFunction escapeFunction,
197                boolean requiresBuffering) {
198            this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, "");
199        }
200
201        ElementContext(
202                Type type,
203                boolean allowsMarkupLinebreaks,
204                TextEscapeFunction escapeFunction,
205                boolean requiresBuffering,
206                String prefix) {
207            this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, prefix, false);
208        }
209
210        ElementContext(
211                Type type,
212                boolean allowsMarkupLinebreaks,
213                TextEscapeFunction escapeFunction,
214                boolean requiresBuffering,
215                String prefix,
216                boolean requiresSurroundingByBlankLines) {
217            this.type = type;
218            this.allowsMarkupLinebreaks = allowsMarkupLinebreaks;
219            this.escapeFunction = escapeFunction;
220            this.requiresBuffering = requiresBuffering;
221            if (type != Type.CONTAINER_BLOCK && prefix.length() != 0) {
222                throw new IllegalArgumentException("Only container blocks may define a prefix (for nesting)");
223            }
224            this.prefix = prefix;
225            this.requiresSurroundingByBlankLines = requiresSurroundingByBlankLines;
226        }
227
228        /**
229         * Must be called for each inline text to be emitted directly within this context (not relevant for nested context)
230         * @param text
231         * @return the escaped text (may be same as {@code text} when no escaping is necessary)
232         */
233        String escape(LastTwoLinesAwareWriter writer, String text) {
234            // is escaping necessary at all?
235            if (escapeFunction == null) {
236                return text;
237            } else {
238                return escapeFunction.escape(this, writer, text);
239            }
240        }
241
242        /**
243         *
244         * @return {@code true} for all block types, {@code false} otherwise
245         */
246        boolean isBlock() {
247            return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK;
248        }
249
250        /**
251         *
252         * @return {@code true} if only HTML is allowed in this context
253         */
254        boolean isHtml() {
255            return this.equals(HTML_BLOCK);
256        }
257        /**
258         *
259         * @return {@code true} for all containers (allowing block elements as children), {@code false} otherwise
260         */
261        boolean isContainer() {
262            return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER;
263        }
264
265        public boolean isAllowsMarkupLinebreaks() {
266            return allowsMarkupLinebreaks;
267        }
268
269        /**
270         * First use XML escaping (leveraging the predefined entities, for browsers)
271         * afterwards escape special characters in a text with a leading backslash (for markdown parsers)
272         *
273         * <pre>
274         * \, `, *, _, {, }, [, ], (, ), #, +, -, ., !
275         * </pre>
276         *
277         * @param text the string to escape, may be null
278         * @return the text escaped, "" if null String input
279         * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a>
280         */
281        private String escapeMarkdown(LastTwoLinesAwareWriter writer, String text) {
282            if (text == null) {
283                return "";
284            }
285            text = escapeHtmlForMarkdown(text); // assume UTF-8 output, i.e. only use the mandatory XML entities
286            int length = text.length();
287            StringBuilder buffer = new StringBuilder(length);
288
289            for (int i = 0; i < length; ++i) {
290                char c = text.charAt(i);
291                switch (c) {
292                    case '\\':
293                    case '_':
294                    case '`':
295                    case '[':
296                    case ']':
297                    case '!':
298                        // always escape the previous characters as potentially everywhere relevant
299                        buffer.append(escapeMarkdown(c));
300                        break;
301                    case '*':
302                    case '+':
303                    case '-':
304                        // only relevant for unordered lists or horizontal rules
305                        if (isInBlankLine(buffer, writer)) {
306                            buffer.append(escapeMarkdown(c));
307                        } else {
308                            buffer.append(c);
309                        }
310                        break;
311                    case '=':
312                    case '#':
313                        if (this == HEADING || isInBlankLine(buffer, writer)) {
314                            buffer.append(escapeMarkdown(c));
315                        } else {
316                            buffer.append(c);
317                        }
318                        break;
319                    case '.':
320                        if (isAfterOnlyLeadingDigits(buffer, writer)) {
321                            buffer.append(escapeMarkdown(c));
322                        } else {
323                            buffer.append(c);
324                        }
325                        break;
326                    default:
327                        buffer.append(c);
328                }
329            }
330            return buffer.toString();
331        }
332
333        /**
334         * A {@code .} only needs escaping when it would turn leading digits into the marker of an
335         * ordered list item, i.e. when nothing but whitespace and digits precede it on the line.
336         * Elsewhere (as in version numbers such as {@code 1.6}) it is an ordinary character.
337         */
338        private static boolean isAfterOnlyLeadingDigits(StringBuilder buffer, LastTwoLinesAwareWriter writer) {
339            if (buffer.length() > 0) {
340                return writer.isInBlankLine() && LastTwoLinesAwareWriter.isOnlyLeadingDigits(buffer);
341            } else {
342                return writer.isAfterOnlyLeadingDigits();
343            }
344        }
345
346        private static boolean isInBlankLine(StringBuilder buffer, LastTwoLinesAwareWriter writer) {
347            if (DoxiaStringUtils.isBlank(buffer.toString())) {
348                return writer.isInBlankLine();
349            }
350            return false;
351        }
352
353        private static String escapeMarkdown(char c) {
354            return "\\" + c;
355        }
356
357        private String escapeHtml(LastTwoLinesAwareWriter writer, String text) {
358            return HtmlTools.escapeHTML(text, true);
359        }
360
361        /**
362         * Escapes only those characters which would otherwise be interpreted as raw HTML inside a
363         * Markdown document. Unlike {@link HtmlTools#escapeHTML(String, boolean)} this leaves
364         * {@code "} and {@code '} alone: Markdown is not XML, both are ordinary characters there,
365         * and turning them into {@code &quot;}/{@code &apos;} only makes the source unreadable.
366         *
367         * @param text the string to escape, may be null
368         * @return the escaped text, "" if null String input
369         */
370        private static String escapeHtmlForMarkdown(String text) {
371            if (text == null) {
372                return "";
373            }
374            int length = text.length();
375            StringBuilder buffer = new StringBuilder(length);
376            for (int i = 0; i < length; ++i) {
377                char c = text.charAt(i);
378                switch (c) {
379                    case '<':
380                        buffer.append("&lt;");
381                        break;
382                    case '>':
383                        buffer.append("&gt;");
384                        break;
385                    case '&':
386                        buffer.append("&amp;");
387                        break;
388                    default:
389                        buffer.append(c);
390                }
391            }
392            return buffer.toString();
393        }
394
395        /**
396         * Escapes the pipe character according to <a href="https://github.github.com/gfm/#tables-extension-">GFM Table Extension</a> in addition
397         * to the regular markdown escaping.
398         * @param text
399         * @return the escaped text
400         * @see {@link #escapeMarkdown(String)
401         */
402        private String escapeForTableCell(LastTwoLinesAwareWriter writer, String text) {
403            return escapeMarkdown(writer, text).replace("|", "\\|");
404        }
405    }
406    // ----------------------------------------------------------------------
407    // Public protected methods
408    // ----------------------------------------------------------------------
409
410    /**
411     * A link destination must not contain a space: everything from the space on is read as the
412     * link title instead, so {@code [text](#My Anchor)} does not resolve. An in-page anchor
413     * therefore gets the same encoding the anchor definition receives, and any other destination
414     * has its spaces percent-encoded.
415     *
416     * @param destination the link destination, may be null
417     * @return the destination, safe to write into a Markdown inline link
418     */
419    private static String encodeLinkDestination(String destination) {
420        if (destination == null || destination.indexOf(' ') < 0) {
421            return destination;
422        }
423        if (destination.charAt(0) == '#') {
424            return "#" + DoxiaUtils.encodeId(destination.substring(1));
425        }
426        return destination.replace(" ", "%20");
427    }
428
429    protected static MarkdownSink newInstance(Writer writer) {
430        BufferingStackWriter bufferingStackWriter = new BufferingStackWriter(writer);
431        LastTwoLinesAwareWriter lineAwareWriter = new LastTwoLinesAwareWriter(bufferingStackWriter);
432        return new MarkdownSink(lineAwareWriter, bufferingStackWriter);
433    }
434
435    /**
436     * Constructor, initialize the Writer and the variables.
437     *
438     * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
439     */
440    private MarkdownSink(LastTwoLinesAwareWriter lineAwareWriter, BufferingStackWriter bufferingStackWriter) {
441        super(lineAwareWriter);
442        this.lineAwareWriter = lineAwareWriter;
443        this.bufferingStackWriter = bufferingStackWriter;
444        initInternal();
445        setInsertNewline(
446                false); // we want to control newlines on our own to prevent (mostly to not break encapsulating markdown
447        // tables)
448    }
449
450    private void initInternal() {
451        this.authors = new LinkedList<>();
452        this.title = null;
453        this.date = null;
454        this.linkName = null;
455        this.tableHeaderCellFlag = false;
456        this.cellCount = 0;
457        this.cellJustif = null;
458        this.elementContextStack = Collections.asLifoQueue(new LinkedList<>());
459        this.inlineStack = Collections.asLifoQueue(new LinkedList<>());
460        startContext(ElementContext.ROOT_WITH_BUFFERING);
461    }
462
463    private void endContext(ElementContext expectedContext) {
464        ElementContext removedContext = elementContextStack.remove();
465        if (removedContext != expectedContext) {
466            throw new IllegalStateException("Unexpected context " + removedContext + ", expected " + expectedContext);
467        }
468        if (removedContext.isBlock()) {
469            endBlock(removedContext.requiresSurroundingByBlankLines
470                    || (isInListItem() && (removedContext == ElementContext.BLOCKQUOTE)
471                            || (removedContext == ElementContext.CODE_BLOCK)));
472        }
473        if (removedContext.requiresBuffering) {
474            // remove buffer from stack (assume it has been evaluated already)
475            bufferingStackWriter.removeBuffer();
476        }
477    }
478
479    private void startContext(ElementContext newContext) {
480        if (newContext.requiresBuffering) {
481            bufferingStackWriter.addBuffer();
482        }
483        if (newContext.isBlock()) {
484            // every block element within a list item must be surrounded by blank lines
485            startBlock(newContext.requiresSurroundingByBlankLines
486                    || (isInListItem() && (newContext == ElementContext.BLOCKQUOTE)
487                            || (newContext == ElementContext.CODE_BLOCK)));
488        }
489        elementContextStack.add(newContext);
490    }
491
492    private String toogleToRootContextWithoutBuffering(boolean dumpBuffer) {
493        final String buffer;
494        if (elementContextStack.element() == ElementContext.ROOT_WITH_BUFFERING) {
495            buffer = bufferingStackWriter.getCurrentBuffer().toString();
496            endContext(ElementContext.ROOT_WITH_BUFFERING);
497            if (dumpBuffer) {
498                write(buffer);
499            }
500            startContext(ElementContext.ROOT_WITHOUT_BUFFERING);
501        } else if (elementContextStack.element() != ElementContext.ROOT_WITHOUT_BUFFERING) {
502            throw new IllegalStateException("Unexpected context " + elementContextStack.element()
503                    + ", expected ROOT_WITH_BUFFERING or ROOT_WITHOUT_BUFFERING");
504        } else {
505            buffer = "";
506        }
507        return buffer;
508    }
509    /**
510     * Ensures that the {@link #writer} is currently at the beginning of a new line.
511     * Optionally writes a line separator to ensure that.
512     */
513    private void ensureBeginningOfLine() {
514        // make sure that we are at the start of a line without adding unnecessary blank lines
515        if (!lineAwareWriter.isWriterAtStartOfNewLine()) {
516            write(EOL);
517        }
518    }
519
520    /**
521     * Ensures that the {@link #writer} is preceded by a blank line.
522     * Optionally writes a blank line or just line delimiter to ensure that.
523     */
524    private void ensureBlankLine() {
525        // prevent duplicate blank lines
526        if (!lineAwareWriter.isWriterAfterBlankLine()) {
527            if (lineAwareWriter.isWriterAtStartOfNewLine()) {
528                write(EOL);
529            } else {
530                write(BLANK_LINE);
531            }
532        }
533    }
534
535    private void startBlock(boolean requireBlankLine) {
536        if (requireBlankLine) {
537            ensureBlankLine();
538        } else {
539            ensureBeginningOfLine();
540        }
541        write(getLinePrefix());
542    }
543
544    private void endBlock(boolean requireBlankLine) {
545        if (requireBlankLine) {
546            ensureBlankLine();
547        } else {
548            ensureBeginningOfLine();
549        }
550    }
551
552    /**
553     * @return the prefix to be used for each line in the current context (i.e. the prefix of the current container context and all its ancestors), may be empty
554     */
555    private String getLinePrefix() {
556        StringBuilder prefix = new StringBuilder();
557        elementContextStack.stream().filter(c -> c.prefix.length() > 0).forEachOrdered(c -> prefix.insert(0, c.prefix));
558        return prefix.toString();
559    }
560
561    private boolean isInListItem() {
562        return elementContextStack.stream()
563                .filter(c -> c == ElementContext.LIST_ITEM)
564                .findFirst()
565                .isPresent();
566    }
567
568    @Override
569    protected void init() {
570        super.init();
571        initInternal();
572    }
573
574    @Override
575    public void head(SinkEventAttributes attributes) {
576        startContext(ElementContext.HEAD);
577    }
578
579    @Override
580    public void head_() {
581        endContext(ElementContext.HEAD);
582        String priorHeadBuffer = toogleToRootContextWithoutBuffering(false);
583        // only write head block if really necessary
584        if (title == null && authors.isEmpty() && date == null) {
585            return;
586        }
587        write(METADATA_MARKUP + EOL);
588        if (title != null) {
589            write("title: " + title + EOL);
590        }
591        if (!authors.isEmpty()) {
592            write("author: " + EOL);
593            for (String author : authors) {
594                write("  - " + author + EOL);
595            }
596        }
597        if (date != null) {
598            write("date: " + date + EOL);
599        }
600        write(METADATA_MARKUP + BLANK_LINE);
601        write(priorHeadBuffer);
602    }
603
604    @Override
605    public void body(SinkEventAttributes attributes) {
606        toogleToRootContextWithoutBuffering(true);
607        startContext(ElementContext.BODY);
608    }
609
610    @Override
611    public void body_() {
612        endContext(ElementContext.BODY);
613    }
614
615    @Override
616    public void title_() {
617        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
618        if (!buffer.isEmpty()) {
619            this.title = buffer;
620        }
621    }
622
623    @Override
624    public void author_() {
625        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
626        if (!buffer.isEmpty()) {
627            authors.add(buffer);
628        }
629    }
630
631    @Override
632    public void date_() {
633        String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
634        if (!buffer.isEmpty()) {
635            date = buffer;
636        }
637    }
638
639    @Override
640    public void section(int level, SinkEventAttributes attributes) {
641        // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown
642    }
643
644    @Override
645    public void section_(int level) {
646        // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown
647    }
648
649    @Override
650    public void header(SinkEventAttributes attributes) {
651        // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown
652    }
653
654    @Override
655    public void header_() {
656        // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown
657    }
658
659    @Override
660    public void sectionTitle(int level, SinkEventAttributes attributes) {
661        startContext(ElementContext.HEADING);
662        if (level > 0) {
663            write(DoxiaStringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE);
664        }
665    }
666
667    @Override
668    public void sectionTitle_(int level) {
669        endContext(ElementContext.HEADING);
670        if (level > 0) {
671            ensureBlankLine(); // always end headings with blank line to increase compatibility with arbitrary MD
672            // editors
673        }
674    }
675
676    @Override
677    public void list(SinkEventAttributes attributes) {
678        if (elementContextStack.element().isHtml()) {
679            super.list(attributes);
680        }
681    }
682
683    @Override
684    public void list_() {
685        ensureBeginningOfLine();
686    }
687
688    @Override
689    public void listItem(SinkEventAttributes attributes) {
690        startContext(ElementContext.LIST_ITEM);
691        write(LIST_UNORDERED_ITEM_START_MARKUP);
692    }
693
694    @Override
695    public void listItem_() {
696        endContext(ElementContext.LIST_ITEM);
697    }
698
699    @Override
700    public void numberedList(int numbering, SinkEventAttributes attributes) {
701        // markdown only supports decimal numbering
702        if (numbering != NUMBERING_DECIMAL) {
703            LOGGER.warn(
704                    "{}Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style",
705                    getLocationLogPrefix(),
706                    NUMBERING_DECIMAL,
707                    numbering);
708        }
709    }
710
711    @Override
712    public void numberedList_() {
713        ensureBeginningOfLine();
714    }
715
716    @Override
717    public void numberedListItem(SinkEventAttributes attributes) {
718        startContext(ElementContext.LIST_ITEM);
719        write(LIST_ORDERED_ITEM_START_MARKUP);
720    }
721
722    @Override
723    public void numberedListItem_() {
724        listItem_(); // identical for both numbered and not numbered list item
725    }
726
727    @Override
728    public void definitionList(SinkEventAttributes attributes) {
729        LOGGER.warn(
730                "{}Definition list not natively supported in Markdown, rendering HTML instead", getLocationLogPrefix());
731        startContext(ElementContext.HTML_BLOCK);
732        write("<dl>" + EOL);
733    }
734
735    @Override
736    public void definitionList_() {
737        write("</dl>");
738        endContext(ElementContext.HTML_BLOCK);
739    }
740
741    @Override
742    public void definedTerm(SinkEventAttributes attributes) {
743        write("<dt>");
744    }
745
746    @Override
747    public void definedTerm_() {
748        write("</dt>" + EOL);
749    }
750
751    @Override
752    public void definition(SinkEventAttributes attributes) {
753        write("<dd>");
754    }
755
756    @Override
757    public void definition_() {
758        write("</dd>" + EOL);
759    }
760
761    @Override
762    public void pageBreak() {
763        LOGGER.warn("{}Ignoring unsupported page break in Markdown", getLocationLogPrefix());
764    }
765
766    @Override
767    public void paragraph(SinkEventAttributes attributes) {
768        // ignore paragraphs outside container contexts
769        if (elementContextStack.element().isContainer()) {
770            ensureBlankLine();
771            write(getLinePrefix());
772        } else {
773            LOGGER.warn(
774                    "{}Paragraphs outside of container contexts are not supported in Markdown, ignoring paragraph event in context {}",
775                    getLocationLogPrefix(),
776                    elementContextStack.element());
777        }
778    }
779
780    @Override
781    public void paragraph_() {
782        // ignore paragraphs outside container contexts
783        if (elementContextStack.element().isContainer()) {
784            ensureBlankLine();
785        }
786    }
787
788    @Override
789    public void verbatim(SinkEventAttributes attributes) {
790        if (!elementContextStack.element().isContainer()) {
791            // markdown doesn't allow block elements but one can instead rely on html blocks for this
792            startContext(ElementContext.HTML_BLOCK);
793            isVerbatimHtmlContext = true;
794        } else {
795            isVerbatimHtmlContext = false;
796        }
797
798        if (elementContextStack.element().isHtml()) {
799            super.verbatim(attributes);
800        } else {
801            // if no source attribute, then don't emit an info string
802            startContext(ElementContext.CODE_BLOCK);
803            write(VERBATIM_START_MARKUP);
804            if (attributes != null && attributes.containsAttributes(SinkEventAttributeSet.SOURCE)) {
805                write("unknown"); // unknown language
806            }
807            write(EOL);
808            write(getLinePrefix());
809        }
810    }
811
812    @Override
813    public void verbatim_() {
814        if (elementContextStack.element().isHtml()) {
815            super.verbatim_();
816            if (isVerbatimHtmlContext) {
817                endContext(ElementContext.HTML_BLOCK);
818                isVerbatimHtmlContext = false;
819            }
820        } else {
821            ensureBeginningOfLine();
822            write(getLinePrefix());
823            write(VERBATIM_END_MARKUP + BLANK_LINE);
824            endContext(ElementContext.CODE_BLOCK);
825        }
826    }
827
828    @Override
829    public void blockquote(SinkEventAttributes attributes) {
830        if (elementContextStack.element().isHtml()) {
831            super.blockquote(attributes);
832        } else {
833            startContext(ElementContext.BLOCKQUOTE);
834            write(BLOCKQUOTE_START_MARKUP);
835        }
836    }
837
838    @Override
839    public void blockquote_() {
840        if (elementContextStack.element().isHtml()) {
841            super.blockquote_();
842        } else {
843            endContext(ElementContext.BLOCKQUOTE);
844        }
845    }
846
847    @Override
848    public void horizontalRule(SinkEventAttributes attributes) {
849        ensureBeginningOfLine();
850        write(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
851        write(getLinePrefix());
852    }
853
854    @Override
855    public void table(SinkEventAttributes attributes) {
856        if (elementContextStack.element().isHtml()) {
857            super.table(attributes);
858        } else {
859            startContext(ElementContext.TABLE);
860        }
861    }
862
863    @Override
864    public void table_() {
865        if (elementContextStack.element().isHtml()) {
866            super.table_();
867        } else {
868            endContext(ElementContext.TABLE);
869        }
870    }
871
872    @Override
873    public void tableRows(int[] justification, boolean grid) {
874        if (elementContextStack.element().isHtml()) {
875            super.tableRows(justification, grid);
876        } else {
877            if (justification != null) {
878                cellJustif = Arrays.stream(justification).boxed().collect(Collectors.toCollection(ArrayList::new));
879            } else {
880                cellJustif = new ArrayList<>();
881            }
882            // grid flag is not supported
883            isFirstTableRow = true;
884        }
885    }
886
887    @Override
888    public void tableRows_() {
889        if (elementContextStack.element().isHtml()) {
890            super.tableRows_();
891        } else {
892            cellJustif = null;
893        }
894    }
895
896    @Override
897    public void tableRow(SinkEventAttributes attributes) {
898        if (elementContextStack.element().isHtml()) {
899            super.tableRow(attributes);
900        } else {
901            startContext(ElementContext.TABLE_ROW);
902            cellCount = 0;
903        }
904    }
905
906    @Override
907    public void tableRow_() {
908        if (elementContextStack.element().isHtml()) {
909            super.tableRow_();
910        } else {
911            String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
912            endContext(ElementContext.TABLE_ROW);
913            if (isFirstTableRow && !tableHeaderCellFlag) {
914                // emit empty table header as this is mandatory for GFM table extension
915                // (https://stackoverflow.com/a/17543474/5155923)
916                writeEmptyTableHeader();
917                writeTableDelimiterRow();
918                tableHeaderCellFlag = false;
919                isFirstTableRow = false;
920                // afterwards emit the first row
921            }
922            write(TABLE_ROW_PREFIX);
923            write(buffer);
924            write(EOL);
925            if (isFirstTableRow) {
926                // emit delimiter row
927                writeTableDelimiterRow();
928                isFirstTableRow = false;
929            }
930            // only reset cell count if this is the last row
931            cellCount = 0;
932        }
933    }
934
935    private void writeEmptyTableHeader() {
936        write(TABLE_ROW_PREFIX);
937        for (int i = 0; i < cellCount; i++) {
938            write(DoxiaStringUtils.repeat(String.valueOf(SPACE), 3) + TABLE_CELL_SEPARATOR_MARKUP);
939        }
940        write(EOL);
941        write(getLinePrefix());
942    }
943
944    /** Emit the delimiter row which determines the alignment */
945    private void writeTableDelimiterRow() {
946        write(TABLE_ROW_PREFIX);
947        int justification = Sink.JUSTIFY_DEFAULT;
948        for (int i = 0; i < cellCount; i++) {
949            // keep previous column's alignment in case too few are specified
950            if (cellJustif != null && cellJustif.size() > i) {
951                justification = cellJustif.get(i);
952            }
953            switch (justification) {
954                case Sink.JUSTIFY_RIGHT:
955                    write(TABLE_COL_RIGHT_ALIGNED_MARKUP);
956                    break;
957                case Sink.JUSTIFY_CENTER:
958                    write(TABLE_COL_CENTER_ALIGNED_MARKUP);
959                    break;
960                case Sink.JUSTIFY_LEFT:
961                    write(TABLE_COL_LEFT_ALIGNED_MARKUP);
962                    break;
963                default:
964                    write(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
965                    break;
966            }
967            write(TABLE_CELL_SEPARATOR_MARKUP);
968        }
969        write(EOL);
970    }
971
972    @Override
973    public void tableCell(SinkEventAttributes attributes) {
974        if (elementContextStack.element().isHtml()) {
975            super.tableCell(attributes);
976        } else {
977            startContext(ElementContext.TABLE_CELL);
978            if (attributes != null) {
979                // evaluate alignment attributes
980                final int cellJustification;
981                if (attributes.containsAttributes(SinkEventAttributeSet.LEFT)) {
982                    cellJustification = Sink.JUSTIFY_LEFT;
983                } else if (attributes.containsAttributes(SinkEventAttributeSet.RIGHT)) {
984                    cellJustification = Sink.JUSTIFY_RIGHT;
985                } else if (attributes.containsAttributes(SinkEventAttributeSet.CENTER)) {
986                    cellJustification = Sink.JUSTIFY_CENTER;
987                } else {
988                    cellJustification = -1;
989                }
990                if (cellJustification > -1) {
991                    if (cellJustif.size() > cellCount) {
992                        cellJustif.set(cellCount, cellJustification);
993                    } else if (cellJustif.size() == cellCount) {
994                        cellJustif.add(cellJustification);
995                    } else {
996                        // create non-existing justifications for preceding columns
997                        for (int precedingCol = cellJustif.size(); precedingCol < cellCount; precedingCol++) {
998                            cellJustif.add(Sink.JUSTIFY_DEFAULT);
999                        }
1000                        cellJustif.add(cellJustification);
1001                    }
1002                }
1003            }
1004        }
1005    }
1006
1007    @Override
1008    public void tableHeaderCell(SinkEventAttributes attributes) {
1009        if (elementContextStack.element().isHtml()) {
1010            super.tableHeaderCell(attributes);
1011        } else {
1012            tableCell(attributes);
1013            tableHeaderCellFlag = true;
1014        }
1015    }
1016
1017    @Override
1018    public void tableCell_() {
1019        if (elementContextStack.element().isHtml()) {
1020            super.tableCell_();
1021        } else {
1022            endTableCell();
1023        }
1024    }
1025
1026    @Override
1027    public void tableHeaderCell_() {
1028        if (elementContextStack.element().isHtml()) {
1029            super.tableHeaderCell_();
1030        } else {
1031            endTableCell();
1032        }
1033    }
1034
1035    /**
1036     * Ends a table cell.
1037     */
1038    private void endTableCell() {
1039        endContext(ElementContext.TABLE_CELL);
1040        write(TABLE_CELL_SEPARATOR_MARKUP);
1041        cellCount++;
1042    }
1043
1044    @Override
1045    public void tableCaption(SinkEventAttributes attributes) {
1046        if (elementContextStack.element().isHtml()) {
1047            super.tableCaption(attributes);
1048        } else {
1049            elementContextStack.add(ElementContext.TABLE_CAPTION);
1050        }
1051    }
1052
1053    @Override
1054    public void tableCaption_() {
1055        if (elementContextStack.element().isHtml()) {
1056            super.tableCaption_();
1057        } else {
1058            endContext(ElementContext.TABLE_CAPTION);
1059        }
1060    }
1061
1062    @Override
1063    public void figure(SinkEventAttributes attributes) {
1064        if (elementContextStack.element().isHtml()) {
1065            super.figure(attributes);
1066        } else {
1067            figureSrc = null;
1068            startContext(ElementContext.FIGURE);
1069        }
1070    }
1071
1072    @Override
1073    public void figureCaption(SinkEventAttributes attributes) {
1074        if (elementContextStack.element().isHtml()) {
1075            super.figureCaption(attributes);
1076        }
1077    }
1078
1079    @Override
1080    public void figureCaption_() {
1081        if (elementContextStack.element().isHtml()) {
1082            super.figureCaption_();
1083        }
1084    }
1085
1086    @Override
1087    public void figureGraphics(String name, SinkEventAttributes attributes) {
1088        if (elementContextStack.element().isHtml()) {
1089            super.figureGraphics(name, attributes);
1090        } else {
1091            figureSrc = name;
1092            // is it a standalone image (outside a figure)?
1093            if (elementContextStack.peek() != ElementContext.FIGURE) {
1094                Object alt = attributes.getAttribute(SinkEventAttributes.ALT);
1095                if (alt == null) {
1096                    alt = "";
1097                }
1098                writeImage(elementContextStack.element().escape(lineAwareWriter, alt.toString()), name);
1099            }
1100        }
1101    }
1102
1103    @Override
1104    public void figure_() {
1105        if (elementContextStack.element().isHtml()) {
1106            super.figure_();
1107        } else {
1108            String label = bufferingStackWriter.getCurrentBuffer().toString();
1109            endContext(ElementContext.FIGURE);
1110            writeImage(label, figureSrc);
1111        }
1112    }
1113
1114    private void writeImage(String alt, String src) {
1115        write("![");
1116        write(alt);
1117        write("](" + src + ")");
1118    }
1119
1120    public void anchor(String name, SinkEventAttributes attributes) {
1121        super.anchor(name, attributes);
1122        if (!elementContextStack.element().isHtml()) {
1123            // close anchor tag immediately otherwise markdown would not be allowed afterwards
1124            write("</a>");
1125        }
1126    }
1127
1128    @Override
1129    public void anchor_() {
1130        if (elementContextStack.element().isHtml()) {
1131            super.anchor_();
1132        } else {
1133            // anchor is always empty html element, i.e. already closed with anchor()
1134        }
1135    }
1136
1137    public void link(String name, SinkEventAttributes attributes) {
1138        if (elementContextStack.element().isHtml()) {
1139            super.link(name, attributes);
1140        } else {
1141            if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1142                LOGGER.warn("{}Ignoring unsupported link inside code block", getLocationLogPrefix());
1143            } else if (elementContextStack.element() == ElementContext.CODE_SPAN) {
1144                // emit link outside the code span, i.e. insert at the beginning of the buffer
1145                bufferingStackWriter.getCurrentBuffer().insert(0, LINK_START_1_MARKUP);
1146                linkName = name;
1147            } else {
1148                write(LINK_START_1_MARKUP);
1149                linkName = name;
1150            }
1151        }
1152    }
1153
1154    @Override
1155    public void link_() {
1156        if (elementContextStack.element().isHtml()) {
1157            super.link_();
1158        } else {
1159            if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1160                return;
1161            } else if (elementContextStack.element() == ElementContext.CODE_SPAN) {
1162                // defer emitting link end markup until inline_() is called
1163                StringBuilder linkEndMarkup = new StringBuilder();
1164                linkEndMarkup.append(LINK_START_2_MARKUP);
1165                linkEndMarkup.append(encodeLinkDestination(linkName));
1166                linkEndMarkup.append(LINK_END_MARKUP);
1167                Queue<String> endMarkups = new LinkedList<>(inlineStack.poll());
1168                endMarkups.add(linkEndMarkup.toString());
1169                inlineStack.add(endMarkups);
1170            } else {
1171                write(LINK_START_2_MARKUP + encodeLinkDestination(linkName) + LINK_END_MARKUP);
1172            }
1173            linkName = null;
1174        }
1175    }
1176
1177    @Override
1178    public void inline(SinkEventAttributes attributes) {
1179        Queue<String> endMarkups = Collections.asLifoQueue(new LinkedList<>());
1180        if (elementContextStack.element().isHtml()) {
1181            super.inline(attributes);
1182            endMarkups.add(USE_XHTML_SINK);
1183        } else {
1184            boolean requiresHtml = elementContextStack.element() == ElementContext.HTML_BLOCK;
1185            if (attributes != null
1186                    && elementContextStack.element() != ElementContext.CODE_BLOCK
1187                    && elementContextStack.element() != ElementContext.CODE_SPAN) {
1188                // code excludes other styles in markdown
1189                if (attributes.containsAttributes(SinkEventAttributeSet.Semantics.CODE)
1190                        || attributes.containsAttributes(SinkEventAttributeSet.Semantics.MONOSPACED)
1191                        || attributes.containsAttributes(SinkEventAttributeSet.MONOSPACED)) {
1192                    if (requiresHtml) {
1193                        write("<code>");
1194                        endMarkups.add("</code>");
1195                    } else {
1196                        startContext(ElementContext.CODE_SPAN);
1197                        write(MONOSPACED_START_MARKUP);
1198                        endMarkups.add(MONOSPACED_END_MARKUP);
1199                    }
1200                } else {
1201                    SinkEventAttributeSet remainingAttributes = new SinkEventAttributeSet(attributes);
1202                    // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>"
1203                    if (filterAttributes(
1204                            remainingAttributes,
1205                            SinkEventAttributeSet.Semantics.EMPHASIS,
1206                            SinkEventAttributeSet.Semantics.ITALIC,
1207                            SinkEventAttributeSet.ITALIC)) {
1208                        if (requiresHtml) {
1209                            write("<em>");
1210                            endMarkups.add("</em>");
1211                        } else {
1212                            write(ITALIC_START_MARKUP);
1213                            endMarkups.add(ITALIC_END_MARKUP);
1214                        }
1215                    }
1216                    // in XHTML "<strong>" is used, but some tests still rely on the outdated "<bold>"
1217                    if (filterAttributes(
1218                            remainingAttributes,
1219                            SinkEventAttributeSet.Semantics.STRONG,
1220                            SinkEventAttributeSet.Semantics.BOLD,
1221                            SinkEventAttributeSet.BOLD)) {
1222                        if (requiresHtml) {
1223                            write("<strong>");
1224                            endMarkups.add("</strong>");
1225                        } else {
1226                            write(BOLD_START_MARKUP);
1227                            endMarkups.add(BOLD_END_MARKUP);
1228                        }
1229                    }
1230                    // <del> is supported via GFM strikethrough extension
1231                    if (filterAttributes(remainingAttributes, SinkEventAttributeSet.Semantics.DELETE)) {
1232                        if (requiresHtml) {
1233                            write("<del>");
1234                            endMarkups.add("</del>");
1235                        } else {
1236                            write(STRIKETHROUGH_START_MARKUP);
1237                            endMarkups.add(STRIKETHROUGH_END_MARKUP);
1238                        }
1239                    }
1240                    if (!remainingAttributes.isEmpty()) {
1241                        // use HTML for other inline semantics which are not natively supported in Markdown (e.g.
1242                        // subscript, superscript, small, etc.)
1243                        super.inline(remainingAttributes);
1244                        endMarkups.add(USE_XHTML_SINK);
1245                    }
1246                }
1247            }
1248        }
1249        inlineStack.add(endMarkups);
1250    }
1251
1252    private static boolean filterAttributes(MutableAttributeSet attributes, AttributeSet... attributesToFilter) {
1253        boolean hasAny = false;
1254        for (AttributeSet attributeSet : attributesToFilter) {
1255            if (attributes.containsAttributes(attributeSet)) {
1256                hasAny = true;
1257                attributes.removeAttributes(attributeSet);
1258            }
1259        }
1260        return hasAny;
1261    }
1262
1263    @Override
1264    public void inline_() {
1265        for (String endMarkup : inlineStack.remove()) {
1266            if (USE_XHTML_SINK.equals(endMarkup)) {
1267                super.inline_();
1268            } else {
1269                if (endMarkup.equals(MONOSPACED_END_MARKUP)) {
1270                    String buffer = bufferingStackWriter.getCurrentBuffer().toString();
1271                    endContext(ElementContext.CODE_SPAN);
1272                    write(buffer);
1273                }
1274                write(endMarkup);
1275            }
1276        }
1277    }
1278
1279    @Override
1280    public void italic() {
1281        inline(SinkEventAttributeSet.Semantics.ITALIC);
1282    }
1283
1284    @Override
1285    public void italic_() {
1286        inline_();
1287    }
1288
1289    @Override
1290    public void bold() {
1291        inline(SinkEventAttributeSet.Semantics.BOLD);
1292    }
1293
1294    @Override
1295    public void bold_() {
1296        inline_();
1297    }
1298
1299    @Override
1300    public void monospaced() {
1301        inline(SinkEventAttributeSet.Semantics.CODE);
1302    }
1303
1304    @Override
1305    public void monospaced_() {
1306        inline_();
1307    }
1308
1309    @Override
1310    public void lineBreak(SinkEventAttributes attributes) {
1311        if (elementContextStack.element() == ElementContext.TABLE_CELL) {
1312            super.lineBreak(attributes);
1313        } else {
1314            if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1315                write(EOL);
1316            } else {
1317                write("" + SPACE + SPACE + EOL);
1318            }
1319            write(getLinePrefix());
1320        }
1321    }
1322
1323    @Override
1324    public void nonBreakingSpace() {
1325        write(NON_BREAKING_SPACE_MARKUP);
1326    }
1327
1328    @Override
1329    public void text(String text, SinkEventAttributes attributes) {
1330        if (elementContextStack.element().isHtml()) {
1331            super.text(text, attributes);
1332        } else {
1333            if (attributes != null) {
1334                inline(attributes);
1335            }
1336            ElementContext currentContext = elementContextStack.element();
1337            if (currentContext == ElementContext.TABLE_CAPTION) {
1338                // table caption cannot even be emitted via XHTML in markdown as there is no suitable location
1339                LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix());
1340            } else {
1341                String unifiedText = currentContext.escape(lineAwareWriter, unifyEOLs(text));
1342                // ignore newlines only, because those are emitted often coming from linebreaks in HTML with no
1343                // semantical
1344                // meaning
1345                if (!unifiedText.equals(EOL)) {
1346                    String prefix = getLinePrefix();
1347                    if (prefix.length() > 0) {
1348                        unifiedText = unifiedText.replaceAll(EOL, EOL + prefix);
1349                    }
1350                }
1351                write(unifiedText);
1352            }
1353            if (attributes != null) {
1354                inline_();
1355            }
1356        }
1357    }
1358
1359    @Override
1360    public void rawText(String text) {
1361        write(text);
1362    }
1363
1364    /** Table row-grouping tags without their own content: {@link #unknown(String, Object[], SinkEventAttributes)}
1365     * ignores these rather than rendering them as raw HTML. */
1366    private static final Collection<String> IGNORED_TABLE_GROUPING_TAGS =
1367            Collections.unmodifiableList(Arrays.asList("thead", "tbody", "tfoot"));
1368
1369    /**
1370     * {@inheritDoc}
1371     *
1372     * Delegates tag reconstruction and attribute escaping to
1373     * {@link Xhtml5BaseSink#unknown(String, Object[], SinkEventAttributes)}, so any tag recognized by
1374     * {@link HtmlTools#getHtmlTag(String)} (e.g. {@code <object>}, {@code <map>}/{@code <area>}) is emitted as raw
1375     * HTML instead of being silently dropped.
1376     * <p>
1377     * Unlike that XHTML sink, a {@link #TAG_TYPE_SIMPLE} event for a non-void element is never self-closed
1378     * ({@code <name/>}): HTML5 has no self-closing syntax outside the fixed list of void elements, so a
1379     * self-closed non-void tag such as {@code <object/>} would otherwise be re-parsed as an open tag that
1380     * swallows all following content as its own (hidden) fallback children. Such events are instead written as
1381     * an explicitly closed pair ({@code <name></name>}).
1382     * <p>
1383     * {@code <thead>}/{@code <tbody>}/{@code <tfoot>} are excluded: they carry no content of their own (Doxia's
1384     * table sink API already fully captures the table structure via {@code table()}/{@code tableRows()}/
1385     * {@code tableRow()}/{@code tableCell()}), and {@link MarkdownParser} fires these as unknown events for
1386     * every table it parses (they have no dedicated sink method). Rendering them as raw HTML would inject a
1387     * tag directly before the first cell of a row, which breaks GFM pipe-table syntax since a table row must
1388     * start at the beginning of its line.
1389     *
1390     * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
1391     */
1392    @Override
1393    public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
1394        if (IGNORED_TABLE_GROUPING_TAGS.contains(name)) {
1395            return;
1396        }
1397        if (requiredParams != null
1398                && requiredParams[0] instanceof Integer
1399                && (Integer) requiredParams[0] == TAG_TYPE_SIMPLE
1400                && HtmlTools.getHtmlTag(name) != null
1401                && !Tag.valueOf(name).isSelfClosing()) {
1402            super.unknown(name, new Object[] {TAG_TYPE_START}, attributes);
1403            super.unknown(name, new Object[] {TAG_TYPE_END}, null);
1404            return;
1405        }
1406        super.unknown(name, requiredParams, attributes);
1407    }
1408
1409    @Override
1410    public void markupLineBreak(int indentLevel) {
1411        // not allowed in all contexts
1412        if (elementContextStack.element().isAllowsMarkupLinebreaks()) {
1413            if (!lineAwareWriter.isWriterAfterBlankLine()) {
1414                super.markupLineBreak(indentLevel);
1415            }
1416        }
1417    }
1418
1419    @Override
1420    public void close() {
1421        toogleToRootContextWithoutBuffering(true);
1422        super.close();
1423    }
1424}