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