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 javax.inject.Inject;
22  
23  import java.io.IOException;
24  import java.io.Reader;
25  import java.io.StringReader;
26  import java.io.Writer;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.maven.doxia.parser.ParseException;
30  import org.apache.maven.doxia.parser.Parser;
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.doxia.sink.impl.AbstractSinkTest;
33  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
34  import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
35  import org.apache.maven.doxia.util.HtmlTools;
36  import org.hamcrest.MatcherAssert;
37  import org.hamcrest.Matchers;
38  import org.junit.jupiter.api.Test;
39  
40  import static org.junit.jupiter.api.Assertions.assertEquals;
41  
42  /**
43   * Test the <code>MarkdownSink</code> class
44   */
45  public class MarkdownSinkTest extends AbstractSinkTest {
46      @Inject
47      protected MarkdownParser parser;
48  
49      /** {@inheritDoc} */
50      protected String outputExtension() {
51          return "md";
52      }
53  
54      /** {@inheritDoc} */
55      protected Sink createSink(Writer writer) {
56          return new MarkdownSink(writer);
57      }
58  
59      /** {@inheritDoc} */
60      protected boolean isXmlSink() {
61          return false;
62      }
63  
64      /** {@inheritDoc} */
65      protected String getTitleBlock(String title) {
66          return title;
67      }
68  
69      /** {@inheritDoc} */
70      protected String getAuthorBlock(String author) {
71          return getEscapedText(author);
72      }
73  
74      /** {@inheritDoc} */
75      protected String getDateBlock(String date) {
76          return date;
77      }
78  
79      /** {@inheritDoc} */
80      protected String getHeadBlock() {
81          return "";
82      }
83  
84      /** {@inheritDoc} */
85      protected String getBodyBlock() {
86          return "";
87      }
88  
89      /** {@inheritDoc} */
90      protected String getArticleBlock() {
91          return "";
92      }
93  
94      /** {@inheritDoc} */
95      protected String getNavigationBlock() {
96          return "";
97      }
98  
99      /** {@inheritDoc} */
100     protected String getSidebarBlock() {
101         return "";
102     }
103 
104     /** {@inheritDoc} */
105     protected String getSectionTitleBlock(String title) {
106         return title;
107     }
108 
109     protected String getSectionBlock(String title, int level) {
110         return StringUtils.repeat(MarkdownMarkup.SECTION_TITLE_START_MARKUP, level) + SPACE + title + EOL + EOL;
111     }
112 
113     /** {@inheritDoc} */
114     protected String getSection1Block(String title) {
115         return getSectionBlock(title, 1);
116     }
117 
118     /** {@inheritDoc} */
119     protected String getSection2Block(String title) {
120         return getSectionBlock(title, 2);
121     }
122 
123     /** {@inheritDoc} */
124     protected String getSection3Block(String title) {
125         return getSectionBlock(title, 3);
126     }
127 
128     /** {@inheritDoc} */
129     protected String getSection4Block(String title) {
130         return getSectionBlock(title, 4);
131     }
132 
133     /** {@inheritDoc} */
134     protected String getSection5Block(String title) {
135         return getSectionBlock(title, 5);
136     }
137 
138     /** {@inheritDoc} */
139     protected String getSection6Block(String title) {
140         return getSectionBlock(title, 6);
141     }
142 
143     /** {@inheritDoc} */
144     protected String getHeaderBlock() {
145         return "";
146     }
147 
148     /** {@inheritDoc} */
149     protected String getContentBlock() {
150         return "";
151     }
152 
153     /** {@inheritDoc} */
154     protected String getFooterBlock() {
155         return "";
156     }
157 
158     /** {@inheritDoc} */
159     protected String getListBlock(String item) {
160         return MarkdownMarkup.LIST_UNORDERED_ITEM_START_MARKUP + getEscapedText(item) + EOL + EOL;
161     }
162 
163     /** {@inheritDoc} */
164     protected String getNumberedListBlock(String item) {
165         return MarkdownMarkup.LIST_ORDERED_ITEM_START_MARKUP + getEscapedText(item) + EOL + EOL;
166     }
167 
168     /** {@inheritDoc} */
169     protected String getDefinitionListBlock(String definum, String definition) {
170         // don't reuse constants from compile classes to improve accuracy of tests
171         return "<dl>" + EOL + "<dt>" + getEscapedText(definum) + "</dt>" + EOL + "<dd>" + getEscapedText(definition)
172                 + "</dd>" + EOL + "</dl>" + EOL + EOL;
173     }
174 
175     /** {@inheritDoc} */
176     protected String getFigureBlock(String source, String caption) {
177         return "![" + (caption != null ? getEscapedText(caption) : "") + "](" + getEscapedText(source) + ")";
178     }
179 
180     /** {@inheritDoc} */
181     protected String getTableBlock(String cell, String caption) {
182         return MarkdownMarkup.TABLE_ROW_PREFIX + "   " + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL
183                 + MarkdownMarkup.TABLE_ROW_PREFIX
184                 + ":---:" + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL + MarkdownMarkup.TABLE_ROW_PREFIX
185                 + cell + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL;
186     }
187 
188     @Override
189     protected String getTableWithHeaderBlock(String... rowPrefixes) {
190         StringBuilder expectedMarkup = new StringBuilder();
191         expectedMarkup.append(MarkdownMarkup.TABLE_ROW_PREFIX + getEscapedText(rowPrefixes[0]) + "0|"
192                 + getEscapedText(rowPrefixes[0]) + "1|" + getEscapedText(rowPrefixes[0]) + "2|" + EOL);
193         expectedMarkup.append(MarkdownMarkup.TABLE_ROW_PREFIX + "---|---:|:---:|" + EOL);
194         for (int n = 1; n < rowPrefixes.length; n++) {
195             expectedMarkup.append(MarkdownMarkup.TABLE_ROW_PREFIX + getEscapedText(rowPrefixes[n]) + "0|"
196                     + getEscapedText(rowPrefixes[n]) + "1|" + getEscapedText(rowPrefixes[n]) + "2|" + EOL);
197         }
198         return expectedMarkup.toString();
199     }
200 
201     /** {@inheritDoc} */
202     protected String getParagraphBlock(String text) {
203         return text + EOL + EOL;
204     }
205 
206     /** {@inheritDoc} */
207     protected String getDataBlock(String value, String text) {
208         return text;
209     }
210 
211     /** {@inheritDoc} */
212     protected String getTimeBlock(String datetime, String text) {
213         return text;
214     }
215 
216     /** {@inheritDoc} */
217     protected String getAddressBlock(String text) {
218         return text;
219     }
220 
221     /** {@inheritDoc} */
222     protected String getBlockquoteBlock(String text) {
223         return "> " + text + EOL;
224     }
225 
226     /** {@inheritDoc} */
227     protected String getDivisionBlock(String text) {
228         return text;
229     }
230 
231     /** {@inheritDoc} */
232     protected String getVerbatimSourceBlock(String text) {
233         return MarkdownMarkup.VERBATIM_START_MARKUP + EOL + text + EOL + MarkdownMarkup.VERBATIM_END_MARKUP + EOL + EOL;
234     }
235 
236     /** {@inheritDoc} */
237     protected String getHorizontalRuleBlock() {
238         return MarkdownMarkup.HORIZONTAL_RULE_MARKUP + EOL + EOL;
239     }
240 
241     /** {@inheritDoc} */
242     protected String getPageBreakBlock() {
243         return "";
244     }
245 
246     /** {@inheritDoc} */
247     protected String getAnchorBlock(String anchor) {
248         return anchor;
249     }
250 
251     /** {@inheritDoc} */
252     protected String getLinkBlock(String link, String text) {
253         String lnk = link.startsWith("#") ? link.substring(1) : link;
254         return MarkdownMarkup.LINK_START_1_MARKUP
255                 + text
256                 + MarkdownMarkup.LINK_START_2_MARKUP
257                 + lnk
258                 + MarkdownMarkup.LINK_END_MARKUP;
259     }
260 
261     /** {@inheritDoc} */
262     protected String getInlineBlock(String text) {
263         return text;
264     }
265 
266     /** {@inheritDoc} */
267     protected String getInlineItalicBlock(String text) {
268         return MarkdownMarkup.ITALIC_START_MARKUP + text + MarkdownMarkup.ITALIC_END_MARKUP;
269     }
270 
271     /** {@inheritDoc} */
272     protected String getInlineBoldBlock(String text) {
273         return MarkdownMarkup.BOLD_START_MARKUP + text + MarkdownMarkup.BOLD_END_MARKUP;
274     }
275 
276     /** {@inheritDoc} */
277     protected String getInlineCodeBlock(String text) {
278         return MarkdownMarkup.MONOSPACED_START_MARKUP + text + MarkdownMarkup.MONOSPACED_END_MARKUP;
279     }
280 
281     /** {@inheritDoc} */
282     protected String getItalicBlock(String text) {
283         return MarkdownMarkup.ITALIC_START_MARKUP + text + MarkdownMarkup.ITALIC_END_MARKUP;
284     }
285 
286     /** {@inheritDoc} */
287     protected String getBoldBlock(String text) {
288         return MarkdownMarkup.BOLD_START_MARKUP + text + MarkdownMarkup.BOLD_END_MARKUP;
289     }
290 
291     /** {@inheritDoc} */
292     protected String getMonospacedBlock(String text) {
293         return text;
294     }
295 
296     /** {@inheritDoc} */
297     protected String getLineBreakBlock() {
298         return "" + SPACE + SPACE + EOL;
299     }
300 
301     /** {@inheritDoc} */
302     protected String getLineBreakOpportunityBlock() {
303         return "";
304     }
305 
306     /** {@inheritDoc} */
307     protected String getNonBreakingSpaceBlock() {
308         return MarkdownMarkup.NON_BREAKING_SPACE_MARKUP;
309     }
310 
311     /** {@inheritDoc} */
312     protected String getTextBlock(String text) {
313         // this is only called once, therefore hard-code the expected result
314         // return escaped format of "~,_=,_-,_+,_*,_[,_],_<,_>,_{,_},_\\";
315         // i.e. XML entities for <>&"' and Markdown escape sequences for characters outlined in
316         // https://daringfireball.net/projects/markdown/syntax#backslash
317         return "~,\\_=,\\_\\-,\\_\\+,\\_\\*,\\_\\[,\\_\\],\\_&lt;,\\_&gt;,\\_\\{,\\_\\},\\_\\\\";
318     }
319 
320     /** {@inheritDoc} */
321     protected String getRawTextBlock(String text) {
322         return text;
323     }
324 
325     /**
326      * Escapes special characters outlined in <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Markdown Spec</a>
327      * @param text
328      * @return the text with all special characters escaped
329      */
330     private String getEscapedText(String text) {
331         text = HtmlTools.escapeHTML(text, true);
332         return text.replaceAll("\\\\|\\`|\\*|_|\\{|\\}|\\[|\\]|\\(|\\)|#|\\+|\\-|\\.|\\!", "\\\\$0");
333     }
334 
335     /** {@inheritDoc} */
336     protected String getCommentBlock(String text) {
337         return "<!-- " + text + " -->";
338     }
339 
340     @Test
341     public void testMultipleAuthors() {
342         final Sink sink = getSink();
343         sink.head();
344         sink.author();
345         sink.text("first author");
346         sink.author_();
347         sink.author();
348         sink.text("second author");
349         sink.author_();
350         sink.head_();
351         sink.flush();
352         sink.close();
353 
354         String expected = MarkdownMarkup.METADATA_MARKUP + EOL
355                 + "author: " + EOL
356                 + "  - first author" + EOL
357                 + "  - second author" + EOL
358                 + MarkdownMarkup.METADATA_MARKUP + EOL + EOL;
359 
360         assertEquals(expected, getSinkContent(), "Wrong metadata section");
361     }
362 
363     /** Test MD -> SETSink vs. Test MD -> MDSink -> SETSink */
364     @Test
365     public void testRoundtrip() throws IOException, ParseException {
366         parseFile(parser, "test", getSink());
367 
368         final SinkEventTestingSink regeneratedSink = new SinkEventTestingSink();
369         try (Reader reader = new StringReader(getSinkContent())) {
370             parser.parse(reader, regeneratedSink);
371         }
372 
373         final SinkEventTestingSink originalSink = new SinkEventTestingSink();
374         parseFile(parser, "test", originalSink);
375 
376         // compare sink events from parsing original markdown with sink events from re-generated markdown
377         try {
378             MatcherAssert.assertThat(
379                     regeneratedSink.getEventList(),
380                     Matchers.contains(originalSink.getEventList().toArray()));
381         } catch (AssertionError e) {
382             // emit generated markdown to ease debugging
383             System.err.println(getSinkContent());
384             throw e;
385         }
386     }
387 
388     private void parseFile(Parser parser, String file, Sink sink) throws ParseException, IOException {
389         try (Reader reader = getTestReader(file)) {
390             parser.parse(reader, sink);
391         }
392     }
393 
394     @Test
395     public void testLinksParagraphsAndStylesInTableCells() {
396         final String linkTarget = "target";
397         final String linkText = "link";
398         final String paragraphText = "paragraph text with |";
399         final Sink sink = getSink();
400         sink.table();
401         sink.tableRows();
402         sink.tableRow();
403         sink.tableCell();
404         sink.link(linkTarget);
405         sink.text(linkText);
406         sink.link_();
407         sink.tableCell_();
408         sink.tableCell();
409         sink.paragraph();
410         sink.text(paragraphText);
411         sink.bold();
412         sink.text("bold");
413         sink.bold_();
414         sink.paragraph_();
415         sink.tableCell_();
416         sink.tableRow_();
417         sink.tableRows_();
418         sink.table_();
419         sink.flush();
420         sink.close();
421 
422         String expected =
423                 "|   |   |" + EOL + "|---|---|" + EOL + "|[link](target)|paragraph text with \\|**bold**|" + EOL;
424 
425         assertEquals(expected, getSinkContent(), "Wrong link or paragraph markup in table cell");
426     }
427 
428     @Test
429     public void testInlineCodeWithSpecialCharacters() {
430         String text = "Test&<>*_";
431         final Sink sink = getSink();
432         sink.inline(SinkEventAttributeSet.Semantics.CODE);
433         sink.text(text);
434         sink.inline_();
435         sink.flush();
436         sink.close();
437 
438         String expected = "`" + text + "`";
439 
440         assertEquals(expected, getSinkContent(), "Wrong inline code!");
441     }
442 
443     @Test
444     public void testNestedListWithBlockquotesParagraphsAndCode() {
445         final Sink sink = getSink();
446         sink.list();
447 
448         sink.listItem();
449         sink.text("item1");
450         sink.list();
451         sink.listItem();
452         sink.text("item1a");
453         sink.listItem_();
454         sink.list_();
455         sink.listItem_();
456 
457         sink.listItem();
458         sink.blockquote();
459         sink.text("blockquote");
460         sink.blockquote_();
461         sink.listItem_();
462 
463         sink.listItem();
464         sink.text("item3");
465         sink.paragraph();
466         sink.text("item3paragraph2");
467         sink.paragraph_();
468         sink.listItem_();
469 
470         sink.listItem();
471         sink.text("item4");
472         sink.verbatim();
473         sink.text("item4verbatim");
474         sink.lineBreak();
475         sink.text("item4verbatimline2");
476         sink.verbatim_();
477         sink.listItem_();
478 
479         sink.list_();
480         sink.flush();
481         sink.close();
482 
483         String expected = "- item1" + EOL
484                 + "    - item1a" + EOL
485                 + EOL
486                 + "- " + EOL
487                 + "    > blockquote" + EOL
488                 + "- item3" + EOL
489                 + EOL
490                 + "    item3paragraph2" + EOL
491                 + EOL
492                 + "- item4" + EOL
493                 + "    ```" + EOL
494                 + "    item4verbatim" + EOL
495                 + "    item4verbatimline2" + EOL
496                 + "    ```" + EOL
497                 + EOL;
498 
499         assertEquals(expected, getSinkContent(), "Wrong inline code!");
500     }
501 }