]> git.bts.cx Git - cx.git/blob - cx/third_party/parsedown/Parsedown.php
Updated Parsedown and fixed some bugs for pages
[cx.git] / cx / third_party / parsedown / Parsedown.php
1 <?php
2
3 #
4 #
5 # Parsedown
6 # http://parsedown.org
7 #
8 # (c) Emanuil Rusev
9 # http://erusev.com
10 #
11 # For the full license information, view the LICENSE file that was distributed
12 # with this source code.
13 #
14 #
15
16 class Parsedown
17 {
18     # ~
19
20     const version = '1.8.0';
21
22     # ~
23
24     function text($text)
25     {
26         $Elements = $this->textElements($text);
27
28         # convert to markup
29         $markup = $this->elements($Elements);
30
31         # trim line breaks
32         $markup = trim($markup, "\n");
33
34         return $markup;
35     }
36
37     protected function textElements($text)
38     {
39         # make sure no definitions are set
40         $this->DefinitionData = array();
41
42         # standardize line breaks
43         $text = str_replace(array("\r\n", "\r"), "\n", $text);
44
45         # remove surrounding line breaks
46         $text = trim($text, "\n");
47
48         # split text into lines
49         $lines = explode("\n", $text);
50
51         # iterate through lines to identify blocks
52         return $this->linesElements($lines);
53     }
54
55     #
56     # Setters
57     #
58
59     function setBreaksEnabled($breaksEnabled)
60     {
61         $this->breaksEnabled = $breaksEnabled;
62
63         return $this;
64     }
65
66     protected $breaksEnabled;
67
68     function setMarkupEscaped($markupEscaped)
69     {
70         $this->markupEscaped = $markupEscaped;
71
72         return $this;
73     }
74
75     protected $markupEscaped;
76
77     function setUrlsLinked($urlsLinked)
78     {
79         $this->urlsLinked = $urlsLinked;
80
81         return $this;
82     }
83
84     protected $urlsLinked = true;
85
86     function setSafeMode($safeMode)
87     {
88         $this->safeMode = (bool) $safeMode;
89
90         return $this;
91     }
92
93     protected $safeMode;
94
95     function setStrictMode($strictMode)
96     {
97         $this->strictMode = (bool) $strictMode;
98
99         return $this;
100     }
101
102     protected $strictMode;
103
104     protected $safeLinksWhitelist = array(
105         'http://',
106         'https://',
107         'ftp://',
108         'ftps://',
109         'mailto:',
110         'tel:',
111         'data:image/png;base64,',
112         'data:image/gif;base64,',
113         'data:image/jpeg;base64,',
114         'irc:',
115         'ircs:',
116         'git:',
117         'ssh:',
118         'news:',
119         'steam:',
120     );
121
122     #
123     # Lines
124     #
125
126     protected $BlockTypes = array(
127         '#' => array('Header'),
128         '*' => array('Rule', 'List'),
129         '+' => array('List'),
130         '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
131         '0' => array('List'),
132         '1' => array('List'),
133         '2' => array('List'),
134         '3' => array('List'),
135         '4' => array('List'),
136         '5' => array('List'),
137         '6' => array('List'),
138         '7' => array('List'),
139         '8' => array('List'),
140         '9' => array('List'),
141         ':' => array('Table'),
142         '<' => array('Comment', 'Markup'),
143         '=' => array('SetextHeader'),
144         '>' => array('Quote'),
145         '[' => array('Reference'),
146         '_' => array('Rule'),
147         '`' => array('FencedCode'),
148         '|' => array('Table'),
149         '~' => array('FencedCode'),
150     );
151
152     # ~
153
154     protected $unmarkedBlockTypes = array(
155         'Code',
156     );
157
158     #
159     # Blocks
160     #
161
162     protected function lines(array $lines)
163     {
164         return $this->elements($this->linesElements($lines));
165     }
166
167     protected function linesElements(array $lines)
168     {
169         $Elements = array();
170         $CurrentBlock = null;
171
172         foreach ($lines as $line)
173         {
174             if (chop($line) === '')
175             {
176                 if (isset($CurrentBlock))
177                 {
178                     $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
179                         ? $CurrentBlock['interrupted'] + 1 : 1
180                     );
181                 }
182
183                 continue;
184             }
185
186             while (($beforeTab = strstr($line, "\t", true)) !== false)
187             {
188                 $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
189
190                 $line = $beforeTab
191                     . str_repeat(' ', $shortage)
192                     . substr($line, strlen($beforeTab) + 1)
193                 ;
194             }
195
196             $indent = strspn($line, ' ');
197
198             $text = $indent > 0 ? substr($line, $indent) : $line;
199
200             # ~
201
202             $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
203
204             # ~
205
206             if (isset($CurrentBlock['continuable']))
207             {
208                 $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
209                 $Block = $this->$methodName($Line, $CurrentBlock);
210
211                 if (isset($Block))
212                 {
213                     $CurrentBlock = $Block;
214
215                     continue;
216                 }
217                 else
218                 {
219                     if ($this->isBlockCompletable($CurrentBlock['type']))
220                     {
221                         $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
222                         $CurrentBlock = $this->$methodName($CurrentBlock);
223                     }
224                 }
225             }
226
227             # ~
228
229             $marker = $text[0];
230
231             # ~
232
233             $blockTypes = $this->unmarkedBlockTypes;
234
235             if (isset($this->BlockTypes[$marker]))
236             {
237                 foreach ($this->BlockTypes[$marker] as $blockType)
238                 {
239                     $blockTypes []= $blockType;
240                 }
241             }
242
243             #
244             # ~
245
246             foreach ($blockTypes as $blockType)
247             {
248                 $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
249
250                 if (isset($Block))
251                 {
252                     $Block['type'] = $blockType;
253
254                     if ( ! isset($Block['identified']))
255                     {
256                         if (isset($CurrentBlock))
257                         {
258                             $Elements[] = $this->extractElement($CurrentBlock);
259                         }
260
261                         $Block['identified'] = true;
262                     }
263
264                     if ($this->isBlockContinuable($blockType))
265                     {
266                         $Block['continuable'] = true;
267                     }
268
269                     $CurrentBlock = $Block;
270
271                     continue 2;
272                 }
273             }
274
275             # ~
276
277             if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph')
278             {
279                 $Block = $this->paragraphContinue($Line, $CurrentBlock);
280             }
281
282             if (isset($Block))
283             {
284                 $CurrentBlock = $Block;
285             }
286             else
287             {
288                 if (isset($CurrentBlock))
289                 {
290                     $Elements[] = $this->extractElement($CurrentBlock);
291                 }
292
293                 $CurrentBlock = $this->paragraph($Line);
294
295                 $CurrentBlock['identified'] = true;
296             }
297         }
298
299         # ~
300
301         if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
302         {
303             $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
304             $CurrentBlock = $this->$methodName($CurrentBlock);
305         }
306
307         # ~
308
309         if (isset($CurrentBlock))
310         {
311             $Elements[] = $this->extractElement($CurrentBlock);
312         }
313
314         # ~
315
316         return $Elements;
317     }
318
319     protected function extractElement(array $Component)
320     {
321         if ( ! isset($Component['element']))
322         {
323             if (isset($Component['markup']))
324             {
325                 $Component['element'] = array('rawHtml' => $Component['markup']);
326             }
327             elseif (isset($Component['hidden']))
328             {
329                 $Component['element'] = array();
330             }
331         }
332
333         return $Component['element'];
334     }
335
336     protected function isBlockContinuable($Type)
337     {
338         return method_exists($this, 'block' . $Type . 'Continue');
339     }
340
341     protected function isBlockCompletable($Type)
342     {
343         return method_exists($this, 'block' . $Type . 'Complete');
344     }
345
346     #
347     # Code
348
349     protected function blockCode($Line, $Block = null)
350     {
351         if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted']))
352         {
353             return;
354         }
355
356         if ($Line['indent'] >= 4)
357         {
358             $text = substr($Line['body'], 4);
359
360             $Block = array(
361                 'element' => array(
362                     'name' => 'pre',
363                     'element' => array(
364                         'name' => 'code',
365                         'text' => $text,
366                     ),
367                 ),
368             );
369
370             return $Block;
371         }
372     }
373
374     protected function blockCodeContinue($Line, $Block)
375     {
376         if ($Line['indent'] >= 4)
377         {
378             if (isset($Block['interrupted']))
379             {
380                 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
381
382                 unset($Block['interrupted']);
383             }
384
385             $Block['element']['element']['text'] .= "\n";
386
387             $text = substr($Line['body'], 4);
388
389             $Block['element']['element']['text'] .= $text;
390
391             return $Block;
392         }
393     }
394
395     protected function blockCodeComplete($Block)
396     {
397         return $Block;
398     }
399
400     #
401     # Comment
402
403     protected function blockComment($Line)
404     {
405         if ($this->markupEscaped or $this->safeMode)
406         {
407             return;
408         }
409
410         if (strpos($Line['text'], '<!--') === 0)
411         {
412             $Block = array(
413                 'element' => array(
414                     'rawHtml' => $Line['body'],
415                     'autobreak' => true,
416                 ),
417             );
418
419             if (strpos($Line['text'], '-->') !== false)
420             {
421                 $Block['closed'] = true;
422             }
423
424             return $Block;
425         }
426     }
427
428     protected function blockCommentContinue($Line, array $Block)
429     {
430         if (isset($Block['closed']))
431         {
432             return;
433         }
434
435         $Block['element']['rawHtml'] .= "\n" . $Line['body'];
436
437         if (strpos($Line['text'], '-->') !== false)
438         {
439             $Block['closed'] = true;
440         }
441
442         return $Block;
443     }
444
445     #
446     # Fenced Code
447
448     protected function blockFencedCode($Line)
449     {
450         $marker = $Line['text'][0];
451
452         $openerLength = strspn($Line['text'], $marker);
453
454         if ($openerLength < 3)
455         {
456             return;
457         }
458
459         $infostring = trim(substr($Line['text'], $openerLength), "\t ");
460
461         if (strpos($infostring, '`') !== false)
462         {
463             return;
464         }
465
466         $Element = array(
467             'name' => 'code',
468             'text' => '',
469         );
470
471         if ($infostring !== '')
472         {
473             /**
474              * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
475              * Every HTML element may have a class attribute specified.
476              * The attribute, if specified, must have a value that is a set
477              * of space-separated tokens representing the various classes
478              * that the element belongs to.
479              * [...]
480              * The space characters, for the purposes of this specification,
481              * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
482              * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
483              * U+000D CARRIAGE RETURN (CR).
484              */
485             $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
486
487             $Element['attributes'] = array('class' => "language-$language");
488         }
489
490         $Block = array(
491             'char' => $marker,
492             'openerLength' => $openerLength,
493             'element' => array(
494                 'name' => 'pre',
495                 'element' => $Element,
496             ),
497         );
498
499         return $Block;
500     }
501
502     protected function blockFencedCodeContinue($Line, $Block)
503     {
504         if (isset($Block['complete']))
505         {
506             return;
507         }
508
509         if (isset($Block['interrupted']))
510         {
511             $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
512
513             unset($Block['interrupted']);
514         }
515
516         if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
517             and chop(substr($Line['text'], $len), ' ') === ''
518         ) {
519             $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
520
521             $Block['complete'] = true;
522
523             return $Block;
524         }
525
526         $Block['element']['element']['text'] .= "\n" . $Line['body'];
527
528         return $Block;
529     }
530
531     protected function blockFencedCodeComplete($Block)
532     {
533         return $Block;
534     }
535
536     #
537     # Header
538
539     protected function blockHeader($Line)
540     {
541         $level = strspn($Line['text'], '#');
542
543         if ($level > 6)
544         {
545             return;
546         }
547
548         $text = trim($Line['text'], '#');
549
550         if ($this->strictMode and isset($text[0]) and $text[0] !== ' ')
551         {
552             return;
553         }
554
555         $text = trim($text, ' ');
556
557         $Block = array(
558             'element' => array(
559                 'name' => 'h' . $level,
560                 'handler' => array(
561                     'function' => 'lineElements',
562                     'argument' => $text,
563                     'destination' => 'elements',
564                 )
565             ),
566         );
567
568         return $Block;
569     }
570
571     #
572     # List
573
574     protected function blockList($Line, ?array $CurrentBlock = null)
575     {
576         list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
577
578         if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
579         {
580             $contentIndent = strlen($matches[2]);
581
582             if ($contentIndent >= 5)
583             {
584                 $contentIndent -= 1;
585                 $matches[1] = substr($matches[1], 0, -$contentIndent);
586                 $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
587             }
588             elseif ($contentIndent === 0)
589             {
590                 $matches[1] .= ' ';
591             }
592
593             $markerWithoutWhitespace = strstr($matches[1], ' ', true);
594
595             $Block = array(
596                 'indent' => $Line['indent'],
597                 'pattern' => $pattern,
598                 'data' => array(
599                     'type' => $name,
600                     'marker' => $matches[1],
601                     'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
602                 ),
603                 'element' => array(
604                     'name' => $name,
605                     'elements' => array(),
606                 ),
607             );
608             $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
609
610             if ($name === 'ol')
611             {
612                 $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
613
614                 if ($listStart !== '1')
615                 {
616                     if (
617                         isset($CurrentBlock)
618                         and $CurrentBlock['type'] === 'Paragraph'
619                         and ! isset($CurrentBlock['interrupted'])
620                     ) {
621                         return;
622                     }
623
624                     $Block['element']['attributes'] = array('start' => $listStart);
625                 }
626             }
627
628             $Block['li'] = array(
629                 'name' => 'li',
630                 'handler' => array(
631                     'function' => 'li',
632                     'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
633                     'destination' => 'elements'
634                 )
635             );
636
637             $Block['element']['elements'] []= & $Block['li'];
638
639             return $Block;
640         }
641     }
642
643     protected function blockListContinue($Line, array $Block)
644     {
645         if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument']))
646         {
647             return null;
648         }
649
650         $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
651
652         if ($Line['indent'] < $requiredIndent
653             and (
654                 (
655                     $Block['data']['type'] === 'ol'
656                     and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
657                 ) or (
658                     $Block['data']['type'] === 'ul'
659                     and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
660                 )
661             )
662         ) {
663             if (isset($Block['interrupted']))
664             {
665                 $Block['li']['handler']['argument'] []= '';
666
667                 $Block['loose'] = true;
668
669                 unset($Block['interrupted']);
670             }
671
672             unset($Block['li']);
673
674             $text = isset($matches[1]) ? $matches[1] : '';
675
676             $Block['indent'] = $Line['indent'];
677
678             $Block['li'] = array(
679                 'name' => 'li',
680                 'handler' => array(
681                     'function' => 'li',
682                     'argument' => array($text),
683                     'destination' => 'elements'
684                 )
685             );
686
687             $Block['element']['elements'] []= & $Block['li'];
688
689             return $Block;
690         }
691         elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line))
692         {
693             return null;
694         }
695
696         if ($Line['text'][0] === '[' and $this->blockReference($Line))
697         {
698             return $Block;
699         }
700
701         if ($Line['indent'] >= $requiredIndent)
702         {
703             if (isset($Block['interrupted']))
704             {
705                 $Block['li']['handler']['argument'] []= '';
706
707                 $Block['loose'] = true;
708
709                 unset($Block['interrupted']);
710             }
711
712             $text = substr($Line['body'], $requiredIndent);
713
714             $Block['li']['handler']['argument'] []= $text;
715
716             return $Block;
717         }
718
719         if ( ! isset($Block['interrupted']))
720         {
721             $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
722
723             $Block['li']['handler']['argument'] []= $text;
724
725             return $Block;
726         }
727     }
728
729     protected function blockListComplete(array $Block)
730     {
731         if (isset($Block['loose']))
732         {
733             foreach ($Block['element']['elements'] as &$li)
734             {
735                 if (end($li['handler']['argument']) !== '')
736                 {
737                     $li['handler']['argument'] []= '';
738                 }
739             }
740         }
741
742         return $Block;
743     }
744
745     #
746     # Quote
747
748     protected function blockQuote($Line)
749     {
750         if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
751         {
752             $Block = array(
753                 'element' => array(
754                     'name' => 'blockquote',
755                     'handler' => array(
756                         'function' => 'linesElements',
757                         'argument' => (array) $matches[1],
758                         'destination' => 'elements',
759                     )
760                 ),
761             );
762
763             return $Block;
764         }
765     }
766
767     protected function blockQuoteContinue($Line, array $Block)
768     {
769         if (isset($Block['interrupted']))
770         {
771             return;
772         }
773
774         if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
775         {
776             $Block['element']['handler']['argument'] []= $matches[1];
777
778             return $Block;
779         }
780
781         if ( ! isset($Block['interrupted']))
782         {
783             $Block['element']['handler']['argument'] []= $Line['text'];
784
785             return $Block;
786         }
787     }
788
789     #
790     # Rule
791
792     protected function blockRule($Line)
793     {
794         $marker = $Line['text'][0];
795
796         if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '')
797         {
798             $Block = array(
799                 'element' => array(
800                     'name' => 'hr',
801                 ),
802             );
803
804             return $Block;
805         }
806     }
807
808     #
809     # Setext
810
811     protected function blockSetextHeader($Line, ?array $Block = null)
812     {
813         if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
814         {
815             return;
816         }
817
818         if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '')
819         {
820             $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
821
822             return $Block;
823         }
824     }
825
826     #
827     # Markup
828
829     protected function blockMarkup($Line)
830     {
831         if ($this->markupEscaped or $this->safeMode)
832         {
833             return;
834         }
835
836         if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches))
837         {
838             $element = strtolower($matches[1]);
839
840             if (in_array($element, $this->textLevelElements))
841             {
842                 return;
843             }
844
845             $Block = array(
846                 'name' => $matches[1],
847                 'element' => array(
848                     'rawHtml' => $Line['text'],
849                     'autobreak' => true,
850                 ),
851             );
852
853             return $Block;
854         }
855     }
856
857     protected function blockMarkupContinue($Line, array $Block)
858     {
859         if (isset($Block['closed']) or isset($Block['interrupted']))
860         {
861             return;
862         }
863
864         $Block['element']['rawHtml'] .= "\n" . $Line['body'];
865
866         return $Block;
867     }
868
869     #
870     # Reference
871
872     protected function blockReference($Line)
873     {
874         if (strpos($Line['text'], ']') !== false
875             and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
876         ) {
877             $id = strtolower($matches[1]);
878
879             $Data = array(
880                 'url' => $matches[2],
881                 'title' => isset($matches[3]) ? $matches[3] : null,
882             );
883
884             $this->DefinitionData['Reference'][$id] = $Data;
885
886             $Block = array(
887                 'element' => array(),
888             );
889
890             return $Block;
891         }
892     }
893
894     #
895     # Table
896
897     protected function blockTable($Line, ?array $Block = null)
898     {
899         if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
900         {
901             return;
902         }
903
904         if (
905             strpos($Block['element']['handler']['argument'], '|') === false
906             and strpos($Line['text'], '|') === false
907             and strpos($Line['text'], ':') === false
908             or strpos($Block['element']['handler']['argument'], "\n") !== false
909         ) {
910             return;
911         }
912
913         if (chop($Line['text'], ' -:|') !== '')
914         {
915             return;
916         }
917
918         $alignments = array();
919
920         $divider = $Line['text'];
921
922         $divider = trim($divider);
923         $divider = trim($divider, '|');
924
925         $dividerCells = explode('|', $divider);
926
927         foreach ($dividerCells as $dividerCell)
928         {
929             $dividerCell = trim($dividerCell);
930
931             if ($dividerCell === '')
932             {
933                 return;
934             }
935
936             $alignment = null;
937
938             if ($dividerCell[0] === ':')
939             {
940                 $alignment = 'left';
941             }
942
943             if (substr($dividerCell, - 1) === ':')
944             {
945                 $alignment = $alignment === 'left' ? 'center' : 'right';
946             }
947
948             $alignments []= $alignment;
949         }
950
951         # ~
952
953         $HeaderElements = array();
954
955         $header = $Block['element']['handler']['argument'];
956
957         $header = trim($header);
958         $header = trim($header, '|');
959
960         $headerCells = explode('|', $header);
961
962         if (count($headerCells) !== count($alignments))
963         {
964             return;
965         }
966
967         foreach ($headerCells as $index => $headerCell)
968         {
969             $headerCell = trim($headerCell);
970
971             $HeaderElement = array(
972                 'name' => 'th',
973                 'handler' => array(
974                     'function' => 'lineElements',
975                     'argument' => $headerCell,
976                     'destination' => 'elements',
977                 )
978             );
979
980             if (isset($alignments[$index]))
981             {
982                 $alignment = $alignments[$index];
983
984                 $HeaderElement['attributes'] = array(
985                     'style' => "text-align: $alignment;",
986                 );
987             }
988
989             $HeaderElements []= $HeaderElement;
990         }
991
992         # ~
993
994         $Block = array(
995             'alignments' => $alignments,
996             'identified' => true,
997             'element' => array(
998                 'name' => 'table',
999                 'elements' => array(),
1000             ),
1001         );
1002
1003         $Block['element']['elements'] []= array(
1004             'name' => 'thead',
1005         );
1006
1007         $Block['element']['elements'] []= array(
1008             'name' => 'tbody',
1009             'elements' => array(),
1010         );
1011
1012         $Block['element']['elements'][0]['elements'] []= array(
1013             'name' => 'tr',
1014             'elements' => $HeaderElements,
1015         );
1016
1017         return $Block;
1018     }
1019
1020     protected function blockTableContinue($Line, array $Block)
1021     {
1022         if (isset($Block['interrupted']))
1023         {
1024             return;
1025         }
1026
1027         if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|'))
1028         {
1029             $Elements = array();
1030
1031             $row = $Line['text'];
1032
1033             $row = trim($row);
1034             $row = trim($row, '|');
1035
1036             preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
1037
1038             $cells = array_slice($matches[0], 0, count($Block['alignments']));
1039
1040             foreach ($cells as $index => $cell)
1041             {
1042                 $cell = trim($cell);
1043
1044                 $Element = array(
1045                     'name' => 'td',
1046                     'handler' => array(
1047                         'function' => 'lineElements',
1048                         'argument' => $cell,
1049                         'destination' => 'elements',
1050                     )
1051                 );
1052
1053                 if (isset($Block['alignments'][$index]))
1054                 {
1055                     $Element['attributes'] = array(
1056                         'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
1057                     );
1058                 }
1059
1060                 $Elements []= $Element;
1061             }
1062
1063             $Element = array(
1064                 'name' => 'tr',
1065                 'elements' => $Elements,
1066             );
1067
1068             $Block['element']['elements'][1]['elements'] []= $Element;
1069
1070             return $Block;
1071         }
1072     }
1073
1074     #
1075     # ~
1076     #
1077
1078     protected function paragraph($Line)
1079     {
1080         return array(
1081             'type' => 'Paragraph',
1082             'element' => array(
1083                 'name' => 'p',
1084                 'handler' => array(
1085                     'function' => 'lineElements',
1086                     'argument' => $Line['text'],
1087                     'destination' => 'elements',
1088                 ),
1089             ),
1090         );
1091     }
1092
1093     protected function paragraphContinue($Line, array $Block)
1094     {
1095         if (isset($Block['interrupted']))
1096         {
1097             return;
1098         }
1099
1100         $Block['element']['handler']['argument'] .= "\n".$Line['text'];
1101
1102         return $Block;
1103     }
1104
1105     #
1106     # Inline Elements
1107     #
1108
1109     protected $InlineTypes = array(
1110         '!' => array('Image'),
1111         '&' => array('SpecialCharacter'),
1112         '*' => array('Emphasis'),
1113         ':' => array('Url'),
1114         '<' => array('UrlTag', 'EmailTag', 'Markup'),
1115         '[' => array('Link'),
1116         '_' => array('Emphasis'),
1117         '`' => array('Code'),
1118         '~' => array('Strikethrough'),
1119         '\\' => array('EscapeSequence'),
1120     );
1121
1122     # ~
1123
1124     protected $inlineMarkerList = '!*_&[:<`~\\';
1125
1126     #
1127     # ~
1128     #
1129
1130     public function line($text, $nonNestables = array())
1131     {
1132         return $this->elements($this->lineElements($text, $nonNestables));
1133     }
1134
1135     protected function lineElements($text, $nonNestables = array())
1136     {
1137         # standardize line breaks
1138         $text = str_replace(array("\r\n", "\r"), "\n", $text);
1139
1140         $Elements = array();
1141
1142         $nonNestables = (empty($nonNestables)
1143             ? array()
1144             : array_combine($nonNestables, $nonNestables)
1145         );
1146
1147         # $excerpt is based on the first occurrence of a marker
1148
1149         while ($excerpt = strpbrk($text, $this->inlineMarkerList))
1150         {
1151             $marker = $excerpt[0];
1152
1153             $markerPosition = strlen($text) - strlen($excerpt);
1154
1155             $Excerpt = array('text' => $excerpt, 'context' => $text);
1156
1157             foreach ($this->InlineTypes[$marker] as $inlineType)
1158             {
1159                 # check to see if the current inline type is nestable in the current context
1160
1161                 if (isset($nonNestables[$inlineType]))
1162                 {
1163                     continue;
1164                 }
1165
1166                 $Inline = $this->{"inline$inlineType"}($Excerpt);
1167
1168                 if ( ! isset($Inline))
1169                 {
1170                     continue;
1171                 }
1172
1173                 # makes sure that the inline belongs to "our" marker
1174
1175                 if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1176                 {
1177                     continue;
1178                 }
1179
1180                 # sets a default inline position
1181
1182                 if ( ! isset($Inline['position']))
1183                 {
1184                     $Inline['position'] = $markerPosition;
1185                 }
1186
1187                 # cause the new element to 'inherit' our non nestables
1188
1189
1190                 $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
1191                     ? array_merge($Inline['element']['nonNestables'], $nonNestables)
1192                     : $nonNestables
1193                 ;
1194
1195                 # the text that comes before the inline
1196                 $unmarkedText = substr($text, 0, $Inline['position']);
1197
1198                 # compile the unmarked text
1199                 $InlineText = $this->inlineText($unmarkedText);
1200                 $Elements[] = $InlineText['element'];
1201
1202                 # compile the inline
1203                 $Elements[] = $this->extractElement($Inline);
1204
1205                 # remove the examined text
1206                 $text = substr($text, $Inline['position'] + $Inline['extent']);
1207
1208                 continue 2;
1209             }
1210
1211             # the marker does not belong to an inline
1212
1213             $unmarkedText = substr($text, 0, $markerPosition + 1);
1214
1215             $InlineText = $this->inlineText($unmarkedText);
1216             $Elements[] = $InlineText['element'];
1217
1218             $text = substr($text, $markerPosition + 1);
1219         }
1220
1221         $InlineText = $this->inlineText($text);
1222         $Elements[] = $InlineText['element'];
1223
1224         foreach ($Elements as &$Element)
1225         {
1226             if ( ! isset($Element['autobreak']))
1227             {
1228                 $Element['autobreak'] = false;
1229             }
1230         }
1231
1232         return $Elements;
1233     }
1234
1235     #
1236     # ~
1237     #
1238
1239     protected function inlineText($text)
1240     {
1241         $Inline = array(
1242             'extent' => strlen($text),
1243             'element' => array(),
1244         );
1245
1246         $Inline['element']['elements'] = self::pregReplaceElements(
1247             $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
1248             array(
1249                 array('name' => 'br'),
1250                 array('text' => "\n"),
1251             ),
1252             $text
1253         );
1254
1255         return $Inline;
1256     }
1257
1258     protected function inlineCode($Excerpt)
1259     {
1260         $marker = $Excerpt['text'][0];
1261
1262         if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1263         {
1264             $text = $matches[2];
1265             $text = preg_replace('/[ ]*+\n/', ' ', $text);
1266
1267             return array(
1268                 'extent' => strlen($matches[0]),
1269                 'element' => array(
1270                     'name' => 'code',
1271                     'text' => $text,
1272                 ),
1273             );
1274         }
1275     }
1276
1277     protected function inlineEmailTag($Excerpt)
1278     {
1279         $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
1280
1281         $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
1282             . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
1283
1284         if (strpos($Excerpt['text'], '>') !== false
1285             and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
1286         ){
1287             $url = $matches[1];
1288
1289             if ( ! isset($matches[2]))
1290             {
1291                 $url = "mailto:$url";
1292             }
1293
1294             return array(
1295                 'extent' => strlen($matches[0]),
1296                 'element' => array(
1297                     'name' => 'a',
1298                     'text' => $matches[1],
1299                     'attributes' => array(
1300                         'href' => $url,
1301                     ),
1302                 ),
1303             );
1304         }
1305     }
1306
1307     protected function inlineEmphasis($Excerpt)
1308     {
1309         if ( ! isset($Excerpt['text'][1]))
1310         {
1311             return;
1312         }
1313
1314         $marker = $Excerpt['text'][0];
1315
1316         if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1317         {
1318             $emphasis = 'strong';
1319         }
1320         elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1321         {
1322             $emphasis = 'em';
1323         }
1324         else
1325         {
1326             return;
1327         }
1328
1329         return array(
1330             'extent' => strlen($matches[0]),
1331             'element' => array(
1332                 'name' => $emphasis,
1333                 'handler' => array(
1334                     'function' => 'lineElements',
1335                     'argument' => $matches[1],
1336                     'destination' => 'elements',
1337                 )
1338             ),
1339         );
1340     }
1341
1342     protected function inlineEscapeSequence($Excerpt)
1343     {
1344         if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1345         {
1346             return array(
1347                 'element' => array('rawHtml' => $Excerpt['text'][1]),
1348                 'extent' => 2,
1349             );
1350         }
1351     }
1352
1353     protected function inlineImage($Excerpt)
1354     {
1355         if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1356         {
1357             return;
1358         }
1359
1360         $Excerpt['text']= substr($Excerpt['text'], 1);
1361
1362         $Link = $this->inlineLink($Excerpt);
1363
1364         if ($Link === null)
1365         {
1366             return;
1367         }
1368
1369         $Inline = array(
1370             'extent' => $Link['extent'] + 1,
1371             'element' => array(
1372                 'name' => 'img',
1373                 'attributes' => array(
1374                     'src' => $Link['element']['attributes']['href'],
1375                     'alt' => $Link['element']['handler']['argument'],
1376                 ),
1377                 'autobreak' => true,
1378             ),
1379         );
1380
1381         $Inline['element']['attributes'] += $Link['element']['attributes'];
1382
1383         unset($Inline['element']['attributes']['href']);
1384
1385         return $Inline;
1386     }
1387
1388     protected function inlineLink($Excerpt)
1389     {
1390         $Element = array(
1391             'name' => 'a',
1392             'handler' => array(
1393                 'function' => 'lineElements',
1394                 'argument' => null,
1395                 'destination' => 'elements',
1396             ),
1397             'nonNestables' => array('Url', 'Link'),
1398             'attributes' => array(
1399                 'href' => null,
1400                 'title' => null,
1401             ),
1402         );
1403
1404         $extent = 0;
1405
1406         $remainder = $Excerpt['text'];
1407
1408         if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1409         {
1410             $Element['handler']['argument'] = $matches[1];
1411
1412             $extent += strlen($matches[0]);
1413
1414             $remainder = substr($remainder, $extent);
1415         }
1416         else
1417         {
1418             return;
1419         }
1420
1421         if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
1422         {
1423             $Element['attributes']['href'] = $matches[1];
1424
1425             if (isset($matches[2]))
1426             {
1427                 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1428             }
1429
1430             $extent += strlen($matches[0]);
1431         }
1432         else
1433         {
1434             if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1435             {
1436                 $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
1437                 $definition = strtolower($definition);
1438
1439                 $extent += strlen($matches[0]);
1440             }
1441             else
1442             {
1443                 $definition = strtolower($Element['handler']['argument']);
1444             }
1445
1446             if ( ! isset($this->DefinitionData['Reference'][$definition]))
1447             {
1448                 return;
1449             }
1450
1451             $Definition = $this->DefinitionData['Reference'][$definition];
1452
1453             $Element['attributes']['href'] = $Definition['url'];
1454             $Element['attributes']['title'] = $Definition['title'];
1455         }
1456
1457         return array(
1458             'extent' => $extent,
1459             'element' => $Element,
1460         );
1461     }
1462
1463     protected function inlineMarkup($Excerpt)
1464     {
1465         if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
1466         {
1467             return;
1468         }
1469
1470         if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
1471         {
1472             return array(
1473                 'element' => array('rawHtml' => $matches[0]),
1474                 'extent' => strlen($matches[0]),
1475             );
1476         }
1477
1478         if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
1479         {
1480             return array(
1481                 'element' => array('rawHtml' => $matches[0]),
1482                 'extent' => strlen($matches[0]),
1483             );
1484         }
1485
1486         if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
1487         {
1488             return array(
1489                 'element' => array('rawHtml' => $matches[0]),
1490                 'extent' => strlen($matches[0]),
1491             );
1492         }
1493     }
1494
1495     protected function inlineSpecialCharacter($Excerpt)
1496     {
1497         if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
1498             and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
1499         ) {
1500             return array(
1501                 'element' => array('rawHtml' => '&' . $matches[1] . ';'),
1502                 'extent' => strlen($matches[0]),
1503             );
1504         }
1505     }
1506
1507     protected function inlineStrikethrough($Excerpt)
1508     {
1509         if ( ! isset($Excerpt['text'][1]))
1510         {
1511             return;
1512         }
1513
1514         if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1515         {
1516             return array(
1517                 'extent' => strlen($matches[0]),
1518                 'element' => array(
1519                     'name' => 'del',
1520                     'handler' => array(
1521                         'function' => 'lineElements',
1522                         'argument' => $matches[1],
1523                         'destination' => 'elements',
1524                     )
1525                 ),
1526             );
1527         }
1528     }
1529
1530     protected function inlineUrl($Excerpt)
1531     {
1532         if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1533         {
1534             return;
1535         }
1536
1537         if (strpos($Excerpt['context'], 'http') !== false
1538             and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
1539         ) {
1540             $url = $matches[0][0];
1541
1542             $Inline = array(
1543                 'extent' => strlen($matches[0][0]),
1544                 'position' => $matches[0][1],
1545                 'element' => array(
1546                     'name' => 'a',
1547                     'text' => $url,
1548                     'attributes' => array(
1549                         'href' => $url,
1550                     ),
1551                 ),
1552             );
1553
1554             return $Inline;
1555         }
1556     }
1557
1558     protected function inlineUrlTag($Excerpt)
1559     {
1560         if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
1561         {
1562             $url = $matches[1];
1563
1564             return array(
1565                 'extent' => strlen($matches[0]),
1566                 'element' => array(
1567                     'name' => 'a',
1568                     'text' => $url,
1569                     'attributes' => array(
1570                         'href' => $url,
1571                     ),
1572                 ),
1573             );
1574         }
1575     }
1576
1577     # ~
1578
1579     protected function unmarkedText($text)
1580     {
1581         $Inline = $this->inlineText($text);
1582         return $this->element($Inline['element']);
1583     }
1584
1585     #
1586     # Handlers
1587     #
1588
1589     protected function handle(array $Element)
1590     {
1591         if (isset($Element['handler']))
1592         {
1593             if (!isset($Element['nonNestables']))
1594             {
1595                 $Element['nonNestables'] = array();
1596             }
1597
1598             if (is_string($Element['handler']))
1599             {
1600                 $function = $Element['handler'];
1601                 $argument = $Element['text'];
1602                 unset($Element['text']);
1603                 $destination = 'rawHtml';
1604             }
1605             else
1606             {
1607                 $function = $Element['handler']['function'];
1608                 $argument = $Element['handler']['argument'];
1609                 $destination = $Element['handler']['destination'];
1610             }
1611
1612             $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
1613
1614             if ($destination === 'handler')
1615             {
1616                 $Element = $this->handle($Element);
1617             }
1618
1619             unset($Element['handler']);
1620         }
1621
1622         return $Element;
1623     }
1624
1625     protected function handleElementRecursive(array $Element)
1626     {
1627         return $this->elementApplyRecursive(array($this, 'handle'), $Element);
1628     }
1629
1630     protected function handleElementsRecursive(array $Elements)
1631     {
1632         return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
1633     }
1634
1635     protected function elementApplyRecursive($closure, array $Element)
1636     {
1637         $Element = call_user_func($closure, $Element);
1638
1639         if (isset($Element['elements']))
1640         {
1641             $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
1642         }
1643         elseif (isset($Element['element']))
1644         {
1645             $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
1646         }
1647
1648         return $Element;
1649     }
1650
1651     protected function elementApplyRecursiveDepthFirst($closure, array $Element)
1652     {
1653         if (isset($Element['elements']))
1654         {
1655             $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
1656         }
1657         elseif (isset($Element['element']))
1658         {
1659             $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
1660         }
1661
1662         $Element = call_user_func($closure, $Element);
1663
1664         return $Element;
1665     }
1666
1667     protected function elementsApplyRecursive($closure, array $Elements)
1668     {
1669         foreach ($Elements as &$Element)
1670         {
1671             $Element = $this->elementApplyRecursive($closure, $Element);
1672         }
1673
1674         return $Elements;
1675     }
1676
1677     protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
1678     {
1679         foreach ($Elements as &$Element)
1680         {
1681             $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
1682         }
1683
1684         return $Elements;
1685     }
1686
1687     protected function element(array $Element)
1688     {
1689         if ($this->safeMode)
1690         {
1691             $Element = $this->sanitiseElement($Element);
1692         }
1693
1694         # identity map if element has no handler
1695         $Element = $this->handle($Element);
1696
1697         $hasName = isset($Element['name']);
1698
1699         $markup = '';
1700
1701         if ($hasName)
1702         {
1703             $markup .= '<' . $Element['name'];
1704
1705             if (isset($Element['attributes']))
1706             {
1707                 foreach ($Element['attributes'] as $name => $value)
1708                 {
1709                     if ($value === null)
1710                     {
1711                         continue;
1712                     }
1713
1714                     $markup .= " $name=\"".self::escape($value).'"';
1715                 }
1716             }
1717         }
1718
1719         $permitRawHtml = false;
1720
1721         if (isset($Element['text']))
1722         {
1723             $text = $Element['text'];
1724         }
1725         // very strongly consider an alternative if you're writing an
1726         // extension
1727         elseif (isset($Element['rawHtml']))
1728         {
1729             $text = $Element['rawHtml'];
1730
1731             $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1732             $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1733         }
1734
1735         $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
1736
1737         if ($hasContent)
1738         {
1739             $markup .= $hasName ? '>' : '';
1740
1741             if (isset($Element['elements']))
1742             {
1743                 $markup .= $this->elements($Element['elements']);
1744             }
1745             elseif (isset($Element['element']))
1746             {
1747                 $markup .= $this->element($Element['element']);
1748             }
1749             else
1750             {
1751                 if (!$permitRawHtml)
1752                 {
1753                     $markup .= self::escape($text, true);
1754                 }
1755                 else
1756                 {
1757                     $markup .= $text;
1758                 }
1759             }
1760
1761             $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
1762         }
1763         elseif ($hasName)
1764         {
1765             $markup .= ' />';
1766         }
1767
1768         return $markup;
1769     }
1770
1771     protected function elements(array $Elements)
1772     {
1773         $markup = '';
1774
1775         $autoBreak = true;
1776
1777         foreach ($Elements as $Element)
1778         {
1779             if (empty($Element))
1780             {
1781                 continue;
1782             }
1783
1784             $autoBreakNext = (isset($Element['autobreak'])
1785                 ? $Element['autobreak'] : isset($Element['name'])
1786             );
1787             // (autobreak === false) covers both sides of an element
1788             $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
1789
1790             $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
1791             $autoBreak = $autoBreakNext;
1792         }
1793
1794         $markup .= $autoBreak ? "\n" : '';
1795
1796         return $markup;
1797     }
1798
1799     # ~
1800
1801     protected function li($lines)
1802     {
1803         $Elements = $this->linesElements($lines);
1804
1805         if ( ! in_array('', $lines)
1806             and isset($Elements[0]) and isset($Elements[0]['name'])
1807             and $Elements[0]['name'] === 'p'
1808         ) {
1809             unset($Elements[0]['name']);
1810         }
1811
1812         return $Elements;
1813     }
1814
1815     #
1816     # AST Convenience
1817     #
1818
1819     /**
1820      * Replace occurrences $regexp with $Elements in $text. Return an array of
1821      * elements representing the replacement.
1822      */
1823     protected static function pregReplaceElements($regexp, $Elements, $text)
1824     {
1825         $newElements = array();
1826
1827         while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
1828         {
1829             $offset = $matches[0][1];
1830             $before = substr($text, 0, $offset);
1831             $after = substr($text, $offset + strlen($matches[0][0]));
1832
1833             $newElements[] = array('text' => $before);
1834
1835             foreach ($Elements as $Element)
1836             {
1837                 $newElements[] = $Element;
1838             }
1839
1840             $text = $after;
1841         }
1842
1843         $newElements[] = array('text' => $text);
1844
1845         return $newElements;
1846     }
1847
1848     #
1849     # Deprecated Methods
1850     #
1851
1852     /**
1853      * @deprecated use text() instead
1854      */
1855     function parse($text)
1856     {
1857         $markup = $this->text($text);
1858
1859         return $markup;
1860     }
1861
1862     protected function sanitiseElement(array $Element)
1863     {
1864         static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1865         static $safeUrlNameToAtt  = array(
1866             'a'   => 'href',
1867             'img' => 'src',
1868         );
1869
1870         if ( ! isset($Element['name']))
1871         {
1872             unset($Element['attributes']);
1873             return $Element;
1874         }
1875
1876         if (isset($safeUrlNameToAtt[$Element['name']]))
1877         {
1878             $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1879         }
1880
1881         if ( ! empty($Element['attributes']))
1882         {
1883             foreach ($Element['attributes'] as $att => $val)
1884             {
1885                 # filter out badly parsed attribute
1886                 if ( ! preg_match($goodAttribute, $att))
1887                 {
1888                     unset($Element['attributes'][$att]);
1889                 }
1890                 # dump onevent attribute
1891                 elseif (self::striAtStart($att, 'on'))
1892                 {
1893                     unset($Element['attributes'][$att]);
1894                 }
1895             }
1896         }
1897
1898         return $Element;
1899     }
1900
1901     protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1902     {
1903         foreach ($this->safeLinksWhitelist as $scheme)
1904         {
1905             if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1906             {
1907                 return $Element;
1908             }
1909         }
1910
1911         $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1912
1913         return $Element;
1914     }
1915
1916     #
1917     # Static Methods
1918     #
1919
1920     protected static function escape($text, $allowQuotes = false)
1921     {
1922         return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1923     }
1924
1925     protected static function striAtStart($string, $needle)
1926     {
1927         $len = strlen($needle);
1928
1929         if ($len > strlen($string))
1930         {
1931             return false;
1932         }
1933         else
1934         {
1935             return strtolower(substr($string, 0, $len)) === strtolower($needle);
1936         }
1937     }
1938
1939     static function instance($name = 'default')
1940     {
1941         if (isset(self::$instances[$name]))
1942         {
1943             return self::$instances[$name];
1944         }
1945
1946         $instance = new static();
1947
1948         self::$instances[$name] = $instance;
1949
1950         return $instance;
1951     }
1952
1953     private static $instances = array();
1954
1955     #
1956     # Fields
1957     #
1958
1959     protected $DefinitionData;
1960
1961     #
1962     # Read-Only
1963
1964     protected $specialCharacters = array(
1965         '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
1966     );
1967
1968     protected $StrongRegex = array(
1969         '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
1970         '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
1971     );
1972
1973     protected $EmRegex = array(
1974         '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1975         '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1976     );
1977
1978     protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
1979
1980     protected $voidElements = array(
1981         'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1982     );
1983
1984     protected $textLevelElements = array(
1985         'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1986         'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1987         'i', 'rp', 'del', 'code',          'strike', 'marquee',
1988         'q', 'rt', 'ins', 'font',          'strong',
1989         's', 'tt', 'kbd', 'mark',
1990         'u', 'xm', 'sub', 'nobr',
1991                    'sup', 'ruby',
1992                    'var', 'span',
1993                    'wbr', 'time',
1994     );
1995 }