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