1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
38
39
40
41 public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup {
42 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
43
44
45
46
47
48
49 private StringBuffer buffer;
50
51
52 private StringBuilder tableCaptionBuffer;
53
54
55 private Collection<String> authors;
56
57
58 private String title;
59
60
61 private String date;
62
63
64 private String linkName;
65
66
67 private boolean startFlag;
68
69
70 private boolean tableCaptionFlag;
71
72
73 private boolean tableCellFlag;
74
75
76 private boolean headerFlag;
77
78
79 private boolean bufferFlag;
80
81
82 private boolean itemFlag;
83
84
85 private boolean verbatimFlag;
86
87
88 private boolean gridFlag;
89
90
91 private int cellCount;
92
93
94 private final PrintWriter writer;
95
96
97 private boolean isWriterAtStartOfNewLine;
98
99
100 private int[] cellJustif;
101
102
103 private String rowLine;
104
105
106 private boolean headerRow;
107
108
109 private int listNestingLevel;
110
111
112 private final Stack<String> listStyles;
113
114
115 protected Stack<List<String>> inlineStack = new Stack<>();
116
117
118
119
120
121
122
123
124
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
136
137
138
139 protected StringBuffer getBuffer() {
140 return buffer;
141 }
142
143
144
145
146
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
182
183 protected void resetBuffer() {
184 buffer = new StringBuffer();
185 }
186
187
188
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
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);
317
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
334 public void numberedList(int numbering) {
335 listNestingLevel++;
336
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);
352
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(" ");
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
433 } else {
434 write(BLANK_LINE);
435 }
436 }
437
438
439 @Override
440 public void verbatim() {
441 verbatim(null);
442 }
443
444
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
479 public void tableRows(int[] justification, boolean grid) {
480 cellJustif = null;
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
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
518 cellCount = 0;
519 }
520
521
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
558
559
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
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
600 public void figureGraphics(String name) {
601 write("<img src=\"" + name + "\" />");
602 }
603
604
605 public void anchor(String name) {
606
607
608 }
609
610 @Override
611 public void anchor_() {
612
613 }
614
615
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
635
636
637
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
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
761
762
763
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
772
773
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
788
789
790
791 protected void content(String text) {
792 write(escapeMarkdown(text));
793 }
794
795
796
797
798
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
818
819
820
821
822
823
824
825
826
827
828
829
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
870
871
872 private void ensureBeginningOfLine() {
873
874 if (!isWriterAtStartOfNewLine) {
875 write(EOL);
876 }
877 }
878 }