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 javax.swing.text.AttributeSet;
22 import javax.swing.text.MutableAttributeSet;
23
24 import java.io.Writer;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.LinkedList;
30 import java.util.List;
31 import java.util.Queue;
32 import java.util.stream.Collectors;
33
34 import org.apache.maven.doxia.sink.Sink;
35 import org.apache.maven.doxia.sink.SinkEventAttributes;
36 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
37 import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink;
38 import org.apache.maven.doxia.util.DoxiaStringUtils;
39 import org.apache.maven.doxia.util.HtmlTools;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43
44
45
46
47
48
49 public class MarkdownSink extends Xhtml5BaseSink implements MarkdownMarkup {
50 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
51
52
53
54
55
56
57 private Collection<String> authors;
58
59
60 private String title;
61
62
63 private String date;
64
65
66 private String linkName;
67
68
69 private boolean tableHeaderCellFlag;
70
71
72 private int cellCount;
73
74
75 private List<Integer> cellJustif;
76
77
78 private boolean isFirstTableRow;
79
80
81 private final BufferingStackWriter bufferingStackWriter;
82
83
84 private final LastTwoLinesAwareWriter lineAwareWriter;
85
86 private static final String USE_XHTML_SINK = "XhtmlSink";
87
88
89 protected Queue<Queue<String>> inlineStack;
90
91
92 protected Queue<ElementContext> elementContextStack;
93
94 private String figureSrc;
95
96
97 private boolean isVerbatimHtmlContext;
98
99 @FunctionalInterface
100 interface TextEscapeFunction {
101 String escape(ElementContext context, LastTwoLinesAwareWriter writer, String text);
102 }
103
104 enum ElementContext {
105 ROOT_WITH_BUFFERING(
106 Type.GENERIC_CONTAINER,
107 true,
108 ElementContext::escapeMarkdown,
109 true),
110 ROOT_WITHOUT_BUFFERING(
111 Type.GENERIC_CONTAINER,
112 true,
113 null,
114 false),
115 HEAD(Type.GENERIC_CONTAINER, false, null, true),
116 BODY(Type.GENERIC_CONTAINER, true, ElementContext::escapeMarkdown),
117
118 FIGURE(Type.INLINE, false, ElementContext::escapeMarkdown, true),
119 HEADING(Type.LEAF_BLOCK, false, ElementContext::escapeMarkdown),
120 CODE_BLOCK(Type.LEAF_BLOCK, false, null),
121 CODE_SPAN(Type.INLINE, false, null, true),
122 TABLE(Type.CONTAINER_BLOCK, false, null, false, "", true),
123 TABLE_CAPTION(Type.INLINE, false, ElementContext::escapeMarkdown),
124 TABLE_ROW(Type.INLINE, false, null, true),
125 TABLE_CELL(
126 Type.INLINE,
127 false,
128 ElementContext::escapeForTableCell,
129 false),
130
131 LIST_ITEM(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, INDENT),
132 BLOCKQUOTE(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP),
133 HTML_BLOCK(Type.GENERIC_CONTAINER, true, ElementContext::escapeHtml, false, "", false);
134
135
136
137
138 enum Type {
139
140
141
142 GENERIC_CONTAINER,
143
144
145
146 CONTAINER_BLOCK,
147
148
149
150 LEAF_BLOCK,
151
152
153
154 INLINE
155 }
156
157
158
159 final Type type;
160
161
162
163
164 final TextEscapeFunction escapeFunction;
165
166
167
168
169 final boolean requiresBuffering;
170
171
172
173
174 final String prefix;
175
176
177
178
179 final boolean requiresSurroundingByBlankLines;
180
181
182
183
184
185 final boolean allowsMarkupLinebreaks;
186
187 ElementContext(Type type, boolean allowsMarkupLinebreaks, TextEscapeFunction escapeFunction) {
188 this(type, allowsMarkupLinebreaks, escapeFunction, false);
189 }
190
191 ElementContext(
192 Type type,
193 boolean allowsMarkupLinebreaks,
194 TextEscapeFunction escapeFunction,
195 boolean requiresBuffering) {
196 this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, "");
197 }
198
199 ElementContext(
200 Type type,
201 boolean allowsMarkupLinebreaks,
202 TextEscapeFunction escapeFunction,
203 boolean requiresBuffering,
204 String prefix) {
205 this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, prefix, false);
206 }
207
208 ElementContext(
209 Type type,
210 boolean allowsMarkupLinebreaks,
211 TextEscapeFunction escapeFunction,
212 boolean requiresBuffering,
213 String prefix,
214 boolean requiresSurroundingByBlankLines) {
215 this.type = type;
216 this.allowsMarkupLinebreaks = allowsMarkupLinebreaks;
217 this.escapeFunction = escapeFunction;
218 this.requiresBuffering = requiresBuffering;
219 if (type != Type.CONTAINER_BLOCK && prefix.length() != 0) {
220 throw new IllegalArgumentException("Only container blocks may define a prefix (for nesting)");
221 }
222 this.prefix = prefix;
223 this.requiresSurroundingByBlankLines = requiresSurroundingByBlankLines;
224 }
225
226
227
228
229
230
231 String escape(LastTwoLinesAwareWriter writer, String text) {
232
233 if (escapeFunction == null) {
234 return text;
235 } else {
236 return escapeFunction.escape(this, writer, text);
237 }
238 }
239
240
241
242
243
244 boolean isBlock() {
245 return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK;
246 }
247
248
249
250
251
252 boolean isHtml() {
253 return this.equals(HTML_BLOCK);
254 }
255
256
257
258
259 boolean isContainer() {
260 return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER;
261 }
262
263 public boolean isAllowsMarkupLinebreaks() {
264 return allowsMarkupLinebreaks;
265 }
266
267
268
269
270
271
272
273
274
275
276
277
278
279 private String escapeMarkdown(LastTwoLinesAwareWriter writer, String text) {
280 if (text == null) {
281 return "";
282 }
283 text = escapeHtml(writer, text);
284 int length = text.length();
285 StringBuilder buffer = new StringBuilder(length);
286
287 for (int i = 0; i < length; ++i) {
288 char c = text.charAt(i);
289 switch (c) {
290 case '\\':
291 case '_':
292 case '`':
293 case '[':
294 case ']':
295 case '(':
296 case ')':
297 case '!':
298
299 buffer.append(escapeMarkdown(c));
300 break;
301 case '*':
302 case '+':
303 case '-':
304
305 if (isInBlankLine(buffer, writer)) {
306 buffer.append(escapeMarkdown(c));
307 } else {
308 buffer.append(c);
309 }
310 break;
311 case '=':
312 case '#':
313 if (this == HEADING || isInBlankLine(buffer, writer)) {
314 buffer.append(escapeMarkdown(c));
315 } else {
316 buffer.append(c);
317 }
318 break;
319 case '.':
320 if (isAfterDigit(buffer, writer)) {
321 buffer.append(escapeMarkdown(c));
322 } else {
323 buffer.append(c);
324 }
325 break;
326 default:
327 buffer.append(c);
328 }
329 }
330 return buffer.toString();
331 }
332
333 private static boolean isAfterDigit(StringBuilder buffer, LastTwoLinesAwareWriter writer) {
334 if (buffer.length() > 0) {
335 return Character.isDigit(buffer.charAt(buffer.length() - 1));
336 } else {
337 return writer.isAfterDigit();
338 }
339 }
340
341 private static boolean isInBlankLine(StringBuilder buffer, LastTwoLinesAwareWriter writer) {
342 if (DoxiaStringUtils.isBlank(buffer.toString())) {
343 return writer.isInBlankLine();
344 }
345 return false;
346 }
347
348 private static String escapeMarkdown(char c) {
349 return "\\" + c;
350 }
351
352 private String escapeHtml(LastTwoLinesAwareWriter writer, String text) {
353 return HtmlTools.escapeHTML(text, true);
354 }
355
356
357
358
359
360
361
362
363 private String escapeForTableCell(LastTwoLinesAwareWriter writer, String text) {
364 return escapeMarkdown(writer, text).replace("|", "\\|");
365 }
366 }
367
368
369
370
371 protected static MarkdownSink newInstance(Writer writer) {
372 BufferingStackWriter bufferingStackWriter = new BufferingStackWriter(writer);
373 LastTwoLinesAwareWriter lineAwareWriter = new LastTwoLinesAwareWriter(bufferingStackWriter);
374 return new MarkdownSink(lineAwareWriter, bufferingStackWriter);
375 }
376
377
378
379
380
381
382 private MarkdownSink(LastTwoLinesAwareWriter lineAwareWriter, BufferingStackWriter bufferingStackWriter) {
383 super(lineAwareWriter);
384 this.lineAwareWriter = lineAwareWriter;
385 this.bufferingStackWriter = bufferingStackWriter;
386 initInternal();
387 setInsertNewline(
388 false);
389
390 }
391
392 private void initInternal() {
393 this.authors = new LinkedList<>();
394 this.title = null;
395 this.date = null;
396 this.linkName = null;
397 this.tableHeaderCellFlag = false;
398 this.cellCount = 0;
399 this.cellJustif = null;
400 this.elementContextStack = Collections.asLifoQueue(new LinkedList<>());
401 this.inlineStack = Collections.asLifoQueue(new LinkedList<>());
402 startContext(ElementContext.ROOT_WITH_BUFFERING);
403 }
404
405 private void endContext(ElementContext expectedContext) {
406 ElementContext removedContext = elementContextStack.remove();
407 if (removedContext != expectedContext) {
408 throw new IllegalStateException("Unexpected context " + removedContext + ", expected " + expectedContext);
409 }
410 if (removedContext.isBlock()) {
411 endBlock(removedContext.requiresSurroundingByBlankLines
412 || (isInListItem() && (removedContext == ElementContext.BLOCKQUOTE)
413 || (removedContext == ElementContext.CODE_BLOCK)));
414 }
415 if (removedContext.requiresBuffering) {
416
417 bufferingStackWriter.removeBuffer();
418 }
419 }
420
421 private void startContext(ElementContext newContext) {
422 if (newContext.requiresBuffering) {
423 bufferingStackWriter.addBuffer();
424 }
425 if (newContext.isBlock()) {
426
427 startBlock(newContext.requiresSurroundingByBlankLines
428 || (isInListItem() && (newContext == ElementContext.BLOCKQUOTE)
429 || (newContext == ElementContext.CODE_BLOCK)));
430 }
431 elementContextStack.add(newContext);
432 }
433
434 private String toogleToRootContextWithoutBuffering(boolean dumpBuffer) {
435 final String buffer;
436 if (elementContextStack.element() == ElementContext.ROOT_WITH_BUFFERING) {
437 buffer = bufferingStackWriter.getCurrentBuffer().toString();
438 endContext(ElementContext.ROOT_WITH_BUFFERING);
439 if (dumpBuffer) {
440 write(buffer);
441 }
442 startContext(ElementContext.ROOT_WITHOUT_BUFFERING);
443 } else if (elementContextStack.element() != ElementContext.ROOT_WITHOUT_BUFFERING) {
444 throw new IllegalStateException("Unexpected context " + elementContextStack.element()
445 + ", expected ROOT_WITH_BUFFERING or ROOT_WITHOUT_BUFFERING");
446 } else {
447 buffer = "";
448 }
449 return buffer;
450 }
451
452
453
454
455 private void ensureBeginningOfLine() {
456
457 if (!lineAwareWriter.isWriterAtStartOfNewLine()) {
458 write(EOL);
459 }
460 }
461
462
463
464
465
466 private void ensureBlankLine() {
467
468 if (!lineAwareWriter.isWriterAfterBlankLine()) {
469 if (lineAwareWriter.isWriterAtStartOfNewLine()) {
470 write(EOL);
471 } else {
472 write(BLANK_LINE);
473 }
474 }
475 }
476
477 private void startBlock(boolean requireBlankLine) {
478 if (requireBlankLine) {
479 ensureBlankLine();
480 } else {
481 ensureBeginningOfLine();
482 }
483 write(getLinePrefix());
484 }
485
486 private void endBlock(boolean requireBlankLine) {
487 if (requireBlankLine) {
488 ensureBlankLine();
489 } else {
490 ensureBeginningOfLine();
491 }
492 }
493
494
495
496
497 private String getLinePrefix() {
498 StringBuilder prefix = new StringBuilder();
499 elementContextStack.stream().filter(c -> c.prefix.length() > 0).forEachOrdered(c -> prefix.insert(0, c.prefix));
500 return prefix.toString();
501 }
502
503 private boolean isInListItem() {
504 return elementContextStack.stream()
505 .filter(c -> c == ElementContext.LIST_ITEM)
506 .findFirst()
507 .isPresent();
508 }
509
510 @Override
511 protected void init() {
512 super.init();
513 initInternal();
514 }
515
516 @Override
517 public void head(SinkEventAttributes attributes) {
518 startContext(ElementContext.HEAD);
519 }
520
521 @Override
522 public void head_() {
523 endContext(ElementContext.HEAD);
524 String priorHeadBuffer = toogleToRootContextWithoutBuffering(false);
525
526 if (title == null && authors.isEmpty() && date == null) {
527 return;
528 }
529 write(METADATA_MARKUP + EOL);
530 if (title != null) {
531 write("title: " + title + EOL);
532 }
533 if (!authors.isEmpty()) {
534 write("author: " + EOL);
535 for (String author : authors) {
536 write(" - " + author + EOL);
537 }
538 }
539 if (date != null) {
540 write("date: " + date + EOL);
541 }
542 write(METADATA_MARKUP + BLANK_LINE);
543 write(priorHeadBuffer);
544 }
545
546 @Override
547 public void body(SinkEventAttributes attributes) {
548 toogleToRootContextWithoutBuffering(true);
549 startContext(ElementContext.BODY);
550 }
551
552 @Override
553 public void body_() {
554 endContext(ElementContext.BODY);
555 }
556
557 @Override
558 public void title_() {
559 String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
560 if (!buffer.isEmpty()) {
561 this.title = buffer;
562 }
563 }
564
565 @Override
566 public void author_() {
567 String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
568 if (!buffer.isEmpty()) {
569 authors.add(buffer);
570 }
571 }
572
573 @Override
574 public void date_() {
575 String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
576 if (!buffer.isEmpty()) {
577 date = buffer;
578 }
579 }
580
581 @Override
582 public void section(int level, SinkEventAttributes attributes) {
583
584 }
585
586 @Override
587 public void section_(int level) {
588
589 }
590
591 @Override
592 public void header(SinkEventAttributes attributes) {
593
594 }
595
596 @Override
597 public void header_() {
598
599 }
600
601 @Override
602 public void sectionTitle(int level, SinkEventAttributes attributes) {
603 startContext(ElementContext.HEADING);
604 if (level > 0) {
605 write(DoxiaStringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE);
606 }
607 }
608
609 @Override
610 public void sectionTitle_(int level) {
611 endContext(ElementContext.HEADING);
612 if (level > 0) {
613 ensureBlankLine();
614
615 }
616 }
617
618 @Override
619 public void list(SinkEventAttributes attributes) {
620 if (elementContextStack.element().isHtml()) {
621 super.list(attributes);
622 }
623 }
624
625 @Override
626 public void list_() {
627 ensureBeginningOfLine();
628 }
629
630 @Override
631 public void listItem(SinkEventAttributes attributes) {
632 startContext(ElementContext.LIST_ITEM);
633 write(LIST_UNORDERED_ITEM_START_MARKUP);
634 }
635
636 @Override
637 public void listItem_() {
638 endContext(ElementContext.LIST_ITEM);
639 }
640
641 @Override
642 public void numberedList(int numbering, SinkEventAttributes attributes) {
643
644 if (numbering != NUMBERING_DECIMAL) {
645 LOGGER.warn(
646 "{}Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style",
647 getLocationLogPrefix(),
648 NUMBERING_DECIMAL,
649 numbering);
650 }
651 }
652
653 @Override
654 public void numberedList_() {
655 ensureBeginningOfLine();
656 }
657
658 @Override
659 public void numberedListItem(SinkEventAttributes attributes) {
660 startContext(ElementContext.LIST_ITEM);
661 write(LIST_ORDERED_ITEM_START_MARKUP);
662 }
663
664 @Override
665 public void numberedListItem_() {
666 listItem_();
667 }
668
669 @Override
670 public void definitionList(SinkEventAttributes attributes) {
671 LOGGER.warn(
672 "{}Definition list not natively supported in Markdown, rendering HTML instead", getLocationLogPrefix());
673 startContext(ElementContext.HTML_BLOCK);
674 write("<dl>" + EOL);
675 }
676
677 @Override
678 public void definitionList_() {
679 write("</dl>");
680 endContext(ElementContext.HTML_BLOCK);
681 }
682
683 @Override
684 public void definedTerm(SinkEventAttributes attributes) {
685 write("<dt>");
686 }
687
688 @Override
689 public void definedTerm_() {
690 write("</dt>" + EOL);
691 }
692
693 @Override
694 public void definition(SinkEventAttributes attributes) {
695 write("<dd>");
696 }
697
698 @Override
699 public void definition_() {
700 write("</dd>" + EOL);
701 }
702
703 @Override
704 public void pageBreak() {
705 LOGGER.warn("{}Ignoring unsupported page break in Markdown", getLocationLogPrefix());
706 }
707
708 @Override
709 public void paragraph(SinkEventAttributes attributes) {
710
711 if (elementContextStack.element().isContainer()) {
712 ensureBlankLine();
713 write(getLinePrefix());
714 } else {
715 LOGGER.warn(
716 "{}Paragraphs outside of container contexts are not supported in Markdown, ignoring paragraph event in context {}",
717 getLocationLogPrefix(),
718 elementContextStack.element());
719 }
720 }
721
722 @Override
723 public void paragraph_() {
724
725 if (elementContextStack.element().isContainer()) {
726 ensureBlankLine();
727 }
728 }
729
730 @Override
731 public void verbatim(SinkEventAttributes attributes) {
732 if (!elementContextStack.element().isContainer()) {
733
734 startContext(ElementContext.HTML_BLOCK);
735 isVerbatimHtmlContext = true;
736 } else {
737 isVerbatimHtmlContext = false;
738 }
739
740 if (elementContextStack.element().isHtml()) {
741 super.verbatim(attributes);
742 } else {
743
744 startContext(ElementContext.CODE_BLOCK);
745 write(VERBATIM_START_MARKUP);
746 if (attributes != null && attributes.containsAttributes(SinkEventAttributeSet.SOURCE)) {
747 write("unknown");
748 }
749 write(EOL);
750 write(getLinePrefix());
751 }
752 }
753
754 @Override
755 public void verbatim_() {
756 if (elementContextStack.element().isHtml()) {
757 super.verbatim_();
758 if (isVerbatimHtmlContext) {
759 endContext(ElementContext.HTML_BLOCK);
760 isVerbatimHtmlContext = false;
761 }
762 } else {
763 ensureBeginningOfLine();
764 write(getLinePrefix());
765 write(VERBATIM_END_MARKUP + BLANK_LINE);
766 endContext(ElementContext.CODE_BLOCK);
767 }
768 }
769
770 @Override
771 public void blockquote(SinkEventAttributes attributes) {
772 if (elementContextStack.element().isHtml()) {
773 super.blockquote(attributes);
774 } else {
775 startContext(ElementContext.BLOCKQUOTE);
776 write(BLOCKQUOTE_START_MARKUP);
777 }
778 }
779
780 @Override
781 public void blockquote_() {
782 if (elementContextStack.element().isHtml()) {
783 super.blockquote_();
784 } else {
785 endContext(ElementContext.BLOCKQUOTE);
786 }
787 }
788
789 @Override
790 public void horizontalRule(SinkEventAttributes attributes) {
791 ensureBeginningOfLine();
792 write(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
793 write(getLinePrefix());
794 }
795
796 @Override
797 public void table(SinkEventAttributes attributes) {
798 if (elementContextStack.element().isHtml()) {
799 super.table(attributes);
800 } else {
801 startContext(ElementContext.TABLE);
802 }
803 }
804
805 @Override
806 public void table_() {
807 if (elementContextStack.element().isHtml()) {
808 super.table_();
809 } else {
810 endContext(ElementContext.TABLE);
811 }
812 }
813
814 @Override
815 public void tableRows(int[] justification, boolean grid) {
816 if (elementContextStack.element().isHtml()) {
817 super.tableRows(justification, grid);
818 } else {
819 if (justification != null) {
820 cellJustif = Arrays.stream(justification).boxed().collect(Collectors.toCollection(ArrayList::new));
821 } else {
822 cellJustif = new ArrayList<>();
823 }
824
825 isFirstTableRow = true;
826 }
827 }
828
829 @Override
830 public void tableRows_() {
831 if (elementContextStack.element().isHtml()) {
832 super.tableRows_();
833 } else {
834 cellJustif = null;
835 }
836 }
837
838 @Override
839 public void tableRow(SinkEventAttributes attributes) {
840 if (elementContextStack.element().isHtml()) {
841 super.tableRow(attributes);
842 } else {
843 startContext(ElementContext.TABLE_ROW);
844 cellCount = 0;
845 }
846 }
847
848 @Override
849 public void tableRow_() {
850 if (elementContextStack.element().isHtml()) {
851 super.tableRow_();
852 } else {
853 String buffer = bufferingStackWriter.getAndClearCurrentBuffer();
854 endContext(ElementContext.TABLE_ROW);
855 if (isFirstTableRow && !tableHeaderCellFlag) {
856
857
858 writeEmptyTableHeader();
859 writeTableDelimiterRow();
860 tableHeaderCellFlag = false;
861 isFirstTableRow = false;
862
863 }
864 write(TABLE_ROW_PREFIX);
865 write(buffer);
866 write(EOL);
867 if (isFirstTableRow) {
868
869 writeTableDelimiterRow();
870 isFirstTableRow = false;
871 }
872
873 cellCount = 0;
874 }
875 }
876
877 private void writeEmptyTableHeader() {
878 write(TABLE_ROW_PREFIX);
879 for (int i = 0; i < cellCount; i++) {
880 write(DoxiaStringUtils.repeat(String.valueOf(SPACE), 3) + TABLE_CELL_SEPARATOR_MARKUP);
881 }
882 write(EOL);
883 write(getLinePrefix());
884 }
885
886
887 private void writeTableDelimiterRow() {
888 write(TABLE_ROW_PREFIX);
889 int justification = Sink.JUSTIFY_DEFAULT;
890 for (int i = 0; i < cellCount; i++) {
891
892 if (cellJustif != null && cellJustif.size() > i) {
893 justification = cellJustif.get(i);
894 }
895 switch (justification) {
896 case Sink.JUSTIFY_RIGHT:
897 write(TABLE_COL_RIGHT_ALIGNED_MARKUP);
898 break;
899 case Sink.JUSTIFY_CENTER:
900 write(TABLE_COL_CENTER_ALIGNED_MARKUP);
901 break;
902 case Sink.JUSTIFY_LEFT:
903 write(TABLE_COL_LEFT_ALIGNED_MARKUP);
904 break;
905 default:
906 write(TABLE_COL_DEFAULT_ALIGNED_MARKUP);
907 break;
908 }
909 write(TABLE_CELL_SEPARATOR_MARKUP);
910 }
911 write(EOL);
912 }
913
914 @Override
915 public void tableCell(SinkEventAttributes attributes) {
916 if (elementContextStack.element().isHtml()) {
917 super.tableCell(attributes);
918 } else {
919 startContext(ElementContext.TABLE_CELL);
920 if (attributes != null) {
921
922 final int cellJustification;
923 if (attributes.containsAttributes(SinkEventAttributeSet.LEFT)) {
924 cellJustification = Sink.JUSTIFY_LEFT;
925 } else if (attributes.containsAttributes(SinkEventAttributeSet.RIGHT)) {
926 cellJustification = Sink.JUSTIFY_RIGHT;
927 } else if (attributes.containsAttributes(SinkEventAttributeSet.CENTER)) {
928 cellJustification = Sink.JUSTIFY_CENTER;
929 } else {
930 cellJustification = -1;
931 }
932 if (cellJustification > -1) {
933 if (cellJustif.size() > cellCount) {
934 cellJustif.set(cellCount, cellJustification);
935 } else if (cellJustif.size() == cellCount) {
936 cellJustif.add(cellJustification);
937 } else {
938
939 for (int precedingCol = cellJustif.size(); precedingCol < cellCount; precedingCol++) {
940 cellJustif.add(Sink.JUSTIFY_DEFAULT);
941 }
942 cellJustif.add(cellJustification);
943 }
944 }
945 }
946 }
947 }
948
949 @Override
950 public void tableHeaderCell(SinkEventAttributes attributes) {
951 if (elementContextStack.element().isHtml()) {
952 super.tableHeaderCell(attributes);
953 } else {
954 tableCell(attributes);
955 tableHeaderCellFlag = true;
956 }
957 }
958
959 @Override
960 public void tableCell_() {
961 if (elementContextStack.element().isHtml()) {
962 super.tableCell_();
963 } else {
964 endTableCell();
965 }
966 }
967
968 @Override
969 public void tableHeaderCell_() {
970 if (elementContextStack.element().isHtml()) {
971 super.tableHeaderCell_();
972 } else {
973 endTableCell();
974 }
975 }
976
977
978
979
980 private void endTableCell() {
981 endContext(ElementContext.TABLE_CELL);
982 write(TABLE_CELL_SEPARATOR_MARKUP);
983 cellCount++;
984 }
985
986 @Override
987 public void tableCaption(SinkEventAttributes attributes) {
988 if (elementContextStack.element().isHtml()) {
989 super.tableCaption(attributes);
990 } else {
991 elementContextStack.add(ElementContext.TABLE_CAPTION);
992 }
993 }
994
995 @Override
996 public void tableCaption_() {
997 if (elementContextStack.element().isHtml()) {
998 super.tableCaption_();
999 } else {
1000 endContext(ElementContext.TABLE_CAPTION);
1001 }
1002 }
1003
1004 @Override
1005 public void figure(SinkEventAttributes attributes) {
1006 if (elementContextStack.element().isHtml()) {
1007 super.figure(attributes);
1008 } else {
1009 figureSrc = null;
1010 startContext(ElementContext.FIGURE);
1011 }
1012 }
1013
1014 @Override
1015 public void figureCaption(SinkEventAttributes attributes) {
1016 if (elementContextStack.element().isHtml()) {
1017 super.figureCaption(attributes);
1018 }
1019 }
1020
1021 @Override
1022 public void figureCaption_() {
1023 if (elementContextStack.element().isHtml()) {
1024 super.figureCaption_();
1025 }
1026 }
1027
1028 @Override
1029 public void figureGraphics(String name, SinkEventAttributes attributes) {
1030 if (elementContextStack.element().isHtml()) {
1031 super.figureGraphics(name, attributes);
1032 } else {
1033 figureSrc = name;
1034
1035 if (elementContextStack.peek() != ElementContext.FIGURE) {
1036 Object alt = attributes.getAttribute(SinkEventAttributes.ALT);
1037 if (alt == null) {
1038 alt = "";
1039 }
1040 writeImage(elementContextStack.element().escape(lineAwareWriter, alt.toString()), name);
1041 }
1042 }
1043 }
1044
1045 @Override
1046 public void figure_() {
1047 if (elementContextStack.element().isHtml()) {
1048 super.figure_();
1049 } else {
1050 String label = bufferingStackWriter.getCurrentBuffer().toString();
1051 endContext(ElementContext.FIGURE);
1052 writeImage(label, figureSrc);
1053 }
1054 }
1055
1056 private void writeImage(String alt, String src) {
1057 write("");
1060 }
1061
1062 public void anchor(String name, SinkEventAttributes attributes) {
1063 super.anchor(name, attributes);
1064 if (!elementContextStack.element().isHtml()) {
1065
1066 write("</a>");
1067 }
1068 }
1069
1070 @Override
1071 public void anchor_() {
1072 if (elementContextStack.element().isHtml()) {
1073 super.anchor_();
1074 } else {
1075
1076 }
1077 }
1078
1079 public void link(String name, SinkEventAttributes attributes) {
1080 if (elementContextStack.element().isHtml()) {
1081 super.link(name, attributes);
1082 } else {
1083 if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1084 LOGGER.warn("{}Ignoring unsupported link inside code block", getLocationLogPrefix());
1085 } else if (elementContextStack.element() == ElementContext.CODE_SPAN) {
1086
1087 bufferingStackWriter.getCurrentBuffer().insert(0, LINK_START_1_MARKUP);
1088 linkName = name;
1089 } else {
1090 write(LINK_START_1_MARKUP);
1091 linkName = name;
1092 }
1093 }
1094 }
1095
1096 @Override
1097 public void link_() {
1098 if (elementContextStack.element().isHtml()) {
1099 super.link_();
1100 } else {
1101 if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1102 return;
1103 } else if (elementContextStack.element() == ElementContext.CODE_SPAN) {
1104
1105 StringBuilder linkEndMarkup = new StringBuilder();
1106 linkEndMarkup.append(LINK_START_2_MARKUP);
1107 linkEndMarkup.append(linkName);
1108 linkEndMarkup.append(LINK_END_MARKUP);
1109 Queue<String> endMarkups = new LinkedList<>(inlineStack.poll());
1110 endMarkups.add(linkEndMarkup.toString());
1111 inlineStack.add(endMarkups);
1112 } else {
1113 write(LINK_START_2_MARKUP + linkName + LINK_END_MARKUP);
1114 }
1115 linkName = null;
1116 }
1117 }
1118
1119 @Override
1120 public void inline(SinkEventAttributes attributes) {
1121 Queue<String> endMarkups = Collections.asLifoQueue(new LinkedList<>());
1122 if (elementContextStack.element().isHtml()) {
1123 super.inline(attributes);
1124 endMarkups.add(USE_XHTML_SINK);
1125 } else {
1126 boolean requiresHtml = elementContextStack.element() == ElementContext.HTML_BLOCK;
1127 if (attributes != null
1128 && elementContextStack.element() != ElementContext.CODE_BLOCK
1129 && elementContextStack.element() != ElementContext.CODE_SPAN) {
1130
1131 if (attributes.containsAttributes(SinkEventAttributeSet.Semantics.CODE)
1132 || attributes.containsAttributes(SinkEventAttributeSet.Semantics.MONOSPACED)
1133 || attributes.containsAttributes(SinkEventAttributeSet.MONOSPACED)) {
1134 if (requiresHtml) {
1135 write("<code>");
1136 endMarkups.add("</code>");
1137 } else {
1138 startContext(ElementContext.CODE_SPAN);
1139 write(MONOSPACED_START_MARKUP);
1140 endMarkups.add(MONOSPACED_END_MARKUP);
1141 }
1142 } else {
1143 SinkEventAttributeSet remainingAttributes = new SinkEventAttributeSet(attributes);
1144
1145 if (filterAttributes(
1146 remainingAttributes,
1147 SinkEventAttributeSet.Semantics.EMPHASIS,
1148 SinkEventAttributeSet.Semantics.ITALIC,
1149 SinkEventAttributeSet.ITALIC)) {
1150 if (requiresHtml) {
1151 write("<em>");
1152 endMarkups.add("</em>");
1153 } else {
1154 write(ITALIC_START_MARKUP);
1155 endMarkups.add(ITALIC_END_MARKUP);
1156 }
1157 }
1158
1159 if (filterAttributes(
1160 remainingAttributes,
1161 SinkEventAttributeSet.Semantics.STRONG,
1162 SinkEventAttributeSet.Semantics.BOLD,
1163 SinkEventAttributeSet.BOLD)) {
1164 if (requiresHtml) {
1165 write("<strong>");
1166 endMarkups.add("</strong>");
1167 } else {
1168 write(BOLD_START_MARKUP);
1169 endMarkups.add(BOLD_END_MARKUP);
1170 }
1171 }
1172
1173 if (filterAttributes(remainingAttributes, SinkEventAttributeSet.Semantics.DELETE)) {
1174 if (requiresHtml) {
1175 write("<del>");
1176 endMarkups.add("</del>");
1177 } else {
1178 write(STRIKETHROUGH_START_MARKUP);
1179 endMarkups.add(STRIKETHROUGH_END_MARKUP);
1180 }
1181 }
1182 if (!remainingAttributes.isEmpty()) {
1183
1184
1185 super.inline(remainingAttributes);
1186 endMarkups.add(USE_XHTML_SINK);
1187 }
1188 }
1189 }
1190 }
1191 inlineStack.add(endMarkups);
1192 }
1193
1194 private static boolean filterAttributes(MutableAttributeSet attributes, AttributeSet... attributesToFilter) {
1195 boolean hasAny = false;
1196 for (AttributeSet attributeSet : attributesToFilter) {
1197 if (attributes.containsAttributes(attributeSet)) {
1198 hasAny = true;
1199 attributes.removeAttributes(attributeSet);
1200 }
1201 }
1202 return hasAny;
1203 }
1204
1205 @Override
1206 public void inline_() {
1207 for (String endMarkup : inlineStack.remove()) {
1208 if (USE_XHTML_SINK.equals(endMarkup)) {
1209 super.inline_();
1210 } else {
1211 if (endMarkup.equals(MONOSPACED_END_MARKUP)) {
1212 String buffer = bufferingStackWriter.getCurrentBuffer().toString();
1213 endContext(ElementContext.CODE_SPAN);
1214 write(buffer);
1215 }
1216 write(endMarkup);
1217 }
1218 }
1219 }
1220
1221 @Override
1222 public void italic() {
1223 inline(SinkEventAttributeSet.Semantics.ITALIC);
1224 }
1225
1226 @Override
1227 public void italic_() {
1228 inline_();
1229 }
1230
1231 @Override
1232 public void bold() {
1233 inline(SinkEventAttributeSet.Semantics.BOLD);
1234 }
1235
1236 @Override
1237 public void bold_() {
1238 inline_();
1239 }
1240
1241 @Override
1242 public void monospaced() {
1243 inline(SinkEventAttributeSet.Semantics.CODE);
1244 }
1245
1246 @Override
1247 public void monospaced_() {
1248 inline_();
1249 }
1250
1251 @Override
1252 public void lineBreak(SinkEventAttributes attributes) {
1253 if (elementContextStack.element() == ElementContext.TABLE_CELL) {
1254 super.lineBreak(attributes);
1255 } else {
1256 if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
1257 write(EOL);
1258 } else {
1259 write("" + SPACE + SPACE + EOL);
1260 }
1261 write(getLinePrefix());
1262 }
1263 }
1264
1265 @Override
1266 public void nonBreakingSpace() {
1267 write(NON_BREAKING_SPACE_MARKUP);
1268 }
1269
1270 @Override
1271 public void text(String text, SinkEventAttributes attributes) {
1272 if (elementContextStack.element().isHtml()) {
1273 super.text(text, attributes);
1274 } else {
1275 if (attributes != null) {
1276 inline(attributes);
1277 }
1278 ElementContext currentContext = elementContextStack.element();
1279 if (currentContext == ElementContext.TABLE_CAPTION) {
1280
1281 LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix());
1282 } else {
1283 String unifiedText = currentContext.escape(lineAwareWriter, unifyEOLs(text));
1284
1285
1286
1287 if (!unifiedText.equals(EOL)) {
1288 String prefix = getLinePrefix();
1289 if (prefix.length() > 0) {
1290 unifiedText = unifiedText.replaceAll(EOL, EOL + prefix);
1291 }
1292 }
1293 write(unifiedText);
1294 }
1295 if (attributes != null) {
1296 inline_();
1297 }
1298 }
1299 }
1300
1301 @Override
1302 public void rawText(String text) {
1303 write(text);
1304 }
1305
1306
1307
1308
1309
1310
1311
1312 @Override
1313 public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
1314 LOGGER.warn("{}Unknown Sink event '" + name + "', ignoring!", getLocationLogPrefix());
1315 }
1316
1317 @Override
1318 public void markupLineBreak(int indentLevel) {
1319
1320 if (elementContextStack.element().isAllowsMarkupLinebreaks()) {
1321 if (!lineAwareWriter.isWriterAfterBlankLine()) {
1322 super.markupLineBreak(indentLevel);
1323 }
1324 }
1325 }
1326
1327 @Override
1328 public void close() {
1329 toogleToRootContextWithoutBuffering(true);
1330 super.close();
1331 }
1332 }