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.StringWriter;
27  import java.io.Writer;
28  
29  import org.apache.maven.doxia.module.xhtml5.Xhtml5Parser;
30  import org.apache.maven.doxia.parser.ParseException;
31  import org.apache.maven.doxia.parser.Parser;
32  import org.apache.maven.doxia.sink.Sink;
33  import org.apache.maven.doxia.sink.impl.AbstractSinkTest;
34  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
35  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics;
36  import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
37  import org.apache.maven.doxia.util.DoxiaStringUtils;
38  import org.junit.jupiter.api.Test;
39  
40  import static org.junit.jupiter.api.Assertions.assertEquals;
41  import static org.junit.jupiter.api.Assertions.assertIterableEquals;
42  
43  /**
44   * Test the <code>MarkdownSink</code> class
45   */
46  class MarkdownSinkTest extends AbstractSinkTest {
47      @Inject
48      protected MarkdownParser parser;
49  
50      @Inject
51      protected Xhtml5Parser htmlParser;
52  
53      protected String outputExtension() {
54          return "md";
55      }
56  
57      protected Sink createSink(Writer writer) {
58          return MarkdownSink.newInstance(writer);
59      }
60  
61      protected boolean isXmlSink() {
62          return false;
63      }
64  
65      protected String getTitleBlock(String title) {
66          // only written inside metadata section once head()_ is called
67          return "";
68      }
69  
70      protected String getAuthorBlock(String author) {
71          // only written inside metadata section once head()_ is called
72          return "";
73      }
74  
75      protected String getDateBlock(String date) {
76          // only written inside metadata section once head()_ is called
77          return "";
78      }
79  
80      protected String getHeadBlock() {
81          return "";
82      }
83  
84      protected String getBodyBlock() {
85          return "";
86      }
87  
88      protected String getArticleBlock() {
89          return "<article></article>";
90      }
91  
92      protected String getNavigationBlock() {
93          return "<nav></nav>";
94      }
95  
96      protected String getSidebarBlock() {
97          return "<aside></aside>";
98      }
99  
100     protected String getSectionBlock(String title, int level) {
101         return DoxiaStringUtils.repeat(MarkdownMarkup.SECTION_TITLE_START_MARKUP, level) + SPACE + title + EOL + EOL;
102     }
103 
104     protected String getSection1Block(String title) {
105         return getSectionBlock(title, 1);
106     }
107 
108     protected String getSection2Block(String title) {
109         return getSectionBlock(title, 2);
110     }
111 
112     protected String getSection3Block(String title) {
113         return getSectionBlock(title, 3);
114     }
115 
116     protected String getSection4Block(String title) {
117         return getSectionBlock(title, 4);
118     }
119 
120     protected String getSection5Block(String title) {
121         return getSectionBlock(title, 5);
122     }
123 
124     protected String getSection6Block(String title) {
125         return getSectionBlock(title, 6);
126     }
127 
128     protected String getHeaderBlock() {
129         // never emitted by Markdown sink as otherwise too often markdown could not be used
130         return "";
131     }
132 
133     protected String getContentBlock() {
134         return "<main><div class=\"content\"></div></main>";
135     }
136 
137     protected String getFooterBlock() {
138         return "<footer></footer>";
139     }
140 
141     protected String getListBlock(String item) {
142         return MarkdownMarkup.LIST_UNORDERED_ITEM_START_MARKUP + getEscapedText(item) + EOL;
143     }
144 
145     protected String getNumberedListBlock(String item) {
146         return MarkdownMarkup.LIST_ORDERED_ITEM_START_MARKUP + getEscapedText(item) + EOL;
147     }
148 
149     protected String getDefinitionListBlock(String definum, String definition) {
150         // don't reuse constants from compile classes to improve accuracy of tests
151         return "<dl>" + EOL + "<dt>" + getEscapedText(definum) + "</dt>" + EOL + "<dd>" + getEscapedText(definition)
152                 + "</dd>" + EOL + "</dl>";
153     }
154 
155     protected String getFigureBlock(String source, String caption) {
156         return "![" + (caption != null ? getEscapedText(caption) : "") + "](" + source + ")";
157     }
158 
159     protected String getTableBlock(String cell, String caption) {
160         return MarkdownMarkup.TABLE_ROW_PREFIX + "   " + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + "   "
161                 + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + "   " + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL
162                 + MarkdownMarkup.TABLE_ROW_PREFIX
163                 + ":---:" + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + "---"
164                 + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + "---" + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL
165                 + MarkdownMarkup.TABLE_ROW_PREFIX
166                 + cell + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + cell + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP
167                 + cell + MarkdownMarkup.TABLE_CELL_SEPARATOR_MARKUP + EOL + EOL;
168     }
169 
170     @Override
171     protected String getTableWithHeaderBlock(String... rowPrefixes) {
172         StringBuilder expectedMarkup = new StringBuilder();
173         expectedMarkup
174                 .append(MarkdownMarkup.TABLE_ROW_PREFIX)
175                 .append(getEscapedText(rowPrefixes[0]))
176                 .append("0|")
177                 .append(getEscapedText(rowPrefixes[0]))
178                 .append("1|")
179                 .append(getEscapedText(rowPrefixes[0]))
180                 .append("2|")
181                 .append(EOL);
182         expectedMarkup
183                 .append(MarkdownMarkup.TABLE_ROW_PREFIX)
184                 .append(":---|---:|:---:|")
185                 .append(EOL);
186         for (int n = 1; n < rowPrefixes.length; n++) {
187             expectedMarkup
188                     .append(MarkdownMarkup.TABLE_ROW_PREFIX)
189                     .append(getEscapedText(rowPrefixes[n]))
190                     .append("0|")
191                     .append(getEscapedText(rowPrefixes[n]))
192                     .append("1|")
193                     .append(getEscapedText(rowPrefixes[n]))
194                     .append("2|")
195                     .append(EOL);
196         }
197         expectedMarkup.append(EOL);
198         return expectedMarkup.toString();
199     }
200 
201     protected String getParagraphBlock(String text) {
202         return text + EOL + EOL;
203     }
204 
205     protected String getDataBlock(String value, String text) {
206         return "<data value=\"" + value + "\">" + text + "</data>";
207     }
208 
209     protected String getTimeBlock(String datetime, String text) {
210         return "<time datetime=\"" + datetime + "\">" + text + "</time>";
211     }
212 
213     protected String getAddressBlock(String text) {
214         return "<address>" + text + "</address>";
215     }
216 
217     protected String getBlockquoteBlock(String text) {
218         return "> " + text + EOL;
219     }
220 
221     protected String getDivisionBlock(String text) {
222         return "<div>" + text + "</div>";
223     }
224 
225     protected String getVerbatimBlock(String text) {
226         return MarkdownMarkup.VERBATIM_START_MARKUP + EOL + text + EOL + MarkdownMarkup.VERBATIM_END_MARKUP + EOL + EOL;
227     }
228 
229     protected String getVerbatimSourceBlock(String text) {
230         return MarkdownMarkup.VERBATIM_START_MARKUP + "unknown" + EOL + text + EOL + MarkdownMarkup.VERBATIM_END_MARKUP
231                 + EOL + EOL;
232     }
233 
234     protected String getHorizontalRuleBlock() {
235         return MarkdownMarkup.HORIZONTAL_RULE_MARKUP + EOL + EOL;
236     }
237 
238     protected String getPageBreakBlock() {
239         return "";
240     }
241 
242     protected String getAnchorBlock(String anchor) {
243         return "<a id=\"" + anchor + "\"></a>" + anchor;
244     }
245 
246     protected String getLinkBlock(String link, String text) {
247         return MarkdownMarkup.LINK_START_1_MARKUP
248                 + text
249                 + MarkdownMarkup.LINK_START_2_MARKUP
250                 + link
251                 + MarkdownMarkup.LINK_END_MARKUP;
252     }
253 
254     protected String getInlineBlock(String text) {
255         return text;
256     }
257 
258     protected String getInlineItalicBlock(String text) {
259         return MarkdownMarkup.ITALIC_START_MARKUP + text + MarkdownMarkup.ITALIC_END_MARKUP;
260     }
261 
262     protected String getInlineBoldBlock(String text) {
263         return MarkdownMarkup.BOLD_START_MARKUP + text + MarkdownMarkup.BOLD_END_MARKUP;
264     }
265 
266     protected String getInlineCodeBlock(String text) {
267         return MarkdownMarkup.MONOSPACED_START_MARKUP + text + MarkdownMarkup.MONOSPACED_END_MARKUP;
268     }
269 
270     protected String getInlineDeleteBlock(String text) {
271         return MarkdownMarkup.STRIKETHROUGH_START_MARKUP + text + MarkdownMarkup.STRIKETHROUGH_END_MARKUP;
272     }
273 
274     protected String getMonospacedBlock(String text) {
275         return text;
276     }
277 
278     protected String getLineBreakBlock() {
279         return "" + SPACE + SPACE + EOL;
280     }
281 
282     protected String getLineBreakOpportunityBlock() {
283         return "<wbr />";
284     }
285 
286     protected String getNonBreakingSpaceBlock() {
287         return MarkdownMarkup.NON_BREAKING_SPACE_MARKUP;
288     }
289 
290     protected String getTextBlock(String text) {
291         if (text.equals("~,_=,_-,_+,_*,_[,_],_<,_>,_{,_},_\\")) {
292             // i.e. XML entities for <>&"' and Markdown escape sequences for characters outlined in
293             // https://daringfireball.net/projects/markdown/syntax#backslash
294             return "~,\\_=,\\_-,\\_+,\\_*,\\_\\[,\\_\\],\\_&lt;,\\_&gt;,\\_{,\\_},\\_\\\\";
295         } else {
296             // assume BODY context everywhere else
297             return getEscapedText(text);
298         }
299     }
300 
301     protected String getRawTextBlock(String text) {
302         return text;
303     }
304 
305     /**
306      * Escapes as if in the BODY context and the writer is at the start of a new line.
307      * @param text
308      * @return the text with all special characters escaped
309      */
310     private String getEscapedText(String text) {
311         return MarkdownSink.ElementContext.BODY.escape(new LastTwoLinesAwareWriter(new StringWriter()), text);
312     }
313 
314     @Override
315     protected String getCommentBlock(String text) {
316         return "<!--" + toXmlComment(text) + "-->";
317     }
318 
319     @Override
320     protected String getCommentBlockFollowedByParagraph(String comment, String paragraph) {
321         return getCommentBlock(comment) + EOL + EOL + getParagraphBlock(paragraph); // paragraph separated by blank line
322     }
323 
324     @Test
325     void multipleAuthors() {
326         final Sink sink = getSink();
327         sink.head();
328         sink.author();
329         sink.text("first author");
330         sink.author_();
331         sink.author();
332         sink.text("second author");
333         sink.author_();
334         sink.head_();
335         sink.flush();
336         sink.close();
337 
338         String expected = MarkdownMarkup.METADATA_MARKUP + EOL
339                 + "author: " + EOL
340                 + "  - first author" + EOL
341                 + "  - second author" + EOL
342                 + MarkdownMarkup.METADATA_MARKUP + EOL + EOL;
343 
344         assertEquals(expected, getSinkContent(), "Wrong metadata section");
345     }
346 
347     /** Test MD -> SETSink vs. Test MD -> MDSink -> SETSink */
348     @Test
349     void roundtrip() throws Exception {
350         parseFile(parser, "test", getSink());
351 
352         final SinkEventTestingSink regeneratedSink = new SinkEventTestingSink();
353         try (Reader reader = new StringReader(getSinkContent())) {
354             parser.parse(reader, regeneratedSink);
355         }
356 
357         final SinkEventTestingSink originalSink = new SinkEventTestingSink();
358         parseFile(parser, "test", originalSink);
359         // strip empty lines from sink content
360 
361         // compare sink events from parsing original markdown with sink events from re-generated markdown
362         try {
363             assertIterableEquals(originalSink.getEventList(), regeneratedSink.getEventList());
364         } catch (AssertionError e) {
365             // emit generated markdown to ease debugging
366             System.err.println(getSinkContent());
367             throw e;
368         }
369     }
370 
371     private void parseFile(Parser parser, String file, Sink sink) throws ParseException, IOException {
372         parseFile(parser, file, outputExtension(), sink);
373     }
374 
375     private void parseFile(Parser parser, String file, String extension, Sink sink) throws ParseException, IOException {
376         try (Reader reader = getTestReader(file, extension)) {
377             parser.parse(reader, sink);
378         }
379     }
380 
381     @Test
382     void linksParagraphsAndStylesInTableCells() {
383         final String linkTarget = "target";
384         final String linkText = "link";
385         final String paragraphText = "paragraph text with |";
386         final Sink sink = getSink();
387         sink.table();
388         sink.tableRows();
389         sink.tableRow();
390         sink.tableCell();
391         sink.link(linkTarget);
392         sink.text(linkText);
393         sink.link_();
394         sink.tableCell_();
395         sink.tableCell();
396         sink.paragraph();
397         sink.text(paragraphText);
398         sink.bold();
399         sink.text("bold");
400         sink.bold_();
401         sink.paragraph_();
402         sink.tableCell_();
403         sink.tableRow_();
404         sink.tableRows_();
405         sink.table_();
406         sink.flush();
407         sink.close();
408 
409         String expected =
410                 "|   |   |" + EOL + "|---|---|" + EOL + "|[link](target)|paragraph text with \\|**bold**|" + EOL + EOL;
411 
412         assertEquals(expected, getSinkContent(), "Wrong link or paragraph markup in table cell");
413     }
414 
415     @Test
416     void inlineCodeWithSpecialCharacters() {
417         String text = "Test&<>*_";
418         final Sink sink = getSink();
419         sink.inline(SinkEventAttributeSet.Semantics.CODE);
420         sink.text(text);
421         sink.inline_();
422         sink.flush();
423         sink.close();
424 
425         String expected = "`" + text + "`";
426 
427         assertEquals(expected, getSinkContent(), "Wrong inline code!");
428     }
429 
430     @Test
431     void nestedListWithBlockquotesParagraphsAndCode() {
432         final Sink sink = getSink();
433         sink.list();
434 
435         sink.listItem();
436         sink.text("item1");
437         sink.list();
438         sink.listItem();
439         sink.text("item1a");
440         sink.listItem_();
441         sink.list_();
442         sink.listItem_();
443 
444         sink.listItem();
445         sink.blockquote();
446         sink.text("blockquote");
447         sink.blockquote_();
448         sink.listItem_();
449 
450         sink.listItem();
451         sink.text("item3");
452         sink.paragraph();
453         sink.text("item3paragraph2");
454         sink.paragraph_();
455         sink.listItem_();
456 
457         sink.listItem();
458         sink.text("item4");
459         sink.verbatim();
460         sink.text("item4verbatim");
461         sink.lineBreak();
462         sink.text("item4verbatimline2");
463         sink.verbatim_();
464         sink.listItem_();
465 
466         sink.list_();
467         sink.flush();
468         sink.close();
469 
470         String expected = "- item1" + EOL
471                 + "    - item1a" + EOL
472                 + "- " + EOL + EOL
473                 + "    > blockquote" + EOL + EOL
474                 + "- item3" + EOL
475                 + EOL
476                 + "    item3paragraph2" + EOL
477                 + EOL
478                 + "- item4" + EOL + EOL
479                 + "    ```" + EOL
480                 + "    item4verbatim" + EOL
481                 + "    item4verbatimline2" + EOL
482                 + "    ```" + EOL
483                 + EOL;
484 
485         assertEquals(expected, getSinkContent(), "Wrong inline code!");
486     }
487 
488     @Test
489     void headingAfterInlineElement() {
490         try (Sink sink = getSink()) {
491             sink.text("Text");
492             sink.section1();
493             sink.sectionTitle1();
494             sink.text("Section1");
495             sink.sectionTitle1_();
496             sink.section1_();
497         }
498         // heading must be on a new line
499         String expected = "Text" + EOL + "# Section1" + EOL + EOL;
500         assertEquals(expected, getSinkContent(), "Wrong heading after inline element!");
501     }
502 
503     @Test
504     void codeLink() {
505         try (Sink sink = getSink()) {
506             sink.inline(Semantics.CODE);
507             sink.link("http://example.com");
508             sink.text("label");
509             sink.link_();
510             sink.inline_();
511         }
512         // heading must be on a new line
513         String expected = "[`label`](http://example.com)";
514         assertEquals(expected, getSinkContent(), "Wrong link on code!");
515     }
516 
517     @Test
518     void multilineVerbatimSourceAfterListItem() {
519         try (Sink sink = getSink()) {
520             sink.list();
521             sink.listItem();
522             sink.text("Before");
523             sink.verbatim(SinkEventAttributeSet.SOURCE);
524             sink.text("codeline1" + EOL + "codeline2");
525             sink.verbatim_();
526             sink.text("After");
527             sink.listItem_();
528             sink.list_();
529         }
530 
531         String expected =
532                 "- Before" + EOL + EOL + MarkdownMarkup.INDENT + MarkdownMarkup.VERBATIM_START_MARKUP + "unknown" + EOL
533                         + MarkdownMarkup.INDENT + "codeline1" + EOL + MarkdownMarkup.INDENT + "codeline2" + EOL
534                         + MarkdownMarkup.INDENT + MarkdownMarkup.VERBATIM_END_MARKUP + EOL + EOL + "After" + EOL;
535         assertEquals(expected, getSinkContent(), "Wrong verbatim!");
536     }
537 
538     @Test
539     void definitionListWithInlineStyles() {
540         try (Sink sink = getSink()) {
541             sink.definitionList();
542             sink.definedTerm();
543             sink.text("term");
544             sink.definedTerm_();
545             sink.definition();
546             sink.text("prefix ");
547             sink.text("code", SinkEventAttributeSet.Semantics.CODE);
548             sink.text("suffix<a>test</a>", SinkEventAttributeSet.Semantics.EMPHASIS);
549             sink.definition_();
550             sink.definitionList_();
551         }
552         String expected = "<dl>" + EOL + "<dt>term</dt>" + EOL
553                 + "<dd>prefix <code>code</code><em>suffix&lt;a&gt;test&lt;/a&gt;</em></dd>" + EOL + "</dl>";
554         assertEquals(expected, getSinkContent(), "Wrong heading after inline element!");
555     }
556 
557     @Test
558     void nestedListBeingTight() {
559         try (Sink sink = getSink()) {
560             sink.list();
561             sink.listItem();
562             sink.text("item 1");
563             sink.listItem_();
564             sink.listItem();
565             sink.text("item 2");
566             sink.list();
567             sink.listItem();
568             sink.text("item 2 a");
569             sink.listItem_();
570             sink.listItem();
571             sink.text("item 2 b");
572             sink.listItem_();
573             sink.list_();
574             sink.listItem_();
575             sink.list_();
576             sink.listItem();
577             sink.text("item 3");
578             sink.listItem_();
579             sink.list();
580         }
581         String expected = "- item 1" + EOL
582                 + "- item 2" + EOL
583                 + "    - item 2 a" + EOL
584                 + "    - item 2 b" + EOL
585                 + "- item 3" + EOL;
586         assertEquals(expected, getSinkContent());
587     }
588 
589     @Test
590     void linkInsideHtmlSection() {
591         try (Sink sink = getSink()) {
592             sink.definitionList();
593             sink.definedTerm();
594             sink.text("question1");
595             sink.definedTerm_();
596             sink.definition();
597             sink.link("#top");
598             sink.text("[top]");
599             sink.link_();
600             sink.definition_();
601             sink.definitionList_();
602         }
603         String expected =
604                 "<dl>" + EOL + "<dt>question1</dt>" + EOL + "<dd><a href=\"#top\">[top]</a></dd>" + EOL + "</dl>";
605         assertEquals(expected, getSinkContent());
606     }
607 
608     @Test
609     void commentPriorHead() {
610         try (Sink sink = getSink()) {
611             sink.comment("This is a comment");
612             sink.head();
613             sink.title();
614             sink.text("Title");
615             sink.title_();
616             sink.author();
617             sink.text("author1");
618             sink.author_();
619             sink.head_();
620         }
621         String expected = "---" + EOL
622                 + "title: Title" + EOL
623                 + "author: " + EOL
624                 + "  - author1" + EOL
625                 + "---" + EOL
626                 + EOL
627                 + "<!--This is a comment-->";
628         assertEquals(expected, getSinkContent(), "Wrong metadata section");
629     }
630 
631     @Test
632     void insignificantWhitespaceAfterBlockElement() {
633         try (Sink sink = getSink()) {
634             sink.paragraph();
635             sink.text("paragraph");
636             sink.paragraph_();
637             sink.markupLineBreak(
638                     4); // this should be ignored as paragraph_() in markdown is already followed by a blank line
639             sink.text("text after insignificant whitespace");
640         }
641         String expected = "paragraph" + EOL + EOL + "text after insignificant whitespace";
642         assertEquals(expected, getSinkContent());
643     }
644 
645     @Test
646     void listItemsContainingInsignificantWhitespace() {
647         try (Sink sink = getSink()) {
648             sink.list();
649             sink.listItem();
650             sink.markupLineBreak(4);
651             sink.text("item 1");
652             sink.listItem_();
653             sink.listItem();
654             sink.markupLineBreak(4);
655             sink.text("item 2");
656             sink.listItem_();
657             sink.list_();
658         }
659         String expected = "- item 1" + EOL + "- item 2" + EOL;
660         assertEquals(expected, getSinkContent());
661     }
662 
663     @Test
664     void tableWithInsignificantNewLines() throws ParseException, IOException {
665         try (Sink sink = getSink()) {
666             parseFile(htmlParser, "table", "html", sink);
667         }
668         String expected = "|Format<br />Newline|Short description|Doxia Module|" + EOL
669                 + "|---|---|---|" + EOL
670                 + "|[iText](../modules/index.html#iText)|iText PDF Library|[`doxia-module-itext`](../doxia/doxia-modules/doxia-module-itext/)|"
671                 + EOL
672                 + "|[FO](../modules/index.html#FO)<sup>*</sup>|XSL formatting objects \\(XSL-FO\\)|[`doxia-module-fo`](../doxia/doxia-modules/doxia-module-fo/)|"
673                 + EOL
674                 + "|[LaTeX](../modules/index.html#LaTeX)|LaTeX typesetting system|[`doxia-module-latex`](../doxia/doxia-modules/doxia-module-latex/)|"
675                 + EOL
676                 + "|[RTF](../modules/index.html#RTF)|Microsoft Rich Text Format|[`doxia-module-rtf`](../doxia/doxia-modules/doxia-module-rtf/)|"
677                 + EOL + EOL;
678         assertEquals(expected, getSinkContent());
679     }
680 
681     @Test
682     void linkFromHtml() throws ParseException, IOException {
683         try (Sink sink = getSink()) {
684             parseFile(htmlParser, "link", "html", getSink());
685         }
686         String expected = "[plugin](http://maven.apache.org/maven-1.x/plugins/xdoc/reference/xdocs.html) documentation."
687                 + EOL + EOL;
688         assertEquals(expected, getSinkContent());
689     }
690 
691     @Test
692     void verbatimInTableCell() throws ParseException, IOException {
693         try (Sink sink = getSink()) {
694             sink.table();
695             sink.tableRows();
696             sink.tableRow();
697             sink.tableCell();
698             sink.verbatim(SinkEventAttributeSet.SOURCE);
699             sink.text("code with | and ` inside");
700             sink.verbatim_();
701             sink.tableCell_();
702             sink.tableRow_();
703             sink.tableRow();
704             sink.tableCell();
705             sink.verbatim(SinkEventAttributeSet.SOURCE);
706             sink.text("code with | and ` inside");
707             sink.verbatim_();
708             sink.tableCell_();
709             sink.tableRow_();
710             sink.tableRows_();
711             sink.table_();
712         }
713         String expected = "|   |" + EOL + "|---|" + EOL
714                 + "|<pre><code>code with | and ` inside</code></pre>|" + EOL
715                 + "|<pre><code>code with | and ` inside</code></pre>|" + EOL + EOL;
716         assertEquals(expected, getSinkContent());
717     }
718 }