001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.doxia.module.markdown; 020 021import java.io.PrintWriter; 022import java.io.Writer; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.LinkedList; 028import java.util.List; 029import java.util.Queue; 030import java.util.function.UnaryOperator; 031import java.util.stream.Collectors; 032 033import org.apache.commons.lang3.StringUtils; 034import org.apache.maven.doxia.sink.Sink; 035import org.apache.maven.doxia.sink.SinkEventAttributes; 036import org.apache.maven.doxia.sink.impl.AbstractTextSink; 037import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 038import org.apache.maven.doxia.util.HtmlTools; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * Markdown generator implementation. 044 * <br> 045 * <b>Note</b>: The encoding used is UTF-8. 046 */ 047public class MarkdownSink extends AbstractTextSink implements MarkdownMarkup { 048 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class); 049 050 // ---------------------------------------------------------------------- 051 // Instance fields 052 // ---------------------------------------------------------------------- 053 054 /** A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>. 055 * The content of this buffer is already escaped. */ 056 private StringBuilder buffer; 057 058 /** author. */ 059 private Collection<String> authors; 060 061 /** title. */ 062 private String title; 063 064 /** date. */ 065 private String date; 066 067 /** linkName. */ 068 private String linkName; 069 070 /** tableHeaderCellFlag, set to {@code true} for table rows containing at least one table header cell */ 071 private boolean tableHeaderCellFlag; 072 073 /** number of cells in a table. */ 074 private int cellCount; 075 076 /** justification of table cells per column. */ 077 private List<Integer> cellJustif; 078 079 /** is header row */ 080 private boolean isFirstTableRow; 081 082 /** The writer to use. */ 083 private final PrintWriter writer; 084 085 /** A temporary writer used to buffer the last two lines */ 086 private final LastTwoLinesBufferingWriter bufferingWriter; 087 088 /** Keep track of end markup for inline events. */ 089 protected Queue<Queue<String>> inlineStack = Collections.asLifoQueue(new LinkedList<>()); 090 091 /** The context of the surrounding elements as stack (LIFO) */ 092 protected Queue<ElementContext> elementContextStack = Collections.asLifoQueue(new LinkedList<>()); 093 094 private String figureSrc; 095 096 /** Most important contextual metadata (of the surrounding element) */ 097 enum ElementContext { 098 HEAD("head", Type.GENERIC_CONTAINER, null, true), 099 BODY("body", Type.GENERIC_CONTAINER, MarkdownSink::escapeMarkdown), 100 // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here 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), // special type, as allows containing inlines, but not starting on a separate line 110 // same parameters as BODY but paragraphs inside list items are handled differently 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 * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a> 118 */ 119 enum Type { 120 /** 121 * Container with no special meaning for (nested) child element contexts 122 */ 123 GENERIC_CONTAINER, 124 /** 125 * Is supposed to start on a new line, and must have a prefix (for nested blocks) 126 */ 127 CONTAINER_BLOCK, 128 /** 129 * Is supposed to start on a new line, must not contain any other block element context (neither leaf nor container) 130 */ 131 LEAF_BLOCK, 132 /** 133 * Are not allowed to contain any other element context (i.e. leaf contexts), except for some other inlines (depends on the actual type) 134 */ 135 INLINE 136 } 137 /** 138 * {@code true} if block element, otherwise {@code false} for inline elements 139 */ 140 final Type type; 141 142 /** 143 * The function to call to escape the given text. The function is supposed to return the escaped text or return just the given text if no escaping is necessary in this context 144 */ 145 final UnaryOperator<String> escapeFunction; 146 147 /** 148 * if {@code true} requires buffering any text appearing inside this context 149 */ 150 final boolean requiresBuffering; 151 152 /** 153 * prefix to be used for a (nested) block elements inside the current container context (only not empty for {@link #type} being {@link Type#CONTAINER_BLOCK}) 154 */ 155 final String prefix; 156 157 /** 158 * Only relevant for block element, if set to {@code true} the element requires to be surrounded by blank lines. 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 * Must be called for each inline text to be emitted directly within this context (not relevant for nested context) 199 * @param text 200 * @return the escaped text (may be same as {@code text} when no escaping is necessary) 201 */ 202 String escape(String text) { 203 // is escaping necessary at all? 204 if (escapeFunction == null) { 205 return text; 206 } else { 207 return escapeFunction.apply(text); 208 } 209 } 210 211 /** 212 * 213 * @return {@code true} for all block types, {@code false} otherwise 214 */ 215 boolean isBlock() { 216 return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK; 217 } 218 219 /** 220 * 221 * @return {@code true} for all containers (allowing block elements as children), {@code false} otherwise 222 */ 223 boolean isContainer() { 224 return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER; 225 } 226 } 227 // ---------------------------------------------------------------------- 228 // Public protected methods 229 // ---------------------------------------------------------------------- 230 231 /** 232 * Constructor, initialize the Writer and the variables. 233 * 234 * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer. 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 * Ensures that the {@link #writer} is currently at the beginning of a new line. 262 * Optionally writes a line separator to ensure that. 263 */ 264 private void ensureBeginningOfLine() { 265 // make sure that we are at the start of a line without adding unnecessary blank lines 266 if (!bufferingWriter.isWriterAtStartOfNewLine()) { 267 writeUnescaped(EOL); 268 } 269 } 270 271 /** 272 * Ensures that the {@link #writer} is either at the beginning or preceded by a blank line. 273 * Optionally writes a blank line to ensure that. 274 */ 275 private void ensureBlankLine() { 276 // prevent duplicate blank lines 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 * Returns the buffer that holds the current text. 311 * 312 * @return A StringBuffer. 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 // always set a default context (at least for tests not emitting a body) 334 elementContextStack.add(ElementContext.BODY); 335 } 336 337 /** 338 * Reset the StringBuilder. 339 */ 340 protected void resetBuffer() { 341 buffer = new StringBuilder(); 342 } 343 344 @Override 345 public void head(SinkEventAttributes attributes) { 346 init(); 347 // remove default body context here 348 endContext(ElementContext.BODY); 349 elementContextStack.add(ElementContext.HEAD); 350 } 351 352 @Override 353 public void head_() { 354 endContext(ElementContext.HEAD); 355 // only write head block if really necessary 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(); // always end headings with blank line to increase compatibility with arbitrary MD 420 // editors 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 // markdown only supports decimal numbering 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_(); // identical for both numbered and not numbered list item 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 // ignore paragraphs outside container contexts 509 if (elementContextStack.element().isContainer()) { 510 ensureBlankLine(); 511 writeUnescaped(getContainerLinePrefixes()); 512 } 513 } 514 515 @Override 516 public void paragraph_() { 517 // ignore paragraphs outside container contexts 518 if (elementContextStack.element().isContainer()) { 519 ensureBlankLine(); 520 } 521 } 522 523 @Override 524 public void verbatim(SinkEventAttributes attributes) { 525 // always assume is supposed to be monospaced (i.e. emitted inside a <pre><code>...</code></pre>) 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 // grid flag is not supported 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 // emit empty table header as this is mandatory for GFM table extension 588 // (https://stackoverflow.com/a/17543474/5155923) 589 writeEmptyTableHeader(); 590 writeTableDelimiterRow(); 591 tableHeaderCellFlag = false; 592 isFirstTableRow = false; 593 // afterwards emit the first row 594 } 595 596 writeUnescaped(TABLE_ROW_PREFIX); 597 598 writeUnescaped(buffer.toString()); 599 600 resetBuffer(); 601 602 writeUnescaped(EOL); 603 604 if (isFirstTableRow) { 605 // emit delimiter row 606 writeTableDelimiterRow(); 607 isFirstTableRow = false; 608 } 609 610 // only reset cell count if this is the last row 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 /** Emit the delimiter row which determines the alignment */ 624 private void writeTableDelimiterRow() { 625 writeUnescaped(TABLE_ROW_PREFIX); 626 int justification = Sink.JUSTIFY_LEFT; 627 for (int i = 0; i < cellCount; i++) { 628 // keep previous column's alignment in case too few are specified 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 // evaluate alignment attributes 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 // create non-existing justifications for preceding columns 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 * Ends a table cell. 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 // is it a standalone image (outside a figure)? 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 /** {@inheritDoc} */ 746 public void anchor(String name, SinkEventAttributes attributes) { 747 // write(ANCHOR_START_MARKUP + name); 748 // TODO get implementation from Xhtml5 base sink 749 } 750 751 @Override 752 public void anchor_() { 753 // write(ANCHOR_END_MARKUP); 754 } 755 756 /** {@inheritDoc} */ 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 // code excludes other styles in markdown 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 // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>" 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 // in XHTML "<strong>" is used, but some tests still rely on the outdated "<bold>" 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 // table caption cannot even be emitted via XHTML in markdown as there is no suitable location 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 * {@inheritDoc} 889 * 890 * Unknown events just log a warning message but are ignored otherwise. 891 * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes) 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 * @return {@code true} if any of the parent contexts require buffering 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 // Private methods 928 // ---------------------------------------------------------------------- 929 930 /** 931 * First use XML escaping (leveraging the predefined entities, for browsers) 932 * afterwards escape special characters in a text with a leading backslash (for markdown parsers) 933 * 934 * <pre> 935 * \, `, *, _, {, }, [, ], (, ), #, +, -, ., ! 936 * </pre> 937 * 938 * @param text the String to escape, may be null 939 * @return the text escaped, "" if null String input 940 * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a> 941 */ 942 private static String escapeMarkdown(String text) { 943 if (text == null) { 944 return ""; 945 } 946 text = HtmlTools.escapeHTML(text, true); // assume UTF-8 output, i.e. only use the mandatory XML entities 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 * Escapes the pipe character according to <a href="https://github.github.com/gfm/#tables-extension-">GFM Table Extension</a> in addition 981 * to the regular markdown escaping. 982 * @param text 983 * @return the escaped text 984 * @see {@link #escapeMarkdown(String) 985 */ 986 private static String escapeForTableCell(String text) { 987 988 return escapeMarkdown(text).replace("|", "\\|"); 989 } 990}