src/Helper/Parsedown.php line 706

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