File indexing completed on 2024-12-22 05:37:18

0001 <?php
0002 
0003 #
0004 #
0005 # Parsedown
0006 # http://parsedown.org
0007 #
0008 # (c) Emanuil Rusev
0009 # http://erusev.com
0010 #
0011 # For the full license information, view the LICENSE file that was distributed
0012 # with this source code.
0013 #
0014 #
0015 
0016 class Parsedown
0017 {
0018     # ~
0019 
0020     const version = '1.8.0-beta-5';
0021 
0022     # ~
0023 
0024     function text($text)
0025     {
0026         $Elements = $this->textElements($text);
0027 
0028         # convert to markup
0029         $markup = $this->elements($Elements);
0030 
0031         # trim line breaks
0032         $markup = trim($markup, "\n");
0033 
0034         return $markup;
0035     }
0036 
0037     protected function textElements($text)
0038     {
0039         # make sure no definitions are set
0040         $this->DefinitionData = array();
0041 
0042         # standardize line breaks
0043         $text = str_replace(array("\r\n", "\r"), "\n", $text);
0044 
0045         # remove surrounding line breaks
0046         $text = trim($text, "\n");
0047 
0048         # split text into lines
0049         $lines = explode("\n", $text);
0050 
0051         # iterate through lines to identify blocks
0052         return $this->linesElements($lines);
0053     }
0054 
0055     #
0056     # Setters
0057     #
0058 
0059     function setBreaksEnabled($breaksEnabled)
0060     {
0061         $this->breaksEnabled = $breaksEnabled;
0062 
0063         return $this;
0064     }
0065 
0066     protected $breaksEnabled;
0067 
0068     function setMarkupEscaped($markupEscaped)
0069     {
0070         $this->markupEscaped = $markupEscaped;
0071 
0072         return $this;
0073     }
0074 
0075     protected $markupEscaped;
0076 
0077     function setUrlsLinked($urlsLinked)
0078     {
0079         $this->urlsLinked = $urlsLinked;
0080 
0081         return $this;
0082     }
0083 
0084     protected $urlsLinked = true;
0085 
0086     function setSafeMode($safeMode)
0087     {
0088         $this->safeMode = (bool) $safeMode;
0089 
0090         return $this;
0091     }
0092 
0093     protected $safeMode;
0094 
0095     function setStrictMode($strictMode)
0096     {
0097         $this->strictMode = (bool) $strictMode;
0098 
0099         return $this;
0100     }
0101 
0102     protected $strictMode;
0103 
0104     protected $safeLinksWhitelist = array(
0105         'http://',
0106         'https://',
0107         'ftp://',
0108         'ftps://',
0109         'mailto:',
0110         'tel:',
0111         'data:image/png;base64,',
0112         'data:image/gif;base64,',
0113         'data:image/jpeg;base64,',
0114         'irc:',
0115         'ircs:',
0116         'git:',
0117         'ssh:',
0118         'news:',
0119         'steam:',
0120     );
0121 
0122     #
0123     # Lines
0124     #
0125 
0126     protected $BlockTypes = array(
0127         '#' => array('Header'),
0128         '*' => array('Rule', 'List'),
0129         '+' => array('List'),
0130         '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
0131         '0' => array('List'),
0132         '1' => array('List'),
0133         '2' => array('List'),
0134         '3' => array('List'),
0135         '4' => array('List'),
0136         '5' => array('List'),
0137         '6' => array('List'),
0138         '7' => array('List'),
0139         '8' => array('List'),
0140         '9' => array('List'),
0141         ':' => array('Table'),
0142         '<' => array('Comment', 'Markup'),
0143         '=' => array('SetextHeader'),
0144         '>' => array('Quote'),
0145         '[' => array('Reference'),
0146         '_' => array('Rule'),
0147         '`' => array('FencedCode'),
0148         '|' => array('Table'),
0149         '~' => array('FencedCode'),
0150     );
0151 
0152     # ~
0153 
0154     protected $unmarkedBlockTypes = array(
0155         'Code',
0156     );
0157 
0158     #
0159     # Blocks
0160     #
0161 
0162     protected function lines(array $lines)
0163     {
0164         return $this->elements($this->linesElements($lines));
0165     }
0166 
0167     protected function linesElements(array $lines)
0168     {
0169         $Elements = array();
0170         $CurrentBlock = null;
0171 
0172         foreach ($lines as $line)
0173         {
0174             if (chop($line) === '')
0175             {
0176                 if (isset($CurrentBlock))
0177                 {
0178                     $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
0179                         ? $CurrentBlock['interrupted'] + 1 : 1
0180                     );
0181                 }
0182 
0183                 continue;
0184             }
0185 
0186             while (($beforeTab = strstr($line, "\t", true)) !== false)
0187             {
0188                 $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
0189 
0190                 $line = $beforeTab
0191                     . str_repeat(' ', $shortage)
0192                     . substr($line, strlen($beforeTab) + 1)
0193                 ;
0194             }
0195 
0196             $indent = strspn($line, ' ');
0197 
0198             $text = $indent > 0 ? substr($line, $indent) : $line;
0199 
0200             # ~
0201 
0202             $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
0203 
0204             # ~
0205 
0206             if (isset($CurrentBlock['continuable']))
0207             {
0208                 $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
0209                 $Block = $this->$methodName($Line, $CurrentBlock);
0210 
0211                 if (isset($Block))
0212                 {
0213                     $CurrentBlock = $Block;
0214 
0215                     continue;
0216                 }
0217                 else
0218                 {
0219                     if ($this->isBlockCompletable($CurrentBlock['type']))
0220                     {
0221                         $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
0222                         $CurrentBlock = $this->$methodName($CurrentBlock);
0223                     }
0224                 }
0225             }
0226 
0227             # ~
0228 
0229             $marker = $text[0];
0230 
0231             # ~
0232 
0233             $blockTypes = $this->unmarkedBlockTypes;
0234 
0235             if (isset($this->BlockTypes[$marker]))
0236             {
0237                 foreach ($this->BlockTypes[$marker] as $blockType)
0238                 {
0239                     $blockTypes []= $blockType;
0240                 }
0241             }
0242 
0243             #
0244             # ~
0245 
0246             foreach ($blockTypes as $blockType)
0247             {
0248                 $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
0249 
0250                 if (isset($Block))
0251                 {
0252                     $Block['type'] = $blockType;
0253 
0254                     if ( ! isset($Block['identified']))
0255                     {
0256                         if (isset($CurrentBlock))
0257                         {
0258                             $Elements[] = $this->extractElement($CurrentBlock);
0259                         }
0260 
0261                         $Block['identified'] = true;
0262                     }
0263 
0264                     if ($this->isBlockContinuable($blockType))
0265                     {
0266                         $Block['continuable'] = true;
0267                     }
0268 
0269                     $CurrentBlock = $Block;
0270 
0271                     continue 2;
0272                 }
0273             }
0274 
0275             # ~
0276 
0277             if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph')
0278             {
0279                 $Block = $this->paragraphContinue($Line, $CurrentBlock);
0280             }
0281 
0282             if (isset($Block))
0283             {
0284                 $CurrentBlock = $Block;
0285             }
0286             else
0287             {
0288                 if (isset($CurrentBlock))
0289                 {
0290                     $Elements[] = $this->extractElement($CurrentBlock);
0291                 }
0292 
0293                 $CurrentBlock = $this->paragraph($Line);
0294 
0295                 $CurrentBlock['identified'] = true;
0296             }
0297         }
0298 
0299         # ~
0300 
0301         if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
0302         {
0303             $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
0304             $CurrentBlock = $this->$methodName($CurrentBlock);
0305         }
0306 
0307         # ~
0308 
0309         if (isset($CurrentBlock))
0310         {
0311             $Elements[] = $this->extractElement($CurrentBlock);
0312         }
0313 
0314         # ~
0315 
0316         return $Elements;
0317     }
0318 
0319     protected function extractElement(array $Component)
0320     {
0321         if ( ! isset($Component['element']))
0322         {
0323             if (isset($Component['markup']))
0324             {
0325                 $Component['element'] = array('rawHtml' => $Component['markup']);
0326             }
0327             elseif (isset($Component['hidden']))
0328             {
0329                 $Component['element'] = array();
0330             }
0331         }
0332 
0333         return $Component['element'];
0334     }
0335 
0336     protected function isBlockContinuable($Type)
0337     {
0338         return method_exists($this, 'block' . $Type . 'Continue');
0339     }
0340 
0341     protected function isBlockCompletable($Type)
0342     {
0343         return method_exists($this, 'block' . $Type . 'Complete');
0344     }
0345 
0346     #
0347     # Code
0348 
0349     protected function blockCode($Line, $Block = null)
0350     {
0351         if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted']))
0352         {
0353             return;
0354         }
0355 
0356         if ($Line['indent'] >= 4)
0357         {
0358             $text = substr($Line['body'], 4);
0359 
0360             $Block = array(
0361                 'element' => array(
0362                     'name' => 'pre',
0363                     'element' => array(
0364                         'name' => 'code',
0365                         'text' => $text,
0366                     ),
0367                 ),
0368             );
0369 
0370             return $Block;
0371         }
0372     }
0373 
0374     protected function blockCodeContinue($Line, $Block)
0375     {
0376         if ($Line['indent'] >= 4)
0377         {
0378             if (isset($Block['interrupted']))
0379             {
0380                 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
0381 
0382                 unset($Block['interrupted']);
0383             }
0384 
0385             $Block['element']['element']['text'] .= "\n";
0386 
0387             $text = substr($Line['body'], 4);
0388 
0389             $Block['element']['element']['text'] .= $text;
0390 
0391             return $Block;
0392         }
0393     }
0394 
0395     protected function blockCodeComplete($Block)
0396     {
0397         return $Block;
0398     }
0399 
0400     #
0401     # Comment
0402 
0403     protected function blockComment($Line)
0404     {
0405         if ($this->markupEscaped or $this->safeMode)
0406         {
0407             return;
0408         }
0409 
0410         if (strpos($Line['text'], '<!--') === 0)
0411         {
0412             $Block = array(
0413                 'element' => array(
0414                     'rawHtml' => $Line['body'],
0415                     'autobreak' => true,
0416                 ),
0417             );
0418 
0419             if (strpos($Line['text'], '-->') !== false)
0420             {
0421                 $Block['closed'] = true;
0422             }
0423 
0424             return $Block;
0425         }
0426     }
0427 
0428     protected function blockCommentContinue($Line, array $Block)
0429     {
0430         if (isset($Block['closed']))
0431         {
0432             return;
0433         }
0434 
0435         $Block['element']['rawHtml'] .= "\n" . $Line['body'];
0436 
0437         if (strpos($Line['text'], '-->') !== false)
0438         {
0439             $Block['closed'] = true;
0440         }
0441 
0442         return $Block;
0443     }
0444 
0445     #
0446     # Fenced Code
0447 
0448     protected function blockFencedCode($Line)
0449     {
0450         $marker = $Line['text'][0];
0451 
0452         $openerLength = strspn($Line['text'], $marker);
0453 
0454         if ($openerLength < 3)
0455         {
0456             return;
0457         }
0458 
0459         $infostring = trim(substr($Line['text'], $openerLength), "\t ");
0460 
0461         if (strpos($infostring, '`') !== false)
0462         {
0463             return;
0464         }
0465 
0466         $Element = array(
0467             'name' => 'code',
0468             'text' => '',
0469         );
0470 
0471         if ($infostring !== '')
0472         {
0473             $Element['attributes'] = array('class' => "language-$infostring");
0474         }
0475 
0476         $Block = array(
0477             'char' => $marker,
0478             'openerLength' => $openerLength,
0479             'element' => array(
0480                 'name' => 'pre',
0481                 'element' => $Element,
0482             ),
0483         );
0484 
0485         return $Block;
0486     }
0487 
0488     protected function blockFencedCodeContinue($Line, $Block)
0489     {
0490         if (isset($Block['complete']))
0491         {
0492             return;
0493         }
0494 
0495         if (isset($Block['interrupted']))
0496         {
0497             $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
0498 
0499             unset($Block['interrupted']);
0500         }
0501 
0502         if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
0503             and chop(substr($Line['text'], $len), ' ') === ''
0504         ) {
0505             $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
0506 
0507             $Block['complete'] = true;
0508 
0509             return $Block;
0510         }
0511 
0512         $Block['element']['element']['text'] .= "\n" . $Line['body'];
0513 
0514         return $Block;
0515     }
0516 
0517     protected function blockFencedCodeComplete($Block)
0518     {
0519         return $Block;
0520     }
0521 
0522     #
0523     # Header
0524 
0525     protected function blockHeader($Line)
0526     {
0527         $level = strspn($Line['text'], '#');
0528 
0529         if ($level > 6)
0530         {
0531             return;
0532         }
0533 
0534         $text = trim($Line['text'], '#');
0535 
0536         if ($this->strictMode and isset($text[0]) and $text[0] !== ' ')
0537         {
0538             return;
0539         }
0540 
0541         $text = trim($text, ' ');
0542 
0543         $Block = array(
0544             'element' => array(
0545                 'name' => 'h' . $level,
0546                 'handler' => array(
0547                     'function' => 'lineElements',
0548                     'argument' => $text,
0549                     'destination' => 'elements',
0550                 )
0551             ),
0552         );
0553 
0554         return $Block;
0555     }
0556 
0557     #
0558     # List
0559 
0560     protected function blockList($Line, array $CurrentBlock = null)
0561     {
0562         list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
0563 
0564         if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
0565         {
0566             $contentIndent = strlen($matches[2]);
0567 
0568             if ($contentIndent >= 5)
0569             {
0570                 $contentIndent -= 1;
0571                 $matches[1] = substr($matches[1], 0, -$contentIndent);
0572                 $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
0573             }
0574             elseif ($contentIndent === 0)
0575             {
0576                 $matches[1] .= ' ';
0577             }
0578 
0579             $markerWithoutWhitespace = strstr($matches[1], ' ', true);
0580 
0581             $Block = array(
0582                 'indent' => $Line['indent'],
0583                 'pattern' => $pattern,
0584                 'data' => array(
0585                     'type' => $name,
0586                     'marker' => $matches[1],
0587                     'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
0588                 ),
0589                 'element' => array(
0590                     'name' => $name,
0591                     'elements' => array(),
0592                 ),
0593             );
0594             $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
0595 
0596             if ($name === 'ol')
0597             {
0598                 $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
0599 
0600                 if ($listStart !== '1')
0601                 {
0602                     if (
0603                         isset($CurrentBlock)
0604                         and $CurrentBlock['type'] === 'Paragraph'
0605                         and ! isset($CurrentBlock['interrupted'])
0606                     ) {
0607                         return;
0608                     }
0609 
0610                     $Block['element']['attributes'] = array('start' => $listStart);
0611                 }
0612             }
0613 
0614             $Block['li'] = array(
0615                 'name' => 'li',
0616                 'handler' => array(
0617                     'function' => 'li',
0618                     'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
0619                     'destination' => 'elements'
0620                 )
0621             );
0622 
0623             $Block['element']['elements'] []= & $Block['li'];
0624 
0625             return $Block;
0626         }
0627     }
0628 
0629     protected function blockListContinue($Line, array $Block)
0630     {
0631         if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument']))
0632         {
0633             return null;
0634         }
0635 
0636         $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
0637 
0638         if ($Line['indent'] < $requiredIndent
0639             and (
0640                 (
0641                     $Block['data']['type'] === 'ol'
0642                     and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
0643                 ) or (
0644                     $Block['data']['type'] === 'ul'
0645                     and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
0646                 )
0647             )
0648         ) {
0649             if (isset($Block['interrupted']))
0650             {
0651                 $Block['li']['handler']['argument'] []= '';
0652 
0653                 $Block['loose'] = true;
0654 
0655                 unset($Block['interrupted']);
0656             }
0657 
0658             unset($Block['li']);
0659 
0660             $text = isset($matches[1]) ? $matches[1] : '';
0661 
0662             $Block['indent'] = $Line['indent'];
0663 
0664             $Block['li'] = array(
0665                 'name' => 'li',
0666                 'handler' => array(
0667                     'function' => 'li',
0668                     'argument' => array($text),
0669                     'destination' => 'elements'
0670                 )
0671             );
0672 
0673             $Block['element']['elements'] []= & $Block['li'];
0674 
0675             return $Block;
0676         }
0677         elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line))
0678         {
0679             return null;
0680         }
0681 
0682         if ($Line['text'][0] === '[' and $this->blockReference($Line))
0683         {
0684             return $Block;
0685         }
0686 
0687         if ($Line['indent'] >= $requiredIndent)
0688         {
0689             if (isset($Block['interrupted']))
0690             {
0691                 $Block['li']['handler']['argument'] []= '';
0692 
0693                 $Block['loose'] = true;
0694 
0695                 unset($Block['interrupted']);
0696             }
0697 
0698             $text = substr($Line['body'], $requiredIndent);
0699 
0700             $Block['li']['handler']['argument'] []= $text;
0701 
0702             return $Block;
0703         }
0704 
0705         if ( ! isset($Block['interrupted']))
0706         {
0707             $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
0708 
0709             $Block['li']['handler']['argument'] []= $text;
0710 
0711             return $Block;
0712         }
0713     }
0714 
0715     protected function blockListComplete(array $Block)
0716     {
0717         if (isset($Block['loose']))
0718         {
0719             foreach ($Block['element']['elements'] as &$li)
0720             {
0721                 if (end($li['handler']['argument']) !== '')
0722                 {
0723                     $li['handler']['argument'] []= '';
0724                 }
0725             }
0726         }
0727 
0728         return $Block;
0729     }
0730 
0731     #
0732     # Quote
0733 
0734     protected function blockQuote($Line)
0735     {
0736         if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
0737         {
0738             $Block = array(
0739                 'element' => array(
0740                     'name' => 'blockquote',
0741                     'handler' => array(
0742                         'function' => 'linesElements',
0743                         'argument' => (array) $matches[1],
0744                         'destination' => 'elements',
0745                     )
0746                 ),
0747             );
0748 
0749             return $Block;
0750         }
0751     }
0752 
0753     protected function blockQuoteContinue($Line, array $Block)
0754     {
0755         if (isset($Block['interrupted']))
0756         {
0757             return;
0758         }
0759 
0760         if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
0761         {
0762             $Block['element']['handler']['argument'] []= $matches[1];
0763 
0764             return $Block;
0765         }
0766 
0767         if ( ! isset($Block['interrupted']))
0768         {
0769             $Block['element']['handler']['argument'] []= $Line['text'];
0770 
0771             return $Block;
0772         }
0773     }
0774 
0775     #
0776     # Rule
0777 
0778     protected function blockRule($Line)
0779     {
0780         $marker = $Line['text'][0];
0781 
0782         if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '')
0783         {
0784             $Block = array(
0785                 'element' => array(
0786                     'name' => 'hr',
0787                 ),
0788             );
0789 
0790             return $Block;
0791         }
0792     }
0793 
0794     #
0795     # Setext
0796 
0797     protected function blockSetextHeader($Line, array $Block = null)
0798     {
0799         if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
0800         {
0801             return;
0802         }
0803 
0804         if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '')
0805         {
0806             $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
0807 
0808             return $Block;
0809         }
0810     }
0811 
0812     #
0813     # Markup
0814 
0815     protected function blockMarkup($Line)
0816     {
0817         if ($this->markupEscaped or $this->safeMode)
0818         {
0819             return;
0820         }
0821 
0822         if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches))
0823         {
0824             $element = strtolower($matches[1]);
0825 
0826             if (in_array($element, $this->textLevelElements))
0827             {
0828                 return;
0829             }
0830 
0831             $Block = array(
0832                 'name' => $matches[1],
0833                 'element' => array(
0834                     'rawHtml' => $Line['text'],
0835                     'autobreak' => true,
0836                 ),
0837             );
0838 
0839             return $Block;
0840         }
0841     }
0842 
0843     protected function blockMarkupContinue($Line, array $Block)
0844     {
0845         if (isset($Block['closed']) or isset($Block['interrupted']))
0846         {
0847             return;
0848         }
0849 
0850         $Block['element']['rawHtml'] .= "\n" . $Line['body'];
0851 
0852         return $Block;
0853     }
0854 
0855     #
0856     # Reference
0857 
0858     protected function blockReference($Line)
0859     {
0860         if (strpos($Line['text'], ']') !== false
0861             and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
0862         ) {
0863             $id = strtolower($matches[1]);
0864 
0865             $Data = array(
0866                 'url' => $matches[2],
0867                 'title' => isset($matches[3]) ? $matches[3] : null,
0868             );
0869 
0870             $this->DefinitionData['Reference'][$id] = $Data;
0871 
0872             $Block = array(
0873                 'element' => array(),
0874             );
0875 
0876             return $Block;
0877         }
0878     }
0879 
0880     #
0881     # Table
0882 
0883     protected function blockTable($Line, array $Block = null)
0884     {
0885         if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
0886         {
0887             return;
0888         }
0889 
0890         if (
0891             strpos($Block['element']['handler']['argument'], '|') === false
0892             and strpos($Line['text'], '|') === false
0893             and strpos($Line['text'], ':') === false
0894             or strpos($Block['element']['handler']['argument'], "\n") !== false
0895         ) {
0896             return;
0897         }
0898 
0899         if (chop($Line['text'], ' -:|') !== '')
0900         {
0901             return;
0902         }
0903 
0904         $alignments = array();
0905 
0906         $divider = $Line['text'];
0907 
0908         $divider = trim($divider);
0909         $divider = trim($divider, '|');
0910 
0911         $dividerCells = explode('|', $divider);
0912 
0913         foreach ($dividerCells as $dividerCell)
0914         {
0915             $dividerCell = trim($dividerCell);
0916 
0917             if ($dividerCell === '')
0918             {
0919                 return;
0920             }
0921 
0922             $alignment = null;
0923 
0924             if ($dividerCell[0] === ':')
0925             {
0926                 $alignment = 'left';
0927             }
0928 
0929             if (substr($dividerCell, - 1) === ':')
0930             {
0931                 $alignment = $alignment === 'left' ? 'center' : 'right';
0932             }
0933 
0934             $alignments []= $alignment;
0935         }
0936 
0937         # ~
0938 
0939         $HeaderElements = array();
0940 
0941         $header = $Block['element']['handler']['argument'];
0942 
0943         $header = trim($header);
0944         $header = trim($header, '|');
0945 
0946         $headerCells = explode('|', $header);
0947 
0948         if (count($headerCells) !== count($alignments))
0949         {
0950             return;
0951         }
0952 
0953         foreach ($headerCells as $index => $headerCell)
0954         {
0955             $headerCell = trim($headerCell);
0956 
0957             $HeaderElement = array(
0958                 'name' => 'th',
0959                 'handler' => array(
0960                     'function' => 'lineElements',
0961                     'argument' => $headerCell,
0962                     'destination' => 'elements',
0963                 )
0964             );
0965 
0966             if (isset($alignments[$index]))
0967             {
0968                 $alignment = $alignments[$index];
0969 
0970                 $HeaderElement['attributes'] = array(
0971                     'style' => "text-align: $alignment;",
0972                 );
0973             }
0974 
0975             $HeaderElements []= $HeaderElement;
0976         }
0977 
0978         # ~
0979 
0980         $Block = array(
0981             'alignments' => $alignments,
0982             'identified' => true,
0983             'element' => array(
0984                 'name' => 'table',
0985                 'elements' => array(),
0986             ),
0987         );
0988 
0989         $Block['element']['elements'] []= array(
0990             'name' => 'thead',
0991         );
0992 
0993         $Block['element']['elements'] []= array(
0994             'name' => 'tbody',
0995             'elements' => array(),
0996         );
0997 
0998         $Block['element']['elements'][0]['elements'] []= array(
0999             'name' => 'tr',
1000             'elements' => $HeaderElements,
1001         );
1002 
1003         return $Block;
1004     }
1005 
1006     protected function blockTableContinue($Line, array $Block)
1007     {
1008         if (isset($Block['interrupted']))
1009         {
1010             return;
1011         }
1012 
1013         if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|'))
1014         {
1015             $Elements = array();
1016 
1017             $row = $Line['text'];
1018 
1019             $row = trim($row);
1020             $row = trim($row, '|');
1021 
1022             preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
1023 
1024             $cells = array_slice($matches[0], 0, count($Block['alignments']));
1025 
1026             foreach ($cells as $index => $cell)
1027             {
1028                 $cell = trim($cell);
1029 
1030                 $Element = array(
1031                     'name' => 'td',
1032                     'handler' => array(
1033                         'function' => 'lineElements',
1034                         'argument' => $cell,
1035                         'destination' => 'elements',
1036                     )
1037                 );
1038 
1039                 if (isset($Block['alignments'][$index]))
1040                 {
1041                     $Element['attributes'] = array(
1042                         'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
1043                     );
1044                 }
1045 
1046                 $Elements []= $Element;
1047             }
1048 
1049             $Element = array(
1050                 'name' => 'tr',
1051                 'elements' => $Elements,
1052             );
1053 
1054             $Block['element']['elements'][1]['elements'] []= $Element;
1055 
1056             return $Block;
1057         }
1058     }
1059 
1060     #
1061     # ~
1062     #
1063 
1064     protected function paragraph($Line)
1065     {
1066         return array(
1067             'type' => 'Paragraph',
1068             'element' => array(
1069                 'name' => 'p',
1070                 'handler' => array(
1071                     'function' => 'lineElements',
1072                     'argument' => $Line['text'],
1073                     'destination' => 'elements',
1074                 ),
1075             ),
1076         );
1077     }
1078 
1079     protected function paragraphContinue($Line, array $Block)
1080     {
1081         if (isset($Block['interrupted']))
1082         {
1083             return;
1084         }
1085 
1086         $Block['element']['handler']['argument'] .= "\n".$Line['text'];
1087 
1088         return $Block;
1089     }
1090 
1091     #
1092     # Inline Elements
1093     #
1094 
1095     protected $InlineTypes = array(
1096         '!' => array('Image'),
1097         '&' => array('SpecialCharacter'),
1098         '*' => array('Emphasis'),
1099         ':' => array('Url'),
1100         '<' => array('UrlTag', 'EmailTag', 'Markup'),
1101         '[' => array('Link'),
1102         '_' => array('Emphasis'),
1103         '`' => array('Code'),
1104         '~' => array('Strikethrough'),
1105         '\\' => array('EscapeSequence'),
1106     );
1107 
1108     # ~
1109 
1110     protected $inlineMarkerList = '!*_&[:<`~\\';
1111 
1112     #
1113     # ~
1114     #
1115 
1116     public function line($text, $nonNestables = array())
1117     {
1118         return $this->elements($this->lineElements($text, $nonNestables));
1119     }
1120 
1121     protected function lineElements($text, $nonNestables = array())
1122     {
1123         # standardize line breaks
1124         $text = str_replace(array("\r\n", "\r"), "\n", $text);
1125 
1126         $Elements = array();
1127 
1128         $nonNestables = (empty($nonNestables)
1129             ? array()
1130             : array_combine($nonNestables, $nonNestables)
1131         );
1132 
1133         # $excerpt is based on the first occurrence of a marker
1134 
1135         while ($excerpt = strpbrk($text, $this->inlineMarkerList))
1136         {
1137             $marker = $excerpt[0];
1138 
1139             $markerPosition = strlen($text) - strlen($excerpt);
1140 
1141             $Excerpt = array('text' => $excerpt, 'context' => $text);
1142 
1143             foreach ($this->InlineTypes[$marker] as $inlineType)
1144             {
1145                 # check to see if the current inline type is nestable in the current context
1146 
1147                 if (isset($nonNestables[$inlineType]))
1148                 {
1149                     continue;
1150                 }
1151 
1152                 $Inline = $this->{"inline$inlineType"}($Excerpt);
1153 
1154                 if ( ! isset($Inline))
1155                 {
1156                     continue;
1157                 }
1158 
1159                 # makes sure that the inline belongs to "our" marker
1160 
1161                 if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
1162                 {
1163                     continue;
1164                 }
1165 
1166                 # sets a default inline position
1167 
1168                 if ( ! isset($Inline['position']))
1169                 {
1170                     $Inline['position'] = $markerPosition;
1171                 }
1172 
1173                 # cause the new element to 'inherit' our non nestables
1174 
1175 
1176                 $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
1177                     ? array_merge($Inline['element']['nonNestables'], $nonNestables)
1178                     : $nonNestables
1179                 ;
1180 
1181                 # the text that comes before the inline
1182                 $unmarkedText = substr($text, 0, $Inline['position']);
1183 
1184                 # compile the unmarked text
1185                 $InlineText = $this->inlineText($unmarkedText);
1186                 $Elements[] = $InlineText['element'];
1187 
1188                 # compile the inline
1189                 $Elements[] = $this->extractElement($Inline);
1190 
1191                 # remove the examined text
1192                 $text = substr($text, $Inline['position'] + $Inline['extent']);
1193 
1194                 continue 2;
1195             }
1196 
1197             # the marker does not belong to an inline
1198 
1199             $unmarkedText = substr($text, 0, $markerPosition + 1);
1200 
1201             $InlineText = $this->inlineText($unmarkedText);
1202             $Elements[] = $InlineText['element'];
1203 
1204             $text = substr($text, $markerPosition + 1);
1205         }
1206 
1207         $InlineText = $this->inlineText($text);
1208         $Elements[] = $InlineText['element'];
1209 
1210         foreach ($Elements as &$Element)
1211         {
1212             if ( ! isset($Element['autobreak']))
1213             {
1214                 $Element['autobreak'] = false;
1215             }
1216         }
1217 
1218         return $Elements;
1219     }
1220 
1221     #
1222     # ~
1223     #
1224 
1225     protected function inlineText($text)
1226     {
1227         $Inline = array(
1228             'extent' => strlen($text),
1229             'element' => array(),
1230         );
1231 
1232         $Inline['element']['elements'] = self::pregReplaceElements(
1233             $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
1234             array(
1235                 array('name' => 'br'),
1236                 array('text' => "\n"),
1237             ),
1238             $text
1239         );
1240 
1241         return $Inline;
1242     }
1243 
1244     protected function inlineCode($Excerpt)
1245     {
1246         $marker = $Excerpt['text'][0];
1247 
1248         if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
1249         {
1250             $text = $matches[2];
1251             $text = preg_replace('/[ ]*+\n/', ' ', $text);
1252 
1253             return array(
1254                 'extent' => strlen($matches[0]),
1255                 'element' => array(
1256                     'name' => 'code',
1257                     'text' => $text,
1258                 ),
1259             );
1260         }
1261     }
1262 
1263     protected function inlineEmailTag($Excerpt)
1264     {
1265         $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
1266 
1267         $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
1268             . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
1269 
1270         if (strpos($Excerpt['text'], '>') !== false
1271             and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
1272         ){
1273             $url = $matches[1];
1274 
1275             if ( ! isset($matches[2]))
1276             {
1277                 $url = "mailto:$url";
1278             }
1279 
1280             return array(
1281                 'extent' => strlen($matches[0]),
1282                 'element' => array(
1283                     'name' => 'a',
1284                     'text' => $matches[1],
1285                     'attributes' => array(
1286                         'href' => $url,
1287                     ),
1288                 ),
1289             );
1290         }
1291     }
1292 
1293     protected function inlineEmphasis($Excerpt)
1294     {
1295         if ( ! isset($Excerpt['text'][1]))
1296         {
1297             return;
1298         }
1299 
1300         $marker = $Excerpt['text'][0];
1301 
1302         if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
1303         {
1304             $emphasis = 'strong';
1305         }
1306         elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
1307         {
1308             $emphasis = 'em';
1309         }
1310         else
1311         {
1312             return;
1313         }
1314 
1315         return array(
1316             'extent' => strlen($matches[0]),
1317             'element' => array(
1318                 'name' => $emphasis,
1319                 'handler' => array(
1320                     'function' => 'lineElements',
1321                     'argument' => $matches[1],
1322                     'destination' => 'elements',
1323                 )
1324             ),
1325         );
1326     }
1327 
1328     protected function inlineEscapeSequence($Excerpt)
1329     {
1330         if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
1331         {
1332             return array(
1333                 'element' => array('rawHtml' => $Excerpt['text'][1]),
1334                 'extent' => 2,
1335             );
1336         }
1337     }
1338 
1339     protected function inlineImage($Excerpt)
1340     {
1341         if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
1342         {
1343             return;
1344         }
1345 
1346         $Excerpt['text']= substr($Excerpt['text'], 1);
1347 
1348         $Link = $this->inlineLink($Excerpt);
1349 
1350         if ($Link === null)
1351         {
1352             return;
1353         }
1354 
1355         $Inline = array(
1356             'extent' => $Link['extent'] + 1,
1357             'element' => array(
1358                 'name' => 'img',
1359                 'attributes' => array(
1360                     'src' => $Link['element']['attributes']['href'],
1361                     'alt' => $Link['element']['handler']['argument'],
1362                 ),
1363                 'autobreak' => true,
1364             ),
1365         );
1366 
1367         $Inline['element']['attributes'] += $Link['element']['attributes'];
1368 
1369         unset($Inline['element']['attributes']['href']);
1370 
1371         return $Inline;
1372     }
1373 
1374     protected function inlineLink($Excerpt)
1375     {
1376         $Element = array(
1377             'name' => 'a',
1378             'handler' => array(
1379                 'function' => 'lineElements',
1380                 'argument' => null,
1381                 'destination' => 'elements',
1382             ),
1383             'nonNestables' => array('Url', 'Link'),
1384             'attributes' => array(
1385                 'href' => null,
1386                 'title' => null,
1387             ),
1388         );
1389 
1390         $extent = 0;
1391 
1392         $remainder = $Excerpt['text'];
1393 
1394         if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
1395         {
1396             $Element['handler']['argument'] = $matches[1];
1397 
1398             $extent += strlen($matches[0]);
1399 
1400             $remainder = substr($remainder, $extent);
1401         }
1402         else
1403         {
1404             return;
1405         }
1406 
1407         if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
1408         {
1409             $Element['attributes']['href'] = $matches[1];
1410 
1411             if (isset($matches[2]))
1412             {
1413                 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
1414             }
1415 
1416             $extent += strlen($matches[0]);
1417         }
1418         else
1419         {
1420             if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
1421             {
1422                 $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
1423                 $definition = strtolower($definition);
1424 
1425                 $extent += strlen($matches[0]);
1426             }
1427             else
1428             {
1429                 $definition = strtolower($Element['handler']['argument']);
1430             }
1431 
1432             if ( ! isset($this->DefinitionData['Reference'][$definition]))
1433             {
1434                 return;
1435             }
1436 
1437             $Definition = $this->DefinitionData['Reference'][$definition];
1438 
1439             $Element['attributes']['href'] = $Definition['url'];
1440             $Element['attributes']['title'] = $Definition['title'];
1441         }
1442 
1443         return array(
1444             'extent' => $extent,
1445             'element' => $Element,
1446         );
1447     }
1448 
1449     protected function inlineMarkup($Excerpt)
1450     {
1451         if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
1452         {
1453             return;
1454         }
1455 
1456         if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
1457         {
1458             return array(
1459                 'element' => array('rawHtml' => $matches[0]),
1460                 'extent' => strlen($matches[0]),
1461             );
1462         }
1463 
1464         if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches))
1465         {
1466             return array(
1467                 'element' => array('rawHtml' => $matches[0]),
1468                 'extent' => strlen($matches[0]),
1469             );
1470         }
1471 
1472         if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
1473         {
1474             return array(
1475                 'element' => array('rawHtml' => $matches[0]),
1476                 'extent' => strlen($matches[0]),
1477             );
1478         }
1479     }
1480 
1481     protected function inlineSpecialCharacter($Excerpt)
1482     {
1483         if ($Excerpt['text'][1] !== ' ' and strpos($Excerpt['text'], ';') !== false
1484             and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
1485         ) {
1486             return array(
1487                 'element' => array('rawHtml' => '&' . $matches[1] . ';'),
1488                 'extent' => strlen($matches[0]),
1489             );
1490         }
1491 
1492         return;
1493     }
1494 
1495     protected function inlineStrikethrough($Excerpt)
1496     {
1497         if ( ! isset($Excerpt['text'][1]))
1498         {
1499             return;
1500         }
1501 
1502         if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
1503         {
1504             return array(
1505                 'extent' => strlen($matches[0]),
1506                 'element' => array(
1507                     'name' => 'del',
1508                     'handler' => array(
1509                         'function' => 'lineElements',
1510                         'argument' => $matches[1],
1511                         'destination' => 'elements',
1512                     )
1513                 ),
1514             );
1515         }
1516     }
1517 
1518     protected function inlineUrl($Excerpt)
1519     {
1520         if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
1521         {
1522             return;
1523         }
1524 
1525         if (strpos($Excerpt['context'], 'http') !== false
1526             and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
1527         ) {
1528             $url = $matches[0][0];
1529 
1530             $Inline = array(
1531                 'extent' => strlen($matches[0][0]),
1532                 'position' => $matches[0][1],
1533                 'element' => array(
1534                     'name' => 'a',
1535                     'text' => $url,
1536                     'attributes' => array(
1537                         'href' => $url,
1538                     ),
1539                 ),
1540             );
1541 
1542             return $Inline;
1543         }
1544     }
1545 
1546     protected function inlineUrlTag($Excerpt)
1547     {
1548         if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
1549         {
1550             $url = $matches[1];
1551 
1552             return array(
1553                 'extent' => strlen($matches[0]),
1554                 'element' => array(
1555                     'name' => 'a',
1556                     'text' => $url,
1557                     'attributes' => array(
1558                         'href' => $url,
1559                     ),
1560                 ),
1561             );
1562         }
1563     }
1564 
1565     # ~
1566 
1567     protected function unmarkedText($text)
1568     {
1569         $Inline = $this->inlineText($text);
1570         return $this->element($Inline['element']);
1571     }
1572 
1573     #
1574     # Handlers
1575     #
1576 
1577     protected function handle(array $Element)
1578     {
1579         if (isset($Element['handler']))
1580         {
1581             if (!isset($Element['nonNestables']))
1582             {
1583                 $Element['nonNestables'] = array();
1584             }
1585 
1586             if (is_string($Element['handler']))
1587             {
1588                 $function = $Element['handler'];
1589                 $argument = $Element['text'];
1590                 unset($Element['text']);
1591                 $destination = 'rawHtml';
1592             }
1593             else
1594             {
1595                 $function = $Element['handler']['function'];
1596                 $argument = $Element['handler']['argument'];
1597                 $destination = $Element['handler']['destination'];
1598             }
1599 
1600             $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
1601 
1602             if ($destination === 'handler')
1603             {
1604                 $Element = $this->handle($Element);
1605             }
1606 
1607             unset($Element['handler']);
1608         }
1609 
1610         return $Element;
1611     }
1612 
1613     protected function handleElementRecursive(array $Element)
1614     {
1615         return $this->elementApplyRecursive(array($this, 'handle'), $Element);
1616     }
1617 
1618     protected function handleElementsRecursive(array $Elements)
1619     {
1620         return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
1621     }
1622 
1623     protected function elementApplyRecursive($closure, array $Element)
1624     {
1625         $Element = call_user_func($closure, $Element);
1626 
1627         if (isset($Element['elements']))
1628         {
1629             $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
1630         }
1631         elseif (isset($Element['element']))
1632         {
1633             $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
1634         }
1635 
1636         return $Element;
1637     }
1638 
1639     protected function elementApplyRecursiveDepthFirst($closure, array $Element)
1640     {
1641         if (isset($Element['elements']))
1642         {
1643             $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
1644         }
1645         elseif (isset($Element['element']))
1646         {
1647             $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
1648         }
1649 
1650         $Element = call_user_func($closure, $Element);
1651 
1652         return $Element;
1653     }
1654 
1655     protected function elementsApplyRecursive($closure, array $Elements)
1656     {
1657         foreach ($Elements as &$Element)
1658         {
1659             $Element = $this->elementApplyRecursive($closure, $Element);
1660         }
1661 
1662         return $Elements;
1663     }
1664 
1665     protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
1666     {
1667         foreach ($Elements as &$Element)
1668         {
1669             $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
1670         }
1671 
1672         return $Elements;
1673     }
1674 
1675     protected function element(array $Element)
1676     {
1677         if ($this->safeMode)
1678         {
1679             $Element = $this->sanitiseElement($Element);
1680         }
1681 
1682         # identity map if element has no handler
1683         $Element = $this->handle($Element);
1684 
1685         $hasName = isset($Element['name']);
1686 
1687         $markup = '';
1688 
1689         if ($hasName)
1690         {
1691             $markup .= '<' . $Element['name'];
1692 
1693             if (isset($Element['attributes']))
1694             {
1695                 foreach ($Element['attributes'] as $name => $value)
1696                 {
1697                     if ($value === null)
1698                     {
1699                         continue;
1700                     }
1701 
1702                     $markup .= " $name=\"".self::escape($value).'"';
1703                 }
1704             }
1705         }
1706 
1707         $permitRawHtml = false;
1708 
1709         if (isset($Element['text']))
1710         {
1711             $text = $Element['text'];
1712         }
1713         // very strongly consider an alternative if you're writing an
1714         // extension
1715         elseif (isset($Element['rawHtml']))
1716         {
1717             $text = $Element['rawHtml'];
1718 
1719             $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
1720             $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
1721         }
1722 
1723         $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
1724 
1725         if ($hasContent)
1726         {
1727             $markup .= $hasName ? '>' : '';
1728 
1729             if (isset($Element['elements']))
1730             {
1731                 $markup .= $this->elements($Element['elements']);
1732             }
1733             elseif (isset($Element['element']))
1734             {
1735                 $markup .= $this->element($Element['element']);
1736             }
1737             else
1738             {
1739                 if (!$permitRawHtml)
1740                 {
1741                     $markup .= self::escape($text, true);
1742                 }
1743                 else
1744                 {
1745                     $markup .= $text;
1746                 }
1747             }
1748 
1749             $markup .= $hasName ? '</' . $Element['name'] . '>' : '';
1750         }
1751         elseif ($hasName)
1752         {
1753             $markup .= ' />';
1754         }
1755 
1756         return $markup;
1757     }
1758 
1759     protected function elements(array $Elements)
1760     {
1761         $markup = '';
1762 
1763         $autoBreak = true;
1764 
1765         foreach ($Elements as $Element)
1766         {
1767             if (empty($Element))
1768             {
1769                 continue;
1770             }
1771 
1772             $autoBreakNext = (isset($Element['autobreak'])
1773                 ? $Element['autobreak'] : isset($Element['name'])
1774             );
1775             // (autobreak === false) covers both sides of an element
1776             $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
1777 
1778             $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
1779             $autoBreak = $autoBreakNext;
1780         }
1781 
1782         $markup .= $autoBreak ? "\n" : '';
1783 
1784         return $markup;
1785     }
1786 
1787     # ~
1788 
1789     protected function li($lines)
1790     {
1791         $Elements = $this->linesElements($lines);
1792 
1793         if ( ! in_array('', $lines)
1794             and isset($Elements[0]) and isset($Elements[0]['name'])
1795             and $Elements[0]['name'] === 'p'
1796         ) {
1797             unset($Elements[0]['name']);
1798         }
1799 
1800         return $Elements;
1801     }
1802 
1803     #
1804     # AST Convenience
1805     #
1806 
1807     /**
1808      * Replace occurrences $regexp with $Elements in $text. Return an array of
1809      * elements representing the replacement.
1810      */
1811     protected static function pregReplaceElements($regexp, $Elements, $text)
1812     {
1813         $newElements = array();
1814 
1815         while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
1816         {
1817             $offset = $matches[0][1];
1818             $before = substr($text, 0, $offset);
1819             $after = substr($text, $offset + strlen($matches[0][0]));
1820 
1821             $newElements[] = array('text' => $before);
1822 
1823             foreach ($Elements as $Element)
1824             {
1825                 $newElements[] = $Element;
1826             }
1827 
1828             $text = $after;
1829         }
1830 
1831         $newElements[] = array('text' => $text);
1832 
1833         return $newElements;
1834     }
1835 
1836     #
1837     # Deprecated Methods
1838     #
1839 
1840     function parse($text)
1841     {
1842         $markup = $this->text($text);
1843 
1844         return $markup;
1845     }
1846 
1847     protected function sanitiseElement(array $Element)
1848     {
1849         static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
1850         static $safeUrlNameToAtt  = array(
1851             'a'   => 'href',
1852             'img' => 'src',
1853         );
1854 
1855         if ( ! isset($Element['name']))
1856         {
1857             unset($Element['attributes']);
1858             return $Element;
1859         }
1860 
1861         if (isset($safeUrlNameToAtt[$Element['name']]))
1862         {
1863             $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
1864         }
1865 
1866         if ( ! empty($Element['attributes']))
1867         {
1868             foreach ($Element['attributes'] as $att => $val)
1869             {
1870                 # filter out badly parsed attribute
1871                 if ( ! preg_match($goodAttribute, $att))
1872                 {
1873                     unset($Element['attributes'][$att]);
1874                 }
1875                 # dump onevent attribute
1876                 elseif (self::striAtStart($att, 'on'))
1877                 {
1878                     unset($Element['attributes'][$att]);
1879                 }
1880             }
1881         }
1882 
1883         return $Element;
1884     }
1885 
1886     protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
1887     {
1888         foreach ($this->safeLinksWhitelist as $scheme)
1889         {
1890             if (self::striAtStart($Element['attributes'][$attribute], $scheme))
1891             {
1892                 return $Element;
1893             }
1894         }
1895 
1896         $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
1897 
1898         return $Element;
1899     }
1900 
1901     #
1902     # Static Methods
1903     #
1904 
1905     protected static function escape($text, $allowQuotes = false)
1906     {
1907         return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
1908     }
1909 
1910     protected static function striAtStart($string, $needle)
1911     {
1912         $len = strlen($needle);
1913 
1914         if ($len > strlen($string))
1915         {
1916             return false;
1917         }
1918         else
1919         {
1920             return strtolower(substr($string, 0, $len)) === strtolower($needle);
1921         }
1922     }
1923 
1924     static function instance($name = 'default')
1925     {
1926         if (isset(self::$instances[$name]))
1927         {
1928             return self::$instances[$name];
1929         }
1930 
1931         $instance = new static();
1932 
1933         self::$instances[$name] = $instance;
1934 
1935         return $instance;
1936     }
1937 
1938     private static $instances = array();
1939 
1940     #
1941     # Fields
1942     #
1943 
1944     protected $DefinitionData;
1945 
1946     #
1947     # Read-Only
1948 
1949     protected $specialCharacters = array(
1950         '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
1951     );
1952 
1953     protected $StrongRegex = array(
1954         '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
1955         '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
1956     );
1957 
1958     protected $EmRegex = array(
1959         '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
1960         '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
1961     );
1962 
1963     protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
1964 
1965     protected $voidElements = array(
1966         'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
1967     );
1968 
1969     protected $textLevelElements = array(
1970         'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
1971         'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
1972         'i', 'rp', 'del', 'code',          'strike', 'marquee',
1973         'q', 'rt', 'ins', 'font',          'strong',
1974         's', 'tt', 'kbd', 'mark',
1975         'u', 'xm', 'sub', 'nobr',
1976                    'sup', 'ruby',
1977                    'var', 'span',
1978                    'wbr', 'time',
1979     );
1980 }