just-a-blog

A bad blogging software that converts Markdown into PHP for some reason.
Log | Files | Refs | README | LICENSE

Parsedown.php (37940B)


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