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 javax.swing.text.AttributeSet; 022import javax.swing.text.MutableAttributeSet; 023 024import java.io.Writer; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.LinkedList; 030import java.util.List; 031import java.util.Queue; 032import java.util.stream.Collectors; 033 034import org.apache.maven.doxia.sink.Sink; 035import org.apache.maven.doxia.sink.SinkEventAttributes; 036import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 037import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink; 038import org.apache.maven.doxia.util.DoxiaStringUtils; 039import org.apache.maven.doxia.util.HtmlTools; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * Markdown generator implementation. 045 * <br> 046 * <b>Note</b>: The encoding used is UTF-8. 047 * Extends the Xhtml5 sink as in some context HTML needs to be emitted. 048 */ 049public class MarkdownSink extends Xhtml5BaseSink implements MarkdownMarkup { 050 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class); 051 052 // ---------------------------------------------------------------------- 053 // Instance fields 054 // ---------------------------------------------------------------------- 055 056 /** author. */ 057 private Collection<String> authors; 058 059 /** title. */ 060 private String title; 061 062 /** date. */ 063 private String date; 064 065 /** linkName. */ 066 private String linkName; 067 068 /** tableHeaderCellFlag, set to {@code true} for table rows containing at least one table header cell */ 069 private boolean tableHeaderCellFlag; 070 071 /** number of cells in a table. */ 072 private int cellCount; 073 074 /** justification of table cells per column. */ 075 private List<Integer> cellJustif; 076 077 /** is header row */ 078 private boolean isFirstTableRow; 079 080 /** The inner decorated writer to buffer the text of contexts requiring buffering. Writing to this and {@code bufferingWriter} has the same effect. */ 081 private final BufferingStackWriter bufferingStackWriter; 082 083 /** The outer decorated writer taking care of remembering the last two written lines. Writing to this and {@code writer} has the same effect. */ 084 private final LastTwoLinesAwareWriter lineAwareWriter; 085 086 private static final String USE_XHTML_SINK = "XhtmlSink"; 087 088 /** Keep track of end markup for inline events. Special value {@link #USE_XHTML_SINK} is used to indicate usage of the Xhtml5BaseSink.inline_()*/ 089 protected Queue<Queue<String>> inlineStack; 090 091 /** The context of the surrounding elements as stack (LIFO) */ 092 protected Queue<ElementContext> elementContextStack; 093 094 private String figureSrc; 095 096 /** flag if the current verbatim block added a HTML context or not */ 097 private boolean isVerbatimHtmlContext; 098 099 @FunctionalInterface 100 interface TextEscapeFunction { 101 String escape(ElementContext context, LastTwoLinesAwareWriter writer, String text); 102 } 103 /** Most important contextual metadata (of elements). This contains information about necessary escaping rules, potential prefixes and newlines */ 104 enum ElementContext { 105 ROOT_WITH_BUFFERING( 106 Type.GENERIC_CONTAINER, 107 true, 108 ElementContext::escapeMarkdown, 109 true), // only needs buffering until head()_ is called to make sure to emit metadata first 110 ROOT_WITHOUT_BUFFERING( 111 Type.GENERIC_CONTAINER, 112 true, 113 null, 114 false), // used after head()_/body() to prevent unnecessary buffering 115 HEAD(Type.GENERIC_CONTAINER, false, null, true), 116 BODY(Type.GENERIC_CONTAINER, true, ElementContext::escapeMarkdown), 117 // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here 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), // special handling of newlines 125 TABLE_CELL( 126 Type.INLINE, 127 false, 128 ElementContext::escapeForTableCell, 129 false), // special type, as allows containing inlines, but not starting on a separate line 130 // same parameters as BODY but paragraphs inside list items are handled differently 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 * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a> 137 */ 138 enum Type { 139 /** 140 * Container with no special meaning for (nested) child element contexts 141 */ 142 GENERIC_CONTAINER, 143 /** 144 * Is supposed to start on a new line, and must have a prefix (for nested blocks) 145 */ 146 CONTAINER_BLOCK, 147 /** 148 * Is supposed to start on a new line, must not contain any other block element context (neither leaf nor container) 149 */ 150 LEAF_BLOCK, 151 /** 152 * Are not allowed to contain any other element context (i.e. leaf contexts), except for some other inlines (depends on the actual type) 153 */ 154 INLINE 155 } 156 /** 157 * {@code true} if block element, otherwise {@code false} for inline elements 158 */ 159 final Type type; 160 161 /** 162 * 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 163 */ 164 final TextEscapeFunction escapeFunction; 165 166 /** 167 * if {@code true} requires buffering any text appearing inside this context 168 */ 169 final boolean requiresBuffering; 170 171 /** 172 * prefix to be used for each line of (nested) block elements inside the current container context (only not empty for {@link #type} being {@link Type#CONTAINER_BLOCK}) 173 */ 174 final String prefix; 175 176 /** 177 * Only relevant for block element, if set to {@code true} the element requires to be surrounded by blank lines. 178 */ 179 final boolean requiresSurroundingByBlankLines; 180 181 /** 182 * If markup linebreaks (i.e. insignificant linebreaks in the source) are allowed in this context. 183 * This is relevant for markdown as in some contexts (e.g. list items) linebreaks are always significant (while for HTML they wouldn't be) 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 * Must be called for each inline text to be emitted directly within this context (not relevant for nested context) 228 * @param text 229 * @return the escaped text (may be same as {@code text} when no escaping is necessary) 230 */ 231 String escape(LastTwoLinesAwareWriter writer, String text) { 232 // is escaping necessary at all? 233 if (escapeFunction == null) { 234 return text; 235 } else { 236 return escapeFunction.escape(this, writer, text); 237 } 238 } 239 240 /** 241 * 242 * @return {@code true} for all block types, {@code false} otherwise 243 */ 244 boolean isBlock() { 245 return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK; 246 } 247 248 /** 249 * 250 * @return {@code true} if only HTML is allowed in this context 251 */ 252 boolean isHtml() { 253 return this.equals(HTML_BLOCK); 254 } 255 /** 256 * 257 * @return {@code true} for all containers (allowing block elements as children), {@code false} otherwise 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 * First use XML escaping (leveraging the predefined entities, for browsers) 269 * afterwards escape special characters in a text with a leading backslash (for markdown parsers) 270 * 271 * <pre> 272 * \, `, *, _, {, }, [, ], (, ), #, +, -, ., ! 273 * </pre> 274 * 275 * @param text the string to escape, may be null 276 * @return the text escaped, "" if null String input 277 * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a> 278 */ 279 private String escapeMarkdown(LastTwoLinesAwareWriter writer, String text) { 280 if (text == null) { 281 return ""; 282 } 283 text = escapeHtml(writer, text); // assume UTF-8 output, i.e. only use the mandatory XML entities 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 // always escape the previous characters as potentially everywhere relevant 299 buffer.append(escapeMarkdown(c)); 300 break; 301 case '*': 302 case '+': 303 case '-': 304 // only relevant for unordered lists or horizontal rules 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 * Escapes the pipe character according to <a href="https://github.github.com/gfm/#tables-extension-">GFM Table Extension</a> in addition 358 * to the regular markdown escaping. 359 * @param text 360 * @return the escaped text 361 * @see {@link #escapeMarkdown(String) 362 */ 363 private String escapeForTableCell(LastTwoLinesAwareWriter writer, String text) { 364 return escapeMarkdown(writer, text).replace("|", "\\|"); 365 } 366 } 367 // ---------------------------------------------------------------------- 368 // Public protected methods 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 * Constructor, initialize the Writer and the variables. 379 * 380 * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer. 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); // we want to control newlines on our own to prevent (mostly to not break encapsulating markdown 389 // tables) 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 // remove buffer from stack (assume it has been evaluated already) 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 // every block element within a list item must be surrounded by blank lines 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 * Ensures that the {@link #writer} is currently at the beginning of a new line. 453 * Optionally writes a line separator to ensure that. 454 */ 455 private void ensureBeginningOfLine() { 456 // make sure that we are at the start of a line without adding unnecessary blank lines 457 if (!lineAwareWriter.isWriterAtStartOfNewLine()) { 458 write(EOL); 459 } 460 } 461 462 /** 463 * Ensures that the {@link #writer} is preceded by a blank line. 464 * Optionally writes a blank line or just line delimiter to ensure that. 465 */ 466 private void ensureBlankLine() { 467 // prevent duplicate blank lines 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 * @return the prefix to be used for each line in the current context (i.e. the prefix of the current container context and all its ancestors), may be empty 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 // only write head block if really necessary 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 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 584 } 585 586 @Override 587 public void section_(int level) { 588 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 589 } 590 591 @Override 592 public void header(SinkEventAttributes attributes) { 593 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 594 } 595 596 @Override 597 public void header_() { 598 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 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(); // always end headings with blank line to increase compatibility with arbitrary MD 614 // editors 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 // markdown only supports decimal numbering 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_(); // identical for both numbered and not numbered list item 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 // ignore paragraphs outside container contexts 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 // ignore paragraphs outside container contexts 725 if (elementContextStack.element().isContainer()) { 726 ensureBlankLine(); 727 } 728 } 729 730 @Override 731 public void verbatim(SinkEventAttributes attributes) { 732 if (!elementContextStack.element().isContainer()) { 733 // markdown doesn't allow block elements but one can instead rely on html blocks for this 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 // if no source attribute, then don't emit an info string 744 startContext(ElementContext.CODE_BLOCK); 745 write(VERBATIM_START_MARKUP); 746 if (attributes != null && attributes.containsAttributes(SinkEventAttributeSet.SOURCE)) { 747 write("unknown"); // unknown language 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 // grid flag is not supported 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 // emit empty table header as this is mandatory for GFM table extension 857 // (https://stackoverflow.com/a/17543474/5155923) 858 writeEmptyTableHeader(); 859 writeTableDelimiterRow(); 860 tableHeaderCellFlag = false; 861 isFirstTableRow = false; 862 // afterwards emit the first row 863 } 864 write(TABLE_ROW_PREFIX); 865 write(buffer); 866 write(EOL); 867 if (isFirstTableRow) { 868 // emit delimiter row 869 writeTableDelimiterRow(); 870 isFirstTableRow = false; 871 } 872 // only reset cell count if this is the last row 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 /** Emit the delimiter row which determines the alignment */ 887 private void writeTableDelimiterRow() { 888 write(TABLE_ROW_PREFIX); 889 int justification = Sink.JUSTIFY_DEFAULT; 890 for (int i = 0; i < cellCount; i++) { 891 // keep previous column's alignment in case too few are specified 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 // evaluate alignment attributes 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 // create non-existing justifications for preceding columns 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 * Ends a table cell. 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 // is it a standalone image (outside a figure)? 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 // close anchor tag immediately otherwise markdown would not be allowed afterwards 1066 write("</a>"); 1067 } 1068 } 1069 1070 @Override 1071 public void anchor_() { 1072 if (elementContextStack.element().isHtml()) { 1073 super.anchor_(); 1074 } else { 1075 // anchor is always empty html element, i.e. already closed with anchor() 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 // emit link outside the code span, i.e. insert at the beginning of the buffer 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 // defer emitting link end markup until inline_() is called 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 // code excludes other styles in markdown 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 // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>" 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 // in XHTML "<strong>" is used, but some tests still rely on the outdated "<bold>" 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 // <del> is supported via GFM strikethrough extension 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 // use HTML for other inline semantics which are not natively supported in Markdown (e.g. 1184 // subscript, superscript, small, etc.) 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 // table caption cannot even be emitted via XHTML in markdown as there is no suitable location 1281 LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix()); 1282 } else { 1283 String unifiedText = currentContext.escape(lineAwareWriter, unifyEOLs(text)); 1284 // ignore newlines only, because those are emitted often coming from linebreaks in HTML with no 1285 // semantical 1286 // meaning 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 * {@inheritDoc} 1308 * 1309 * Unknown events just log a warning message but are ignored otherwise. 1310 * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes) 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 // not allowed in all contexts 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}