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