001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.metaschema.core.datatype.markup.flexmark; // NOPMD AST processor has many members 028 029import com.vladsch.flexmark.ast.AutoLink; 030import com.vladsch.flexmark.ast.BlockQuote; 031import com.vladsch.flexmark.ast.Code; 032import com.vladsch.flexmark.ast.CodeBlock; 033import com.vladsch.flexmark.ast.FencedCodeBlock; 034import com.vladsch.flexmark.ast.HardLineBreak; 035import com.vladsch.flexmark.ast.Heading; 036import com.vladsch.flexmark.ast.HtmlBlock; 037import com.vladsch.flexmark.ast.HtmlCommentBlock; 038import com.vladsch.flexmark.ast.HtmlEntity; 039import com.vladsch.flexmark.ast.HtmlInline; 040import com.vladsch.flexmark.ast.Image; 041import com.vladsch.flexmark.ast.IndentedCodeBlock; 042import com.vladsch.flexmark.ast.Link; 043import com.vladsch.flexmark.ast.ListBlock; 044import com.vladsch.flexmark.ast.ListItem; 045import com.vladsch.flexmark.ast.MailLink; 046import com.vladsch.flexmark.ast.OrderedList; 047import com.vladsch.flexmark.ast.Paragraph; 048import com.vladsch.flexmark.ast.ParagraphItemContainer; 049import com.vladsch.flexmark.ast.Text; 050import com.vladsch.flexmark.ast.TextBase; 051import com.vladsch.flexmark.ast.ThematicBreak; 052import com.vladsch.flexmark.ext.escaped.character.EscapedCharacter; 053import com.vladsch.flexmark.ext.tables.TableBlock; 054import com.vladsch.flexmark.ext.tables.TableBody; 055import com.vladsch.flexmark.ext.tables.TableCell; 056import com.vladsch.flexmark.ext.tables.TableHead; 057import com.vladsch.flexmark.ext.tables.TableRow; 058import com.vladsch.flexmark.ext.typographic.TypographicQuotes; 059import com.vladsch.flexmark.ext.typographic.TypographicSmarts; 060import com.vladsch.flexmark.parser.ListOptions; 061import com.vladsch.flexmark.util.ast.Block; 062import com.vladsch.flexmark.util.ast.Node; 063import com.vladsch.flexmark.util.sequence.BasedSequence; 064import com.vladsch.flexmark.util.sequence.Escaping; 065 066import gov.nist.secauto.metaschema.core.datatype.markup.flexmark.HtmlQuoteTagExtension.DoubleQuoteNode; 067import gov.nist.secauto.metaschema.core.datatype.markup.flexmark.InsertAnchorExtension.InsertAnchorNode; 068import gov.nist.secauto.metaschema.core.util.CollectionUtil; 069import gov.nist.secauto.metaschema.core.util.ObjectUtils; 070 071import org.apache.commons.text.StringEscapeUtils; 072import org.jsoup.Jsoup; 073import org.jsoup.nodes.Attributes; 074import org.jsoup.nodes.Document; 075import org.jsoup.select.NodeVisitor; 076 077import java.net.URI; 078import java.net.URISyntaxException; 079import java.util.HashMap; 080import java.util.LinkedHashMap; 081import java.util.Map; 082import java.util.regex.Matcher; 083import java.util.regex.Pattern; 084 085import javax.xml.namespace.QName; 086 087import edu.umd.cs.findbugs.annotations.NonNull; 088import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 089 090/** 091 * Used to write HTML-based Markup to various types of streams. 092 * 093 * @param <T> 094 * the type of stream to write to 095 * @param <E> 096 * the type of exception that can be thrown when a writing error occurs 097 */ 098@SuppressFBWarnings( 099 value = "THROWS_METHOD_THROWS_CLAUSE_THROWABLE", 100 justification = "Class supports writers that use both Exception and RuntimeException.") 101public abstract class AbstractMarkupWriter<T, E extends Throwable> // NOPMD not god class 102 implements IMarkupWriter<T, E> { 103 private static final Pattern ENTITY_PATTERN = Pattern.compile("^&([^;]+);$"); 104 private static final Map<String, String> ENTITY_MAP; 105 106 static { 107 ENTITY_MAP = new HashMap<>(); 108 // special cases 109 ENTITY_MAP.put("&npsb;", "&npsb;"); 110 // ENTITY_MAP.put(">", ">"); 111 // normal cases 112 // ENTITY_MAP.put("&", "&"); 113 /* 114 * ENTITY_MAP.put("‘", "‘"); ENTITY_MAP.put("’", "’"); ENTITY_MAP.put("…", "…"); 115 * ENTITY_MAP.put("—", "—"); ENTITY_MAP.put("–", "–"); ENTITY_MAP.put("“", "“"); 116 * ENTITY_MAP.put("”", "”"); ENTITY_MAP.put("«", "«"); ENTITY_MAP.put("»", "»"); 117 */ 118 } 119 120 @NonNull 121 private final String namespace; 122 123 @NonNull 124 private final T stream; 125 126 @NonNull 127 private final ListOptions options; 128 129 public AbstractMarkupWriter(@NonNull String namespace, @NonNull ListOptions options, T stream) { 130 this.namespace = namespace; 131 this.options = options; 132 this.stream = ObjectUtils.requireNonNull(stream); 133 } 134 135 @NonNull 136 protected String getNamespace() { 137 return namespace; 138 } 139 140 protected ListOptions getOptions() { 141 return options; 142 } 143 144 @NonNull 145 protected T getStream() { 146 return stream; 147 } 148 149 @Override 150 @NonNull 151 public QName asQName(@NonNull String localName) { 152 return new QName(getNamespace(), localName); 153 } 154 155 protected void visitChildren( 156 @NonNull Node parentNode, 157 @NonNull ChildHandler<T, E> childHandler) throws E { 158 for (Node node : parentNode.getChildren()) { 159 assert node != null; 160 childHandler.accept(node, this); 161 } 162 } 163 164 protected void writePrecedingNewline(@NonNull Block node) throws E { 165 Node prev = node.getPrevious(); 166 if (prev != null 167 || !(node.getParent() instanceof com.vladsch.flexmark.util.ast.Document)) { 168 writeText("\n"); 169 } 170 } 171 172 protected void writeTrailingNewline(@NonNull Block node) throws E { 173 Node next = node.getNext(); 174 if (next != null && !next.isOrDescendantOfType(Block.class) // handled by preceding block 175 || next == null && !(node.getParent() instanceof com.vladsch.flexmark.util.ast.Document)) { 176 writeText("\n"); 177 } 178 } 179 180 @Override 181 public final void writeElement( 182 QName qname, 183 Node node, 184 Map<String, String> attributes, 185 ChildHandler<T, E> childHandler) throws E { 186 if (node.hasChildren()) { 187 writeElementStart(qname, attributes); 188 if (childHandler != null) { 189 visitChildren(node, childHandler); 190 } 191 writeElementEnd(qname); 192 } else { 193 writeEmptyElement(qname, attributes); 194 } 195 } 196 197 @SuppressWarnings({ 198 "unchecked", 199 "unused", 200 "PMD.UnusedPrivateMethod" 201 }) // while unused, keeping code for when inline HTML is supported 202 private void writeHtml(Node node) throws E { 203 Document doc = Jsoup.parse(node.getChars().toString()); 204 try { 205 doc.body().traverse(new MarkupNodeVisitor()); 206 } catch (NodeVisitorException ex) { 207 throw (E) ex.getCause(); // NOPMD exception is wrapper 208 } 209 } 210 211 @Override 212 public final void writeText(Text node) throws E { 213 BasedSequence text = node.getChars(); 214 Node prev = node.getPrevious(); 215 if (prev instanceof HardLineBreak) { 216 // strip leading after hard line break 217 assert text != null; 218 text = text.trimStart(); 219 } 220 assert text != null; 221 writeText(text); 222 } 223 224 @Override 225 public void writeText(@NonNull TextBase node) throws E { 226 StringBuilder buf = new StringBuilder(node.getChars().length()); 227 for (Node child : node.getChildren()) { 228 CharSequence chars; 229 if (child instanceof Text) { 230 Text text = (Text) child; 231 chars = text.getChars(); 232 } else if (child instanceof EscapedCharacter) { 233 EscapedCharacter ec = (EscapedCharacter) child; 234 chars = ec.getChars().unescape(); 235 } else { 236 throw new UnsupportedOperationException("Node type: " + child.getNodeName()); 237 } 238 buf.append(chars); 239 } 240 writeText(buf); 241 } 242 243 @Override 244 public void writeHtmlEntity(@NonNull HtmlEntity node) throws E { 245 String text = node.getChars().unescape(); 246 assert text != null; 247 writeHtmlEntity(text); 248 } 249 250 @Override 251 public void writeHtmlEntity(@NonNull TypographicSmarts node) throws E { 252 String text = ObjectUtils.requireNonNull(node.getTypographicText()); 253 assert text != null; 254 writeHtmlEntity(text); 255 } 256 257 private void writeHtmlEntity(String entityText) throws E { 258 String replacement = ENTITY_MAP.get(entityText); 259 if (replacement != null) { 260 Matcher matcher = ENTITY_PATTERN.matcher(replacement); 261 if (matcher.matches()) { 262 writeHtmlEntityInternal(ObjectUtils.notNull(matcher.group(1))); 263 } else { 264 writeText(replacement); 265 } 266 } else { 267 String value = StringEscapeUtils.unescapeHtml4(entityText); 268 assert value != null; 269 writeText(value); 270 } 271 } 272 273 protected void writeHtmlEntityInternal(@NonNull String text) throws E { 274 writeText(text); 275 } 276 277 @Override 278 public void writeParagraph( 279 @NonNull Paragraph node, 280 @NonNull ChildHandler<T, E> childHandler) throws E { 281 if (node.getParent() instanceof ParagraphItemContainer && getOptions().isInTightListItem(node)) { 282 if (node.getPrevious() != null) { 283 writeText("\n"); 284 } 285 visitChildren(node, childHandler); 286 } else { 287 writePrecedingNewline(node); 288 writeElement("p", node, childHandler); 289 writeTrailingNewline(node); 290 } 291 } 292 293 @Override 294 public void writeLink( 295 @NonNull Link node, 296 @NonNull ChildHandler<T, E> childHandler) throws E { 297 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 298 String href = Escaping.percentEncodeUrl(node.getUrl().unescape()); 299 try { 300 attributes.put("href", new URI(href).toASCIIString()); 301 } catch (URISyntaxException ex) { 302 throw new IllegalStateException(ex); 303 } 304 305 if (!node.getTitle().isBlank()) { 306 String title = ObjectUtils.requireNonNull(node.getTitle().unescape()); 307 attributes.put("title", title); 308 } 309 310 // writeElement("a", node, attributes, childHandler); 311 QName qname = asQName("a"); 312 writeElementStart(qname, attributes); 313 if (node.hasChildren()) { 314 visitChildren(node, childHandler); 315 } else { 316 writeText(""); 317 } 318 writeElementEnd(qname); 319 } 320 321 @Override 322 public void writeLink(@NonNull MailLink node) throws E { 323 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 324 325 String href = Escaping.percentEncodeUrl(node.getText().unescape()); 326 try { 327 attributes.put("href", new URI("mailto:" + href).toASCIIString()); 328 } catch (URISyntaxException ex) { 329 throw new IllegalStateException(ex); 330 } 331 332 QName qname = asQName("a"); 333 writeElementStart(qname, attributes); 334 335 BasedSequence text = node.getText(); 336 writeText(text == null ? "\n" : ObjectUtils.notNull(text.unescape())); 337 writeElementEnd(qname); 338 } 339 340 @Override 341 public void writeLink(@NonNull AutoLink node) throws E { 342 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 343 344 String href = Escaping.percentEncodeUrl(node.getUrl().unescape()); 345 try { 346 attributes.put("href", new URI(href).toASCIIString()); 347 } catch (URISyntaxException ex) { 348 throw new IllegalStateException(ex); 349 } 350 351 QName qname = asQName("a"); 352 writeElementStart(qname, attributes); 353 writeText(ObjectUtils.notNull(node.getText().unescape())); 354 writeElementEnd(qname); 355 } 356 357 @Override 358 public final void writeTypographicQuotes( 359 TypographicQuotes node, 360 ChildHandler<T, E> childHandler) throws E { 361 if (node instanceof DoubleQuoteNode) { 362 writeElement("q", node, childHandler); 363 } else { 364 String opening = node.getTypographicOpening(); 365 if (opening != null && !opening.isEmpty()) { 366 writeHtmlEntity(opening); 367 } 368 369 visitChildren(node, childHandler); 370 371 String closing = node.getTypographicClosing(); 372 if (closing != null && !closing.isEmpty()) { 373 writeHtmlEntity(closing); 374 } 375 } 376 } 377 378 @Override 379 public final void writeInlineHtml(HtmlInline node) throws E { 380 // throw new UnsupportedOperationException( 381 // String.format("Inline HTML is not supported. Found: %s", node.getChars())); 382 writeHtml(node); 383 } 384 385 @Override 386 public final void writeBlockHtml(HtmlBlock node) throws E { 387 // throw new UnsupportedOperationException( 388 // String.format("Inline HTML is not supported. Found: %s", node.getChars())); 389 390 writePrecedingNewline(node); 391 writeHtml(node); 392 writeTrailingNewline(node); 393 } 394 395 @Override 396 public final void writeTable( 397 TableBlock node, 398 ChildHandler<T, E> cellChildHandler) throws E { 399 writePrecedingNewline(node); 400 QName qname = asQName("table"); 401 writeElementStart(qname); 402 403 TableHead head = (TableHead) node.getChildOfType(TableHead.class); 404 405 QName theadQName = asQName("thead"); 406 if (head != null) { 407 writeText("\n"); 408 writeElementStart(theadQName); 409 for (Node childNode : head.getChildren()) { 410 if (childNode instanceof TableRow) { 411 writeTableRow((TableRow) childNode, cellChildHandler); 412 } 413 } 414 writeElementEnd(theadQName); 415 } 416 417 TableBody body = (TableBody) node.getChildOfType(TableBody.class); 418 419 if (body != null) { 420 QName tbodyQName = asQName("tbody"); 421 writeText("\n"); 422 writeElementStart(tbodyQName); 423 for (Node childNode : body.getChildren()) { 424 if (childNode instanceof TableRow) { 425 writeTableRow((TableRow) childNode, cellChildHandler); 426 } 427 } 428 writeElementEnd(tbodyQName); 429 } 430 431 writeText("\n"); 432 writeElementEnd(qname); 433 writeTrailingNewline(node); 434 } 435 436 private void writeTableRow( 437 @NonNull TableRow node, 438 @NonNull ChildHandler<T, E> cellChildHandler) throws E { 439 writeText("\n"); 440 QName qname = asQName("tr"); 441 writeElementStart(qname); 442 443 for (Node childNode : node.getChildren()) { 444 if (childNode instanceof TableCell) { 445 writeTableCell((TableCell) childNode, cellChildHandler); 446 } 447 } 448 449 writeElementEnd(qname); 450 if (node.getNext() == null) { 451 writeText("\n"); 452 } 453 } 454 455 private void writeTableCell( 456 @NonNull TableCell node, 457 @NonNull ChildHandler<T, E> cellChildHandler) throws E { 458 QName qname = node.isHeader() ? asQName("th") : asQName("td"); 459 460 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 461 if (node.getAlignment() != null) { 462 attributes.put("align", ObjectUtils.requireNonNull(node.getAlignment().toString())); 463 } 464 465 writeElementStart(qname, attributes); 466 visitChildren(node, cellChildHandler); 467 writeElementEnd(qname); 468 } 469 470 @Override 471 public void writeImage( 472 @NonNull Image node) throws E { 473 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 474 String href = ObjectUtils.requireNonNull(Escaping.percentEncodeUrl(node.getUrl().unescape())); 475 try { 476 attributes.put("src", new URI(href).toASCIIString()); 477 } catch (URISyntaxException ex) { 478 throw new IllegalStateException(ex); 479 } 480 481 attributes.put("alt", ObjectUtils.requireNonNull(node.getText().toString())); 482 483 if (!node.getTitle().isBlank()) { 484 attributes.put("title", ObjectUtils.requireNonNull(node.getTitle().toString())); 485 } 486 487 writeEmptyElement("img", attributes); 488 } 489 490 @Override 491 public void writeInsertAnchor(@NonNull InsertAnchorNode node) throws E { 492 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 493 attributes.put("type", ObjectUtils.requireNonNull(node.getType().toString())); 494 attributes.put("id-ref", ObjectUtils.requireNonNull(node.getIdReference().toString())); 495 496 writeElement("insert", node, attributes, null); 497 } 498 499 @Override 500 public void writeHeading( 501 @NonNull Heading node, 502 @NonNull ChildHandler<T, E> childHandler) throws E { 503 writePrecedingNewline(node); 504 int level = node.getLevel(); 505 506 QName qname = asQName(ObjectUtils.notNull(String.format("h%d", level))); 507 508 writeElementStart(qname); 509 if (node.hasChildren()) { 510 visitChildren(node, childHandler); 511 } else { 512 // ensure empty tags are created 513 writeText(""); 514 } 515 writeElementEnd(qname); 516 writeTrailingNewline(node); 517 } 518 519 /** 520 * Normalize whitespace according to 521 * https://spec.commonmark.org/0.30/#code-spans. Based on code from Flexmark. 522 * 523 * @param text 524 * text to process 525 * @return the normalized text 526 */ 527 @NonNull 528 protected static String collapseWhitespace(@NonNull CharSequence text) { 529 StringBuilder sb = new StringBuilder(text.length()); 530 int length = text.length(); 531 boolean needsSpace = false; 532 for (int i = 0; i < length; i++) { 533 char ch = text.charAt(i); 534 // convert line endings to spaces 535 if (ch == '\n' || ch == '\r') { 536 if (sb.length() > 0) { 537 // ignore leading 538 needsSpace = true; 539 } 540 } else { 541 if (needsSpace) { 542 sb.append(' '); 543 needsSpace = false; 544 } 545 sb.append(ch); 546 } 547 } 548 549 String result = sb.toString(); 550 if (result.matches("^[ ]{1,}[^ ].* $")) { 551 // if there is a space at the start and end, remove them 552 result = result.substring(1, result.length() - 1); 553 } 554 return ObjectUtils.notNull(result); 555 } 556 557 @Override 558 public void writeCode(Code node, ChildHandler<T, E> childHandler) throws E { 559 QName qname = asQName("code"); 560 writeElementStart(qname); 561 visitChildren(node, (child, writer) -> { 562 if (child instanceof Text || child instanceof TextBase) { 563 String text = collapseWhitespace(ObjectUtils.notNull(child.getChars())); 564 writeText(text); 565 } else { 566 childHandler.accept(child, writer); 567 } 568 }); 569 writeElementEnd(qname); 570 } 571 572 @Override 573 public final void writeCodeBlock( 574 IndentedCodeBlock node, 575 ChildHandler<T, E> childHandler) throws E { 576 writePrecedingNewline(node); 577 QName preQName = asQName("pre"); 578 579 writeElementStart(preQName); 580 581 QName codeQName = asQName("code"); 582 583 writeElementStart(codeQName); 584 585 if (node.hasChildren()) { 586 visitChildren(node, childHandler); 587 } else { 588 // ensure empty tags are created 589 writeText(""); 590 } 591 592 writeElementEnd(codeQName); 593 594 writeElementEnd(preQName); 595 writeTrailingNewline(node); 596 } 597 598 @Override 599 public final void writeCodeBlock( 600 FencedCodeBlock node, 601 ChildHandler<T, E> childHandler) throws E { 602 writePrecedingNewline(node); 603 QName preQName = asQName("pre"); 604 605 writeElementStart(preQName); 606 607 QName codeQName = asQName("code"); 608 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 609 if (node.getInfo().isNotNull()) { 610 attributes.put("class", "language-" + node.getInfo().unescape()); 611 } 612 613 writeElementStart(codeQName, attributes); 614 615 if (node.hasChildren()) { 616 visitChildren(node, childHandler); 617 } else { 618 // ensure empty tags are created 619 writeText(""); 620 } 621 622 writeElementEnd(codeQName); 623 624 writeElementEnd(preQName); 625 writeTrailingNewline(node); 626 } 627 628 @Override 629 public void writeCodeBlock(CodeBlock node, ChildHandler<T, E> childHandler) throws E { 630 String text; 631 if (node.getParent() instanceof IndentedCodeBlock) { 632 text = node.getContentChars().trimTailBlankLines().toString(); 633 } else { 634 text = node.getContentChars().toString(); 635 } 636 writeText(ObjectUtils.notNull(text)); 637 } 638 639 @Override 640 public void writeBlockQuote(BlockQuote node, ChildHandler<T, E> childHandler) throws E { 641 writePrecedingNewline(node); 642 QName qname = asQName("blockquote"); 643 writeElementStart(qname); 644 // writeText("\n"); 645 646 if (node.hasChildren()) { 647 visitChildren(node, childHandler); 648 } else { 649 // ensure empty tags are created 650 writeText("\n"); 651 } 652 653 // writeText("\n"); 654 writeElementEnd(qname); 655 writeTrailingNewline(node); 656 } 657 658 @Override 659 public void writeList(QName qname, ListBlock node, ChildHandler<T, E> listItemHandler) throws E { 660 Map<String, String> attributes = new LinkedHashMap<>(); // NOPMD local use; thread-safe 661 if (node instanceof OrderedList) { 662 OrderedList ol = (OrderedList) node; 663 int start = ol.getStartNumber(); 664 if (start != 1) { 665 attributes.put("start", String.valueOf(start)); 666 } 667 } 668 669 writePrecedingNewline(node); 670 writeElementStart(qname, attributes); 671 672 visitChildren(node, (child, writer) -> { 673 ListItem item = (ListItem) child; 674 writeListItem(item, listItemHandler); 675 }); 676 677 writeElementEnd(qname); 678 writeTrailingNewline(node); 679 } 680 681 @Override 682 public void writeListItem(ListItem node, ChildHandler<T, E> listItemHandler) throws E { 683 QName qname = asQName("li"); 684 writePrecedingNewline(node); 685 writeElementStart(qname); 686 687 if (node.hasChildren()) { 688 visitChildren(node, listItemHandler); 689 } else { 690 // ensure empty tags are created 691 writeText(""); 692 } 693 writeElementEnd(qname); 694 writeTrailingNewline(node); 695 } 696 697 @Override 698 public void writeBreak(HardLineBreak node) throws E { 699 writeElement("br", node, null); 700 writeText("\n"); 701 } 702 703 @Override 704 public void writeBreak(ThematicBreak node) throws E { 705 writePrecedingNewline(node); 706 writeElement("hr", node, null); 707 writeTrailingNewline(node); 708 } 709 710 @Override 711 public void writeComment(HtmlCommentBlock node) throws E { 712 writePrecedingNewline(node); 713 714 BasedSequence text = node.getChars(); 715 text = text.subSequence(4, text.length() - 4); 716 writeComment(ObjectUtils.notNull(text.unescape())); 717 writeTrailingNewline(node); 718 719 } 720 721 protected abstract void writeComment(@NonNull CharSequence text) throws E; 722 723 protected static class NodeVisitorException 724 extends IllegalStateException { 725 /** 726 * the serial version uid. 727 */ 728 private static final long serialVersionUID = 1L; 729 730 public NodeVisitorException(Throwable cause) { 731 super(cause); 732 } 733 } 734 735 private final class MarkupNodeVisitor implements NodeVisitor { 736 @Override 737 public void head(org.jsoup.nodes.Node node, int depth) { // NOPMD acceptable 738 if (depth > 0) { 739 try { 740 if (node instanceof org.jsoup.nodes.Element) { 741 org.jsoup.nodes.Element element = (org.jsoup.nodes.Element) node; 742 743 Attributes attributes = element.attributes(); 744 745 Map<String, String> attrMap; 746 if (attributes.isEmpty()) { 747 attrMap = CollectionUtil.emptyMap(); 748 } else { 749 attrMap = new LinkedHashMap<>(); 750 for (org.jsoup.nodes.Attribute attr : attributes) { 751 attrMap.put(attr.getKey(), attr.getValue()); 752 } 753 } 754 755 QName qname = asQName(ObjectUtils.notNull(element.tagName())); 756 if (element.childNodes().isEmpty()) { 757 writeEmptyElement(qname, attrMap); 758 } else { 759 writeElementStart(qname, attrMap); 760 } 761 } else if (node instanceof org.jsoup.nodes.TextNode) { 762 org.jsoup.nodes.TextNode text = (org.jsoup.nodes.TextNode) node; 763 writeText(ObjectUtils.requireNonNull(text.text())); 764 } 765 } catch (Throwable ex) { // NOPMD need to catch Throwable 766 throw new NodeVisitorException(ex); 767 } 768 } 769 } 770 771 @Override 772 public void tail(org.jsoup.nodes.Node node, int depth) { 773 if (depth > 0 && node instanceof org.jsoup.nodes.Element) { 774 org.jsoup.nodes.Element element = (org.jsoup.nodes.Element) node; 775 if (!element.childNodes().isEmpty()) { 776 QName qname = asQName(ObjectUtils.notNull(element.tagName())); 777 try { 778 writeElementEnd(qname); 779 } catch (Throwable ex) { // NOPMD need to catch Throwable 780 throw new NodeVisitorException(ex); 781 } 782 } 783 } 784 } 785 } 786}