001package com.ganteater.ae.desktop.ui; 002 003/* 004Copyright (c) 2019 Ashur Rafiev 005https://github.com/ashurrafiev/JParsedown 006MIT Licence: https://github.com/ashurrafiev/JParsedown/blob/master/LICENSE 007 008This work is derived from Parsedown version 1.8.0-beta-5: 009Copyright (c) 2013-2018 Emanuil Rusev 010http://parsedown.org 011*/ 012 013import java.io.UnsupportedEncodingException; 014import java.net.URLEncoder; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.HashMap; 018import java.util.HashSet; 019import java.util.LinkedList; 020import java.util.Map.Entry; 021import java.util.regex.Matcher; 022import java.util.regex.Pattern; 023 024import org.apache.commons.lang.StringUtils; 025 026public class JParsedown { 027 028 public static final String version = "1.0.4"; 029 private static final String TABLE_STYLE = ""; 030 031 protected class ReferenceData { 032 public String url; 033 public String title; 034 035 public ReferenceData(String url, String title) { 036 this.url = url; 037 this.title = title; 038 } 039 } 040 041 protected class Line { 042 public String body; 043 public String text; 044 public int indent; 045 046 public Line(String line) { 047 body = line; 048 text = line.replaceFirst("^\\s+", ""); 049 indent = line.length() - text.length(); 050 } 051 } 052 053 protected abstract class Handler { 054 public abstract Element function(Element element); 055 } 056 057 protected abstract class ElementsHandler extends Handler { 058 public abstract LinkedList<Element> elementFunction(Element element); 059 060 @Override 061 public final Element function(Element element) { 062 element.elements = elementFunction(element); 063 return element; 064 } 065 } 066 067 protected class LineElementsHandler extends ElementsHandler { 068 public String text; 069 070 public LineElementsHandler(String text) { 071 this.text = text; 072 } 073 074 @Override 075 public LinkedList<Element> elementFunction(Element element) { 076 return lineElements(text, element.nonNestables); 077 } 078 } 079 080 protected class LinesElementsHandler extends ElementsHandler { 081 public LinkedList<String> lines = new LinkedList<>(); 082 083 public LinesElementsHandler(String text) { 084 if (text != null) 085 lines.add(text); 086 } 087 088 @Override 089 public LinkedList<Element> elementFunction(Element element) { 090 return linesElements(lines); 091 } 092 } 093 094 protected class ListItemElementHandler extends ElementsHandler { 095 public LinkedList<String> lines = new LinkedList<>(); 096 097 public ListItemElementHandler(String body) { 098 if (body != null) 099 lines.add(body); 100 } 101 102 @Override 103 public LinkedList<Element> elementFunction(Element element) { 104 LinkedList<Element> elements = linesElements(lines); 105 if (!lines.contains("") && !elements.isEmpty() && elements.getFirst().name != null 106 && elements.getFirst().name.equals("p")) { 107 elements.getFirst().name = null; 108 } 109 return elements; 110 } 111 } 112 113 protected static class Element { 114 public String name = null; 115 116 public HashMap<String, String> attributes = new HashMap<>(); 117 public LinkedList<Element> elements = new LinkedList<>(); 118 public String text = null; 119 public String rawHtml = null; 120 121 public HashSet<Class<?>> nonNestables = new HashSet<>(); 122 public Handler handler = null; 123 public Boolean autoBreak = null; 124 125 public Element() { 126 } 127 128 public Element(String name) { 129 this.name = name; 130 } 131 132 public Element(String name, String text) { 133 this.name = name; 134 this.text = text; 135 } 136 137 public Element(String name, Element element) { 138 this.name = name; 139 this.elements.add(element); 140 } 141 142 public Element(String name, Handler handler) { 143 this.name = name; 144 this.handler = handler; 145 } 146 147 public Element addAttribute(String name, String value) { 148 attributes.put(name, value); 149 return this; 150 } 151 152 public Element handle() { 153 Element element = this; 154 if (handler != null) { 155 element = handler.function(element); 156 handler = null; 157 } 158 return element; 159 } 160 } 161 162 protected abstract class Component { 163 public Element element = null; 164 public String markup = null; 165 public boolean hidden = false; 166 167 public Element extractElement() { 168 if (element == null) { 169 if (markup != null) { 170 element = new Element(); 171 element.rawHtml = markup; 172 } else if (hidden) { 173 element = new Element(); 174 } 175 } 176 return element; 177 } 178 } 179 180 protected abstract class Block extends Component { 181 public boolean identified = false; 182 public int interrupted = 0; 183 184 public Block setElement(Element e) { 185 this.element = e; 186 return this; 187 } 188 189 public boolean isContinuable() { 190 return false; 191 } 192 193 public boolean isCompletable() { 194 return false; 195 } 196 197 public abstract Block startBlock(Line line, Block block); 198 199 public Block continueBlock(Line line) { 200 return null; 201 } 202 203 public Block completeBlock() { 204 return null; 205 } 206 } 207 208 protected class BlockParagraph extends Block { 209 @Override 210 public Block startBlock(Line line, Block block) { 211 return new BlockParagraph().setElement(new Element("p", new LineElementsHandler(line.text))); 212 } 213 214 @Override 215 public boolean isContinuable() { 216 return false; 217 } 218 219 @Override 220 public Block continueBlock(Line line) { 221 if (interrupted > 0) 222 return null; 223 ((LineElementsHandler) element.handler).text += "\n" + line.text; 224 return this; 225 } 226 } 227 228 protected class BlockCode extends Block { 229 @Override 230 public Block startBlock(Line line, Block block) { 231 if (block != null && block instanceof BlockParagraph && block.interrupted == 0) 232 return null; 233 if (line.indent >= 4) { 234 return new BlockCode().setElement(new Element("pre", new Element("code", line.body.substring(4)))); 235 } else 236 return null; 237 } 238 239 @Override 240 public boolean isContinuable() { 241 return true; 242 } 243 244 @Override 245 public Block continueBlock(Line line) { 246 if (line.indent >= 4) { 247 Element e = element.elements.getFirst(); 248 while (interrupted > 0) { 249 e.text += "\n"; 250 interrupted--; 251 } 252 e.text += "\n"; 253 e.text += line.body.substring(4); 254 return this; 255 } else 256 return null; 257 } 258 259 @Override 260 public boolean isCompletable() { 261 return true; 262 } 263 264 @Override 265 public Block completeBlock() { 266 return this; 267 } 268 } 269 270 protected class BlockComment extends Block { 271 public boolean closed = false; 272 273 @Override 274 public Block startBlock(Line line, Block block) { 275 if (markupEscaped || safeMode) 276 return null; 277 if (line.text.indexOf("<!--") == 0) { 278 BlockComment b = new BlockComment(); 279 b.element = new Element(); 280 b.element.rawHtml = line.body; 281 b.element.autoBreak = true; 282 if (line.text.contains("-->")) 283 b.closed = true; 284 return b; 285 } else 286 return null; 287 } 288 289 @Override 290 public boolean isContinuable() { 291 return true; 292 } 293 294 @Override 295 public Block continueBlock(Line line) { 296 if (closed) 297 return null; 298 element.rawHtml += "\n" + line.body; 299 if (line.text.contains("-->")) 300 closed = true; 301 return this; 302 } 303 } 304 305 protected class BlockFencedCode extends Block { 306 public char marker; 307 public int openerLength; 308 public boolean complete = false; 309 310 public BlockFencedCode() { 311 } 312 313 public BlockFencedCode(char marker, int openerLength) { 314 this.marker = marker; 315 this.openerLength = openerLength; 316 } 317 318 @Override 319 public Block startBlock(Line line, Block block) { 320 char marker = line.text.charAt(0); 321 int openerLength = startSpan(line.text, marker); 322 if (openerLength < 3) 323 return null; 324 String infostring = line.text.substring(openerLength).trim(); 325 if (infostring.contains("`")) 326 return null; 327 Element e = new Element("code", ""); 328 if (!infostring.isEmpty()) 329 e.attributes.put("class", "language-" + infostring); 330 return new BlockFencedCode(marker, openerLength).setElement(new Element("pre", e)); 331 } 332 333 @Override 334 public boolean isContinuable() { 335 return true; 336 } 337 338 @Override 339 public Block continueBlock(Line line) { 340 if (complete) 341 return null; 342 Element e = element.elements.getFirst(); 343 while (interrupted > 0) { 344 e.text += "\n"; 345 interrupted--; 346 } 347 int len = startSpan(line.text, marker); 348 if (len >= openerLength && line.text.substring(len).trim().isEmpty()) { 349 if (!e.text.isEmpty()) 350 e.text = e.text.substring(1); 351 complete = true; 352 return this; 353 } 354 e.text += "\n" + line.body; 355 return this; 356 } 357 358 @Override 359 public boolean isCompletable() { 360 return true; 361 } 362 363 @Override 364 public Block completeBlock() { 365 return this; 366 } 367 } 368 369 protected class BlockHeader extends Block { 370 @Override 371 public Block startBlock(Line line, Block block) { 372 int level = startSpan(line.text, '#'); 373 if (level > 6) 374 return null; 375 376 String text = line.text.substring(level); 377 if (strictMode && !text.isEmpty() && text.charAt(0) != ' ') 378 return null; 379 text = text.trim(); 380 381 Block b = new BlockHeader().setElement(new Element("h" + level, new LineElementsHandler(text))); 382 b.element.attributes.put("id", generateHeaderId(text, level)); 383 return b; 384 } 385 } 386 387 protected class BlockList extends Block { 388 public int indent; 389 public String pattern; 390 public boolean loose = false; 391 392 public boolean ordered; 393 public String marker; 394 public String markerType; 395 public String markerTypeRegex; 396 397 public Element li; 398 399 @Override 400 public Block startBlock(Line line, Block block) { 401 boolean ordered; 402 String pattern; 403 if (Character.isDigit(line.text.charAt(0))) { 404 ordered = true; // ol 405 pattern = "[0-9]{1,9}+[.\\)]"; 406 } else { 407 ordered = false; // ul 408 pattern = "[*+-]"; 409 } 410 Matcher m = Pattern.compile("^(" + pattern + "([ ]++|$))(.*+)").matcher(line.text); 411 if (m.find()) { 412 String marker = m.group(1); 413 String body = m.group(3); 414 415 int contentIndent = m.group(2).length(); 416 if (contentIndent >= 5) { 417 contentIndent--; 418 marker = marker.substring(0, -contentIndent); 419 while (contentIndent > 0) { 420 body = " " + body; 421 contentIndent--; 422 } 423 } else if (contentIndent == 0) { 424 marker += " "; 425 } 426 String markerWithoutWhitespace = marker.substring(0, marker.indexOf(' ')); 427 428 BlockList b = new BlockList(); 429 b.indent = line.indent; 430 b.pattern = pattern; 431 b.ordered = ordered; 432 b.marker = marker; 433 b.markerType = !ordered ? markerWithoutWhitespace 434 : markerWithoutWhitespace.substring(markerWithoutWhitespace.length() - 1, 435 markerWithoutWhitespace.length()); 436 b.markerTypeRegex = Pattern.quote(b.markerType); 437 438 b.setElement(new Element(ordered ? "ol" : "ul")); 439 440 if (ordered) { 441 String listStart = marker.substring(0, marker.indexOf(b.markerType)).replaceAll("$0+", ""); 442 if (listStart.isEmpty()) 443 listStart = "0"; 444 if (!listStart.equals("1")) { 445 if (block != null && block instanceof BlockParagraph && block.interrupted == 0) 446 return null; 447 b.element.attributes.put("start", listStart); 448 } 449 } 450 451 b.li = new Element("li", new ListItemElementHandler(body)); 452 b.element.elements.add(b.li); 453 454 return b; 455 } else 456 return null; 457 } 458 459 @Override 460 public boolean isContinuable() { 461 return true; 462 } 463 464 @Override 465 public Block continueBlock(Line line) { 466 if (interrupted > 0 && ((ListItemElementHandler) li.handler).lines.isEmpty()) 467 return null; 468 469 int requiredIndent = indent + marker.length(); 470 Matcher m; 471 if (line.indent < requiredIndent && ((ordered 472 && (m = Pattern.compile("^[0-9]++" + markerTypeRegex + "(?:[ ]++(.*)|$)").matcher(line.text)) 473 .find()) 474 || (!ordered && (m = Pattern.compile("^" + markerTypeRegex + "(?:[ ]++(.*)|$)").matcher(line.text)) 475 .find()))) { 476 if (interrupted > 0) { 477 ((ListItemElementHandler) li.handler).lines.add(""); 478 loose = true; 479 interrupted = 0; 480 } 481 String text = m.group(1) != null ? m.group(1) : ""; 482 indent = line.indent; 483 li = new Element("li", new ListItemElementHandler(text)); 484 element.elements.add(li); 485 return this; 486 } else if (line.indent < requiredIndent && new BlockList().startBlock(line, null) != null) { 487 return null; 488 } 489 490 if (line.text.charAt(0) == '[' && new BlockReference().startBlock(line, null) != null) { 491 return this; 492 } 493 494 if (line.indent >= requiredIndent) { 495 if (interrupted > 0) { 496 ((ListItemElementHandler) li.handler).lines.add(""); 497 loose = true; 498 interrupted = 0; 499 } 500 String text = line.body.substring(requiredIndent); 501 ((ListItemElementHandler) li.handler).lines.add(text); 502 return this; 503 } 504 505 if (interrupted == 0) { 506 String text = line.body.replaceAll("^[ ]{0," + requiredIndent + "}+", ""); 507 ((ListItemElementHandler) li.handler).lines.add(text); 508 return this; 509 } 510 511 return null; 512 } 513 514 @Override 515 public boolean isCompletable() { 516 return true; 517 } 518 519 @Override 520 public Block completeBlock() { 521 if (loose) { 522 for (Element li : element.elements) { 523 if (!((ListItemElementHandler) li.handler).lines.getLast().isEmpty()) 524 ((ListItemElementHandler) li.handler).lines.add(""); 525 } 526 } 527 return this; 528 } 529 } 530 531 protected class BlockQuote extends Block { 532 @Override 533 public Block startBlock(Line line, Block block) { 534 Matcher m; 535 if ((m = Pattern.compile("^>[ ]?+(.*+)").matcher(line.text)).find()) { 536 return new BlockQuote().setElement(new Element("blockquote", new LinesElementsHandler(m.group(1)))); 537 } else 538 return null; 539 } 540 541 @Override 542 public boolean isContinuable() { 543 return true; 544 } 545 546 @Override 547 public Block continueBlock(Line line) { 548 if (interrupted > 0) 549 return null; 550 Matcher m; 551 if (line.text.charAt(0) == '>' && (m = Pattern.compile("^>[ ]?+(.*+)").matcher(line.text)).find()) { 552 ((LinesElementsHandler) element.handler).lines.add(m.group(1)); 553 return this; 554 } 555 if (interrupted == 0) { 556 ((LinesElementsHandler) element.handler).lines.add(line.text); 557 return this; 558 } 559 return null; 560 } 561 } 562 563 protected class BlockRule extends Block { 564 @Override 565 public Block startBlock(Line line, Block block) { 566 char marker = line.text.charAt(0); 567 int count = startSpan(line.text, marker); 568 if (count >= 3 && line.text.trim().length() == count) { 569 return new BlockRule().setElement(new Element("hr")); 570 } else 571 return null; 572 } 573 } 574 575 protected class BlockSetextHeader extends Block { 576 @Override 577 public Block startBlock(Line line, Block block) { 578 if (block == null || !(block instanceof BlockParagraph) || block.interrupted > 0) 579 return null; 580 581 char marker = line.text.charAt(0); 582 int count = startSpan(line.text, marker); 583 if (line.indent < 4 && line.text.trim().length() == count) { 584 block.element.name = marker == '=' ? "h1" : "h2"; 585 String text = ((LineElementsHandler) block.element.handler).text; 586 block.element.attributes.put("id", generateHeaderId(text, marker == '=' ? 1 : 2)); 587 return block; 588 } else 589 return null; 590 } 591 } 592 593 protected static String regexHtmlAttribute = "[a-zA-Z_:][\\w:.-]*+(?:\\s*+=\\s*+(?:[^\"\\'=<>`\\s]+|\"[^\"]*+\"|\\'[^\\']*+\\'))?+"; 594 protected static HashSet<String> textLevelElements = new HashSet<>(Arrays.asList(new String[] { "a", "br", "bdo", 595 "abbr", "blink", "nextid", "acronym", "basefont", "b", "em", "big", "cite", "small", "spacer", "listing", 596 "i", "rp", "del", "code", "strike", "marquee", "q", "rt", "ins", "font", "strong", "s", "tt", "kbd", "mark", 597 "u", "xm", "sub", "nobr", "sup", "ruby", "var", "span", "wbr", "time", })); 598 599 protected class BlockMarkup extends Block { 600 public String name; 601 602 @Override 603 public Block startBlock(Line line, Block block) { 604 if (markupEscaped || safeMode) 605 return null; 606 Matcher m; 607 if ((m = Pattern.compile("^<[\\/]?+(\\w*)(?:[ ]*+" + regexHtmlAttribute + ")*+[ ]*+(\\/)?>") 608 .matcher(line.text)).find()) { 609 String element = m.group(1).toLowerCase(); 610 if (textLevelElements.contains(element)) 611 return null; 612 BlockMarkup b = new BlockMarkup(); 613 b.name = m.group(1); 614 b.element = new Element(); 615 b.element.rawHtml = line.text; 616 b.element.autoBreak = true; 617 return b; 618 } else 619 return null; 620 } 621 622 @Override 623 public boolean isContinuable() { 624 return true; 625 } 626 627 @Override 628 public Block continueBlock(Line line) { 629 if (interrupted > 0) 630 return null; 631 element.rawHtml += "\n" + line.body; 632 return this; 633 } 634 } 635 636 protected class BlockReference extends Block { 637 @Override 638 public Block startBlock(Line line, Block block) { 639 Matcher m; 640 if (line.text.indexOf(']') >= 0 641 && (m = Pattern.compile("^\\[(.+?)\\]:[ ]*+<?(\\S+?)>?(?:[ ]+[\"\\'(](.+)[\"\\')])?[ ]*+$") 642 .matcher(line.text)).find()) { 643 String id = m.group(1).toLowerCase(); 644 ReferenceData data = new ReferenceData(convertUrl(m.group(2)), m.group(3)); 645 referenceDefinitions.put(id, data); 646 return new BlockReference().setElement(new Element()); 647 } else 648 return null; 649 } 650 } 651 652 protected class BlockTable extends Block { 653 public ArrayList<String> alignments; 654 655 @Override 656 public Block startBlock(Line line, Block block) { 657 if (block == null || !(block instanceof BlockParagraph) || block.interrupted > 0) 658 return null; 659 if (((LineElementsHandler) block.element.handler).text.indexOf('|') < 0 && line.text.indexOf('|') < 0 660 && line.text.indexOf(':') < 0 661 || ((LineElementsHandler) block.element.handler).text.indexOf('\n') >= 0) 662 return null; 663 if (!line.text.replaceAll("[ -:\\|]", "").isEmpty()) 664 return null; 665 666 ArrayList<String> alignments = new ArrayList<>(); 667 668 String divider = line.text.trim().replaceAll("(^\\|+)|(\\|+$)", ""); 669 String[] dividerCells = divider.split("\\|"); 670 for (String dividerCell : dividerCells) { 671 dividerCell = dividerCell.trim(); 672 if (dividerCell.isEmpty()) 673 return null; 674 String alignment = null; 675 if (dividerCell.charAt(0) == ':') 676 alignment = "left"; 677 if (dividerCell.charAt(dividerCell.length() - 1) == ':') 678 alignment = alignment == null ? "right" : "center"; 679 alignments.add(alignment); 680 } 681 682 LinkedList<Element> headerElements = new LinkedList<>(); 683 684 String header = ((LineElementsHandler) block.element.handler).text; 685 header = header.trim().replaceAll("(^\\|+)|(\\|+$)", ""); 686 String[] headerCells = header.split("\\|"); 687 if (headerCells.length != alignments.size()) 688 return null; 689 690 int index = 0; 691 for (String headerCell : headerCells) { 692 headerCell = headerCell.trim(); 693 Element headerElement = new Element("th", new LineElementsHandler(headerCell)); 694 headerElement.addAttribute("style", TABLE_STYLE); 695 String alignment = alignments.get(index); 696 if (alignment != null) 697 headerElement.attributes.put("style", "text-align:" + alignment); 698 699 headerElements.add(headerElement); 700 index++; 701 } 702 703 BlockTable b = new BlockTable(); 704 b.alignments = alignments; 705 b.identified = true; 706 Element table = new Element("table"); 707 table.addAttribute("border", "1"); 708 b.setElement(table); 709 Element thead = new Element("thead"); 710 thead.addAttribute("style", TABLE_STYLE); 711 b.element.elements.add(thead); 712 Element tbody = new Element("tbody"); 713 tbody.addAttribute("style", TABLE_STYLE); 714 b.element.elements.add(tbody); 715 Element headerRowElement = new Element("tr"); 716 headerRowElement.addAttribute("style", TABLE_STYLE); 717 headerRowElement.elements = headerElements; 718 b.element.elements.getFirst().elements.add(headerRowElement); 719 720 return b; 721 } 722 723 @Override 724 public boolean isContinuable() { 725 return true; 726 } 727 728 @Override 729 public Block continueBlock(Line line) { 730 if (interrupted > 0) 731 return null; 732 if (alignments.size() == 1 || line.text.charAt(0) == '|' || line.text.indexOf('|') > 0) { 733 LinkedList<Element> elements = new LinkedList<>(); 734 String row = line.text.trim().replaceAll("(^\\|+)|(\\|+$)", ""); 735 Matcher m = Pattern.compile("(?:(\\\\[|])|[^|`]|`[^`]++`|`)++").matcher(row); 736 int index = 0; 737 while (index < alignments.size() && m.find()) { 738 String cell = m.group(0).trim(); 739 Element element = new Element("td", new LineElementsHandler(cell)); 740 element.addAttribute("style", TABLE_STYLE); 741 String alignment = alignments.get(index); 742 if (alignment != null) 743 element.attributes.put("style", "text-align:" + alignment); 744 745 elements.add(element); 746 index++; 747 } 748 749 Element rowElement = new Element("tr"); 750 rowElement.addAttribute("style", TABLE_STYLE); 751 rowElement.elements = elements; 752 element.elements.getLast().elements.add(rowElement); 753 754 return this; 755 } else 756 return null; 757 } 758 } 759 760 protected abstract class Inline extends Component { 761 public int extent; 762 public int position = -1; 763 764 public Inline() { 765 } 766 767 public Inline setExtent(String s) { 768 this.extent = s.length(); 769 return this; 770 } 771 772 public Inline setExtent(int len) { 773 this.extent = len; 774 return this; 775 } 776 777 public Inline setElement(Element element) { 778 this.element = element; 779 return this; 780 } 781 782 public abstract Inline inline(String text, String context); 783 } 784 785 protected class InlineText extends Inline { 786 @Override 787 public Inline inline(String text, String context) { 788 Inline inline = new InlineText().setExtent(text).setElement(new Element()); 789 inline.element.elements = replaceAllElements(breaksEnabled ? "[ ]*+\\n" : "(?:[ ]*+\\\\|[ ]{2,}+)\\n", 790 new Element[] { new Element("br"), new Element(null, "\n") }, text); 791 return inline; 792 } 793 } 794 795 protected class InlineCode extends Inline { 796 @Override 797 public Inline inline(String text, String context) { 798 char marker = text.charAt(0); 799 Pattern regex = Pattern.compile( 800 "^([" + marker + "]++)[ ]*+(.+?)[ ]*+(?<![" + marker + "])\\1(?!" + marker + ")", Pattern.DOTALL); 801 Matcher m = regex.matcher(text); 802 if (m.find()) { 803 text = m.group(2).replaceAll("[ ]*+\\n", " "); 804 return new InlineCode().setExtent(m.group(0)).setElement(new Element("code", text)); 805 } else 806 return null; 807 } 808 } 809 810 protected class InlineEmailTag extends Inline { 811 @Override 812 public Inline inline(String text, String context) { 813 if (text.indexOf('>') < 0) 814 return null; 815 String hostnameLabel = "[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"; 816 String commonMarkEmail = "[a-zA-Z0-9.!#$%&\\'*+\\/=?^_`{|}~-]++@" + hostnameLabel + "(?:\\." + hostnameLabel 817 + ")*"; 818 819 Matcher m = Pattern.compile("^<((mailto:)?" + commonMarkEmail + ")>", Pattern.CASE_INSENSITIVE) 820 .matcher(text); 821 if (m.find()) { 822 String url = m.group(1); 823 if (m.group(2) == null) 824 url = "mailto:" + url; 825 return new InlineEmailTag().setExtent(m.group(0)) 826 .setElement(new Element("a", m.group(1)).addAttribute("href", url)); 827 } else 828 return null; 829 } 830 } 831 832 protected static Pattern[] strongRegex = { 833 Pattern.compile("^[*]{2}((?:\\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])", Pattern.DOTALL), Pattern.compile( 834 "^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)", Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS), }; 835 836 protected static Pattern[] emRegex = { 837 Pattern.compile("^[*]((?:\\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])", Pattern.DOTALL), Pattern.compile( 838 "^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\\b", Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS), }; 839 840 protected class InlineEmphasis extends Inline { 841 @Override 842 public Inline inline(String text, String context) { 843 if (text.length() < 2) 844 return null; 845 char marker = text.charAt(0); 846 int markerIndex = marker == '*' ? 0 : 1; 847 848 String emphasis; 849 Matcher m = null; 850 if (text.charAt(1) == marker && (m = strongRegex[markerIndex].matcher(text)).find()) 851 emphasis = "strong"; 852 else if ((m = emRegex[markerIndex].matcher(text)).find()) 853 emphasis = "em"; 854 else 855 return null; 856 857 return new InlineEmphasis().setExtent(m.group(0)) 858 .setElement(new Element(emphasis, new LineElementsHandler(m.group(1)))); 859 } 860 } 861 862 protected static String specialCharacters = "\\`*_{}[]()>#+-.!|~"; 863 864 protected class InlineEscapeSequence extends Inline { 865 @Override 866 public Inline inline(String text, String context) { 867 if (text.length() > 1 && specialCharacters.indexOf(text.charAt(1)) >= 0) { 868 Element element = new Element(); 869 element.rawHtml = Character.toString(text.charAt(1)); 870 return new InlineEscapeSequence().setExtent(2).setElement(element); 871 } else 872 return null; 873 } 874 } 875 876 protected class InlineImage extends Inline { 877 @Override 878 public Inline inline(String text, String context) { 879 if (text.length() < 2 || text.charAt(1) != '[') 880 return null; 881 text = text.substring(1); 882 883 Inline link = new InlineLink().inline(text, context); 884 if (link == null) 885 return null; 886 887 Inline inline = new InlineImage().setExtent(link.extent + 1).setElement(new Element("img")); 888 inline.element.autoBreak = true; 889 inline.element.attributes.put("src", link.element.attributes.get("href")); 890 inline.element.attributes.put("alt", ((LineElementsHandler) link.element.handler).text); 891 892 for (Entry<String, String> attr : link.element.attributes.entrySet()) { 893 if (!attr.getKey().equals("href")) 894 inline.element.attributes.put(attr.getKey(), attr.getValue()); 895 } 896 897 return inline; 898 } 899 } 900 901 protected class InlineLink extends Inline { 902 @Override 903 public Inline inline(String text, String context) { 904 Element element = new Element("a", new LineElementsHandler(null)); 905 element.nonNestables.add(InlineUrl.class); 906 element.nonNestables.add(InlineLink.class); 907 908 int extent = 0; 909 String remainder = text; 910 911 Matcher m; 912 // Parsedown original pattern: "\\[((?:[^][]++|(?R))*+)\\]" (does not compile in 913 // Java) 914 if ((m = Pattern.compile("\\[((?:\\\\.|[^\\[\\]]|!\\[[^\\[\\]]*\\])*)\\]").matcher(remainder)).find()) { 915 ((LineElementsHandler) element.handler).text = m.group(1); 916 extent += m.group(0).length(); 917 remainder = remainder.substring(extent); 918 } else 919 return null; 920 921 if ((m = Pattern 922 .compile("^[(]\\s*+((?:[^()]++|[(][^ )]+[)])++)(?:[ ]+(\"[^\"]*+\"|\\'[^\\']*+\'))?\\s*+[)]") 923 .matcher(remainder)).find()) { 924 element.attributes.put("href", convertUrl(m.group(1))); 925 if (m.group(2) != null) 926 element.attributes.put("title", m.group(2).substring(1, m.group(2).length() - 1)); 927 extent += m.group(0).length(); 928 } else { 929 String definition; 930 if ((m = Pattern.compile("^\\s*\\[(.*?)\\]").matcher(remainder)).find()) { 931 definition = !m.group(1).isEmpty() ? m.group(1) : ((LineElementsHandler) element.handler).text; 932 definition = definition.toLowerCase(); 933 extent += m.group(0).length(); 934 } else { 935 definition = ((LineElementsHandler) element.handler).text.toLowerCase(); 936 } 937 938 ReferenceData reference = referenceDefinitions.get(definition); 939 if (reference == null) 940 return null; 941 element.attributes.put("href", reference.url); 942 element.attributes.put("title", reference.title); 943 } 944 945 return new InlineLink().setExtent(extent).setElement(element); 946 } 947 } 948 949 protected class InlineMarkup extends Inline { 950 @Override 951 public Inline inline(String text, String context) { 952 if (markupEscaped || safeMode || text.indexOf('>') < 0) 953 return null; 954 955 Matcher m; 956 if (text.charAt(1) == '/' 957 && (m = Pattern.compile("^<\\/\\w[\\w-]*+[ ]*+>", Pattern.DOTALL).matcher(text)).find()) { 958 Element element = new Element(); 959 element.rawHtml = m.group(0); 960 return new InlineMarkup().setExtent(m.group(0)).setElement(element); 961 } 962 if (text.charAt(1) == '!' 963 && (m = Pattern.compile("^<!---?[^>-](?:-?+[^-])*-->", Pattern.DOTALL).matcher(text)).find()) { 964 Element element = new Element(); 965 element.rawHtml = m.group(0); 966 return new InlineMarkup().setExtent(m.group(0)).setElement(element); 967 } 968 if (text.charAt(1) != ' ' && (m = Pattern 969 .compile("^<\\w[\\w-]*+(?:[ ]*+" + regexHtmlAttribute + ")*+[ ]*+\\/?>", Pattern.DOTALL) 970 .matcher(text)).find()) { 971 Element element = new Element(); 972 element.rawHtml = m.group(0); 973 return new InlineMarkup().setExtent(m.group(0)).setElement(element); 974 } 975 return null; 976 } 977 } 978 979 protected class InlineSpecialCharacter extends Inline { 980 @Override 981 public Inline inline(String text, String context) { 982 Matcher m; 983 if (text.length() > 1 && text.charAt(1) != ' ' && text.indexOf(';') >= 0 984 && (m = Pattern.compile("^&(#?+[0-9a-zA-Z]++);").matcher(text)).find()) { 985 Element element = new Element(); 986 element.rawHtml = "&" + m.group(1) + ";"; 987 return new InlineSpecialCharacter().setExtent(m.group(0)).setElement(element); 988 } else 989 return null; 990 } 991 } 992 993 protected class InlineStrikeThrough extends Inline { 994 @Override 995 public Inline inline(String text, String context) { 996 if (text.length() < 2) 997 return null; 998 Matcher m; 999 if (text.charAt(1) == '~' && (m = Pattern.compile("^~~(?=\\S)(.+?)(?<=\\S)~~").matcher(text)).find()) { 1000 return new InlineStrikeThrough().setExtent(m.group(0)) 1001 .setElement(new Element("del", new LineElementsHandler(m.group(1)))); 1002 } else 1003 return null; 1004 } 1005 } 1006 1007 protected class InlineUrl extends Inline { 1008 @Override 1009 public Inline inline(String text, String context) { 1010 if (!urlsLinked || text.length() < 3 || text.charAt(2) != '/') 1011 return null; 1012 Matcher m; 1013 if (context.contains("http") && (m = Pattern.compile("\\bhttps?+:[\\/]{2}[^\\s<]+\\b\\/*+", 1014 Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CHARACTER_CLASS).matcher(context)).find()) { 1015 String url = convertUrl(m.group(0)); 1016 Inline inline = new InlineUrl().setExtent(url); 1017 inline.position = m.start(0); 1018 inline.element = new Element("a", url).addAttribute("href", url); 1019 return inline; 1020 } else 1021 return null; 1022 } 1023 } 1024 1025 protected class InlineUrlTag extends Inline { 1026 @Override 1027 public Inline inline(String text, String context) { 1028 Matcher m; 1029 if (text.indexOf('>') >= 0 1030 && (m = Pattern.compile("^<(\\w++:\\/{2}[^ >]++)>", Pattern.DOTALL).matcher(text)).find()) { 1031 String url = convertUrl(m.group(1)); 1032 return new InlineUrlTag().setExtent(m.group(0)) 1033 .setElement(new Element("a", url).addAttribute("href", url)); 1034 } else 1035 return null; 1036 } 1037 } 1038 1039 protected boolean breaksEnabled = false; 1040 protected boolean markupEscaped = false; 1041 protected boolean urlsLinked = true; 1042 protected boolean safeMode = false; 1043 protected boolean strictMode = false; 1044 1045 protected String mdUrlReplacement = null; 1046 1047 protected HashMap<String, ReferenceData> referenceDefinitions; 1048 protected HashMap<String, Integer> headerIds; 1049 1050 public String title; 1051 protected int titleLevel; 1052 1053 public String text(String text) { 1054 String markup; 1055 int leadTabIndex = StringUtils.indexOf(text.replace(" ", "\t"), "\n\t"); 1056 String beforeLeadTab = text.substring(0, leadTabIndex < 0 ? 0 : leadTabIndex); 1057 if (leadTabIndex < 0 || StringUtils.isNotBlank(beforeLeadTab)) { 1058 LinkedList<Element> elements = this.textElements(text); 1059 markup = this.elements(elements); 1060 markup = markup.trim(); 1061 } else { 1062 markup = StringUtils.trim(text); 1063 } 1064 return markup; 1065 } 1066 1067 protected LinkedList<Element> textElements(String text) { 1068 referenceDefinitions = new HashMap<>(); 1069 headerIds = new HashMap<>(); 1070 title = null; 1071 titleLevel = 0; 1072 1073 text = text.replaceAll("\\r\\n?", "\n"); 1074 text = text.replaceAll("(^\\n+)|(\\n+$)", ""); 1075 String[] lines = text.split("\n"); 1076 return this.linesElements(lines); 1077 } 1078 1079 public JParsedown setBreaksEnabled(boolean breaksEnabled) { 1080 this.breaksEnabled = breaksEnabled; 1081 return this; 1082 } 1083 1084 public JParsedown setMarkupEscaped(boolean markupEscaped) { 1085 this.markupEscaped = markupEscaped; 1086 return this; 1087 } 1088 1089 public JParsedown setUrlsLinked(boolean urlsLinked) { 1090 this.urlsLinked = urlsLinked; 1091 return this; 1092 } 1093 1094 public JParsedown setSafeMode(boolean safeMode) { 1095 this.safeMode = safeMode; 1096 return this; 1097 } 1098 1099 public JParsedown setStrictMode(boolean strictMode) { 1100 this.strictMode = strictMode; 1101 return this; 1102 } 1103 1104 public JParsedown setMdUrlReplacement(String replacement) { 1105 this.mdUrlReplacement = replacement; 1106 return this; 1107 } 1108 1109 protected void getBlockTypes(char marker, LinkedList<Block> types) { 1110 switch (marker) { 1111 case '#': 1112 types.add(new BlockHeader()); 1113 return; 1114 case '*': 1115 types.add(new BlockRule()); 1116 types.add(new BlockList()); 1117 return; 1118 case '+': 1119 case '0': 1120 case '1': 1121 case '2': 1122 case '3': 1123 case '4': 1124 case '5': 1125 case '6': 1126 case '7': 1127 case '8': 1128 case '9': 1129 types.add(new BlockList()); 1130 return; 1131 case '-': 1132 types.add(new BlockSetextHeader()); 1133 types.add(new BlockTable()); 1134 types.add(new BlockRule()); 1135 types.add(new BlockList()); 1136 return; 1137 case ':': 1138 case '|': 1139 types.add(new BlockTable()); 1140 return; 1141 case '<': 1142 types.add(new BlockComment()); 1143 types.add(new BlockMarkup()); 1144 return; 1145 case '=': 1146 types.add(new BlockSetextHeader()); 1147 return; 1148 case '>': 1149 types.add(new BlockQuote()); 1150 return; 1151 case '[': 1152 types.add(new BlockReference()); 1153 return; 1154 case '_': 1155 types.add(new BlockRule()); 1156 return; 1157 case '`': 1158 case '~': 1159 types.add(new BlockFencedCode()); 1160 return; 1161 } 1162 } 1163 1164 public void getUnmarkedBlockTypes(LinkedList<Block> types) { 1165 types.add(new BlockCode()); 1166 } 1167 1168 protected String lines(String[] lines) { 1169 return this.elements(this.linesElements(lines)); 1170 } 1171 1172 protected LinkedList<Element> linesElements(LinkedList<String> lines) { 1173 return linesElements(lines.toArray(new String[lines.size()])); 1174 } 1175 1176 protected LinkedList<Element> linesElements(String[] lines) { 1177 LinkedList<Element> elements = new LinkedList<>(); 1178 Block currentBlock = null; 1179 1180 line: for (String line : lines) { 1181 if (line.trim().isEmpty()) { 1182 if (currentBlock != null) 1183 currentBlock.interrupted++; 1184 continue; 1185 } 1186 1187 int tabIndex; 1188 while ((tabIndex = line.indexOf('\t')) >= 0) { 1189 int shortage = 4 - tabIndex % 4; 1190 StringBuilder sb = new StringBuilder(); 1191 sb.append(line.substring(0, tabIndex)); 1192 for (int i = 0; i < shortage; i++) 1193 sb.append(' '); 1194 sb.append(line.substring(tabIndex + 1)); 1195 line = sb.toString(); 1196 } 1197 1198 Line lineObj = new Line(line); 1199 1200 if (currentBlock != null && currentBlock.isContinuable()) { 1201 Block block = currentBlock.continueBlock(lineObj); 1202 if (block != null) { 1203 currentBlock = block; 1204 continue; 1205 } else if (currentBlock.isCompletable()) { 1206 currentBlock = currentBlock.completeBlock(); 1207 } 1208 } 1209 1210 LinkedList<Block> blockTypes = new LinkedList<>(); 1211 getUnmarkedBlockTypes(blockTypes); 1212 getBlockTypes(lineObj.text.charAt(0), blockTypes); 1213 1214 for (Block blockType : blockTypes) { 1215 Block block = blockType.startBlock(lineObj, currentBlock); 1216 if (block != null) { 1217 if (!block.identified) { 1218 if (currentBlock != null) { 1219 elements.add(currentBlock.extractElement()); 1220 } 1221 block.identified = true; 1222 } 1223 currentBlock = block; 1224 continue line; 1225 } 1226 } 1227 1228 Block block = null; 1229 if (currentBlock != null && currentBlock instanceof BlockParagraph) { 1230 block = currentBlock.continueBlock(lineObj); 1231 } 1232 1233 if (block != null) { 1234 currentBlock = block; 1235 } else { 1236 if (currentBlock != null) { 1237 elements.add(currentBlock.extractElement()); 1238 } 1239 currentBlock = new BlockParagraph().startBlock(lineObj, null); 1240 currentBlock.identified = true; 1241 } 1242 } 1243 1244 if (currentBlock != null && currentBlock.isContinuable() && currentBlock.isCompletable()) { 1245 currentBlock = currentBlock.completeBlock(); 1246 } 1247 if (currentBlock != null) { 1248 elements.add(currentBlock.extractElement()); 1249 } 1250 1251 return elements; 1252 } 1253 1254 protected Inline[] getInlineTypes(char marker) { 1255 switch (marker) { 1256 case '!': 1257 return new Inline[] { new InlineImage() }; 1258 case '&': 1259 return new Inline[] { new InlineSpecialCharacter() }; 1260 case '*': 1261 return new Inline[] { new InlineEmphasis() }; 1262 case ':': 1263 return new Inline[] { new InlineUrl() }; 1264 case '<': 1265 return new Inline[] { new InlineUrlTag(), new InlineEmailTag(), new InlineMarkup() }; 1266 case '[': 1267 return new Inline[] { new InlineLink() }; 1268 case '_': 1269 return new Inline[] { new InlineEmphasis() }; 1270 case '`': 1271 return new Inline[] { new InlineCode() }; 1272 case '~': 1273 return new Inline[] { new InlineStrikeThrough() }; 1274 case '\\': 1275 return new Inline[] { new InlineEscapeSequence() }; 1276 default: 1277 return new Inline[] {}; 1278 } 1279 } 1280 1281 protected Pattern inlineMarkerList = Pattern.compile("[!\\*_&\\[:<`~\\\\]"); 1282 1283 public String line(String line) { 1284 return elements(lineElements(line, null)); 1285 } 1286 1287 protected LinkedList<Element> lineElements(String text, HashSet<Class<?>> nonNestables) { 1288 text = text.replaceAll("\\r\\n?", "\n"); 1289 LinkedList<Element> elements = new LinkedList<>(); 1290 if (nonNestables == null) 1291 nonNestables = new HashSet<>(); 1292 1293 text: for (;;) { 1294 Matcher m = inlineMarkerList.matcher(text); 1295 if (!m.find()) 1296 break; 1297 int markerPosition = m.start(); 1298 String excerpt = text.substring(markerPosition); 1299 1300 for (Inline inlineType : getInlineTypes(excerpt.charAt(0))) { 1301 if (nonNestables.contains(inlineType.getClass())) 1302 continue; 1303 Inline inline = inlineType.inline(excerpt, text); 1304 if (inline == null) 1305 continue; 1306 1307 if (inline.position >= 0 && inline.position > markerPosition) 1308 continue; 1309 if (inline.position < 0) 1310 inline.position = markerPosition; 1311 1312 inline.element.nonNestables.addAll(nonNestables); 1313 1314 String unmarkedText = text.substring(0, inline.position); 1315 elements.add(new InlineText().inline(unmarkedText, null).element); 1316 1317 elements.add(inline.extractElement()); 1318 1319 text = text.substring(inline.position + inline.extent); 1320 continue text; 1321 } 1322 1323 String unmarkedText = text.substring(0, markerPosition + 1); 1324 elements.add(new InlineText().inline(unmarkedText, null).element); 1325 1326 text = text.substring(markerPosition + 1); 1327 } 1328 1329 elements.add(new InlineText().inline(text, null).element); 1330 1331 for (Element element : elements) { 1332 if (element.autoBreak == null) 1333 element.autoBreak = false; 1334 } 1335 1336 return elements; 1337 } 1338 1339 protected String generateHeaderId(String text, int level) { 1340 if (title == null || titleLevel > level) { 1341 title = text; 1342 titleLevel = level; 1343 } 1344 1345 String headerId = text.toLowerCase().replaceAll("&#?+[0-9a-zA-Z]++;", "").replaceAll("[^_\\p{L}\\d\\s]", "") 1346 .replaceAll("\\s+", "-"); 1347 try { 1348 headerId = URLEncoder.encode(headerId, "UTF-8"); 1349 } catch (UnsupportedEncodingException e) { 1350 } 1351 1352 Integer count = headerIds.get(headerId); 1353 if (count == null) { 1354 headerIds.put(headerId, 1); 1355 } else { 1356 headerId += "-" + count; 1357 headerIds.put(headerId, count + 1); 1358 } 1359 1360 return headerId; 1361 } 1362 1363 protected String convertUrl(String url) { 1364 if (mdUrlReplacement == null || url.indexOf(':') >= 0) 1365 return url; 1366 Matcher m = Pattern.compile("(\\.md)(#.*)?$").matcher(url); 1367 if (m.find()) 1368 return m.replaceFirst(mdUrlReplacement + "$2"); 1369 else 1370 return url; 1371 } 1372 1373 protected String element(Element element) { 1374 if (safeMode) 1375 element = sanitiseElement(element); 1376 element = element.handle(); 1377 boolean hasName = element.name != null; 1378 1379 StringBuilder markup = new StringBuilder(); 1380 1381 if (hasName) { 1382 markup.append("<"); 1383 markup.append(element.name); 1384 for (Entry<String, String> attribute : element.attributes.entrySet()) { 1385 if (attribute.getValue() == null) 1386 continue; 1387 markup.append(String.format(" %s=\"%s\"", attribute.getKey(), escape(attribute.getValue()))); 1388 } 1389 } 1390 1391 boolean permitRawHtml = false; 1392 1393 String text = null; 1394 if (element.text != null) 1395 text = element.text; 1396 else if (element.rawHtml != null) { 1397 text = element.rawHtml; 1398 boolean allowRawHtmlInSafeMode = false; 1399 permitRawHtml = !safeMode || allowRawHtmlInSafeMode; 1400 } 1401 1402 boolean hasContent = text != null || !element.elements.isEmpty(); 1403 if (hasContent) { 1404 if (hasName) 1405 markup.append(">"); 1406 if (!element.elements.isEmpty()) { 1407 markup.append(elements(element.elements)); 1408 } else if (text != null) { 1409 if (!permitRawHtml) 1410 markup.append(escape(text, true)); 1411 else 1412 markup.append(text); 1413 } 1414 1415 if (hasName) { 1416 markup.append("</"); 1417 markup.append(element.name); 1418 markup.append(">"); 1419 } 1420 } else if (hasName) { 1421 markup.append("/>"); 1422 } 1423 return markup.toString(); 1424 } 1425 1426 protected String elements(LinkedList<Element> elements) { 1427 StringBuilder markup = new StringBuilder(); 1428 1429 boolean autoBreak = true; 1430 for (Element element : elements) { 1431 if (element.name == null && element.rawHtml == null && element.text == null && elements.isEmpty()) // empty 1432 continue; 1433 1434 boolean autoBreakNext = element.autoBreak != null ? element.autoBreak : element.name != null; 1435 autoBreak = autoBreak && autoBreakNext; 1436 1437 if (autoBreak) 1438 markup.append("\n"); 1439 markup.append(element(element)); 1440 autoBreak = autoBreakNext; 1441 } 1442 if (autoBreak) 1443 markup.append("\n"); 1444 1445 return markup.toString(); 1446 } 1447 1448 protected static Pattern goodAttribute = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9-_]*+$"); 1449 protected static HashMap<String, String> safeUrlNameToAtt; 1450 static { 1451 safeUrlNameToAtt = new HashMap<>(); 1452 safeUrlNameToAtt.put("a", "href"); 1453 safeUrlNameToAtt.put("img", "src"); 1454 } 1455 1456 public Element sanitiseElement(Element element) { 1457 if (element.name == null) { 1458 element.attributes.clear(); 1459 return element; 1460 } 1461 1462 String urlAtt = safeUrlNameToAtt.get(element.name); 1463 if (urlAtt != null) { 1464 element = filterUnsafeUrlInAttribute(element, urlAtt); 1465 } 1466 1467 LinkedList<String> attributeNames = new LinkedList<>(element.attributes.keySet()); 1468 for (String att : attributeNames) { 1469 if (!goodAttribute.matcher(att).find()) 1470 element.attributes.remove(att); 1471 else if (att.toLowerCase().startsWith("on")) 1472 element.attributes.remove(att); 1473 } 1474 1475 return element; 1476 } 1477 1478 protected String[] safeLinksWhitelist = { "http://", "https://", "ftp://", "ftps://", "mailto:", "tel:", 1479 "data:image/png;base64,", "data:image/gif;base64,", "data:image/jpeg;base64,", "irc:", "ircs:", "git:", 1480 "ssh:", "news:", "steam:" }; 1481 1482 public Element filterUnsafeUrlInAttribute(Element element, String attribute) { 1483 String attr = element.attributes.get(attribute); 1484 if (attr != null) { 1485 for (String scheme : safeLinksWhitelist) { 1486 if (attr.toLowerCase().startsWith(scheme)) 1487 return element; 1488 } 1489 } 1490 element.attributes.put(attribute, attr.replaceAll(":", "%3A")); 1491 return element; 1492 } 1493 1494 public static LinkedList<Element> replaceAllElements(String regex, Element[] elements, String text) { 1495 LinkedList<Element> newElements = new LinkedList<>(); 1496 Matcher m = Pattern.compile(regex).matcher(text); 1497 int end = 0; 1498 while (m.find()) { 1499 String before = text.substring(end, m.start()); 1500 newElements.add(new Element(null, before)); 1501 for (Element element : elements) 1502 newElements.add(element); 1503 end = m.end(); 1504 } 1505 newElements.add(new Element(null, text.substring(end))); 1506 return newElements; 1507 } 1508 1509 public static String escape(String s) { 1510 return escape(s, false); 1511 } 1512 1513 public static String escape(String s, boolean allowQuotes) { 1514 s = s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); 1515 if (!allowQuotes) 1516 s = s.replaceAll("\\\"", """).replaceAll("'", "'"); 1517 return s; 1518 } 1519 1520 public static int startSpan(String s, char c) { 1521 int i = 0; 1522 int len = s.length(); 1523 while (i < len && s.charAt(i) == c) 1524 i++; 1525 return i; 1526 } 1527 1528}