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 java.io.PrintWriter;
022import java.io.Writer;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Queue;
030import java.util.function.UnaryOperator;
031import java.util.stream.Collectors;
032
033import org.apache.commons.lang3.StringUtils;
034import org.apache.maven.doxia.sink.Sink;
035import org.apache.maven.doxia.sink.SinkEventAttributes;
036import org.apache.maven.doxia.sink.impl.AbstractTextSink;
037import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
038import org.apache.maven.doxia.util.HtmlTools;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * Markdown generator implementation.
044 * <br>
045 * <b>Note</b>: The encoding used is UTF-8.
046 */
047public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup {
048    private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
049
050    // ----------------------------------------------------------------------
051    // Instance fields
052    // ----------------------------------------------------------------------
053
054    /**  A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>.
055     * The content of this buffer is already escaped. */
056    private StringBuilder buffer;
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 writer to use. */
083    private final PrintWriter writer;
084
085    /** A temporary writer used to buffer the last two lines */
086    private final LastTwoLinesBufferingWriter bufferingWriter;
087
088    /** Keep track of end markup for inline events. */
089    protected Queue<Queue<String>> inlineStack = Collections.asLifoQueue(new LinkedList<>());
090
091    /** The context of the surrounding elements as stack (LIFO) */
092    protected Queue<ElementContext> elementContextStack = Collections.asLifoQueue(new LinkedList<>());
093
094    private String figureSrc;
095
096    /** Most important contextual metadata (of the surrounding element) */
097    enum ElementContext {
098        HEAD("head", Type.GENERIC_CONTAINER, null, true),
099        BODY("body", Type.GENERIC_CONTAINER, MarkdownSink::escapeMarkdown),
100        // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here
101        FIGURE("", Type.INLINE, MarkdownSink::escapeMarkdown, true),
102        CODE_BLOCK("code block", Type.LEAF_BLOCK, null, false),
103        CODE_SPAN("code span", Type.INLINE, null),
104        TABLE_CAPTION("table caption", Type.INLINE, MarkdownSink::escapeMarkdown),
105        TABLE_CELL(
106                "table cell",
107                Type.LEAF_BLOCK,
108                MarkdownSink::escapeForTableCell,
109                true), // special type, as allows containing inlines, but not starting on a separate line
110        // same parameters as BODY but paragraphs inside list items are handled differently
111        LIST_ITEM("list item", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, INDENT),
112        BLOCKQUOTE("blockquote", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP);
113
114        final String name;
115
116        /**
117         * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a>
118         */
119        enum Type {
120            /**
121             * Container with no special meaning for (nested) child element contexts
122             */
123            GENERIC_CONTAINER,
124            /**
125             * Is supposed to start on a new line, and must have a prefix (for nested blocks)
126             */
127            CONTAINER_BLOCK,
128            /**
129             * Is supposed to start on a new line, must not contain any other block element context (neither leaf nor container)
130             */
131            LEAF_BLOCK,
132            /**
133             * Are not allowed to contain any other element context (i.e. leaf contexts), except for some other inlines (depends on the actual type)
134             */
135            INLINE
136        }
137        /**
138         * {@code true} if block element, otherwise {@code false} for inline elements
139         */
140        final Type type;
141
142        /**
143         * 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
144         */
145        final UnaryOperator<String> escapeFunction;
146
147        /**
148         * if {@code true} requires buffering any text appearing inside this context
149         */
150        final boolean requiresBuffering;
151
152        /**
153         * prefix to be used for a (nested) block elements inside the current container context (only not empty for {@link #type} being {@link Type#CONTAINER_BLOCK})
154         */
155        final String prefix;
156
157        /**
158         * Only relevant for block element, if set to {@code true} the element requires to be surrounded by blank lines.
159         */
160        final boolean requiresSurroundingByBlankLines;
161
162        ElementContext(String name, Type type, UnaryOperator<String> escapeFunction) {
163            this(name, type, escapeFunction, false);
164        }
165
166        ElementContext(String name, Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering) {
167            this(name, type, escapeFunction, requiresBuffering, "");
168        }
169
170        ElementContext(
171                String name,
172                Type type,
173                UnaryOperator<String> escapeFunction,
174                boolean requiresBuffering,
175                String prefix) {
176            this(name, type, escapeFunction, requiresBuffering, prefix, false);
177        }
178
179        ElementContext(
180                String name,
181                Type type,
182                UnaryOperator<String> escapeFunction,
183                boolean requiresBuffering,
184                String prefix,
185                boolean requiresSurroundingByBlankLines) {
186            this.name = name;
187            this.type = type;
188            this.escapeFunction = escapeFunction;
189            this.requiresBuffering = requiresBuffering;
190            if (type != Type.CONTAINER_BLOCK && prefix.length() != 0) {
191                throw new IllegalArgumentException("Only container blocks may define a prefix (for nesting)");
192            }
193            this.prefix = prefix;
194            this.requiresSurroundingByBlankLines = requiresSurroundingByBlankLines;
195        }
196
197        /**
198         * Must be called for each inline text to be emitted directly within this context (not relevant for nested context)
199         * @param text
200         * @return the escaped text (may be same as {@code text} when no escaping is necessary)
201         */
202        String escape(String text) {
203            // is escaping necessary at all?
204            if (escapeFunction == null) {
205                return text;
206            } else {
207                return escapeFunction.apply(text);
208            }
209        }
210
211        /**
212         *
213         * @return {@code true} for all block types, {@code false} otherwise
214         */
215        boolean isBlock() {
216            return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK;
217        }
218
219        /**
220         *
221         * @return {@code true} for all containers (allowing block elements as children), {@code false} otherwise
222         */
223        boolean isContainer() {
224            return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER;
225        }
226    }
227    // ----------------------------------------------------------------------
228    // Public protected methods
229    // ----------------------------------------------------------------------
230
231    /**
232     * Constructor, initialize the Writer and the variables.
233     *
234     * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
235     */
236    protected MarkdownSink(Writer writer) {
237        this.bufferingWriter = new LastTwoLinesBufferingWriter(writer);
238        this.writer = new PrintWriter(bufferingWriter);
239
240        init();
241    }
242
243    private void endContext(ElementContext expectedContext) {
244        ElementContext removedContext = elementContextStack.remove();
245        if (removedContext != expectedContext) {
246            throw new IllegalStateException("Unexpected context " + removedContext + ", expected " + expectedContext);
247        }
248        if (removedContext.isBlock()) {
249            endBlock(removedContext.requiresSurroundingByBlankLines);
250        }
251    }
252
253    private void startContext(ElementContext newContext) {
254        if (newContext.isBlock()) {
255            startBlock(newContext.requiresSurroundingByBlankLines);
256        }
257        elementContextStack.add(newContext);
258    }
259
260    /**
261     * Ensures that the {@link #writer} is currently at the beginning of a new line.
262     * Optionally writes a line separator to ensure that.
263     */
264    private void ensureBeginningOfLine() {
265        // make sure that we are at the start of a line without adding unnecessary blank lines
266        if (!bufferingWriter.isWriterAtStartOfNewLine()) {
267            writeUnescaped(EOL);
268        }
269    }
270
271    /**
272     * Ensures that the {@link #writer} is either at the beginning or preceded by a blank line.
273     * Optionally writes a blank line to ensure that.
274     */
275    private void ensureBlankLine() {
276        // prevent duplicate blank lines
277        if (!bufferingWriter.isWriterAfterBlankLine()) {
278            if (bufferingWriter.isWriterAtStartOfNewLine()) {
279                writeUnescaped(EOL);
280            } else {
281                writeUnescaped(BLANK_LINE);
282            }
283        }
284    }
285
286    private void startBlock(boolean requireBlankLine) {
287        if (requireBlankLine) {
288            ensureBlankLine();
289        } else {
290            ensureBeginningOfLine();
291        }
292        writeUnescaped(getContainerLinePrefixes());
293    }
294
295    private void endBlock(boolean requireBlankLine) {
296        if (requireBlankLine) {
297            ensureBlankLine();
298        } else {
299            ensureBeginningOfLine();
300        }
301    }
302
303    private String getContainerLinePrefixes() {
304        StringBuilder prefix = new StringBuilder();
305        elementContextStack.stream().filter(c -> c.prefix.length() > 0).forEachOrdered(c -> prefix.insert(0, c.prefix));
306        return prefix.toString();
307    }
308
309    /**
310     * Returns the buffer that holds the current text.
311     *
312     * @return A StringBuffer.
313     */
314    protected StringBuilder getBuffer() {
315        return buffer;
316    }
317
318    @Override
319    protected void init() {
320        super.init();
321
322        resetBuffer();
323
324        this.authors = new LinkedList<>();
325        this.title = null;
326        this.date = null;
327        this.linkName = null;
328        this.tableHeaderCellFlag = false;
329        this.cellCount = 0;
330        this.cellJustif = null;
331        this.elementContextStack.clear();
332        this.inlineStack.clear();
333        // always set a default context (at least for tests not emitting a body)
334        elementContextStack.add(ElementContext.BODY);
335    }
336
337    /**
338     * Reset the StringBuilder.
339     */
340    protected void resetBuffer() {
341        buffer = new StringBuilder();
342    }
343
344    @Override
345    public void head(SinkEventAttributes attributes) {
346        init();
347        // remove default body context here
348        endContext(ElementContext.BODY);
349        elementContextStack.add(ElementContext.HEAD);
350    }
351
352    @Override
353    public void head_() {
354        endContext(ElementContext.HEAD);
355        // only write head block if really necessary
356        if (title == null && authors.isEmpty() && date == null) {
357            return;
358        }
359        writeUnescaped(METADATA_MARKUP + EOL);
360        if (title != null) {
361            writeUnescaped("title: " + title + EOL);
362        }
363        if (!authors.isEmpty()) {
364            writeUnescaped("author: " + EOL);
365            for (String author : authors) {
366                writeUnescaped("  - " + author + EOL);
367            }
368        }
369        if (date != null) {
370            writeUnescaped("date: " + date + EOL);
371        }
372        writeUnescaped(METADATA_MARKUP + BLANK_LINE);
373    }
374
375    @Override
376    public void body(SinkEventAttributes attributes) {
377        elementContextStack.add(ElementContext.BODY);
378    }
379
380    @Override
381    public void body_() {
382        endContext(ElementContext.BODY);
383    }
384
385    @Override
386    public void title_() {
387        if (buffer.length() > 0) {
388            title = buffer.toString();
389            resetBuffer();
390        }
391    }
392
393    @Override
394    public void author_() {
395        if (buffer.length() > 0) {
396            authors.add(buffer.toString());
397            resetBuffer();
398        }
399    }
400
401    @Override
402    public void date_() {
403        if (buffer.length() > 0) {
404            date = buffer.toString();
405            resetBuffer();
406        }
407    }
408
409    @Override
410    public void sectionTitle(int level, SinkEventAttributes attributes) {
411        if (level > 0) {
412            writeUnescaped(StringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE);
413        }
414    }
415
416    @Override
417    public void sectionTitle_(int level) {
418        if (level > 0) {
419            ensureBlankLine(); // always end headings with blank line to increase compatibility with arbitrary MD
420            // editors
421        }
422    }
423
424    @Override
425    public void list_() {
426        ensureBlankLine();
427    }
428
429    @Override
430    public void listItem(SinkEventAttributes attributes) {
431        startContext(ElementContext.LIST_ITEM);
432        writeUnescaped(LIST_UNORDERED_ITEM_START_MARKUP);
433    }
434
435    @Override
436    public void listItem_() {
437        endContext(ElementContext.LIST_ITEM);
438    }
439
440    @Override
441    public void numberedList(int numbering, SinkEventAttributes attributes) {
442        // markdown only supports decimal numbering
443        if (numbering != NUMBERING_DECIMAL) {
444            LOGGER.warn(
445                    "{}Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style",
446                    getLocationLogPrefix(),
447                    NUMBERING_DECIMAL,
448                    numbering);
449        }
450    }
451
452    @Override
453    public void numberedList_() {
454        writeUnescaped(EOL);
455    }
456
457    @Override
458    public void numberedListItem(SinkEventAttributes attributes) {
459        startContext(ElementContext.LIST_ITEM);
460        writeUnescaped(LIST_ORDERED_ITEM_START_MARKUP);
461    }
462
463    @Override
464    public void numberedListItem_() {
465        listItem_(); // identical for both numbered and not numbered list item
466    }
467
468    @Override
469    public void definitionList(SinkEventAttributes attributes) {
470        LOGGER.warn(
471                "{}Definition list not natively supported in Markdown, rendering HTML instead", getLocationLogPrefix());
472        ensureBlankLine();
473        writeUnescaped("<dl>" + EOL);
474    }
475
476    @Override
477    public void definitionList_() {
478        writeUnescaped("</dl>" + BLANK_LINE);
479    }
480
481    @Override
482    public void definedTerm(SinkEventAttributes attributes) {
483        writeUnescaped("<dt>");
484    }
485
486    @Override
487    public void definedTerm_() {
488        writeUnescaped("</dt>" + EOL);
489    }
490
491    @Override
492    public void definition(SinkEventAttributes attributes) {
493        writeUnescaped("<dd>");
494    }
495
496    @Override
497    public void definition_() {
498        writeUnescaped("</dd>" + EOL);
499    }
500
501    @Override
502    public void pageBreak() {
503        LOGGER.warn("Ignoring unsupported page break in Markdown");
504    }
505
506    @Override
507    public void paragraph(SinkEventAttributes attributes) {
508        // ignore paragraphs outside container contexts
509        if (elementContextStack.element().isContainer()) {
510            ensureBlankLine();
511            writeUnescaped(getContainerLinePrefixes());
512        }
513    }
514
515    @Override
516    public void paragraph_() {
517        // ignore paragraphs outside container contexts
518        if (elementContextStack.element().isContainer()) {
519            ensureBlankLine();
520        }
521    }
522
523    @Override
524    public void verbatim(SinkEventAttributes attributes) {
525        // always assume is supposed to be monospaced (i.e. emitted inside a <pre><code>...</code></pre>)
526        startContext(ElementContext.CODE_BLOCK);
527        writeUnescaped(VERBATIM_START_MARKUP + EOL);
528        writeUnescaped(getContainerLinePrefixes());
529    }
530
531    @Override
532    public void verbatim_() {
533        ensureBeginningOfLine();
534        writeUnescaped(getContainerLinePrefixes());
535        writeUnescaped(VERBATIM_END_MARKUP + BLANK_LINE);
536        endContext(ElementContext.CODE_BLOCK);
537    }
538
539    @Override
540    public void blockquote(SinkEventAttributes attributes) {
541        startContext(ElementContext.BLOCKQUOTE);
542        writeUnescaped(BLOCKQUOTE_START_MARKUP);
543    }
544
545    @Override
546    public void blockquote_() {
547        endContext(ElementContext.BLOCKQUOTE);
548    }
549
550    @Override
551    public void horizontalRule(SinkEventAttributes attributes) {
552        ensureBeginningOfLine();
553        writeUnescaped(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
554        writeUnescaped(getContainerLinePrefixes());
555    }
556
557    @Override
558    public void table(SinkEventAttributes attributes) {
559        ensureBlankLine();
560        writeUnescaped(getContainerLinePrefixes());
561    }
562
563    @Override
564    public void tableRows(int[] justification, boolean grid) {
565        if (justification != null) {
566            cellJustif = Arrays.stream(justification).boxed().collect(Collectors.toCollection(ArrayList::new));
567        } else {
568            cellJustif = new ArrayList<>();
569        }
570        // grid flag is not supported
571        isFirstTableRow = true;
572    }
573
574    @Override
575    public void tableRows_() {
576        cellJustif = null;
577    }
578
579    @Override
580    public void tableRow(SinkEventAttributes attributes) {
581        cellCount = 0;
582    }
583
584    @Override
585    public void tableRow_() {
586        if (isFirstTableRow && !tableHeaderCellFlag) {
587            // emit empty table header as this is mandatory for GFM table extension
588            // (https://stackoverflow.com/a/17543474/5155923)
589            writeEmptyTableHeader();
590            writeTableDelimiterRow();
591            tableHeaderCellFlag = false;
592            isFirstTableRow = false;
593            // afterwards emit the first row
594        }
595
596        writeUnescaped(TABLE_ROW_PREFIX);
597
598        writeUnescaped(buffer.toString());
599
600        resetBuffer();
601
602        writeUnescaped(EOL);
603
604        if (isFirstTableRow) {
605            // emit delimiter row
606            writeTableDelimiterRow();
607            isFirstTableRow = false;
608        }
609
610        // only reset cell count if this is the last row
611        cellCount = 0;
612    }
613
614    private void writeEmptyTableHeader() {
615        writeUnescaped(TABLE_ROW_PREFIX);
616        for (int i = 0; i < cellCount; i++) {
617            writeUnescaped(StringUtils.repeat(String.valueOf(SPACE), 3) + TABLE_CELL_SEPARATOR_MARKUP);
618        }
619        writeUnescaped(EOL);
620        writeUnescaped(getContainerLinePrefixes());
621    }
622
623    /** Emit the delimiter row which determines the alignment */
624    private void writeTableDelimiterRow() {
625        writeUnescaped(TABLE_ROW_PREFIX);
626        int justification = Sink.JUSTIFY_LEFT;
627        for (int i = 0; i < cellCount; i++) {
628            // keep previous column's alignment in case too few are specified
629            if (cellJustif != null && cellJustif.size() > i) {
630                justification = cellJustif.get(i);
631            }
632            switch (justification) {
633                case Sink.JUSTIFY_RIGHT:
634                    writeUnescaped(TABLE_COL_RIGHT_ALIGNED_MARKUP);
635                    break;
636                case Sink.JUSTIFY_CENTER:
637                    writeUnescaped(TABLE_COL_CENTER_ALIGNED_MARKUP);
638                    break;
639                default:
640                    writeUnescaped(TABLE_COL_LEFT_ALIGNED_MARKUP);
641                    break;
642            }
643            writeUnescaped(TABLE_CELL_SEPARATOR_MARKUP);
644        }
645        writeUnescaped(EOL);
646    }
647
648    @Override
649    public void tableCell(SinkEventAttributes attributes) {
650        if (attributes != null) {
651            // evaluate alignment attributes
652            final int cellJustification;
653            if (attributes.containsAttributes(SinkEventAttributeSet.LEFT)) {
654                cellJustification = Sink.JUSTIFY_LEFT;
655            } else if (attributes.containsAttributes(SinkEventAttributeSet.RIGHT)) {
656                cellJustification = Sink.JUSTIFY_RIGHT;
657            } else if (attributes.containsAttributes(SinkEventAttributeSet.CENTER)) {
658                cellJustification = Sink.JUSTIFY_CENTER;
659            } else {
660                cellJustification = -1;
661            }
662            if (cellJustification > -1) {
663                if (cellJustif.size() > cellCount) {
664                    cellJustif.set(cellCount, cellJustification);
665                } else if (cellJustif.size() == cellCount) {
666                    cellJustif.add(cellJustification);
667                } else {
668                    // create non-existing justifications for preceding columns
669                    for (int precedingCol = cellJustif.size(); precedingCol < cellCount; precedingCol++) {
670                        cellJustif.add(Sink.JUSTIFY_LEFT);
671                    }
672                    cellJustif.add(cellJustification);
673                }
674            }
675        }
676        elementContextStack.add(ElementContext.TABLE_CELL);
677    }
678
679    @Override
680    public void tableHeaderCell(SinkEventAttributes attributes) {
681        tableCell(attributes);
682        tableHeaderCellFlag = true;
683    }
684
685    @Override
686    public void tableCell_() {
687        endTableCell();
688    }
689
690    @Override
691    public void tableHeaderCell_() {
692        endTableCell();
693    }
694
695    /**
696     * Ends a table cell.
697     */
698    private void endTableCell() {
699        endContext(ElementContext.TABLE_CELL);
700        buffer.append(TABLE_CELL_SEPARATOR_MARKUP);
701        cellCount++;
702    }
703
704    @Override
705    public void tableCaption(SinkEventAttributes attributes) {
706        elementContextStack.add(ElementContext.TABLE_CAPTION);
707    }
708
709    @Override
710    public void tableCaption_() {
711        endContext(ElementContext.TABLE_CAPTION);
712    }
713
714    @Override
715    public void figure(SinkEventAttributes attributes) {
716        figureSrc = null;
717        elementContextStack.add(ElementContext.FIGURE);
718    }
719
720    @Override
721    public void figureGraphics(String name, SinkEventAttributes attributes) {
722        figureSrc = escapeMarkdown(name);
723        // is it a standalone image (outside a figure)?
724        if (elementContextStack.peek() != ElementContext.FIGURE) {
725            Object alt = attributes.getAttribute(SinkEventAttributes.ALT);
726            if (alt == null) {
727                alt = "";
728            }
729            writeImage(escapeMarkdown(alt.toString()), name);
730        }
731    }
732
733    @Override
734    public void figure_() {
735        endContext(ElementContext.FIGURE);
736        writeImage(buffer.toString(), figureSrc);
737    }
738
739    private void writeImage(String alt, String src) {
740        writeUnescaped("![");
741        writeUnescaped(alt);
742        writeUnescaped("](" + src + ")");
743    }
744
745    /** {@inheritDoc} */
746    public void anchor(String name, SinkEventAttributes attributes) {
747        // write(ANCHOR_START_MARKUP + name);
748        // TODO get implementation from Xhtml5 base sink
749    }
750
751    @Override
752    public void anchor_() {
753        // write(ANCHOR_END_MARKUP);
754    }
755
756    /** {@inheritDoc} */
757    public void link(String name, SinkEventAttributes attributes) {
758        writeUnescaped(LINK_START_1_MARKUP);
759        linkName = name;
760    }
761
762    @Override
763    public void link_() {
764        writeUnescaped(LINK_START_2_MARKUP);
765        text(linkName.startsWith("#") ? linkName.substring(1) : linkName);
766        writeUnescaped(LINK_END_MARKUP);
767        linkName = null;
768    }
769
770    @Override
771    public void inline(SinkEventAttributes attributes) {
772        Queue<String> endMarkups = Collections.asLifoQueue(new LinkedList<>());
773
774        if (attributes != null
775                && elementContextStack.element() != ElementContext.CODE_BLOCK
776                && elementContextStack.element() != ElementContext.CODE_SPAN) {
777            // code excludes other styles in markdown
778            if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code")
779                    || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "monospaced")
780                    || attributes.containsAttribute(SinkEventAttributes.STYLE, "monospaced")) {
781                writeUnescaped(MONOSPACED_START_MARKUP);
782                endMarkups.add(MONOSPACED_END_MARKUP);
783                elementContextStack.add(ElementContext.CODE_SPAN);
784            } else {
785                // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>"
786                if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "em")
787                        || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "italic")
788                        || attributes.containsAttribute(SinkEventAttributes.STYLE, "italic")) {
789                    writeUnescaped(ITALIC_START_MARKUP);
790                    endMarkups.add(ITALIC_END_MARKUP);
791                }
792                // in XHTML "<strong>" is used, but some tests still rely on the outdated "<bold>"
793                if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "strong")
794                        || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "bold")
795                        || attributes.containsAttribute(SinkEventAttributes.STYLE, "bold")) {
796                    writeUnescaped(BOLD_START_MARKUP);
797                    endMarkups.add(BOLD_END_MARKUP);
798                }
799            }
800        }
801        inlineStack.add(endMarkups);
802    }
803
804    @Override
805    public void inline_() {
806        for (String endMarkup : inlineStack.remove()) {
807            if (endMarkup.equals(MONOSPACED_END_MARKUP)) {
808                endContext(ElementContext.CODE_SPAN);
809            }
810            writeUnescaped(endMarkup);
811        }
812    }
813
814    @Override
815    public void italic() {
816        inline(SinkEventAttributeSet.Semantics.ITALIC);
817    }
818
819    @Override
820    public void italic_() {
821        inline_();
822    }
823
824    @Override
825    public void bold() {
826        inline(SinkEventAttributeSet.Semantics.BOLD);
827    }
828
829    @Override
830    public void bold_() {
831        inline_();
832    }
833
834    @Override
835    public void monospaced() {
836        inline(SinkEventAttributeSet.Semantics.CODE);
837    }
838
839    @Override
840    public void monospaced_() {
841        inline_();
842    }
843
844    @Override
845    public void lineBreak(SinkEventAttributes attributes) {
846        if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
847            writeUnescaped(EOL);
848        } else {
849            writeUnescaped("" + SPACE + SPACE + EOL);
850        }
851        writeUnescaped(getContainerLinePrefixes());
852    }
853
854    @Override
855    public void nonBreakingSpace() {
856        writeUnescaped(NON_BREAKING_SPACE_MARKUP);
857    }
858
859    @Override
860    public void text(String text, SinkEventAttributes attributes) {
861        if (attributes != null) {
862            inline(attributes);
863        }
864        ElementContext currentContext = elementContextStack.element();
865        if (currentContext == ElementContext.TABLE_CAPTION) {
866            // table caption cannot even be emitted via XHTML in markdown as there is no suitable location
867            LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix());
868        } else {
869            String unifiedText = currentContext.escape(unifyEOLs(text));
870            writeUnescaped(unifiedText);
871        }
872        if (attributes != null) {
873            inline_();
874        }
875    }
876
877    @Override
878    public void rawText(String text) {
879        writeUnescaped(text);
880    }
881
882    @Override
883    public void comment(String comment) {
884        rawText(COMMENT_START + comment + COMMENT_END);
885    }
886
887    /**
888     * {@inheritDoc}
889     *
890     * Unknown events just log a warning message but are ignored otherwise.
891     * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
892     */
893    @Override
894    public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
895        LOGGER.warn("{}Unknown Sink event '" + name + "', ignoring!", getLocationLogPrefix());
896    }
897
898    /**
899     *
900     * @return {@code true} if any of the parent contexts require buffering
901     */
902    private boolean requiresBuffering() {
903        return elementContextStack.stream().anyMatch(c -> c.requiresBuffering);
904    }
905
906    protected void writeUnescaped(String text) {
907        if (requiresBuffering()) {
908            buffer.append(text);
909        } else {
910            writer.write(text);
911        }
912    }
913
914    @Override
915    public void flush() {
916        writer.flush();
917    }
918
919    @Override
920    public void close() {
921        writer.close();
922
923        init();
924    }
925
926    // ----------------------------------------------------------------------
927    // Private methods
928    // ----------------------------------------------------------------------
929
930    /**
931     * First use XML escaping (leveraging the predefined entities, for browsers)
932     * afterwards escape special characters in a text with a leading backslash (for markdown parsers)
933     *
934     * <pre>
935     * \, `, *, _, {, }, [, ], (, ), #, +, -, ., !
936     * </pre>
937     *
938     * @param text the String to escape, may be null
939     * @return the text escaped, "" if null String input
940     * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a>
941     */
942    private static String escapeMarkdown(String text) {
943        if (text == null) {
944            return "";
945        }
946        text = HtmlTools.escapeHTML(text, true); // assume UTF-8 output, i.e. only use the mandatory XML entities
947        int length = text.length();
948        StringBuilder buffer = new StringBuilder(length);
949
950        for (int i = 0; i < length; ++i) {
951            char c = text.charAt(i);
952            switch (c) {
953                case '\\':
954                case '`':
955                case '*':
956                case '_':
957                case '{':
958                case '}':
959                case '[':
960                case ']':
961                case '(':
962                case ')':
963                case '#':
964                case '+':
965                case '-':
966                case '.':
967                case '!':
968                    buffer.append('\\');
969                    buffer.append(c);
970                    break;
971                default:
972                    buffer.append(c);
973            }
974        }
975
976        return buffer.toString();
977    }
978
979    /**
980     * Escapes the pipe character according to <a href="https://github.github.com/gfm/#tables-extension-">GFM Table Extension</a> in addition
981     * to the regular markdown escaping.
982     * @param text
983     * @return the escaped text
984     * @see {@link #escapeMarkdown(String)
985     */
986    private static String escapeForTableCell(String text) {
987
988        return escapeMarkdown(text).replace("|", "\\|");
989    }
990}