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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1515                if (!allowQuotes)
1516                        s = s.replaceAll("\\\"", "&quot;").replaceAll("'", "&#039;");
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}