View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.module.markdown;
20  
21  import java.io.PrintWriter;
22  import java.io.Writer;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Queue;
30  import java.util.function.UnaryOperator;
31  import java.util.stream.Collectors;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.maven.doxia.sink.Sink;
35  import org.apache.maven.doxia.sink.SinkEventAttributes;
36  import org.apache.maven.doxia.sink.impl.AbstractTextSink;
37  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
38  import org.apache.maven.doxia.util.HtmlTools;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Markdown generator implementation.
44   * <br>
45   * <b>Note</b>: The encoding used is UTF-8.
46   */
47  public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup {
48      private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
49  
50      // ----------------------------------------------------------------------
51      // Instance fields
52      // ----------------------------------------------------------------------
53  
54      /**  A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>.
55       * The content of this buffer is already escaped. */
56      private StringBuilder buffer;
57  
58      /** author. */
59      private Collection<String> authors;
60  
61      /** title. */
62      private String title;
63  
64      /** date. */
65      private String date;
66  
67      /** linkName. */
68      private String linkName;
69  
70      /** tableHeaderCellFlag, set to {@code true} for table rows containing at least one table header cell */
71      private boolean tableHeaderCellFlag;
72  
73      /** number of cells in a table. */
74      private int cellCount;
75  
76      /** justification of table cells per column. */
77      private List<Integer> cellJustif;
78  
79      /** is header row */
80      private boolean isFirstTableRow;
81  
82      /** The writer to use. */
83      private final PrintWriter writer;
84  
85      /** A temporary writer used to buffer the last two lines */
86      private final LastTwoLinesBufferingWriter bufferingWriter;
87  
88      /** Keep track of end markup for inline events. */
89      protected Queue<Queue<String>> inlineStack = Collections.asLifoQueue(new LinkedList<>());
90  
91      /** The context of the surrounding elements as stack (LIFO) */
92      protected Queue<ElementContext> elementContextStack = Collections.asLifoQueue(new LinkedList<>());
93  
94      private String figureSrc;
95  
96      /** Most important contextual metadata (of the surrounding element) */
97      enum ElementContext {
98          HEAD("head", Type.GENERIC_CONTAINER, null, true),
99          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 }