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.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.LinkedList;
28 import java.util.List;
29 import java.util.Queue;
30 import java.util.function.UnaryOperator;
31 import java.util.stream.Collectors;
32
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.maven.doxia.sink.Sink;
35 import org.apache.maven.doxia.sink.SinkEventAttributes;
36 import org.apache.maven.doxia.sink.impl.AbstractTextSink;
37 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
38 import org.apache.maven.doxia.util.HtmlTools;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42
43
44
45
46
47 public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup {
48 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class);
49
50
51
52
53
54
55
56 private StringBuilder buffer;
57
58
59 private Collection<String> authors;
60
61
62 private String title;
63
64
65 private String date;
66
67
68 private String linkName;
69
70
71 private boolean tableHeaderCellFlag;
72
73
74 private int cellCount;
75
76
77 private List<Integer> cellJustif;
78
79
80 private boolean isFirstTableRow;
81
82
83 private final PrintWriter writer;
84
85
86 private final LastTwoLinesBufferingWriter bufferingWriter;
87
88
89 protected Queue<Queue<String>> inlineStack = Collections.asLifoQueue(new LinkedList<>());
90
91
92 protected Queue<ElementContext> elementContextStack = Collections.asLifoQueue(new LinkedList<>());
93
94 private String figureSrc;
95
96
97 enum ElementContext {
98 HEAD("head", Type.GENERIC_CONTAINER, null, true),
99 BODY("body", Type.GENERIC_CONTAINER, MarkdownSink::escapeMarkdown),
100
101 FIGURE("", Type.INLINE, MarkdownSink::escapeMarkdown, true),
102 CODE_BLOCK("code block", Type.LEAF_BLOCK, null, false),
103 CODE_SPAN("code span", Type.INLINE, null),
104 TABLE_CAPTION("table caption", Type.INLINE, MarkdownSink::escapeMarkdown),
105 TABLE_CELL(
106 "table cell",
107 Type.LEAF_BLOCK,
108 MarkdownSink::escapeForTableCell,
109 true),
110
111 LIST_ITEM("list item", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, INDENT),
112 BLOCKQUOTE("blockquote", Type.CONTAINER_BLOCK, MarkdownSink::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP);
113
114 final String name;
115
116
117
118
119 enum Type {
120
121
122
123 GENERIC_CONTAINER,
124
125
126
127 CONTAINER_BLOCK,
128
129
130
131 LEAF_BLOCK,
132
133
134
135 INLINE
136 }
137
138
139
140 final Type type;
141
142
143
144
145 final UnaryOperator<String> escapeFunction;
146
147
148
149
150 final boolean requiresBuffering;
151
152
153
154
155 final String prefix;
156
157
158
159
160 final boolean requiresSurroundingByBlankLines;
161
162 ElementContext(String name, Type type, UnaryOperator<String> escapeFunction) {
163 this(name, type, escapeFunction, false);
164 }
165
166 ElementContext(String name, Type type, UnaryOperator<String> escapeFunction, boolean requiresBuffering) {
167 this(name, type, escapeFunction, requiresBuffering, "");
168 }
169
170 ElementContext(
171 String name,
172 Type type,
173 UnaryOperator<String> escapeFunction,
174 boolean requiresBuffering,
175 String prefix) {
176 this(name, type, escapeFunction, requiresBuffering, prefix, false);
177 }
178
179 ElementContext(
180 String name,
181 Type type,
182 UnaryOperator<String> escapeFunction,
183 boolean requiresBuffering,
184 String prefix,
185 boolean requiresSurroundingByBlankLines) {
186 this.name = name;
187 this.type = type;
188 this.escapeFunction = escapeFunction;
189 this.requiresBuffering = requiresBuffering;
190 if (type != Type.CONTAINER_BLOCK && prefix.length() != 0) {
191 throw new IllegalArgumentException("Only container blocks may define a prefix (for nesting)");
192 }
193 this.prefix = prefix;
194 this.requiresSurroundingByBlankLines = requiresSurroundingByBlankLines;
195 }
196
197
198
199
200
201
202 String escape(String text) {
203
204 if (escapeFunction == null) {
205 return text;
206 } else {
207 return escapeFunction.apply(text);
208 }
209 }
210
211
212
213
214
215 boolean isBlock() {
216 return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK;
217 }
218
219
220
221
222
223 boolean isContainer() {
224 return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER;
225 }
226 }
227
228
229
230
231
232
233
234
235
236 protected MarkdownSink(Writer writer) {
237 this.bufferingWriter = new LastTwoLinesBufferingWriter(writer);
238 this.writer = new PrintWriter(bufferingWriter);
239
240 init();
241 }
242
243 private void endContext(ElementContext expectedContext) {
244 ElementContext removedContext = elementContextStack.remove();
245 if (removedContext != expectedContext) {
246 throw new IllegalStateException("Unexpected context " + removedContext + ", expected " + expectedContext);
247 }
248 if (removedContext.isBlock()) {
249 endBlock(removedContext.requiresSurroundingByBlankLines);
250 }
251 }
252
253 private void startContext(ElementContext newContext) {
254 if (newContext.isBlock()) {
255 startBlock(newContext.requiresSurroundingByBlankLines);
256 }
257 elementContextStack.add(newContext);
258 }
259
260
261
262
263
264 private void ensureBeginningOfLine() {
265
266 if (!bufferingWriter.isWriterAtStartOfNewLine()) {
267 writeUnescaped(EOL);
268 }
269 }
270
271
272
273
274
275 private void ensureBlankLine() {
276
277 if (!bufferingWriter.isWriterAfterBlankLine()) {
278 if (bufferingWriter.isWriterAtStartOfNewLine()) {
279 writeUnescaped(EOL);
280 } else {
281 writeUnescaped(BLANK_LINE);
282 }
283 }
284 }
285
286 private void startBlock(boolean requireBlankLine) {
287 if (requireBlankLine) {
288 ensureBlankLine();
289 } else {
290 ensureBeginningOfLine();
291 }
292 writeUnescaped(getContainerLinePrefixes());
293 }
294
295 private void endBlock(boolean requireBlankLine) {
296 if (requireBlankLine) {
297 ensureBlankLine();
298 } else {
299 ensureBeginningOfLine();
300 }
301 }
302
303 private String getContainerLinePrefixes() {
304 StringBuilder prefix = new StringBuilder();
305 elementContextStack.stream().filter(c -> c.prefix.length() > 0).forEachOrdered(c -> prefix.insert(0, c.prefix));
306 return prefix.toString();
307 }
308
309
310
311
312
313
314 protected StringBuilder getBuffer() {
315 return buffer;
316 }
317
318 @Override
319 protected void init() {
320 super.init();
321
322 resetBuffer();
323
324 this.authors = new LinkedList<>();
325 this.title = null;
326 this.date = null;
327 this.linkName = null;
328 this.tableHeaderCellFlag = false;
329 this.cellCount = 0;
330 this.cellJustif = null;
331 this.elementContextStack.clear();
332 this.inlineStack.clear();
333
334 elementContextStack.add(ElementContext.BODY);
335 }
336
337
338
339
340 protected void resetBuffer() {
341 buffer = new StringBuilder();
342 }
343
344 @Override
345 public void head(SinkEventAttributes attributes) {
346 init();
347
348 endContext(ElementContext.BODY);
349 elementContextStack.add(ElementContext.HEAD);
350 }
351
352 @Override
353 public void head_() {
354 endContext(ElementContext.HEAD);
355
356 if (title == null && authors.isEmpty() && date == null) {
357 return;
358 }
359 writeUnescaped(METADATA_MARKUP + EOL);
360 if (title != null) {
361 writeUnescaped("title: " + title + EOL);
362 }
363 if (!authors.isEmpty()) {
364 writeUnescaped("author: " + EOL);
365 for (String author : authors) {
366 writeUnescaped(" - " + author + EOL);
367 }
368 }
369 if (date != null) {
370 writeUnescaped("date: " + date + EOL);
371 }
372 writeUnescaped(METADATA_MARKUP + BLANK_LINE);
373 }
374
375 @Override
376 public void body(SinkEventAttributes attributes) {
377 elementContextStack.add(ElementContext.BODY);
378 }
379
380 @Override
381 public void body_() {
382 endContext(ElementContext.BODY);
383 }
384
385 @Override
386 public void title_() {
387 if (buffer.length() > 0) {
388 title = buffer.toString();
389 resetBuffer();
390 }
391 }
392
393 @Override
394 public void author_() {
395 if (buffer.length() > 0) {
396 authors.add(buffer.toString());
397 resetBuffer();
398 }
399 }
400
401 @Override
402 public void date_() {
403 if (buffer.length() > 0) {
404 date = buffer.toString();
405 resetBuffer();
406 }
407 }
408
409 @Override
410 public void sectionTitle(int level, SinkEventAttributes attributes) {
411 if (level > 0) {
412 writeUnescaped(StringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE);
413 }
414 }
415
416 @Override
417 public void sectionTitle_(int level) {
418 if (level > 0) {
419 ensureBlankLine();
420
421 }
422 }
423
424 @Override
425 public void list_() {
426 ensureBlankLine();
427 }
428
429 @Override
430 public void listItem(SinkEventAttributes attributes) {
431 startContext(ElementContext.LIST_ITEM);
432 writeUnescaped(LIST_UNORDERED_ITEM_START_MARKUP);
433 }
434
435 @Override
436 public void listItem_() {
437 endContext(ElementContext.LIST_ITEM);
438 }
439
440 @Override
441 public void numberedList(int numbering, SinkEventAttributes attributes) {
442
443 if (numbering != NUMBERING_DECIMAL) {
444 LOGGER.warn(
445 "{}Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style",
446 getLocationLogPrefix(),
447 NUMBERING_DECIMAL,
448 numbering);
449 }
450 }
451
452 @Override
453 public void numberedList_() {
454 writeUnescaped(EOL);
455 }
456
457 @Override
458 public void numberedListItem(SinkEventAttributes attributes) {
459 startContext(ElementContext.LIST_ITEM);
460 writeUnescaped(LIST_ORDERED_ITEM_START_MARKUP);
461 }
462
463 @Override
464 public void numberedListItem_() {
465 listItem_();
466 }
467
468 @Override
469 public void definitionList(SinkEventAttributes attributes) {
470 LOGGER.warn(
471 "{}Definition list not natively supported in Markdown, rendering HTML instead", getLocationLogPrefix());
472 ensureBlankLine();
473 writeUnescaped("<dl>" + EOL);
474 }
475
476 @Override
477 public void definitionList_() {
478 writeUnescaped("</dl>" + BLANK_LINE);
479 }
480
481 @Override
482 public void definedTerm(SinkEventAttributes attributes) {
483 writeUnescaped("<dt>");
484 }
485
486 @Override
487 public void definedTerm_() {
488 writeUnescaped("</dt>" + EOL);
489 }
490
491 @Override
492 public void definition(SinkEventAttributes attributes) {
493 writeUnescaped("<dd>");
494 }
495
496 @Override
497 public void definition_() {
498 writeUnescaped("</dd>" + EOL);
499 }
500
501 @Override
502 public void pageBreak() {
503 LOGGER.warn("Ignoring unsupported page break in Markdown");
504 }
505
506 @Override
507 public void paragraph(SinkEventAttributes attributes) {
508
509 if (elementContextStack.element().isContainer()) {
510 ensureBlankLine();
511 writeUnescaped(getContainerLinePrefixes());
512 }
513 }
514
515 @Override
516 public void paragraph_() {
517
518 if (elementContextStack.element().isContainer()) {
519 ensureBlankLine();
520 }
521 }
522
523 @Override
524 public void verbatim(SinkEventAttributes attributes) {
525
526 startContext(ElementContext.CODE_BLOCK);
527 writeUnescaped(VERBATIM_START_MARKUP + EOL);
528 writeUnescaped(getContainerLinePrefixes());
529 }
530
531 @Override
532 public void verbatim_() {
533 ensureBeginningOfLine();
534 writeUnescaped(getContainerLinePrefixes());
535 writeUnescaped(VERBATIM_END_MARKUP + BLANK_LINE);
536 endContext(ElementContext.CODE_BLOCK);
537 }
538
539 @Override
540 public void blockquote(SinkEventAttributes attributes) {
541 startContext(ElementContext.BLOCKQUOTE);
542 writeUnescaped(BLOCKQUOTE_START_MARKUP);
543 }
544
545 @Override
546 public void blockquote_() {
547 endContext(ElementContext.BLOCKQUOTE);
548 }
549
550 @Override
551 public void horizontalRule(SinkEventAttributes attributes) {
552 ensureBeginningOfLine();
553 writeUnescaped(HORIZONTAL_RULE_MARKUP + BLANK_LINE);
554 writeUnescaped(getContainerLinePrefixes());
555 }
556
557 @Override
558 public void table(SinkEventAttributes attributes) {
559 ensureBlankLine();
560 writeUnescaped(getContainerLinePrefixes());
561 }
562
563 @Override
564 public void tableRows(int[] justification, boolean grid) {
565 if (justification != null) {
566 cellJustif = Arrays.stream(justification).boxed().collect(Collectors.toCollection(ArrayList::new));
567 } else {
568 cellJustif = new ArrayList<>();
569 }
570
571 isFirstTableRow = true;
572 }
573
574 @Override
575 public void tableRows_() {
576 cellJustif = null;
577 }
578
579 @Override
580 public void tableRow(SinkEventAttributes attributes) {
581 cellCount = 0;
582 }
583
584 @Override
585 public void tableRow_() {
586 if (isFirstTableRow && !tableHeaderCellFlag) {
587
588
589 writeEmptyTableHeader();
590 writeTableDelimiterRow();
591 tableHeaderCellFlag = false;
592 isFirstTableRow = false;
593
594 }
595
596 writeUnescaped(TABLE_ROW_PREFIX);
597
598 writeUnescaped(buffer.toString());
599
600 resetBuffer();
601
602 writeUnescaped(EOL);
603
604 if (isFirstTableRow) {
605
606 writeTableDelimiterRow();
607 isFirstTableRow = false;
608 }
609
610
611 cellCount = 0;
612 }
613
614 private void writeEmptyTableHeader() {
615 writeUnescaped(TABLE_ROW_PREFIX);
616 for (int i = 0; i < cellCount; i++) {
617 writeUnescaped(StringUtils.repeat(String.valueOf(SPACE), 3) + TABLE_CELL_SEPARATOR_MARKUP);
618 }
619 writeUnescaped(EOL);
620 writeUnescaped(getContainerLinePrefixes());
621 }
622
623
624 private void writeTableDelimiterRow() {
625 writeUnescaped(TABLE_ROW_PREFIX);
626 int justification = Sink.JUSTIFY_LEFT;
627 for (int i = 0; i < cellCount; i++) {
628
629 if (cellJustif != null && cellJustif.size() > i) {
630 justification = cellJustif.get(i);
631 }
632 switch (justification) {
633 case Sink.JUSTIFY_RIGHT:
634 writeUnescaped(TABLE_COL_RIGHT_ALIGNED_MARKUP);
635 break;
636 case Sink.JUSTIFY_CENTER:
637 writeUnescaped(TABLE_COL_CENTER_ALIGNED_MARKUP);
638 break;
639 default:
640 writeUnescaped(TABLE_COL_LEFT_ALIGNED_MARKUP);
641 break;
642 }
643 writeUnescaped(TABLE_CELL_SEPARATOR_MARKUP);
644 }
645 writeUnescaped(EOL);
646 }
647
648 @Override
649 public void tableCell(SinkEventAttributes attributes) {
650 if (attributes != null) {
651
652 final int cellJustification;
653 if (attributes.containsAttributes(SinkEventAttributeSet.LEFT)) {
654 cellJustification = Sink.JUSTIFY_LEFT;
655 } else if (attributes.containsAttributes(SinkEventAttributeSet.RIGHT)) {
656 cellJustification = Sink.JUSTIFY_RIGHT;
657 } else if (attributes.containsAttributes(SinkEventAttributeSet.CENTER)) {
658 cellJustification = Sink.JUSTIFY_CENTER;
659 } else {
660 cellJustification = -1;
661 }
662 if (cellJustification > -1) {
663 if (cellJustif.size() > cellCount) {
664 cellJustif.set(cellCount, cellJustification);
665 } else if (cellJustif.size() == cellCount) {
666 cellJustif.add(cellJustification);
667 } else {
668
669 for (int precedingCol = cellJustif.size(); precedingCol < cellCount; precedingCol++) {
670 cellJustif.add(Sink.JUSTIFY_LEFT);
671 }
672 cellJustif.add(cellJustification);
673 }
674 }
675 }
676 elementContextStack.add(ElementContext.TABLE_CELL);
677 }
678
679 @Override
680 public void tableHeaderCell(SinkEventAttributes attributes) {
681 tableCell(attributes);
682 tableHeaderCellFlag = true;
683 }
684
685 @Override
686 public void tableCell_() {
687 endTableCell();
688 }
689
690 @Override
691 public void tableHeaderCell_() {
692 endTableCell();
693 }
694
695
696
697
698 private void endTableCell() {
699 endContext(ElementContext.TABLE_CELL);
700 buffer.append(TABLE_CELL_SEPARATOR_MARKUP);
701 cellCount++;
702 }
703
704 @Override
705 public void tableCaption(SinkEventAttributes attributes) {
706 elementContextStack.add(ElementContext.TABLE_CAPTION);
707 }
708
709 @Override
710 public void tableCaption_() {
711 endContext(ElementContext.TABLE_CAPTION);
712 }
713
714 @Override
715 public void figure(SinkEventAttributes attributes) {
716 figureSrc = null;
717 elementContextStack.add(ElementContext.FIGURE);
718 }
719
720 @Override
721 public void figureGraphics(String name, SinkEventAttributes attributes) {
722 figureSrc = escapeMarkdown(name);
723
724 if (elementContextStack.peek() != ElementContext.FIGURE) {
725 Object alt = attributes.getAttribute(SinkEventAttributes.ALT);
726 if (alt == null) {
727 alt = "";
728 }
729 writeImage(escapeMarkdown(alt.toString()), name);
730 }
731 }
732
733 @Override
734 public void figure_() {
735 endContext(ElementContext.FIGURE);
736 writeImage(buffer.toString(), figureSrc);
737 }
738
739 private void writeImage(String alt, String src) {
740 writeUnescaped("![");
741 writeUnescaped(alt);
742 writeUnescaped("](" + src + ")");
743 }
744
745
746 public void anchor(String name, SinkEventAttributes attributes) {
747
748
749 }
750
751 @Override
752 public void anchor_() {
753
754 }
755
756
757 public void link(String name, SinkEventAttributes attributes) {
758 writeUnescaped(LINK_START_1_MARKUP);
759 linkName = name;
760 }
761
762 @Override
763 public void link_() {
764 writeUnescaped(LINK_START_2_MARKUP);
765 text(linkName.startsWith("#") ? linkName.substring(1) : linkName);
766 writeUnescaped(LINK_END_MARKUP);
767 linkName = null;
768 }
769
770 @Override
771 public void inline(SinkEventAttributes attributes) {
772 Queue<String> endMarkups = Collections.asLifoQueue(new LinkedList<>());
773
774 if (attributes != null
775 && elementContextStack.element() != ElementContext.CODE_BLOCK
776 && elementContextStack.element() != ElementContext.CODE_SPAN) {
777
778 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code")
779 || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "monospaced")
780 || attributes.containsAttribute(SinkEventAttributes.STYLE, "monospaced")) {
781 writeUnescaped(MONOSPACED_START_MARKUP);
782 endMarkups.add(MONOSPACED_END_MARKUP);
783 elementContextStack.add(ElementContext.CODE_SPAN);
784 } else {
785
786 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "em")
787 || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "italic")
788 || attributes.containsAttribute(SinkEventAttributes.STYLE, "italic")) {
789 writeUnescaped(ITALIC_START_MARKUP);
790 endMarkups.add(ITALIC_END_MARKUP);
791 }
792
793 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "strong")
794 || attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "bold")
795 || attributes.containsAttribute(SinkEventAttributes.STYLE, "bold")) {
796 writeUnescaped(BOLD_START_MARKUP);
797 endMarkups.add(BOLD_END_MARKUP);
798 }
799 }
800 }
801 inlineStack.add(endMarkups);
802 }
803
804 @Override
805 public void inline_() {
806 for (String endMarkup : inlineStack.remove()) {
807 if (endMarkup.equals(MONOSPACED_END_MARKUP)) {
808 endContext(ElementContext.CODE_SPAN);
809 }
810 writeUnescaped(endMarkup);
811 }
812 }
813
814 @Override
815 public void italic() {
816 inline(SinkEventAttributeSet.Semantics.ITALIC);
817 }
818
819 @Override
820 public void italic_() {
821 inline_();
822 }
823
824 @Override
825 public void bold() {
826 inline(SinkEventAttributeSet.Semantics.BOLD);
827 }
828
829 @Override
830 public void bold_() {
831 inline_();
832 }
833
834 @Override
835 public void monospaced() {
836 inline(SinkEventAttributeSet.Semantics.CODE);
837 }
838
839 @Override
840 public void monospaced_() {
841 inline_();
842 }
843
844 @Override
845 public void lineBreak(SinkEventAttributes attributes) {
846 if (elementContextStack.element() == ElementContext.CODE_BLOCK) {
847 writeUnescaped(EOL);
848 } else {
849 writeUnescaped("" + SPACE + SPACE + EOL);
850 }
851 writeUnescaped(getContainerLinePrefixes());
852 }
853
854 @Override
855 public void nonBreakingSpace() {
856 writeUnescaped(NON_BREAKING_SPACE_MARKUP);
857 }
858
859 @Override
860 public void text(String text, SinkEventAttributes attributes) {
861 if (attributes != null) {
862 inline(attributes);
863 }
864 ElementContext currentContext = elementContextStack.element();
865 if (currentContext == ElementContext.TABLE_CAPTION) {
866
867 LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix());
868 } else {
869 String unifiedText = currentContext.escape(unifyEOLs(text));
870 writeUnescaped(unifiedText);
871 }
872 if (attributes != null) {
873 inline_();
874 }
875 }
876
877 @Override
878 public void rawText(String text) {
879 writeUnescaped(text);
880 }
881
882 @Override
883 public void comment(String comment) {
884 rawText(COMMENT_START + comment + COMMENT_END);
885 }
886
887
888
889
890
891
892
893 @Override
894 public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
895 LOGGER.warn("{}Unknown Sink event '" + name + "', ignoring!", getLocationLogPrefix());
896 }
897
898
899
900
901
902 private boolean requiresBuffering() {
903 return elementContextStack.stream().anyMatch(c -> c.requiresBuffering);
904 }
905
906 protected void writeUnescaped(String text) {
907 if (requiresBuffering()) {
908 buffer.append(text);
909 } else {
910 writer.write(text);
911 }
912 }
913
914 @Override
915 public void flush() {
916 writer.flush();
917 }
918
919 @Override
920 public void close() {
921 writer.close();
922
923 init();
924 }
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942 private static String escapeMarkdown(String text) {
943 if (text == null) {
944 return "";
945 }
946 text = HtmlTools.escapeHTML(text, true);
947 int length = text.length();
948 StringBuilder buffer = new StringBuilder(length);
949
950 for (int i = 0; i < length; ++i) {
951 char c = text.charAt(i);
952 switch (c) {
953 case '\\':
954 case '`':
955 case '*':
956 case '_':
957 case '{':
958 case '}':
959 case '[':
960 case ']':
961 case '(':
962 case ')':
963 case '#':
964 case '+':
965 case '-':
966 case '.':
967 case '!':
968 buffer.append('\\');
969 buffer.append(c);
970 break;
971 default:
972 buffer.append(c);
973 }
974 }
975
976 return buffer.toString();
977 }
978
979
980
981
982
983
984
985
986 private static String escapeForTableCell(String text) {
987
988 return escapeMarkdown(text).replace("|", "\\|");
989 }
990 }