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.Collection;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Stack;
28  
29  import org.apache.maven.doxia.sink.SinkEventAttributes;
30  import org.apache.maven.doxia.sink.impl.AbstractTextSink;
31  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
32  import org.codehaus.plexus.util.StringUtils;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  
36  /**
37   * Markdown generator implementation.
38   * <br>
39   * <b>Note</b>: The encoding used is UTF-8.
40   */
41  public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup {
42      private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
43  
44      // ----------------------------------------------------------------------
45      // Instance fields
46      // ----------------------------------------------------------------------
47  
48      /**  A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>. */
49      private StringBuffer buffer;
50  
51      /**  A buffer that holds the table caption. */
52      private StringBuilder tableCaptionBuffer;
53  
54      /**  author. */
55      private Collection<String> authors;
56  
57      /**  title. */
58      private String title;
59  
60      /**  date. */
61      private String date;
62  
63      /**  linkName. */
64      private String linkName;
65  
66      /** startFlag. */
67      private boolean startFlag;
68  
69      /**  tableCaptionFlag. */
70      private boolean tableCaptionFlag;
71  
72      /**  tableCellFlag. */
73      private boolean tableCellFlag;
74  
75      /**  headerFlag. */
76      private boolean headerFlag;
77  
78      /**  bufferFlag. */
79      private boolean bufferFlag;
80  
81      /**  itemFlag. */
82      private boolean itemFlag;
83  
84      /**  verbatimFlag. */
85      private boolean verbatimFlag;
86  
87      /**  gridFlag for tables. */
88      private boolean gridFlag;
89  
90      /**  number of cells in a table. */
91      private int cellCount;
92  
93      /**  The writer to use. */
94      private final PrintWriter writer;
95  
96      /** {@code true} when last written character in {@link #writer} was a line separator, or writer is still at the beginning */
97      private boolean isWriterAtStartOfNewLine;
98  
99      /**  justification of table cells. */
100     private int[] cellJustif;
101 
102     /**  a line of a row in a table. */
103     private String rowLine;
104 
105     /**  is header row */
106     private boolean headerRow;
107 
108     /**  listNestingLevel, 0 outside the list, 1 for the top-level list, 2 for a nested list, 3 for a list nested inside a nested list, .... */
109     private int listNestingLevel;
110 
111     /**  listStyles. */
112     private final Stack<String> listStyles;
113 
114     /** Keep track of the closing tags for inline events. */
115     protected Stack<List<String>> inlineStack = new Stack<>();
116 
117     // ----------------------------------------------------------------------
118     // Public protected methods
119     // ----------------------------------------------------------------------
120 
121     /**
122      * Constructor, initialize the Writer and the variables.
123      *
124      * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
125      */
126     protected MarkdownSink(Writer writer) {
127         this.writer = new PrintWriter(writer);
128         isWriterAtStartOfNewLine = true;
129         this.listStyles = new Stack<>();
130 
131         init();
132     }
133 
134     /**
135      * Returns the buffer that holds the current text.
136      *
137      * @return A StringBuffer.
138      */
139     protected StringBuffer getBuffer() {
140         return buffer;
141     }
142 
143     /**
144      * Used to determine whether we are in head mode.
145      *
146      * @param headFlag True for head mode.
147      */
148     protected void setHeadFlag(boolean headFlag) {
149         this.headerFlag = headFlag;
150     }
151 
152     @Override
153     protected void init() {
154         super.init();
155 
156         resetBuffer();
157 
158         this.tableCaptionBuffer = new StringBuilder();
159         this.listNestingLevel = 0;
160 
161         this.authors = new LinkedList<>();
162         this.title = null;
163         this.date = null;
164         this.linkName = null;
165         this.startFlag = true;
166         this.tableCaptionFlag = false;
167         this.tableCellFlag = false;
168         this.headerFlag = false;
169         this.bufferFlag = false;
170         this.itemFlag = false;
171         this.verbatimFlag = false;
172         this.gridFlag = false;
173         this.cellCount = 0;
174         this.cellJustif = null;
175         this.rowLine = null;
176         this.listStyles.clear();
177         this.inlineStack.clear();
178     }
179 
180     /**
181      * Reset the StringBuilder.
182      */
183     protected void resetBuffer() {
184         buffer = new StringBuffer();
185     }
186 
187     /**
188      * Reset the TableCaptionBuffer.
189      */
190     protected void resetTableCaptionBuffer() {
191         tableCaptionBuffer = new StringBuilder();
192     }
193 
194     @Override
195     public void head() {
196         boolean startFlag = this.startFlag;
197 
198         init();
199 
200         headerFlag = true;
201         this.startFlag = startFlag;
202     }
203 
204     @Override
205     public void head_() {
206         headerFlag = false;
207 
208         // only write head block if really necessary
209         if (title == null && authors.isEmpty() && date == null) {
210             return;
211         }
212         write(METADATA_MARKUP + EOL);
213         if (title != null) {
214             write("title: " + title + EOL);
215         }
216         if (!authors.isEmpty()) {
217             write("author: " + EOL);
218             for (String author : authors) {
219                 write("  - " + author + EOL);
220             }
221         }
222         if (date != null) {
223             write("date: " + date + EOL);
224         }
225         write(METADATA_MARKUP + BLANK_LINE);
226     }
227 
228     @Override
229     public void title_() {
230         if (buffer.length() > 0) {
231             title = buffer.toString();
232             resetBuffer();
233         }
234     }
235 
236     @Override
237     public void author_() {
238         if (buffer.length() > 0) {
239             authors.add(buffer.toString());
240             resetBuffer();
241         }
242     }
243 
244     @Override
245     public void date_() {
246         if (buffer.length() > 0) {
247             date = buffer.toString();
248             resetBuffer();
249         }
250     }
251 
252     private void sectionTitle(int level) {
253         write(StringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE);
254     }
255 
256     @Override
257     public void sectionTitle1() {
258         sectionTitle(1);
259     }
260 
261     @Override
262     public void sectionTitle1_() {
263         write(BLANK_LINE);
264     }
265 
266     @Override
267     public void sectionTitle2() {
268         sectionTitle(2);
269     }
270 
271     @Override
272     public void sectionTitle2_() {
273         write(BLANK_LINE);
274     }
275 
276     @Override
277     public void sectionTitle3() {
278         sectionTitle(3);
279     }
280 
281     @Override
282     public void sectionTitle3_() {
283         write(BLANK_LINE);
284     }
285 
286     @Override
287     public void sectionTitle4() {
288         sectionTitle(4);
289     }
290 
291     @Override
292     public void sectionTitle4_() {
293         write(BLANK_LINE);
294     }
295 
296     @Override
297     public void sectionTitle5() {
298         sectionTitle(5);
299     }
300 
301     @Override
302     public void sectionTitle5_() {
303         write(BLANK_LINE);
304     }
305 
306     @Override
307     public void list() {
308         listNestingLevel++;
309         listStyles.push(LIST_UNORDERED_ITEM_START_MARKUP);
310     }
311 
312     @Override
313     public void list_() {
314         listNestingLevel--;
315         if (listNestingLevel == 0) {
316             write(EOL); // end add blank line (together with the preceding EOL of the item) only in case this was not
317             // nested
318         }
319         listStyles.pop();
320         itemFlag = false;
321     }
322 
323     @Override
324     public void listItem() {
325         orderedOrUnorderedListItem();
326     }
327 
328     @Override
329     public void listItem_() {
330         orderedOrUnorderedListItem_();
331     }
332 
333     /** {@inheritDoc} */
334     public void numberedList(int numbering) {
335         listNestingLevel++;
336         // markdown only supports decimal numbering
337         if (numbering != NUMBERING_DECIMAL) {
338             LOGGER.warn(
339                     "Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style",
340                     NUMBERING_DECIMAL,
341                     numbering);
342         }
343         String style = LIST_ORDERED_ITEM_START_MARKUP;
344         listStyles.push(style);
345     }
346 
347     @Override
348     public void numberedList_() {
349         listNestingLevel--;
350         if (listNestingLevel == 0) {
351             write(EOL); // end add blank line (together with the preceding EOL of the item) only in case this was not
352             // nested
353         }
354         listStyles.pop();
355         itemFlag = false;
356     }
357 
358     @Override
359     public void numberedListItem() {
360         orderedOrUnorderedListItem();
361     }
362 
363     @Override
364     public void numberedListItem_() {
365         orderedOrUnorderedListItem_();
366     }
367 
368     private void orderedOrUnorderedListItem() {
369         write(getListPrefix());
370         itemFlag = true;
371     }
372 
373     private void orderedOrUnorderedListItem_() {
374         ensureBeginningOfLine();
375         itemFlag = false;
376     }
377 
378     private String getListPrefix() {
379         StringBuilder prefix = new StringBuilder();
380         for (int indent = 1; indent < listNestingLevel; indent++) {
381             prefix.append("    "); // 4 spaces per indentation level
382         }
383         prefix.append(listStyles.peek());
384         prefix.append(SPACE);
385         return prefix.toString();
386     }
387 
388     @Override
389     public void definitionList() {
390         LOGGER.warn("Definition list not natively supported in Markdown, rendering HTML instead");
391         write("<dl>" + EOL);
392     }
393 
394     public void definitionList_() {
395         verbatimFlag = true;
396         write("</dl>" + BLANK_LINE);
397     }
398 
399     public void definedTerm() {
400         write("<dt>");
401         verbatimFlag = false;
402     }
403 
404     @Override
405     public void definedTerm_() {
406         write("</dt>" + EOL);
407     }
408 
409     @Override
410     public void definition() {
411         write("<dd>");
412     }
413 
414     @Override
415     public void definition_() {
416         write("</dd>" + EOL);
417     }
418 
419     @Override
420     public void pageBreak() {
421         LOGGER.warn("Ignoring unsupported page break in Markdown");
422     }
423 
424     @Override
425     public void paragraph() {
426         ensureBeginningOfLine();
427     }
428 
429     @Override
430     public void paragraph_() {
431         if (tableCellFlag || listNestingLevel > 0) {
432             // ignore paragraphs in table cells or lists
433         } else {
434             write(BLANK_LINE);
435         }
436     }
437 
438     /** {@inheritDoc} */
439     @Override
440     public void verbatim() {
441         verbatim(null);
442     }
443 
444     /** {@inheritDoc} */
445     public void verbatim(SinkEventAttributes attributes) {
446         ensureBeginningOfLine();
447         verbatimFlag = true;
448         write(VERBATIM_START_MARKUP + EOL);
449     }
450 
451     @Override
452     public void verbatim_() {
453         ensureBeginningOfLine();
454         write(VERBATIM_END_MARKUP + BLANK_LINE);
455         verbatimFlag = false;
456     }
457 
458     @Override
459     public void horizontalRule() {
460         ensureBeginningOfLine();
461         write(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
462     }
463 
464     @Override
465     public void table() {
466         ensureBeginningOfLine();
467     }
468 
469     @Override
470     public void table_() {
471         if (tableCaptionBuffer.length() > 0) {
472             text(tableCaptionBuffer.toString() + EOL);
473         }
474 
475         resetTableCaptionBuffer();
476     }
477 
478     /** {@inheritDoc} */
479     public void tableRows(int[] justification, boolean grid) {
480         cellJustif = null; // justification;
481         gridFlag = grid;
482         headerRow = true;
483     }
484 
485     @Override
486     public void tableRows_() {
487         cellJustif = null;
488         gridFlag = false;
489     }
490 
491     @Override
492     public void tableRow() {
493         bufferFlag = true;
494         cellCount = 0;
495     }
496 
497     @Override
498     public void tableRow_() {
499         bufferFlag = false;
500 
501         // write out the header row first, then the data in the buffer
502         buildRowLine();
503 
504         write(TABLE_ROW_SEPARATOR_MARKUP);
505 
506         write(buffer.toString());
507 
508         resetBuffer();
509 
510         write(EOL);
511 
512         if (headerRow) {
513             write(rowLine);
514             headerRow = false;
515         }
516 
517         // only reset cell count if this is the last row
518         cellCount = 0;
519     }
520 
521     /** Construct a table row. */
522     private void buildRowLine() {
523         StringBuilder rLine = new StringBuilder(TABLE_ROW_SEPARATOR_MARKUP);
524 
525         for (int i = 0; i < cellCount; i++) {
526             if (cellJustif != null) {
527                 switch (cellJustif[i]) {
528                     case 1:
529                         rLine.append(TABLE_COL_LEFT_ALIGNED_MARKUP);
530                         break;
531                     case 2:
532                         rLine.append(TABLE_COL_RIGHT_ALIGNED_MARKUP);
533                         break;
534                     default:
535                         rLine.append(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
536                 }
537             } else {
538                 rLine.append(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
539             }
540         }
541         rLine.append(EOL);
542 
543         this.rowLine = rLine.toString();
544     }
545 
546     @Override
547     public void tableCell() {
548         tableCell(false);
549     }
550 
551     @Override
552     public void tableHeaderCell() {
553         tableCell(true);
554     }
555 
556     /**
557      * Starts a table cell.
558      *
559      * @param headerRow If this cell is part of a header row.
560      */
561     public void tableCell(boolean headerRow) {
562         tableCellFlag = true;
563     }
564 
565     @Override
566     public void tableCell_() {
567         endTableCell();
568     }
569 
570     @Override
571     public void tableHeaderCell_() {
572         endTableCell();
573     }
574 
575     /**
576      * Ends a table cell.
577      */
578     private void endTableCell() {
579         tableCellFlag = false;
580         buffer.append(TABLE_CELL_SEPARATOR_MARKUP);
581         cellCount++;
582     }
583 
584     @Override
585     public void tableCaption() {
586         tableCaptionFlag = true;
587     }
588 
589     @Override
590     public void tableCaption_() {
591         tableCaptionFlag = false;
592     }
593 
594     @Override
595     public void figureCaption_() {
596         write(EOL);
597     }
598 
599     /** {@inheritDoc} */
600     public void figureGraphics(String name) {
601         write("<img src=\"" + name + "\" />");
602     }
603 
604     /** {@inheritDoc} */
605     public void anchor(String name) {
606         // write(ANCHOR_START_MARKUP + name);
607         // TODO get implementation from Xhtml5 base sink
608     }
609 
610     @Override
611     public void anchor_() {
612         // write(ANCHOR_END_MARKUP);
613     }
614 
615     /** {@inheritDoc} */
616     public void link(String name) {
617         if (!headerFlag) {
618             write(LINK_START_1_MARKUP);
619             linkName = name;
620         }
621     }
622 
623     @Override
624     public void link_() {
625         if (!headerFlag) {
626             write(LINK_START_2_MARKUP);
627             text(linkName.startsWith("#") ? linkName.substring(1) : linkName);
628             write(LINK_END_MARKUP);
629             linkName = null;
630         }
631     }
632 
633     /**
634      * A link with a target.
635      *
636      * @param name The name of the link.
637      * @param target The link target.
638      */
639     public void link(String name, String target) {
640         if (!headerFlag) {
641             write(LINK_START_1_MARKUP);
642         }
643     }
644 
645     @Override
646     public void inline() {
647         inline(null);
648     }
649 
650     /** {@inheritDoc} */
651     public void inline(SinkEventAttributes attributes) {
652         if (!headerFlag && !verbatimFlag) {
653             List<String> tags = new ArrayList<>();
654 
655             if (attributes != null) {
656 
657                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "italic")) {
658                     write(ITALIC_START_MARKUP);
659                     tags.add(0, ITALIC_END_MARKUP);
660                 }
661 
662                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "bold")) {
663                     write(BOLD_START_MARKUP);
664                     tags.add(0, BOLD_END_MARKUP);
665                 }
666 
667                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code")) {
668                     write(MONOSPACED_START_MARKUP);
669                     tags.add(0, MONOSPACED_END_MARKUP);
670                 }
671             }
672 
673             inlineStack.push(tags);
674         }
675     }
676 
677     @Override
678     public void inline_() {
679         if (!headerFlag && !verbatimFlag) {
680             for (String tag : inlineStack.pop()) {
681                 write(tag);
682             }
683         }
684     }
685 
686     @Override
687     public void italic() {
688         inline(SinkEventAttributeSet.Semantics.ITALIC);
689     }
690 
691     @Override
692     public void italic_() {
693         inline_();
694     }
695 
696     @Override
697     public void bold() {
698         inline(SinkEventAttributeSet.Semantics.BOLD);
699     }
700 
701     @Override
702     public void bold_() {
703         inline_();
704     }
705 
706     @Override
707     public void monospaced() {
708         inline(SinkEventAttributeSet.Semantics.CODE);
709     }
710 
711     @Override
712     public void monospaced_() {
713         inline_();
714     }
715 
716     @Override
717     public void lineBreak() {
718         if (headerFlag || bufferFlag) {
719             buffer.append(EOL);
720         } else if (verbatimFlag) {
721             write(EOL);
722         } else {
723             write("" + SPACE + SPACE + EOL);
724         }
725     }
726 
727     @Override
728     public void nonBreakingSpace() {
729         if (headerFlag || bufferFlag) {
730             buffer.append(NON_BREAKING_SPACE_MARKUP);
731         } else {
732             write(NON_BREAKING_SPACE_MARKUP);
733         }
734     }
735 
736     @Override
737     public void text(String text) {
738         if (tableCaptionFlag) {
739             tableCaptionBuffer.append(text);
740         } else if (headerFlag || bufferFlag) {
741             buffer.append(text);
742         } else if (verbatimFlag) {
743             verbatimContent(text);
744         } else {
745             content(text);
746         }
747     }
748 
749     @Override
750     public void rawText(String text) {
751         write(text);
752     }
753 
754     @Override
755     public void comment(String comment) {
756         rawText((startFlag ? "" : EOL) + COMMENT_START + comment + COMMENT_END);
757     }
758 
759     /**
760      * {@inheritDoc}
761      *
762      * Unkown events just log a warning message but are ignored otherwise.
763      * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
764      */
765     @Override
766     public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
767         LOGGER.warn("Unknown Sink event '" + name + "', ignoring!");
768     }
769 
770     /**
771      * Write text to output.
772      *
773      * @param text The text to write.
774      */
775     protected void write(String text) {
776         startFlag = false;
777         if (tableCellFlag) {
778             buffer.append(text);
779         } else {
780             String unifiedText = unifyEOLs(text);
781             isWriterAtStartOfNewLine = unifiedText.endsWith(EOL);
782             writer.write(unifiedText);
783         }
784     }
785 
786     /**
787      * Write Markdown escaped text to output.
788      *
789      * @param text The text to write.
790      */
791     protected void content(String text) {
792         write(escapeMarkdown(text));
793     }
794 
795     /**
796      * Write verbatim text to output.
797      *
798      * @param text The text to write.
799      */
800     protected void verbatimContent(String text) {
801         write(text);
802     }
803 
804     @Override
805     public void flush() {
806         writer.flush();
807     }
808 
809     @Override
810     public void close() {
811         writer.close();
812 
813         init();
814     }
815 
816     // ----------------------------------------------------------------------
817     // Private methods
818     // ----------------------------------------------------------------------
819 
820     /**
821      * Escape special characters in a text in Markdown:
822      *
823      * <pre>
824      * \, `, *, _, {, }, [, ], (, ), #, +, -, ., !
825      * </pre>
826      *
827      * @param text the String to escape, may be null
828      * @return the text escaped, "" if null String input
829      * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a>
830      */
831     private static String escapeMarkdown(String text) {
832         if (text == null) {
833             return "";
834         }
835 
836         int length = text.length();
837         StringBuilder buffer = new StringBuilder(length);
838 
839         for (int i = 0; i < length; ++i) {
840             char c = text.charAt(i);
841             switch (c) {
842                 case '\\':
843                 case '`':
844                 case '*':
845                 case '_':
846                 case '{':
847                 case '}':
848                 case '[':
849                 case ']':
850                 case '(':
851                 case ')':
852                 case '#':
853                 case '+':
854                 case '-':
855                 case '.':
856                 case '!':
857                     buffer.append('\\');
858                     buffer.append(c);
859                     break;
860                 default:
861                     buffer.append(c);
862             }
863         }
864 
865         return buffer.toString();
866     }
867 
868     /**
869      * Ensures that the {@link #writer} is currently at the beginning of a new line.
870      * Optionally writes a line separator to ensure that.
871      */
872     private void ensureBeginningOfLine() {
873         // make sure that we are at the start of a line without adding unnecessary blank lines
874         if (!isWriterAtStartOfNewLine) {
875             write(EOL);
876         }
877     }
878 }