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.SinkEventAttributeSet; 036import org.apache.maven.doxia.sink.SinkEventAttributes; 037import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink; 038import org.apache.maven.doxia.util.DoxiaStringUtils; 039import org.apache.maven.doxia.util.DoxiaUtils; 040import org.apache.maven.doxia.util.HtmlTools; 041import org.jsoup.parser.Tag; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045/** 046 * Markdown generator implementation. 047 * <br> 048 * <b>Note</b>: The encoding used is UTF-8. 049 * Extends the Xhtml5 sink as in some context HTML needs to be emitted. 050 */ 051public class MarkdownSink extends Xhtml5BaseSink implements MarkdownMarkup { 052 private static final Logger LOGGER = LoggerFactory.getLogger(MarkdownSink.class); 053 054 // ---------------------------------------------------------------------- 055 // Instance fields 056 // ---------------------------------------------------------------------- 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 inner decorated writer to buffer the text of contexts requiring buffering. Writing to this and {@code bufferingWriter} has the same effect. */ 083 private final BufferingStackWriter bufferingStackWriter; 084 085 /** The outer decorated writer taking care of remembering the last two written lines. Writing to this and {@code writer} has the same effect. */ 086 private final LastTwoLinesAwareWriter lineAwareWriter; 087 088 private static final String USE_XHTML_SINK = "XhtmlSink"; 089 090 /** Keep track of end markup for inline events. Special value {@link #USE_XHTML_SINK} is used to indicate usage of the Xhtml5BaseSink.inline_()*/ 091 protected Queue<Queue<String>> inlineStack; 092 093 /** The context of the surrounding elements as stack (LIFO) */ 094 protected Queue<ElementContext> elementContextStack; 095 096 private String figureSrc; 097 098 /** flag if the current verbatim block added a HTML context or not */ 099 private boolean isVerbatimHtmlContext; 100 101 @FunctionalInterface 102 interface TextEscapeFunction { 103 String escape(ElementContext context, LastTwoLinesAwareWriter writer, String text); 104 } 105 /** Most important contextual metadata (of elements). This contains information about necessary escaping rules, potential prefixes and newlines */ 106 enum ElementContext { 107 ROOT_WITH_BUFFERING( 108 Type.GENERIC_CONTAINER, 109 true, 110 ElementContext::escapeMarkdown, 111 true), // only needs buffering until head()_ is called to make sure to emit metadata first 112 ROOT_WITHOUT_BUFFERING( 113 Type.GENERIC_CONTAINER, 114 true, 115 null, 116 false), // used after head()_/body() to prevent unnecessary buffering 117 HEAD(Type.GENERIC_CONTAINER, false, null, true), 118 BODY(Type.GENERIC_CONTAINER, true, ElementContext::escapeMarkdown), 119 // only the elements, which affect rendering of children and are different from BODY or HEAD are listed here 120 FIGURE(Type.INLINE, false, ElementContext::escapeMarkdown, true), 121 HEADING(Type.LEAF_BLOCK, false, ElementContext::escapeMarkdown), 122 CODE_BLOCK(Type.LEAF_BLOCK, false, null), 123 CODE_SPAN(Type.INLINE, false, null, true), 124 TABLE(Type.CONTAINER_BLOCK, false, null, false, "", true), 125 TABLE_CAPTION(Type.INLINE, false, ElementContext::escapeMarkdown), 126 TABLE_ROW(Type.INLINE, false, null, true), // special handling of newlines 127 TABLE_CELL( 128 Type.INLINE, 129 false, 130 ElementContext::escapeForTableCell, 131 false), // special type, as allows containing inlines, but not starting on a separate line 132 // same parameters as BODY but paragraphs inside list items are handled differently 133 LIST_ITEM(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, INDENT), 134 BLOCKQUOTE(Type.CONTAINER_BLOCK, false, ElementContext::escapeMarkdown, false, BLOCKQUOTE_START_MARKUP), 135 HTML_BLOCK(Type.GENERIC_CONTAINER, true, ElementContext::escapeHtml, false, "", false); 136 137 /** 138 * @see <a href="https://spec.commonmark.org/0.30/#blocks-and-inlines">CommonMark, 3 Blocks and inlines</a> 139 */ 140 enum Type { 141 /** 142 * Container with no special meaning for (nested) child element contexts 143 */ 144 GENERIC_CONTAINER, 145 /** 146 * Is supposed to start on a new line, and must have a prefix (for nested blocks) 147 */ 148 CONTAINER_BLOCK, 149 /** 150 * Is supposed to start on a new line, must not contain any other block element context (neither leaf nor container) 151 */ 152 LEAF_BLOCK, 153 /** 154 * Are not allowed to contain any other element context (i.e. leaf contexts), except for some other inlines (depends on the actual type) 155 */ 156 INLINE 157 } 158 /** 159 * {@code true} if block element, otherwise {@code false} for inline elements 160 */ 161 final Type type; 162 163 /** 164 * 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 165 */ 166 final TextEscapeFunction escapeFunction; 167 168 /** 169 * if {@code true} requires buffering any text appearing inside this context 170 */ 171 final boolean requiresBuffering; 172 173 /** 174 * 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}) 175 */ 176 final String prefix; 177 178 /** 179 * Only relevant for block element, if set to {@code true} the element requires to be surrounded by blank lines. 180 */ 181 final boolean requiresSurroundingByBlankLines; 182 183 /** 184 * If markup linebreaks (i.e. insignificant linebreaks in the source) are allowed in this context. 185 * This is relevant for markdown as in some contexts (e.g. list items) linebreaks are always significant (while for HTML they wouldn't be) 186 */ 187 final boolean allowsMarkupLinebreaks; 188 189 ElementContext(Type type, boolean allowsMarkupLinebreaks, TextEscapeFunction escapeFunction) { 190 this(type, allowsMarkupLinebreaks, escapeFunction, false); 191 } 192 193 ElementContext( 194 Type type, 195 boolean allowsMarkupLinebreaks, 196 TextEscapeFunction escapeFunction, 197 boolean requiresBuffering) { 198 this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, ""); 199 } 200 201 ElementContext( 202 Type type, 203 boolean allowsMarkupLinebreaks, 204 TextEscapeFunction escapeFunction, 205 boolean requiresBuffering, 206 String prefix) { 207 this(type, allowsMarkupLinebreaks, escapeFunction, requiresBuffering, prefix, false); 208 } 209 210 ElementContext( 211 Type type, 212 boolean allowsMarkupLinebreaks, 213 TextEscapeFunction escapeFunction, 214 boolean requiresBuffering, 215 String prefix, 216 boolean requiresSurroundingByBlankLines) { 217 this.type = type; 218 this.allowsMarkupLinebreaks = allowsMarkupLinebreaks; 219 this.escapeFunction = escapeFunction; 220 this.requiresBuffering = requiresBuffering; 221 if (type != Type.CONTAINER_BLOCK && prefix.length() != 0) { 222 throw new IllegalArgumentException("Only container blocks may define a prefix (for nesting)"); 223 } 224 this.prefix = prefix; 225 this.requiresSurroundingByBlankLines = requiresSurroundingByBlankLines; 226 } 227 228 /** 229 * Must be called for each inline text to be emitted directly within this context (not relevant for nested context) 230 * @param text 231 * @return the escaped text (may be same as {@code text} when no escaping is necessary) 232 */ 233 String escape(LastTwoLinesAwareWriter writer, String text) { 234 // is escaping necessary at all? 235 if (escapeFunction == null) { 236 return text; 237 } else { 238 return escapeFunction.escape(this, writer, text); 239 } 240 } 241 242 /** 243 * 244 * @return {@code true} for all block types, {@code false} otherwise 245 */ 246 boolean isBlock() { 247 return type == Type.CONTAINER_BLOCK || type == Type.LEAF_BLOCK; 248 } 249 250 /** 251 * 252 * @return {@code true} if only HTML is allowed in this context 253 */ 254 boolean isHtml() { 255 return this.equals(HTML_BLOCK); 256 } 257 /** 258 * 259 * @return {@code true} for all containers (allowing block elements as children), {@code false} otherwise 260 */ 261 boolean isContainer() { 262 return type == Type.CONTAINER_BLOCK || type == Type.GENERIC_CONTAINER; 263 } 264 265 public boolean isAllowsMarkupLinebreaks() { 266 return allowsMarkupLinebreaks; 267 } 268 269 /** 270 * First use XML escaping (leveraging the predefined entities, for browsers) 271 * afterwards escape special characters in a text with a leading backslash (for markdown parsers) 272 * 273 * <pre> 274 * \, `, *, _, {, }, [, ], (, ), #, +, -, ., ! 275 * </pre> 276 * 277 * @param text the string to escape, may be null 278 * @return the text escaped, "" if null String input 279 * @see <a href="https://daringfireball.net/projects/markdown/syntax#backslash">Backslash Escapes</a> 280 */ 281 private String escapeMarkdown(LastTwoLinesAwareWriter writer, String text) { 282 if (text == null) { 283 return ""; 284 } 285 text = escapeHtmlForMarkdown(text); // assume UTF-8 output, i.e. only use the mandatory XML entities 286 int length = text.length(); 287 StringBuilder buffer = new StringBuilder(length); 288 289 for (int i = 0; i < length; ++i) { 290 char c = text.charAt(i); 291 switch (c) { 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 (isAfterOnlyLeadingDigits(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 /** 334 * A {@code .} only needs escaping when it would turn leading digits into the marker of an 335 * ordered list item, i.e. when nothing but whitespace and digits precede it on the line. 336 * Elsewhere (as in version numbers such as {@code 1.6}) it is an ordinary character. 337 */ 338 private static boolean isAfterOnlyLeadingDigits(StringBuilder buffer, LastTwoLinesAwareWriter writer) { 339 if (buffer.length() > 0) { 340 return writer.isInBlankLine() && LastTwoLinesAwareWriter.isOnlyLeadingDigits(buffer); 341 } else { 342 return writer.isAfterOnlyLeadingDigits(); 343 } 344 } 345 346 private static boolean isInBlankLine(StringBuilder buffer, LastTwoLinesAwareWriter writer) { 347 if (DoxiaStringUtils.isBlank(buffer.toString())) { 348 return writer.isInBlankLine(); 349 } 350 return false; 351 } 352 353 private static String escapeMarkdown(char c) { 354 return "\\" + c; 355 } 356 357 private String escapeHtml(LastTwoLinesAwareWriter writer, String text) { 358 return HtmlTools.escapeHTML(text, true); 359 } 360 361 /** 362 * Escapes only those characters which would otherwise be interpreted as raw HTML inside a 363 * Markdown document. Unlike {@link HtmlTools#escapeHTML(String, boolean)} this leaves 364 * {@code "} and {@code '} alone: Markdown is not XML, both are ordinary characters there, 365 * and turning them into {@code "}/{@code '} only makes the source unreadable. 366 * 367 * @param text the string to escape, may be null 368 * @return the escaped text, "" if null String input 369 */ 370 private static String escapeHtmlForMarkdown(String text) { 371 if (text == null) { 372 return ""; 373 } 374 int length = text.length(); 375 StringBuilder buffer = new StringBuilder(length); 376 for (int i = 0; i < length; ++i) { 377 char c = text.charAt(i); 378 switch (c) { 379 case '<': 380 buffer.append("<"); 381 break; 382 case '>': 383 buffer.append(">"); 384 break; 385 case '&': 386 buffer.append("&"); 387 break; 388 default: 389 buffer.append(c); 390 } 391 } 392 return buffer.toString(); 393 } 394 395 /** 396 * Escapes the pipe character according to <a href="https://github.github.com/gfm/#tables-extension-">GFM Table Extension</a> in addition 397 * to the regular markdown escaping. 398 * @param text 399 * @return the escaped text 400 * @see {@link #escapeMarkdown(String) 401 */ 402 private String escapeForTableCell(LastTwoLinesAwareWriter writer, String text) { 403 return escapeMarkdown(writer, text).replace("|", "\\|"); 404 } 405 } 406 // ---------------------------------------------------------------------- 407 // Public protected methods 408 // ---------------------------------------------------------------------- 409 410 /** 411 * A link destination must not contain a space: everything from the space on is read as the 412 * link title instead, so {@code [text](#My Anchor)} does not resolve. An in-page anchor 413 * therefore gets the same encoding the anchor definition receives, and any other destination 414 * has its spaces percent-encoded. 415 * 416 * @param destination the link destination, may be null 417 * @return the destination, safe to write into a Markdown inline link 418 */ 419 private static String encodeLinkDestination(String destination) { 420 if (destination == null || destination.indexOf(' ') < 0) { 421 return destination; 422 } 423 if (destination.charAt(0) == '#') { 424 return "#" + DoxiaUtils.encodeId(destination.substring(1)); 425 } 426 return destination.replace(" ", "%20"); 427 } 428 429 protected static MarkdownSink newInstance(Writer writer) { 430 BufferingStackWriter bufferingStackWriter = new BufferingStackWriter(writer); 431 LastTwoLinesAwareWriter lineAwareWriter = new LastTwoLinesAwareWriter(bufferingStackWriter); 432 return new MarkdownSink(lineAwareWriter, bufferingStackWriter); 433 } 434 435 /** 436 * Constructor, initialize the Writer and the variables. 437 * 438 * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer. 439 */ 440 private MarkdownSink(LastTwoLinesAwareWriter lineAwareWriter, BufferingStackWriter bufferingStackWriter) { 441 super(lineAwareWriter); 442 this.lineAwareWriter = lineAwareWriter; 443 this.bufferingStackWriter = bufferingStackWriter; 444 initInternal(); 445 setInsertNewline( 446 false); // we want to control newlines on our own to prevent (mostly to not break encapsulating markdown 447 // tables) 448 } 449 450 private void initInternal() { 451 this.authors = new LinkedList<>(); 452 this.title = null; 453 this.date = null; 454 this.linkName = null; 455 this.tableHeaderCellFlag = false; 456 this.cellCount = 0; 457 this.cellJustif = null; 458 this.elementContextStack = Collections.asLifoQueue(new LinkedList<>()); 459 this.inlineStack = Collections.asLifoQueue(new LinkedList<>()); 460 startContext(ElementContext.ROOT_WITH_BUFFERING); 461 } 462 463 private void endContext(ElementContext expectedContext) { 464 ElementContext removedContext = elementContextStack.remove(); 465 if (removedContext != expectedContext) { 466 throw new IllegalStateException("Unexpected context " + removedContext + ", expected " + expectedContext); 467 } 468 if (removedContext.isBlock()) { 469 endBlock(removedContext.requiresSurroundingByBlankLines 470 || (isInListItem() && (removedContext == ElementContext.BLOCKQUOTE) 471 || (removedContext == ElementContext.CODE_BLOCK))); 472 } 473 if (removedContext.requiresBuffering) { 474 // remove buffer from stack (assume it has been evaluated already) 475 bufferingStackWriter.removeBuffer(); 476 } 477 } 478 479 private void startContext(ElementContext newContext) { 480 if (newContext.requiresBuffering) { 481 bufferingStackWriter.addBuffer(); 482 } 483 if (newContext.isBlock()) { 484 // every block element within a list item must be surrounded by blank lines 485 startBlock(newContext.requiresSurroundingByBlankLines 486 || (isInListItem() && (newContext == ElementContext.BLOCKQUOTE) 487 || (newContext == ElementContext.CODE_BLOCK))); 488 } 489 elementContextStack.add(newContext); 490 } 491 492 private String toogleToRootContextWithoutBuffering(boolean dumpBuffer) { 493 final String buffer; 494 if (elementContextStack.element() == ElementContext.ROOT_WITH_BUFFERING) { 495 buffer = bufferingStackWriter.getCurrentBuffer().toString(); 496 endContext(ElementContext.ROOT_WITH_BUFFERING); 497 if (dumpBuffer) { 498 write(buffer); 499 } 500 startContext(ElementContext.ROOT_WITHOUT_BUFFERING); 501 } else if (elementContextStack.element() != ElementContext.ROOT_WITHOUT_BUFFERING) { 502 throw new IllegalStateException("Unexpected context " + elementContextStack.element() 503 + ", expected ROOT_WITH_BUFFERING or ROOT_WITHOUT_BUFFERING"); 504 } else { 505 buffer = ""; 506 } 507 return buffer; 508 } 509 /** 510 * Ensures that the {@link #writer} is currently at the beginning of a new line. 511 * Optionally writes a line separator to ensure that. 512 */ 513 private void ensureBeginningOfLine() { 514 // make sure that we are at the start of a line without adding unnecessary blank lines 515 if (!lineAwareWriter.isWriterAtStartOfNewLine()) { 516 write(EOL); 517 } 518 } 519 520 /** 521 * Ensures that the {@link #writer} is preceded by a blank line. 522 * Optionally writes a blank line or just line delimiter to ensure that. 523 */ 524 private void ensureBlankLine() { 525 // prevent duplicate blank lines 526 if (!lineAwareWriter.isWriterAfterBlankLine()) { 527 if (lineAwareWriter.isWriterAtStartOfNewLine()) { 528 write(EOL); 529 } else { 530 write(BLANK_LINE); 531 } 532 } 533 } 534 535 private void startBlock(boolean requireBlankLine) { 536 if (requireBlankLine) { 537 ensureBlankLine(); 538 } else { 539 ensureBeginningOfLine(); 540 } 541 write(getLinePrefix()); 542 } 543 544 private void endBlock(boolean requireBlankLine) { 545 if (requireBlankLine) { 546 ensureBlankLine(); 547 } else { 548 ensureBeginningOfLine(); 549 } 550 } 551 552 /** 553 * @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 554 */ 555 private String getLinePrefix() { 556 StringBuilder prefix = new StringBuilder(); 557 elementContextStack.stream().filter(c -> c.prefix.length() > 0).forEachOrdered(c -> prefix.insert(0, c.prefix)); 558 return prefix.toString(); 559 } 560 561 private boolean isInListItem() { 562 return elementContextStack.stream() 563 .filter(c -> c == ElementContext.LIST_ITEM) 564 .findFirst() 565 .isPresent(); 566 } 567 568 @Override 569 protected void init() { 570 super.init(); 571 initInternal(); 572 } 573 574 @Override 575 public void head(SinkEventAttributes attributes) { 576 startContext(ElementContext.HEAD); 577 } 578 579 @Override 580 public void head_() { 581 endContext(ElementContext.HEAD); 582 String priorHeadBuffer = toogleToRootContextWithoutBuffering(false); 583 // only write head block if really necessary 584 if (title == null && authors.isEmpty() && date == null) { 585 return; 586 } 587 write(METADATA_MARKUP + EOL); 588 if (title != null) { 589 write("title: " + title + EOL); 590 } 591 if (!authors.isEmpty()) { 592 write("author: " + EOL); 593 for (String author : authors) { 594 write(" - " + author + EOL); 595 } 596 } 597 if (date != null) { 598 write("date: " + date + EOL); 599 } 600 write(METADATA_MARKUP + BLANK_LINE); 601 write(priorHeadBuffer); 602 } 603 604 @Override 605 public void body(SinkEventAttributes attributes) { 606 toogleToRootContextWithoutBuffering(true); 607 startContext(ElementContext.BODY); 608 } 609 610 @Override 611 public void body_() { 612 endContext(ElementContext.BODY); 613 } 614 615 @Override 616 public void title_() { 617 String buffer = bufferingStackWriter.getAndClearCurrentBuffer(); 618 if (!buffer.isEmpty()) { 619 this.title = buffer; 620 } 621 } 622 623 @Override 624 public void author_() { 625 String buffer = bufferingStackWriter.getAndClearCurrentBuffer(); 626 if (!buffer.isEmpty()) { 627 authors.add(buffer); 628 } 629 } 630 631 @Override 632 public void date_() { 633 String buffer = bufferingStackWriter.getAndClearCurrentBuffer(); 634 if (!buffer.isEmpty()) { 635 date = buffer; 636 } 637 } 638 639 @Override 640 public void section(int level, SinkEventAttributes attributes) { 641 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 642 } 643 644 @Override 645 public void section_(int level) { 646 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 647 } 648 649 @Override 650 public void header(SinkEventAttributes attributes) { 651 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 652 } 653 654 @Override 655 public void header_() { 656 // not supported as often used around sectionTitles which would otherwise no longer be emitted as markdown 657 } 658 659 @Override 660 public void sectionTitle(int level, SinkEventAttributes attributes) { 661 startContext(ElementContext.HEADING); 662 if (level > 0) { 663 write(DoxiaStringUtils.repeat(SECTION_TITLE_START_MARKUP, level) + SPACE); 664 } 665 } 666 667 @Override 668 public void sectionTitle_(int level) { 669 endContext(ElementContext.HEADING); 670 if (level > 0) { 671 ensureBlankLine(); // always end headings with blank line to increase compatibility with arbitrary MD 672 // editors 673 } 674 } 675 676 @Override 677 public void list(SinkEventAttributes attributes) { 678 if (elementContextStack.element().isHtml()) { 679 super.list(attributes); 680 } 681 } 682 683 @Override 684 public void list_() { 685 ensureBeginningOfLine(); 686 } 687 688 @Override 689 public void listItem(SinkEventAttributes attributes) { 690 startContext(ElementContext.LIST_ITEM); 691 write(LIST_UNORDERED_ITEM_START_MARKUP); 692 } 693 694 @Override 695 public void listItem_() { 696 endContext(ElementContext.LIST_ITEM); 697 } 698 699 @Override 700 public void numberedList(int numbering, SinkEventAttributes attributes) { 701 // markdown only supports decimal numbering 702 if (numbering != NUMBERING_DECIMAL) { 703 LOGGER.warn( 704 "{}Markdown only supports numbered item with decimal style ({}) but requested was style {}, falling back to decimal style", 705 getLocationLogPrefix(), 706 NUMBERING_DECIMAL, 707 numbering); 708 } 709 } 710 711 @Override 712 public void numberedList_() { 713 ensureBeginningOfLine(); 714 } 715 716 @Override 717 public void numberedListItem(SinkEventAttributes attributes) { 718 startContext(ElementContext.LIST_ITEM); 719 write(LIST_ORDERED_ITEM_START_MARKUP); 720 } 721 722 @Override 723 public void numberedListItem_() { 724 listItem_(); // identical for both numbered and not numbered list item 725 } 726 727 @Override 728 public void definitionList(SinkEventAttributes attributes) { 729 LOGGER.warn( 730 "{}Definition list not natively supported in Markdown, rendering HTML instead", getLocationLogPrefix()); 731 startContext(ElementContext.HTML_BLOCK); 732 write("<dl>" + EOL); 733 } 734 735 @Override 736 public void definitionList_() { 737 write("</dl>"); 738 endContext(ElementContext.HTML_BLOCK); 739 } 740 741 @Override 742 public void definedTerm(SinkEventAttributes attributes) { 743 write("<dt>"); 744 } 745 746 @Override 747 public void definedTerm_() { 748 write("</dt>" + EOL); 749 } 750 751 @Override 752 public void definition(SinkEventAttributes attributes) { 753 write("<dd>"); 754 } 755 756 @Override 757 public void definition_() { 758 write("</dd>" + EOL); 759 } 760 761 @Override 762 public void pageBreak() { 763 LOGGER.warn("{}Ignoring unsupported page break in Markdown", getLocationLogPrefix()); 764 } 765 766 @Override 767 public void paragraph(SinkEventAttributes attributes) { 768 // ignore paragraphs outside container contexts 769 if (elementContextStack.element().isContainer()) { 770 ensureBlankLine(); 771 write(getLinePrefix()); 772 } else { 773 LOGGER.warn( 774 "{}Paragraphs outside of container contexts are not supported in Markdown, ignoring paragraph event in context {}", 775 getLocationLogPrefix(), 776 elementContextStack.element()); 777 } 778 } 779 780 @Override 781 public void paragraph_() { 782 // ignore paragraphs outside container contexts 783 if (elementContextStack.element().isContainer()) { 784 ensureBlankLine(); 785 } 786 } 787 788 @Override 789 public void verbatim(SinkEventAttributes attributes) { 790 if (!elementContextStack.element().isContainer()) { 791 // markdown doesn't allow block elements but one can instead rely on html blocks for this 792 startContext(ElementContext.HTML_BLOCK); 793 isVerbatimHtmlContext = true; 794 } else { 795 isVerbatimHtmlContext = false; 796 } 797 798 if (elementContextStack.element().isHtml()) { 799 super.verbatim(attributes); 800 } else { 801 // if no source attribute, then don't emit an info string 802 startContext(ElementContext.CODE_BLOCK); 803 write(VERBATIM_START_MARKUP); 804 if (attributes != null && attributes.containsAttributes(SinkEventAttributeSet.SOURCE)) { 805 write("unknown"); // unknown language 806 } 807 write(EOL); 808 write(getLinePrefix()); 809 } 810 } 811 812 @Override 813 public void verbatim_() { 814 if (elementContextStack.element().isHtml()) { 815 super.verbatim_(); 816 if (isVerbatimHtmlContext) { 817 endContext(ElementContext.HTML_BLOCK); 818 isVerbatimHtmlContext = false; 819 } 820 } else { 821 ensureBeginningOfLine(); 822 write(getLinePrefix()); 823 write(VERBATIM_END_MARKUP + BLANK_LINE); 824 endContext(ElementContext.CODE_BLOCK); 825 } 826 } 827 828 @Override 829 public void blockquote(SinkEventAttributes attributes) { 830 if (elementContextStack.element().isHtml()) { 831 super.blockquote(attributes); 832 } else { 833 startContext(ElementContext.BLOCKQUOTE); 834 write(BLOCKQUOTE_START_MARKUP); 835 } 836 } 837 838 @Override 839 public void blockquote_() { 840 if (elementContextStack.element().isHtml()) { 841 super.blockquote_(); 842 } else { 843 endContext(ElementContext.BLOCKQUOTE); 844 } 845 } 846 847 @Override 848 public void horizontalRule(SinkEventAttributes attributes) { 849 ensureBeginningOfLine(); 850 write(HORIZONTAL_RULE_MARKUP + BLANK_LINE); 851 write(getLinePrefix()); 852 } 853 854 @Override 855 public void table(SinkEventAttributes attributes) { 856 if (elementContextStack.element().isHtml()) { 857 super.table(attributes); 858 } else { 859 startContext(ElementContext.TABLE); 860 } 861 } 862 863 @Override 864 public void table_() { 865 if (elementContextStack.element().isHtml()) { 866 super.table_(); 867 } else { 868 endContext(ElementContext.TABLE); 869 } 870 } 871 872 @Override 873 public void tableRows(int[] justification, boolean grid) { 874 if (elementContextStack.element().isHtml()) { 875 super.tableRows(justification, grid); 876 } else { 877 if (justification != null) { 878 cellJustif = Arrays.stream(justification).boxed().collect(Collectors.toCollection(ArrayList::new)); 879 } else { 880 cellJustif = new ArrayList<>(); 881 } 882 // grid flag is not supported 883 isFirstTableRow = true; 884 } 885 } 886 887 @Override 888 public void tableRows_() { 889 if (elementContextStack.element().isHtml()) { 890 super.tableRows_(); 891 } else { 892 cellJustif = null; 893 } 894 } 895 896 @Override 897 public void tableRow(SinkEventAttributes attributes) { 898 if (elementContextStack.element().isHtml()) { 899 super.tableRow(attributes); 900 } else { 901 startContext(ElementContext.TABLE_ROW); 902 cellCount = 0; 903 } 904 } 905 906 @Override 907 public void tableRow_() { 908 if (elementContextStack.element().isHtml()) { 909 super.tableRow_(); 910 } else { 911 String buffer = bufferingStackWriter.getAndClearCurrentBuffer(); 912 endContext(ElementContext.TABLE_ROW); 913 if (isFirstTableRow && !tableHeaderCellFlag) { 914 // emit empty table header as this is mandatory for GFM table extension 915 // (https://stackoverflow.com/a/17543474/5155923) 916 writeEmptyTableHeader(); 917 writeTableDelimiterRow(); 918 tableHeaderCellFlag = false; 919 isFirstTableRow = false; 920 // afterwards emit the first row 921 } 922 write(TABLE_ROW_PREFIX); 923 write(buffer); 924 write(EOL); 925 if (isFirstTableRow) { 926 // emit delimiter row 927 writeTableDelimiterRow(); 928 isFirstTableRow = false; 929 } 930 // only reset cell count if this is the last row 931 cellCount = 0; 932 } 933 } 934 935 private void writeEmptyTableHeader() { 936 write(TABLE_ROW_PREFIX); 937 for (int i = 0; i < cellCount; i++) { 938 write(DoxiaStringUtils.repeat(String.valueOf(SPACE), 3) + TABLE_CELL_SEPARATOR_MARKUP); 939 } 940 write(EOL); 941 write(getLinePrefix()); 942 } 943 944 /** Emit the delimiter row which determines the alignment */ 945 private void writeTableDelimiterRow() { 946 write(TABLE_ROW_PREFIX); 947 int justification = Sink.JUSTIFY_DEFAULT; 948 for (int i = 0; i < cellCount; i++) { 949 // keep previous column's alignment in case too few are specified 950 if (cellJustif != null && cellJustif.size() > i) { 951 justification = cellJustif.get(i); 952 } 953 switch (justification) { 954 case Sink.JUSTIFY_RIGHT: 955 write(TABLE_COL_RIGHT_ALIGNED_MARKUP); 956 break; 957 case Sink.JUSTIFY_CENTER: 958 write(TABLE_COL_CENTER_ALIGNED_MARKUP); 959 break; 960 case Sink.JUSTIFY_LEFT: 961 write(TABLE_COL_LEFT_ALIGNED_MARKUP); 962 break; 963 default: 964 write(TABLE_COL_DEFAULT_ALIGNED_MARKUP); 965 break; 966 } 967 write(TABLE_CELL_SEPARATOR_MARKUP); 968 } 969 write(EOL); 970 } 971 972 @Override 973 public void tableCell(SinkEventAttributes attributes) { 974 if (elementContextStack.element().isHtml()) { 975 super.tableCell(attributes); 976 } else { 977 startContext(ElementContext.TABLE_CELL); 978 if (attributes != null) { 979 // evaluate alignment attributes 980 final int cellJustification; 981 if (attributes.containsAttributes(SinkEventAttributeSet.LEFT)) { 982 cellJustification = Sink.JUSTIFY_LEFT; 983 } else if (attributes.containsAttributes(SinkEventAttributeSet.RIGHT)) { 984 cellJustification = Sink.JUSTIFY_RIGHT; 985 } else if (attributes.containsAttributes(SinkEventAttributeSet.CENTER)) { 986 cellJustification = Sink.JUSTIFY_CENTER; 987 } else { 988 cellJustification = -1; 989 } 990 if (cellJustification > -1) { 991 if (cellJustif.size() > cellCount) { 992 cellJustif.set(cellCount, cellJustification); 993 } else if (cellJustif.size() == cellCount) { 994 cellJustif.add(cellJustification); 995 } else { 996 // create non-existing justifications for preceding columns 997 for (int precedingCol = cellJustif.size(); precedingCol < cellCount; precedingCol++) { 998 cellJustif.add(Sink.JUSTIFY_DEFAULT); 999 } 1000 cellJustif.add(cellJustification); 1001 } 1002 } 1003 } 1004 } 1005 } 1006 1007 @Override 1008 public void tableHeaderCell(SinkEventAttributes attributes) { 1009 if (elementContextStack.element().isHtml()) { 1010 super.tableHeaderCell(attributes); 1011 } else { 1012 tableCell(attributes); 1013 tableHeaderCellFlag = true; 1014 } 1015 } 1016 1017 @Override 1018 public void tableCell_() { 1019 if (elementContextStack.element().isHtml()) { 1020 super.tableCell_(); 1021 } else { 1022 endTableCell(); 1023 } 1024 } 1025 1026 @Override 1027 public void tableHeaderCell_() { 1028 if (elementContextStack.element().isHtml()) { 1029 super.tableHeaderCell_(); 1030 } else { 1031 endTableCell(); 1032 } 1033 } 1034 1035 /** 1036 * Ends a table cell. 1037 */ 1038 private void endTableCell() { 1039 endContext(ElementContext.TABLE_CELL); 1040 write(TABLE_CELL_SEPARATOR_MARKUP); 1041 cellCount++; 1042 } 1043 1044 @Override 1045 public void tableCaption(SinkEventAttributes attributes) { 1046 if (elementContextStack.element().isHtml()) { 1047 super.tableCaption(attributes); 1048 } else { 1049 elementContextStack.add(ElementContext.TABLE_CAPTION); 1050 } 1051 } 1052 1053 @Override 1054 public void tableCaption_() { 1055 if (elementContextStack.element().isHtml()) { 1056 super.tableCaption_(); 1057 } else { 1058 endContext(ElementContext.TABLE_CAPTION); 1059 } 1060 } 1061 1062 @Override 1063 public void figure(SinkEventAttributes attributes) { 1064 if (elementContextStack.element().isHtml()) { 1065 super.figure(attributes); 1066 } else { 1067 figureSrc = null; 1068 startContext(ElementContext.FIGURE); 1069 } 1070 } 1071 1072 @Override 1073 public void figureCaption(SinkEventAttributes attributes) { 1074 if (elementContextStack.element().isHtml()) { 1075 super.figureCaption(attributes); 1076 } 1077 } 1078 1079 @Override 1080 public void figureCaption_() { 1081 if (elementContextStack.element().isHtml()) { 1082 super.figureCaption_(); 1083 } 1084 } 1085 1086 @Override 1087 public void figureGraphics(String name, SinkEventAttributes attributes) { 1088 if (elementContextStack.element().isHtml()) { 1089 super.figureGraphics(name, attributes); 1090 } else { 1091 figureSrc = name; 1092 // is it a standalone image (outside a figure)? 1093 if (elementContextStack.peek() != ElementContext.FIGURE) { 1094 Object alt = attributes.getAttribute(SinkEventAttributes.ALT); 1095 if (alt == null) { 1096 alt = ""; 1097 } 1098 writeImage(elementContextStack.element().escape(lineAwareWriter, alt.toString()), name); 1099 } 1100 } 1101 } 1102 1103 @Override 1104 public void figure_() { 1105 if (elementContextStack.element().isHtml()) { 1106 super.figure_(); 1107 } else { 1108 String label = bufferingStackWriter.getCurrentBuffer().toString(); 1109 endContext(ElementContext.FIGURE); 1110 writeImage(label, figureSrc); 1111 } 1112 } 1113 1114 private void writeImage(String alt, String src) { 1115 write(""); 1118 } 1119 1120 public void anchor(String name, SinkEventAttributes attributes) { 1121 super.anchor(name, attributes); 1122 if (!elementContextStack.element().isHtml()) { 1123 // close anchor tag immediately otherwise markdown would not be allowed afterwards 1124 write("</a>"); 1125 } 1126 } 1127 1128 @Override 1129 public void anchor_() { 1130 if (elementContextStack.element().isHtml()) { 1131 super.anchor_(); 1132 } else { 1133 // anchor is always empty html element, i.e. already closed with anchor() 1134 } 1135 } 1136 1137 public void link(String name, SinkEventAttributes attributes) { 1138 if (elementContextStack.element().isHtml()) { 1139 super.link(name, attributes); 1140 } else { 1141 if (elementContextStack.element() == ElementContext.CODE_BLOCK) { 1142 LOGGER.warn("{}Ignoring unsupported link inside code block", getLocationLogPrefix()); 1143 } else if (elementContextStack.element() == ElementContext.CODE_SPAN) { 1144 // emit link outside the code span, i.e. insert at the beginning of the buffer 1145 bufferingStackWriter.getCurrentBuffer().insert(0, LINK_START_1_MARKUP); 1146 linkName = name; 1147 } else { 1148 write(LINK_START_1_MARKUP); 1149 linkName = name; 1150 } 1151 } 1152 } 1153 1154 @Override 1155 public void link_() { 1156 if (elementContextStack.element().isHtml()) { 1157 super.link_(); 1158 } else { 1159 if (elementContextStack.element() == ElementContext.CODE_BLOCK) { 1160 return; 1161 } else if (elementContextStack.element() == ElementContext.CODE_SPAN) { 1162 // defer emitting link end markup until inline_() is called 1163 StringBuilder linkEndMarkup = new StringBuilder(); 1164 linkEndMarkup.append(LINK_START_2_MARKUP); 1165 linkEndMarkup.append(encodeLinkDestination(linkName)); 1166 linkEndMarkup.append(LINK_END_MARKUP); 1167 Queue<String> endMarkups = new LinkedList<>(inlineStack.poll()); 1168 endMarkups.add(linkEndMarkup.toString()); 1169 inlineStack.add(endMarkups); 1170 } else { 1171 write(LINK_START_2_MARKUP + encodeLinkDestination(linkName) + LINK_END_MARKUP); 1172 } 1173 linkName = null; 1174 } 1175 } 1176 1177 @Override 1178 public void inline(SinkEventAttributes attributes) { 1179 Queue<String> endMarkups = Collections.asLifoQueue(new LinkedList<>()); 1180 if (elementContextStack.element().isHtml()) { 1181 super.inline(attributes); 1182 endMarkups.add(USE_XHTML_SINK); 1183 } else { 1184 boolean requiresHtml = elementContextStack.element() == ElementContext.HTML_BLOCK; 1185 if (attributes != null 1186 && elementContextStack.element() != ElementContext.CODE_BLOCK 1187 && elementContextStack.element() != ElementContext.CODE_SPAN) { 1188 // code excludes other styles in markdown 1189 if (attributes.containsAttributes(SinkEventAttributeSet.Semantics.CODE) 1190 || attributes.containsAttributes(SinkEventAttributeSet.Semantics.MONOSPACED) 1191 || attributes.containsAttributes(SinkEventAttributeSet.MONOSPACED)) { 1192 if (requiresHtml) { 1193 write("<code>"); 1194 endMarkups.add("</code>"); 1195 } else { 1196 startContext(ElementContext.CODE_SPAN); 1197 write(MONOSPACED_START_MARKUP); 1198 endMarkups.add(MONOSPACED_END_MARKUP); 1199 } 1200 } else { 1201 SinkEventAttributeSet remainingAttributes = new SinkEventAttributeSet(attributes); 1202 // in XHTML "<em>" is used, but some tests still rely on the outdated "<italic>" 1203 if (filterAttributes( 1204 remainingAttributes, 1205 SinkEventAttributeSet.Semantics.EMPHASIS, 1206 SinkEventAttributeSet.Semantics.ITALIC, 1207 SinkEventAttributeSet.ITALIC)) { 1208 if (requiresHtml) { 1209 write("<em>"); 1210 endMarkups.add("</em>"); 1211 } else { 1212 write(ITALIC_START_MARKUP); 1213 endMarkups.add(ITALIC_END_MARKUP); 1214 } 1215 } 1216 // in XHTML "<strong>" is used, but some tests still rely on the outdated "<bold>" 1217 if (filterAttributes( 1218 remainingAttributes, 1219 SinkEventAttributeSet.Semantics.STRONG, 1220 SinkEventAttributeSet.Semantics.BOLD, 1221 SinkEventAttributeSet.BOLD)) { 1222 if (requiresHtml) { 1223 write("<strong>"); 1224 endMarkups.add("</strong>"); 1225 } else { 1226 write(BOLD_START_MARKUP); 1227 endMarkups.add(BOLD_END_MARKUP); 1228 } 1229 } 1230 // <del> is supported via GFM strikethrough extension 1231 if (filterAttributes(remainingAttributes, SinkEventAttributeSet.Semantics.DELETE)) { 1232 if (requiresHtml) { 1233 write("<del>"); 1234 endMarkups.add("</del>"); 1235 } else { 1236 write(STRIKETHROUGH_START_MARKUP); 1237 endMarkups.add(STRIKETHROUGH_END_MARKUP); 1238 } 1239 } 1240 if (!remainingAttributes.isEmpty()) { 1241 // use HTML for other inline semantics which are not natively supported in Markdown (e.g. 1242 // subscript, superscript, small, etc.) 1243 super.inline(remainingAttributes); 1244 endMarkups.add(USE_XHTML_SINK); 1245 } 1246 } 1247 } 1248 } 1249 inlineStack.add(endMarkups); 1250 } 1251 1252 private static boolean filterAttributes(MutableAttributeSet attributes, AttributeSet... attributesToFilter) { 1253 boolean hasAny = false; 1254 for (AttributeSet attributeSet : attributesToFilter) { 1255 if (attributes.containsAttributes(attributeSet)) { 1256 hasAny = true; 1257 attributes.removeAttributes(attributeSet); 1258 } 1259 } 1260 return hasAny; 1261 } 1262 1263 @Override 1264 public void inline_() { 1265 for (String endMarkup : inlineStack.remove()) { 1266 if (USE_XHTML_SINK.equals(endMarkup)) { 1267 super.inline_(); 1268 } else { 1269 if (endMarkup.equals(MONOSPACED_END_MARKUP)) { 1270 String buffer = bufferingStackWriter.getCurrentBuffer().toString(); 1271 endContext(ElementContext.CODE_SPAN); 1272 write(buffer); 1273 } 1274 write(endMarkup); 1275 } 1276 } 1277 } 1278 1279 @Override 1280 public void italic() { 1281 inline(SinkEventAttributeSet.Semantics.ITALIC); 1282 } 1283 1284 @Override 1285 public void italic_() { 1286 inline_(); 1287 } 1288 1289 @Override 1290 public void bold() { 1291 inline(SinkEventAttributeSet.Semantics.BOLD); 1292 } 1293 1294 @Override 1295 public void bold_() { 1296 inline_(); 1297 } 1298 1299 @Override 1300 public void monospaced() { 1301 inline(SinkEventAttributeSet.Semantics.CODE); 1302 } 1303 1304 @Override 1305 public void monospaced_() { 1306 inline_(); 1307 } 1308 1309 @Override 1310 public void lineBreak(SinkEventAttributes attributes) { 1311 if (elementContextStack.element() == ElementContext.TABLE_CELL) { 1312 super.lineBreak(attributes); 1313 } else { 1314 if (elementContextStack.element() == ElementContext.CODE_BLOCK) { 1315 write(EOL); 1316 } else { 1317 write("" + SPACE + SPACE + EOL); 1318 } 1319 write(getLinePrefix()); 1320 } 1321 } 1322 1323 @Override 1324 public void nonBreakingSpace() { 1325 write(NON_BREAKING_SPACE_MARKUP); 1326 } 1327 1328 @Override 1329 public void text(String text, SinkEventAttributes attributes) { 1330 if (elementContextStack.element().isHtml()) { 1331 super.text(text, attributes); 1332 } else { 1333 if (attributes != null) { 1334 inline(attributes); 1335 } 1336 ElementContext currentContext = elementContextStack.element(); 1337 if (currentContext == ElementContext.TABLE_CAPTION) { 1338 // table caption cannot even be emitted via XHTML in markdown as there is no suitable location 1339 LOGGER.warn("{}Ignoring unsupported table caption in Markdown", getLocationLogPrefix()); 1340 } else { 1341 String unifiedText = currentContext.escape(lineAwareWriter, unifyEOLs(text)); 1342 // ignore newlines only, because those are emitted often coming from linebreaks in HTML with no 1343 // semantical 1344 // meaning 1345 if (!unifiedText.equals(EOL)) { 1346 String prefix = getLinePrefix(); 1347 if (prefix.length() > 0) { 1348 unifiedText = unifiedText.replaceAll(EOL, EOL + prefix); 1349 } 1350 } 1351 write(unifiedText); 1352 } 1353 if (attributes != null) { 1354 inline_(); 1355 } 1356 } 1357 } 1358 1359 @Override 1360 public void rawText(String text) { 1361 write(text); 1362 } 1363 1364 /** Table row-grouping tags without their own content: {@link #unknown(String, Object[], SinkEventAttributes)} 1365 * ignores these rather than rendering them as raw HTML. */ 1366 private static final Collection<String> IGNORED_TABLE_GROUPING_TAGS = 1367 Collections.unmodifiableList(Arrays.asList("thead", "tbody", "tfoot")); 1368 1369 /** 1370 * {@inheritDoc} 1371 * 1372 * Delegates tag reconstruction and attribute escaping to 1373 * {@link Xhtml5BaseSink#unknown(String, Object[], SinkEventAttributes)}, so any tag recognized by 1374 * {@link HtmlTools#getHtmlTag(String)} (e.g. {@code <object>}, {@code <map>}/{@code <area>}) is emitted as raw 1375 * HTML instead of being silently dropped. 1376 * <p> 1377 * Unlike that XHTML sink, a {@link #TAG_TYPE_SIMPLE} event for a non-void element is never self-closed 1378 * ({@code <name/>}): HTML5 has no self-closing syntax outside the fixed list of void elements, so a 1379 * self-closed non-void tag such as {@code <object/>} would otherwise be re-parsed as an open tag that 1380 * swallows all following content as its own (hidden) fallback children. Such events are instead written as 1381 * an explicitly closed pair ({@code <name></name>}). 1382 * <p> 1383 * {@code <thead>}/{@code <tbody>}/{@code <tfoot>} are excluded: they carry no content of their own (Doxia's 1384 * table sink API already fully captures the table structure via {@code table()}/{@code tableRows()}/ 1385 * {@code tableRow()}/{@code tableCell()}), and {@link MarkdownParser} fires these as unknown events for 1386 * every table it parses (they have no dedicated sink method). Rendering them as raw HTML would inject a 1387 * tag directly before the first cell of a row, which breaks GFM pipe-table syntax since a table row must 1388 * start at the beginning of its line. 1389 * 1390 * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes) 1391 */ 1392 @Override 1393 public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) { 1394 if (IGNORED_TABLE_GROUPING_TAGS.contains(name)) { 1395 return; 1396 } 1397 if (requiredParams != null 1398 && requiredParams[0] instanceof Integer 1399 && (Integer) requiredParams[0] == TAG_TYPE_SIMPLE 1400 && HtmlTools.getHtmlTag(name) != null 1401 && !Tag.valueOf(name).isSelfClosing()) { 1402 super.unknown(name, new Object[] {TAG_TYPE_START}, attributes); 1403 super.unknown(name, new Object[] {TAG_TYPE_END}, null); 1404 return; 1405 } 1406 super.unknown(name, requiredParams, attributes); 1407 } 1408 1409 @Override 1410 public void markupLineBreak(int indentLevel) { 1411 // not allowed in all contexts 1412 if (elementContextStack.element().isAllowsMarkupLinebreaks()) { 1413 if (!lineAwareWriter.isWriterAfterBlankLine()) { 1414 super.markupLineBreak(indentLevel); 1415 } 1416 } 1417 } 1418 1419 @Override 1420 public void close() { 1421 toogleToRootContextWithoutBuffering(true); 1422 super.close(); 1423 } 1424}