"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Framework/Parsedown/Parsedown.php" (1 Sep 2020, 38082 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Parsedown.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Framework\Parsedown
    5  *
    6  * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
    7  * @license    MIT License; see LICENSE file for details.
    8  */
    9 
   10 namespace Grav\Framework\Parsedown;
   11 
   12 /*
   13  * Parsedown
   14  * http://parsedown.org
   15  *
   16  * (c) Emanuil Rusev
   17  * http://erusev.com
   18  *
   19  * This file ported from officiall Parsedown repo and kept for compatibility.
   20  */
   21 
   22 class Parsedown
   23 {
   24     # ~
   25 
   26     const version = '1.6.0';
   27 
   28     # ~
   29 
   30     function text($text)
   31     {
   32         # make sure no definitions are set
   33         $this->DefinitionData = array();
   34 
   35         # standardize line breaks
   36         $text = str_replace(array("\r\n", "\r"), "\n", $text);
   37 
   38         # remove surrounding line breaks
   39         $text = trim($text, "\n");
   40 
   41         # split text into lines
   42         $lines = explode("\n", $text);
   43 
   44         # iterate through lines to identify blocks
   45         $markup = $this->lines($lines);
   46 
   47         # trim line breaks
   48         $markup = trim($markup, "\n");
   49 
   50         return $markup;
   51     }
   52 
   53     #
   54     # Setters
   55     #
   56 
   57     function setBreaksEnabled($breaksEnabled)
   58     {
   59         $this->breaksEnabled = $breaksEnabled;
   60 
   61         return $this;
   62     }
   63 
   64     protected $breaksEnabled;
   65 
   66     function setMarkupEscaped($markupEscaped)
   67     {
   68         $this->markupEscaped = $markupEscaped;
   69 
   70         return $this;
   71     }
   72 
   73     protected $markupEscaped;
   74 
   75     function setUrlsLinked($urlsLinked)
   76     {
   77         $this->urlsLinked = $urlsLinked;
   78 
   79         return $this;
   80     }
   81 
   82     protected $urlsLinked = true;
   83 
   84     #
   85     # Lines
   86     #
   87 
   88     protected $BlockTypes = array(
   89         '#' => array('Header'),
   90         '*' => array('Rule', 'List'),
   91         '+' => array('List'),
   92         '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
   93         '0' => array('List'),
   94         '1' => array('List'),
   95         '2' => array('List'),
   96         '3' => array('List'),
   97         '4' => array('List'),
   98         '5' => array('List'),
   99         '6' => array('List'),
  100         '7' => array('List'),
  101         '8' => array('List'),
  102         '9' => array('List'),
  103         ':' => array('Table'),
  104         '<' => array('Comment', 'Markup'),
  105         '=' => array('SetextHeader'),
  106         '>' => array('Quote'),
  107         '[' => array('Reference'),
  108         '_' => array('Rule'),
  109         '`' => array('FencedCode'),
  110         '|' => array('Table'),
  111         '~' => array('FencedCode'),
  112     );
  113 
  114     # ~
  115 
  116     protected $unmarkedBlockTypes = array(
  117         'Code',
  118     );
  119 
  120     #
  121     # Blocks
  122     #
  123 
  124     protected function lines(array $lines)
  125     {
  126         $CurrentBlock = null;
  127 
  128         foreach ($lines as $line)
  129         {
  130             if (chop($line) === '')
  131             {
  132                 if (isset($CurrentBlock))
  133                 {
  134                     $CurrentBlock['interrupted'] = true;
  135                 }
  136 
  137                 continue;
  138             }
  139 
  140             if (strpos($line, "\t") !== false)
  141             {
  142                 $parts = explode("\t", $line);
  143 
  144                 $line = $parts[0];
  145 
  146                 unset($parts[0]);
  147 
  148                 foreach ($parts as $part)
  149                 {
  150                     $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
  151 
  152                     $line .= str_repeat(' ', $shortage);
  153                     $line .= $part;
  154                 }
  155             }
  156 
  157             $indent = 0;
  158 
  159             while (isset($line[$indent]) and $line[$indent] === ' ')
  160             {
  161                 $indent ++;
  162             }
  163 
  164             $text = $indent > 0 ? substr($line, $indent) : $line;
  165 
  166             # ~
  167 
  168             $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
  169 
  170             # ~
  171 
  172             if (isset($CurrentBlock['continuable']))
  173             {
  174                 $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
  175 
  176                 if (isset($Block))
  177                 {
  178                     $CurrentBlock = $Block;
  179 
  180                     continue;
  181                 }
  182                 else
  183                 {
  184                     if ($this->isBlockCompletable($CurrentBlock['type']))
  185                     {
  186                         $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
  187                     }
  188                 }
  189             }
  190 
  191             # ~
  192 
  193             $marker = $text[0];
  194 
  195             # ~
  196 
  197             $blockTypes = $this->unmarkedBlockTypes;
  198 
  199             if (isset($this->BlockTypes[$marker]))
  200             {
  201                 foreach ($this->BlockTypes[$marker] as $blockType)
  202                 {
  203                     $blockTypes []= $blockType;
  204                 }
  205             }
  206 
  207             #
  208             # ~
  209 
  210             foreach ($blockTypes as $blockType)
  211             {
  212                 $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
  213 
  214                 if (isset($Block))
  215                 {
  216                     $Block['type'] = $blockType;
  217 
  218                     if ( ! isset($Block['identified']))
  219                     {
  220                         $Blocks []= $CurrentBlock;
  221 
  222                         $Block['identified'] = true;
  223                     }
  224 
  225                     if ($this->isBlockContinuable($blockType))
  226                     {
  227                         $Block['continuable'] = true;
  228                     }
  229 
  230                     $CurrentBlock = $Block;
  231 
  232                     continue 2;
  233                 }
  234             }
  235 
  236             # ~
  237 
  238             if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
  239             {
  240                 $CurrentBlock['element']['text'] .= "\n".$text;
  241             }
  242             else
  243             {
  244                 $Blocks []= $CurrentBlock;
  245 
  246                 $CurrentBlock = $this->paragraph($Line);
  247 
  248                 $CurrentBlock['identified'] = true;
  249             }
  250         }
  251 
  252         # ~
  253 
  254         if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
  255         {
  256             $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
  257         }
  258 
  259         # ~
  260 
  261         $Blocks []= $CurrentBlock;
  262 
  263         unset($Blocks[0]);
  264 
  265         # ~
  266 
  267         $markup = '';
  268 
  269         foreach ($Blocks as $Block)
  270         {
  271             if (isset($Block['hidden']))
  272             {
  273                 continue;
  274             }
  275 
  276             $markup .= "\n";
  277             $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
  278         }
  279 
  280         $markup .= "\n";
  281 
  282         # ~
  283 
  284         return $markup;
  285     }
  286 
  287     protected function isBlockContinuable($Type)
  288     {
  289         return method_exists($this, 'block'.$Type.'Continue');
  290     }
  291 
  292     protected function isBlockCompletable($Type)
  293     {
  294         return method_exists($this, 'block'.$Type.'Complete');
  295     }
  296 
  297     #
  298     # Code
  299 
  300     protected function blockCode($Line, $Block = null)
  301     {
  302         if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
  303         {
  304             return;
  305         }
  306 
  307         if ($Line['indent'] >= 4)
  308         {
  309             $text = substr($Line['body'], 4);
  310 
  311             $Block = array(
  312                 'element' => array(
  313                     'name' => 'pre',
  314                     'handler' => 'element',
  315                     'text' => array(
  316                         'name' => 'code',
  317                         'text' => $text,
  318                     ),
  319                 ),
  320             );
  321 
  322             return $Block;
  323         }
  324     }
  325 
  326     protected function blockCodeContinue($Line, $Block)
  327     {
  328         if ($Line['indent'] >= 4)
  329         {
  330             if (isset($Block['interrupted']))
  331             {
  332                 $Block['element']['text']['text'] .= "\n";
  333 
  334                 unset($Block['interrupted']);
  335             }
  336 
  337             $Block['element']['text']['text'] .= "\n";
  338 
  339             $text = substr($Line['body'], 4);
  340 
  341             $Block['element']['text']['text'] .= $text;
  342 
  343             return $Block;
  344         }
  345     }
  346 
  347     protected function blockCodeComplete($Block)
  348     {
  349         $text = $Block['element']['text']['text'];
  350 
  351         $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
  352 
  353         $Block['element']['text']['text'] = $text;
  354 
  355         return $Block;
  356     }
  357 
  358     #
  359     # Comment
  360 
  361     protected function blockComment($Line)
  362     {
  363         if ($this->markupEscaped)
  364         {
  365             return;
  366         }
  367 
  368         if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
  369         {
  370             $Block = array(
  371                 'markup' => $Line['body'],
  372             );
  373 
  374             if (preg_match('/-->$/', $Line['text']))
  375             {
  376                 $Block['closed'] = true;
  377             }
  378 
  379             return $Block;
  380         }
  381     }
  382 
  383     protected function blockCommentContinue($Line, array $Block)
  384     {
  385         if (isset($Block['closed']))
  386         {
  387             return;
  388         }
  389 
  390         $Block['markup'] .= "\n" . $Line['body'];
  391 
  392         if (preg_match('/-->$/', $Line['text']))
  393         {
  394             $Block['closed'] = true;
  395         }
  396 
  397         return $Block;
  398     }
  399 
  400     #
  401     # Fenced Code
  402 
  403     protected function blockFencedCode($Line)
  404     {
  405         if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
  406         {
  407             $Element = array(
  408                 'name' => 'code',
  409                 'text' => '',
  410             );
  411 
  412             if (isset($matches[1]))
  413             {
  414                 $class = 'language-'.$matches[1];
  415 
  416                 $Element['attributes'] = array(
  417                     'class' => $class,
  418                 );
  419             }
  420 
  421             $Block = array(
  422                 'char' => $Line['text'][0],
  423                 'element' => array(
  424                     'name' => 'pre',
  425                     'handler' => 'element',
  426                     'text' => $Element,
  427                 ),
  428             );
  429 
  430             return $Block;
  431         }
  432     }
  433 
  434     protected function blockFencedCodeContinue($Line, $Block)
  435     {
  436         if (isset($Block['complete']))
  437         {
  438             return;
  439         }
  440 
  441         if (isset($Block['interrupted']))
  442         {
  443             $Block['element']['text']['text'] .= "\n";
  444 
  445             unset($Block['interrupted']);
  446         }
  447 
  448         if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
  449         {
  450             $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
  451 
  452             $Block['complete'] = true;
  453 
  454             return $Block;
  455         }
  456 
  457         $Block['element']['text']['text'] .= "\n".$Line['body'];
  458 
  459         return $Block;
  460     }
  461 
  462     protected function blockFencedCodeComplete($Block)
  463     {
  464         $text = $Block['element']['text']['text'];
  465 
  466         $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
  467 
  468         $Block['element']['text']['text'] = $text;
  469 
  470         return $Block;
  471     }
  472 
  473     #
  474     # Header
  475 
  476     protected function blockHeader($Line)
  477     {
  478         if (isset($Line['text'][1]))
  479         {
  480             $level = 1;
  481 
  482             while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
  483             {
  484                 $level ++;
  485             }
  486 
  487             if ($level > 6)
  488             {
  489                 return;
  490             }
  491 
  492             $text = trim($Line['text'], '# ');
  493 
  494             $Block = array(
  495                 'element' => array(
  496                     'name' => 'h' . min(6, $level),
  497                     'text' => $text,
  498                     'handler' => 'line',
  499                 ),
  500             );
  501 
  502             return $Block;
  503         }
  504     }
  505 
  506     #
  507     # List
  508 
  509     protected function blockList($Line)
  510     {
  511         list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');
  512 
  513         if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
  514         {
  515             $Block = array(
  516                 'indent' => $Line['indent'],
  517                 'pattern' => $pattern,
  518                 'element' => array(
  519                     'name' => $name,
  520                     'handler' => 'elements',
  521                 ),
  522             );
  523 
  524             if($name === 'ol')
  525             {
  526                 $listStart = stristr($matches[0], '.', true);
  527 
  528                 if($listStart !== '1')
  529                 {
  530                     $Block['element']['attributes'] = array('start' => $listStart);
  531                 }
  532             }
  533 
  534             $Block['li'] = array(
  535                 'name' => 'li',
  536                 'handler' => 'li',
  537                 'text' => array(
  538                     $matches[2],
  539                 ),
  540             );
  541 
  542             $Block['element']['text'] []= & $Block['li'];
  543 
  544             return $Block;
  545         }
  546     }
  547 
  548     protected function blockListContinue($Line, array $Block)
  549     {
  550         if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
  551         {
  552             if (isset($Block['interrupted']))
  553             {
  554                 $Block['li']['text'] []= '';
  555 
  556                 unset($Block['interrupted']);
  557             }
  558 
  559             unset($Block['li']);
  560 
  561             $text = isset($matches[1]) ? $matches[1] : '';
  562 
  563             $Block['li'] = array(
  564                 'name' => 'li',
  565                 'handler' => 'li',
  566                 'text' => array(
  567                     $text,
  568                 ),
  569             );
  570 
  571             $Block['element']['text'] []= & $Block['li'];
  572 
  573             return $Block;
  574         }
  575 
  576         if ($Line['text'][0] === '[' and $this->blockReference($Line))
  577         {
  578             return $Block;
  579         }
  580 
  581         if ( ! isset($Block['interrupted']))
  582         {
  583             $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
  584 
  585             $Block['li']['text'] []= $text;
  586 
  587             return $Block;
  588         }
  589 
  590         if ($Line['indent'] > 0)
  591         {
  592             $Block['li']['text'] []= '';
  593 
  594             $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
  595 
  596             $Block['li']['text'] []= $text;
  597 
  598             unset($Block['interrupted']);
  599 
  600             return $Block;
  601         }
  602     }
  603 
  604     #
  605     # Quote
  606 
  607     protected function blockQuote($Line)
  608     {
  609         if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
  610         {
  611             $Block = array(
  612                 'element' => array(
  613                     'name' => 'blockquote',
  614                     'handler' => 'lines',
  615                     'text' => (array) $matches[1],
  616                 ),
  617             );
  618 
  619             return $Block;
  620         }
  621     }
  622 
  623     protected function blockQuoteContinue($Line, array $Block)
  624     {
  625         if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
  626         {
  627             if (isset($Block['interrupted']))
  628             {
  629                 $Block['element']['text'] []= '';
  630 
  631                 unset($Block['interrupted']);
  632             }
  633 
  634             $Block['element']['text'] []= $matches[1];
  635 
  636             return $Block;
  637         }
  638 
  639         if ( ! isset($Block['interrupted']))
  640         {
  641             $Block['element']['text'] []= $Line['text'];
  642 
  643             return $Block;
  644         }
  645     }
  646 
  647     #
  648     # Rule
  649 
  650     protected function blockRule($Line)
  651     {
  652         if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
  653         {
  654             $Block = array(
  655                 'element' => array(
  656                     'name' => 'hr'
  657                 ),
  658             );
  659 
  660             return $Block;
  661         }
  662     }
  663 
  664     #
  665     # Setext
  666 
  667     protected function blockSetextHeader($Line, array $Block = null)
  668     {
  669         if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
  670         {
  671             return;
  672         }
  673 
  674         if (chop($Line['text'], $Line['text'][0]) === '')
  675         {
  676             $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
  677 
  678             return $Block;
  679         }
  680     }
  681 
  682     #
  683     # Markup
  684 
  685     protected function blockMarkup($Line)
  686     {
  687         if ($this->markupEscaped)
  688         {
  689             return;
  690         }
  691 
  692         if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
  693         {
  694             $element = strtolower($matches[1]);
  695 
  696             if (in_array($element, $this->textLevelElements))
  697             {
  698                 return;
  699             }
  700 
  701             $Block = array(
  702                 'name' => $matches[1],
  703                 'depth' => 0,
  704                 'markup' => $Line['text'],
  705             );
  706 
  707             $length = strlen($matches[0]);
  708 
  709             $remainder = substr($Line['text'], $length);
  710 
  711             if (trim($remainder) === '')
  712             {
  713                 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  714                 {
  715                     $Block['closed'] = true;
  716 
  717                     $Block['void'] = true;
  718                 }
  719             }
  720             else
  721             {
  722                 if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
  723                 {
  724                     return;
  725                 }
  726 
  727                 if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
  728                 {
  729                     $Block['closed'] = true;
  730                 }
  731             }
  732 
  733             return $Block;
  734         }
  735     }
  736 
  737     protected function blockMarkupContinue($Line, array $Block)
  738     {
  739         if (isset($Block['closed']))
  740         {
  741             return;
  742         }
  743 
  744         if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
  745         {
  746             $Block['depth'] ++;
  747         }
  748 
  749         if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
  750         {
  751             if ($Block['depth'] > 0)
  752             {
  753                 $Block['depth'] --;
  754             }
  755             else
  756             {
  757                 $Block['closed'] = true;
  758             }
  759         }
  760 
  761         if (isset($Block['interrupted']))
  762         {
  763             $Block['markup'] .= "\n";
  764 
  765             unset($Block['interrupted']);
  766         }
  767 
  768         $Block['markup'] .= "\n".$Line['body'];
  769 
  770         return $Block;
  771     }
  772 
  773     #
  774     # Reference
  775 
  776     protected function blockReference($Line)
  777     {
  778         if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
  779         {
  780             $id = strtolower($matches[1]);
  781 
  782             $Data = array(
  783                 'url' => $matches[2],
  784                 'title' => null,
  785             );
  786 
  787             if (isset($matches[3]))
  788             {
  789                 $Data['title'] = $matches[3];
  790             }
  791 
  792             $this->DefinitionData['Reference'][$id] = $Data;
  793 
  794             $Block = array(
  795                 'hidden' => true,
  796             );
  797 
  798             return $Block;
  799         }
  800     }
  801 
  802     #
  803     # Table
  804 
  805     protected function blockTable($Line, array $Block = null)
  806     {
  807         if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
  808         {
  809             return;
  810         }
  811 
  812         if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
  813         {
  814             $alignments = array();
  815 
  816             $divider = $Line['text'];
  817 
  818             $divider = trim($divider);
  819             $divider = trim($divider, '|');
  820 
  821             $dividerCells = explode('|', $divider);
  822 
  823             foreach ($dividerCells as $dividerCell)
  824             {
  825                 $dividerCell = trim($dividerCell);
  826 
  827                 if ($dividerCell === '')
  828                 {
  829                     continue;
  830                 }
  831 
  832                 $alignment = null;
  833 
  834                 if ($dividerCell[0] === ':')
  835                 {
  836                     $alignment = 'left';
  837                 }
  838 
  839                 if (substr($dividerCell, - 1) === ':')
  840                 {
  841                     $alignment = $alignment === 'left' ? 'center' : 'right';
  842                 }
  843 
  844                 $alignments []= $alignment;
  845             }
  846 
  847             # ~
  848 
  849             $HeaderElements = array();
  850 
  851             $header = $Block['element']['text'];
  852 
  853             $header = trim($header);
  854             $header = trim($header, '|');
  855 
  856             $headerCells = explode('|', $header);
  857 
  858             foreach ($headerCells as $index => $headerCell)
  859             {
  860                 $headerCell = trim($headerCell);
  861 
  862                 $HeaderElement = array(
  863                     'name' => 'th',
  864                     'text' => $headerCell,
  865                     'handler' => 'line',
  866                 );
  867 
  868                 if (isset($alignments[$index]))
  869                 {
  870                     $alignment = $alignments[$index];
  871 
  872                     $HeaderElement['attributes'] = array(
  873                         'style' => 'text-align: '.$alignment.';',
  874                     );
  875                 }
  876 
  877                 $HeaderElements []= $HeaderElement;
  878             }
  879 
  880             # ~
  881 
  882             $Block = array(
  883                 'alignments' => $alignments,
  884                 'identified' => true,
  885                 'element' => array(
  886                     'name' => 'table',
  887                     'handler' => 'elements',
  888                 ),
  889             );
  890 
  891             $Block['element']['text'] []= array(
  892                 'name' => 'thead',
  893                 'handler' => 'elements',
  894             );
  895 
  896             $Block['element']['text'] []= array(
  897                 'name' => 'tbody',
  898                 'handler' => 'elements',
  899                 'text' => array(),
  900             );
  901 
  902             $Block['element']['text'][0]['text'] []= array(
  903                 'name' => 'tr',
  904                 'handler' => 'elements',
  905                 'text' => $HeaderElements,
  906             );
  907 
  908             return $Block;
  909         }
  910     }
  911 
  912     protected function blockTableContinue($Line, array $Block)
  913     {
  914         if (isset($Block['interrupted']))
  915         {
  916             return;
  917         }
  918 
  919         if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
  920         {
  921             $Elements = array();
  922 
  923             $row = $Line['text'];
  924 
  925             $row = trim($row);
  926             $row = trim($row, '|');
  927 
  928             preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
  929 
  930             foreach ($matches[0] as $index => $cell)
  931             {
  932                 $cell = trim($cell);
  933 
  934                 $Element = array(
  935                     'name' => 'td',
  936                     'handler' => 'line',
  937                     'text' => $cell,
  938                 );
  939 
  940                 if (isset($Block['alignments'][$index]))
  941                 {
  942                     $Element['attributes'] = array(
  943                         'style' => 'text-align: '.$Block['alignments'][$index].';',
  944                     );
  945                 }
  946 
  947                 $Elements []= $Element;
  948             }
  949 
  950             $Element = array(
  951                 'name' => 'tr',
  952                 'handler' => 'elements',
  953                 'text' => $Elements,
  954             );
  955 
  956             $Block['element']['text'][1]['text'] []= $Element;
  957 
  958             return $Block;
  959         }
  960     }
  961 
  962     #
  963     # ~
  964     #
  965 
  966     protected function paragraph($Line)
  967     {
  968         $Block = array(
  969             'element' => array(
  970                 'name' => 'p',
  971                 'text' => $Line['text'],
  972                 'handler' => 'line',
  973             ),
  974         );
  975 
  976         return $Block;
  977     }
  978 
  979     #
  980     # Inline Elements
  981     #
  982 
  983     protected $InlineTypes = array(
  984         '"' => array('SpecialCharacter'),
  985         '!' => array('Image'),
  986         '&' => array('SpecialCharacter'),
  987         '*' => array('Emphasis'),
  988         ':' => array('Url'),
  989         '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
  990         '>' => array('SpecialCharacter'),
  991         '[' => array('Link'),
  992         '_' => array('Emphasis'),
  993         '`' => array('Code'),
  994         '~' => array('Strikethrough'),
  995         '\\' => array('EscapeSequence'),
  996     );
  997 
  998     # ~
  999 
 1000     protected $inlineMarkerList = '!"*_&[:<>`~\\';
 1001 
 1002     #
 1003     # ~
 1004     #
 1005 
 1006     public function line($text)
 1007     {
 1008         $markup = '';
 1009 
 1010         # $excerpt is based on the first occurrence of a marker
 1011 
 1012         while ($excerpt = strpbrk($text, $this->inlineMarkerList))
 1013         {
 1014             $marker = $excerpt[0];
 1015 
 1016             $markerPosition = strpos($text, $marker);
 1017 
 1018             $Excerpt = array('text' => $excerpt, 'context' => $text);
 1019 
 1020             foreach ($this->InlineTypes[$marker] as $inlineType)
 1021             {
 1022                 $Inline = $this->{'inline'.$inlineType}($Excerpt);
 1023 
 1024                 if ( ! isset($Inline))
 1025                 {
 1026                     continue;
 1027                 }
 1028 
 1029                 # makes sure that the inline belongs to "our" marker
 1030 
 1031                 if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
 1032                 {
 1033                     continue;
 1034                 }
 1035 
 1036                 # sets a default inline position
 1037 
 1038                 if ( ! isset($Inline['position']))
 1039                 {
 1040                     $Inline['position'] = $markerPosition;
 1041                 }
 1042 
 1043                 # the text that comes before the inline
 1044                 $unmarkedText = substr($text, 0, $Inline['position']);
 1045 
 1046                 # compile the unmarked text
 1047                 $markup .= $this->unmarkedText($unmarkedText);
 1048 
 1049                 # compile the inline
 1050                 $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
 1051 
 1052                 # remove the examined text
 1053                 $text = substr($text, $Inline['position'] + $Inline['extent']);
 1054 
 1055                 continue 2;
 1056             }
 1057 
 1058             # the marker does not belong to an inline
 1059 
 1060             $unmarkedText = substr($text, 0, $markerPosition + 1);
 1061 
 1062             $markup .= $this->unmarkedText($unmarkedText);
 1063 
 1064             $text = substr($text, $markerPosition + 1);
 1065         }
 1066 
 1067         $markup .= $this->unmarkedText($text);
 1068 
 1069         return $markup;
 1070     }
 1071 
 1072     #
 1073     # ~
 1074     #
 1075 
 1076     protected function inlineCode($Excerpt)
 1077     {
 1078         $marker = $Excerpt['text'][0];
 1079 
 1080         if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
 1081         {
 1082             $text = $matches[2];
 1083             $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
 1084             $text = preg_replace("/[ ]*\n/", ' ', $text);
 1085 
 1086             return array(
 1087                 'extent' => strlen($matches[0]),
 1088                 'element' => array(
 1089                     'name' => 'code',
 1090                     'text' => $text,
 1091                 ),
 1092             );
 1093         }
 1094     }
 1095 
 1096     protected function inlineEmailTag($Excerpt)
 1097     {
 1098         if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
 1099         {
 1100             $url = $matches[1];
 1101 
 1102             if ( ! isset($matches[2]))
 1103             {
 1104                 $url = 'mailto:' . $url;
 1105             }
 1106 
 1107             return array(
 1108                 'extent' => strlen($matches[0]),
 1109                 'element' => array(
 1110                     'name' => 'a',
 1111                     'text' => $matches[1],
 1112                     'attributes' => array(
 1113                         'href' => $url,
 1114                     ),
 1115                 ),
 1116             );
 1117         }
 1118     }
 1119 
 1120     protected function inlineEmphasis($Excerpt)
 1121     {
 1122         if ( ! isset($Excerpt['text'][1]))
 1123         {
 1124             return;
 1125         }
 1126 
 1127         $marker = $Excerpt['text'][0];
 1128 
 1129         if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
 1130         {
 1131             $emphasis = 'strong';
 1132         }
 1133         elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
 1134         {
 1135             $emphasis = 'em';
 1136         }
 1137         else
 1138         {
 1139             return;
 1140         }
 1141 
 1142         return array(
 1143             'extent' => strlen($matches[0]),
 1144             'element' => array(
 1145                 'name' => $emphasis,
 1146                 'handler' => 'line',
 1147                 'text' => $matches[1],
 1148             ),
 1149         );
 1150     }
 1151 
 1152     protected function inlineEscapeSequence($Excerpt)
 1153     {
 1154         if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
 1155         {
 1156             return array(
 1157                 'markup' => $Excerpt['text'][1],
 1158                 'extent' => 2,
 1159             );
 1160         }
 1161     }
 1162 
 1163     protected function inlineImage($Excerpt)
 1164     {
 1165         if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
 1166         {
 1167             return;
 1168         }
 1169 
 1170         $Excerpt['text']= substr($Excerpt['text'], 1);
 1171 
 1172         $Link = $this->inlineLink($Excerpt);
 1173 
 1174         if ($Link === null)
 1175         {
 1176             return;
 1177         }
 1178 
 1179         $Inline = array(
 1180             'extent' => $Link['extent'] + 1,
 1181             'element' => array(
 1182                 'name' => 'img',
 1183                 'attributes' => array(
 1184                     'src' => $Link['element']['attributes']['href'],
 1185                     'alt' => $Link['element']['text'],
 1186                 ),
 1187             ),
 1188         );
 1189 
 1190         $Inline['element']['attributes'] += $Link['element']['attributes'];
 1191 
 1192         unset($Inline['element']['attributes']['href']);
 1193 
 1194         return $Inline;
 1195     }
 1196 
 1197     protected function inlineLink($Excerpt)
 1198     {
 1199         $Element = array(
 1200             'name' => 'a',
 1201             'handler' => 'line',
 1202             'text' => null,
 1203             'attributes' => array(
 1204                 'href' => null,
 1205                 'title' => null,
 1206             ),
 1207         );
 1208 
 1209         $extent = 0;
 1210 
 1211         $remainder = $Excerpt['text'];
 1212 
 1213         if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
 1214         {
 1215             $Element['text'] = $matches[1];
 1216 
 1217             $extent += strlen($matches[0]);
 1218 
 1219             $remainder = substr($remainder, $extent);
 1220         }
 1221         else
 1222         {
 1223             return;
 1224         }
 1225 
 1226         if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches))
 1227         {
 1228             $Element['attributes']['href'] = $matches[1];
 1229 
 1230             if (isset($matches[2]))
 1231             {
 1232                 $Element['attributes']['title'] = substr($matches[2], 1, - 1);
 1233             }
 1234 
 1235             $extent += strlen($matches[0]);
 1236         }
 1237         else
 1238         {
 1239             if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
 1240             {
 1241                 $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
 1242                 $definition = strtolower($definition);
 1243 
 1244                 $extent += strlen($matches[0]);
 1245             }
 1246             else
 1247             {
 1248                 $definition = strtolower($Element['text']);
 1249             }
 1250 
 1251             if ( ! isset($this->DefinitionData['Reference'][$definition]))
 1252             {
 1253                 return;
 1254             }
 1255 
 1256             $Definition = $this->DefinitionData['Reference'][$definition];
 1257 
 1258             $Element['attributes']['href'] = $Definition['url'];
 1259             $Element['attributes']['title'] = $Definition['title'];
 1260         }
 1261 
 1262         $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);
 1263 
 1264         return array(
 1265             'extent' => $extent,
 1266             'element' => $Element,
 1267         );
 1268     }
 1269 
 1270     protected function inlineMarkup($Excerpt)
 1271     {
 1272         if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
 1273         {
 1274             return;
 1275         }
 1276 
 1277         if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
 1278         {
 1279             return array(
 1280                 'markup' => $matches[0],
 1281                 'extent' => strlen($matches[0]),
 1282             );
 1283         }
 1284 
 1285         if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
 1286         {
 1287             return array(
 1288                 'markup' => $matches[0],
 1289                 'extent' => strlen($matches[0]),
 1290             );
 1291         }
 1292 
 1293         if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
 1294         {
 1295             return array(
 1296                 'markup' => $matches[0],
 1297                 'extent' => strlen($matches[0]),
 1298             );
 1299         }
 1300     }
 1301 
 1302     protected function inlineSpecialCharacter($Excerpt)
 1303     {
 1304         if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
 1305         {
 1306             return array(
 1307                 'markup' => '&amp;',
 1308                 'extent' => 1,
 1309             );
 1310         }
 1311 
 1312         $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');
 1313 
 1314         if (isset($SpecialCharacter[$Excerpt['text'][0]]))
 1315         {
 1316             return array(
 1317                 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
 1318                 'extent' => 1,
 1319             );
 1320         }
 1321     }
 1322 
 1323     protected function inlineStrikethrough($Excerpt)
 1324     {
 1325         if ( ! isset($Excerpt['text'][1]))
 1326         {
 1327             return;
 1328         }
 1329 
 1330         if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
 1331         {
 1332             return array(
 1333                 'extent' => strlen($matches[0]),
 1334                 'element' => array(
 1335                     'name' => 'del',
 1336                     'text' => $matches[1],
 1337                     'handler' => 'line',
 1338                 ),
 1339             );
 1340         }
 1341     }
 1342 
 1343     protected function inlineUrl($Excerpt)
 1344     {
 1345         if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
 1346         {
 1347             return;
 1348         }
 1349 
 1350         if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
 1351         {
 1352             $Inline = array(
 1353                 'extent' => strlen($matches[0][0]),
 1354                 'position' => $matches[0][1],
 1355                 'element' => array(
 1356                     'name' => 'a',
 1357                     'text' => $matches[0][0],
 1358                     'attributes' => array(
 1359                         'href' => $matches[0][0],
 1360                     ),
 1361                 ),
 1362             );
 1363 
 1364             return $Inline;
 1365         }
 1366     }
 1367 
 1368     protected function inlineUrlTag($Excerpt)
 1369     {
 1370         if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
 1371         {
 1372             $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);
 1373 
 1374             return array(
 1375                 'extent' => strlen($matches[0]),
 1376                 'element' => array(
 1377                     'name' => 'a',
 1378                     'text' => $url,
 1379                     'attributes' => array(
 1380                         'href' => $url,
 1381                     ),
 1382                 ),
 1383             );
 1384         }
 1385     }
 1386 
 1387     # ~
 1388 
 1389     protected function unmarkedText($text)
 1390     {
 1391         if ($this->breaksEnabled)
 1392         {
 1393             $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
 1394         }
 1395         else
 1396         {
 1397             $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
 1398             $text = str_replace(" \n", "\n", $text);
 1399         }
 1400 
 1401         return $text;
 1402     }
 1403 
 1404     #
 1405     # Handlers
 1406     #
 1407 
 1408     protected function element(array $Element)
 1409     {
 1410         $markup = '<'.$Element['name'];
 1411 
 1412         if (isset($Element['attributes']))
 1413         {
 1414             foreach ($Element['attributes'] as $name => $value)
 1415             {
 1416                 if ($value === null)
 1417                 {
 1418                     continue;
 1419                 }
 1420 
 1421                 $markup .= ' '.$name.'="'.$value.'"';
 1422             }
 1423         }
 1424 
 1425         if (isset($Element['text']))
 1426         {
 1427             $markup .= '>';
 1428 
 1429             if (isset($Element['handler']))
 1430             {
 1431                 $markup .= $this->{$Element['handler']}($Element['text']);
 1432             }
 1433             else
 1434             {
 1435                 $markup .= $Element['text'];
 1436             }
 1437 
 1438             $markup .= '</'.$Element['name'].'>';
 1439         }
 1440         else
 1441         {
 1442             $markup .= ' />';
 1443         }
 1444 
 1445         return $markup;
 1446     }
 1447 
 1448     protected function elements(array $Elements)
 1449     {
 1450         $markup = '';
 1451 
 1452         foreach ($Elements as $Element)
 1453         {
 1454             $markup .= "\n" . $this->element($Element);
 1455         }
 1456 
 1457         $markup .= "\n";
 1458 
 1459         return $markup;
 1460     }
 1461 
 1462     # ~
 1463 
 1464     protected function li($lines)
 1465     {
 1466         $markup = $this->lines($lines);
 1467 
 1468         $trimmedMarkup = trim($markup);
 1469 
 1470         if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
 1471         {
 1472             $markup = $trimmedMarkup;
 1473             $markup = substr($markup, 3);
 1474 
 1475             $position = strpos($markup, "</p>");
 1476 
 1477             $markup = substr_replace($markup, '', $position, 4);
 1478         }
 1479 
 1480         return $markup;
 1481     }
 1482 
 1483     #
 1484     # Deprecated Methods
 1485     #
 1486 
 1487     function parse($text)
 1488     {
 1489         $markup = $this->text($text);
 1490 
 1491         return $markup;
 1492     }
 1493 
 1494     #
 1495     # Static Methods
 1496     #
 1497 
 1498     static function instance($name = 'default')
 1499     {
 1500         if (isset(self::$instances[$name]))
 1501         {
 1502             return self::$instances[$name];
 1503         }
 1504 
 1505         $instance = new static();
 1506 
 1507         self::$instances[$name] = $instance;
 1508 
 1509         return $instance;
 1510     }
 1511 
 1512     private static $instances = array();
 1513 
 1514     #
 1515     # Fields
 1516     #
 1517 
 1518     protected $DefinitionData;
 1519 
 1520     #
 1521     # Read-Only
 1522 
 1523     protected $specialCharacters = array(
 1524         '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
 1525     );
 1526 
 1527     protected $StrongRegex = array(
 1528         '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
 1529         '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
 1530     );
 1531 
 1532     protected $EmRegex = array(
 1533         '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
 1534         '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
 1535     );
 1536 
 1537     protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
 1538 
 1539     protected $voidElements = array(
 1540         'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
 1541     );
 1542 
 1543     protected $textLevelElements = array(
 1544         'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
 1545         'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
 1546         'i', 'rp', 'del', 'code',          'strike', 'marquee',
 1547         'q', 'rt', 'ins', 'font',          'strong',
 1548         's', 'tt', 'kbd', 'mark',
 1549         'u', 'xm', 'sub', 'nobr',
 1550         'sup', 'ruby',
 1551         'var', 'span',
 1552         'wbr', 'time',
 1553     );
 1554 }