diff --git a/vendor/erusev/parsedown/Parsedown.php b/vendor/erusev/parsedown/Parsedown.php index 1b9d6d5b..38edfe92 100644 --- a/vendor/erusev/parsedown/Parsedown.php +++ b/vendor/erusev/parsedown/Parsedown.php @@ -17,11 +17,24 @@ class Parsedown { # ~ - const version = '1.7.4'; + const version = '1.8.0'; # ~ function text($text) + { + $Elements = $this->textElements($text); + + # convert to markup + $markup = $this->elements($Elements); + + # trim line breaks + $markup = trim($markup, "\n"); + + return $markup; + } + + protected function textElements($text) { # make sure no definitions are set $this->DefinitionData = array(); @@ -36,12 +49,7 @@ class Parsedown $lines = explode("\n", $text); # iterate through lines to identify blocks - $markup = $this->lines($lines); - - # trim line breaks - $markup = trim($markup, "\n"); - - return $markup; + return $this->linesElements($lines); } # @@ -84,12 +92,22 @@ class Parsedown protected $safeMode; + function setStrictMode($strictMode) + { + $this->strictMode = (bool) $strictMode; + + return $this; + } + + protected $strictMode; + protected $safeLinksWhitelist = array( 'http://', 'https://', 'ftp://', 'ftps://', 'mailto:', + 'tel:', 'data:image/png;base64,', 'data:image/gif;base64,', 'data:image/jpeg;base64,', @@ -143,6 +161,12 @@ class Parsedown protected function lines(array $lines) { + return $this->elements($this->linesElements($lines)); + } + + protected function linesElements(array $lines) + { + $Elements = array(); $CurrentBlock = null; foreach ($lines as $line) @@ -151,35 +175,25 @@ class Parsedown { if (isset($CurrentBlock)) { - $CurrentBlock['interrupted'] = true; + $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) + ? $CurrentBlock['interrupted'] + 1 : 1 + ); } continue; } - if (strpos($line, "\t") !== false) + while (($beforeTab = strstr($line, "\t", true)) !== false) { - $parts = explode("\t", $line); + $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; - $line = $parts[0]; - - unset($parts[0]); - - foreach ($parts as $part) - { - $shortage = 4 - mb_strlen($line, 'utf-8') % 4; - - $line .= str_repeat(' ', $shortage); - $line .= $part; - } + $line = $beforeTab + . str_repeat(' ', $shortage) + . substr($line, strlen($beforeTab) + 1) + ; } - $indent = 0; - - while (isset($line[$indent]) and $line[$indent] === ' ') - { - $indent ++; - } + $indent = strspn($line, ' '); $text = $indent > 0 ? substr($line, $indent) : $line; @@ -191,7 +205,8 @@ class Parsedown if (isset($CurrentBlock['continuable'])) { - $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); + $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; + $Block = $this->$methodName($Line, $CurrentBlock); if (isset($Block)) { @@ -203,7 +218,8 @@ class Parsedown { if ($this->isBlockCompletable($CurrentBlock['type'])) { - $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); } } } @@ -229,7 +245,7 @@ class Parsedown foreach ($blockTypes as $blockType) { - $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); + $Block = $this->{"block$blockType"}($Line, $CurrentBlock); if (isset($Block)) { @@ -237,7 +253,10 @@ class Parsedown if ( ! isset($Block['identified'])) { - $Blocks []= $CurrentBlock; + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } $Block['identified'] = true; } @@ -255,13 +274,21 @@ class Parsedown # ~ - if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) + if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') { - $CurrentBlock['element']['text'] .= "\n".$text; + $Block = $this->paragraphContinue($Line, $CurrentBlock); + } + + if (isset($Block)) + { + $CurrentBlock = $Block; } else { - $Blocks []= $CurrentBlock; + if (isset($CurrentBlock)) + { + $Elements[] = $this->extractElement($CurrentBlock); + } $CurrentBlock = $this->paragraph($Line); @@ -273,45 +300,47 @@ class Parsedown if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) { - $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); + $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; + $CurrentBlock = $this->$methodName($CurrentBlock); } # ~ - $Blocks []= $CurrentBlock; - - unset($Blocks[0]); - - # ~ - - $markup = ''; - - foreach ($Blocks as $Block) + if (isset($CurrentBlock)) { - if (isset($Block['hidden'])) - { - continue; - } - - $markup .= "\n"; - $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); + $Elements[] = $this->extractElement($CurrentBlock); } - $markup .= "\n"; - # ~ - return $markup; + return $Elements; + } + + protected function extractElement(array $Component) + { + if ( ! isset($Component['element'])) + { + if (isset($Component['markup'])) + { + $Component['element'] = array('rawHtml' => $Component['markup']); + } + elseif (isset($Component['hidden'])) + { + $Component['element'] = array(); + } + } + + return $Component['element']; } protected function isBlockContinuable($Type) { - return method_exists($this, 'block'.$Type.'Continue'); + return method_exists($this, 'block' . $Type . 'Continue'); } protected function isBlockCompletable($Type) { - return method_exists($this, 'block'.$Type.'Complete'); + return method_exists($this, 'block' . $Type . 'Complete'); } # @@ -319,7 +348,7 @@ class Parsedown protected function blockCode($Line, $Block = null) { - if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) + if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) { return; } @@ -331,8 +360,7 @@ class Parsedown $Block = array( 'element' => array( 'name' => 'pre', - 'handler' => 'element', - 'text' => array( + 'element' => array( 'name' => 'code', 'text' => $text, ), @@ -349,16 +377,16 @@ class Parsedown { if (isset($Block['interrupted'])) { - $Block['element']['text']['text'] .= "\n"; + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); unset($Block['interrupted']); } - $Block['element']['text']['text'] .= "\n"; + $Block['element']['element']['text'] .= "\n"; $text = substr($Line['body'], 4); - $Block['element']['text']['text'] .= $text; + $Block['element']['element']['text'] .= $text; return $Block; } @@ -366,10 +394,6 @@ class Parsedown protected function blockCodeComplete($Block) { - $text = $Block['element']['text']['text']; - - $Block['element']['text']['text'] = $text; - return $Block; } @@ -383,13 +407,16 @@ class Parsedown return; } - if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') + if (strpos($Line['text'], '$/', $Line['text'])) + if (strpos($Line['text'], '-->') !== false) { $Block['closed'] = true; } @@ -405,9 +432,9 @@ class Parsedown return; } - $Block['markup'] .= "\n" . $Line['body']; + $Block['element']['rawHtml'] .= "\n" . $Line['body']; - if (preg_match('/-->$/', $Line['text'])) + if (strpos($Line['text'], '-->') !== false) { $Block['closed'] = true; } @@ -420,47 +447,56 @@ class Parsedown protected function blockFencedCode($Line) { - if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) + $marker = $Line['text'][0]; + + $openerLength = strspn($Line['text'], $marker); + + if ($openerLength < 3) { - $Element = array( - 'name' => 'code', - 'text' => '', - ); - - if (isset($matches[1])) - { - /** - * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes - * Every HTML element may have a class attribute specified. - * The attribute, if specified, must have a value that is a set - * of space-separated tokens representing the various classes - * that the element belongs to. - * [...] - * The space characters, for the purposes of this specification, - * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), - * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and - * U+000D CARRIAGE RETURN (CR). - */ - $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r")); - - $class = 'language-'.$language; - - $Element['attributes'] = array( - 'class' => $class, - ); - } - - $Block = array( - 'char' => $Line['text'][0], - 'element' => array( - 'name' => 'pre', - 'handler' => 'element', - 'text' => $Element, - ), - ); - - return $Block; + return; } + + $infostring = trim(substr($Line['text'], $openerLength), "\t "); + + if (strpos($infostring, '`') !== false) + { + return; + } + + $Element = array( + 'name' => 'code', + 'text' => '', + ); + + if ($infostring !== '') + { + /** + * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes + * Every HTML element may have a class attribute specified. + * The attribute, if specified, must have a value that is a set + * of space-separated tokens representing the various classes + * that the element belongs to. + * [...] + * The space characters, for the purposes of this specification, + * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), + * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and + * U+000D CARRIAGE RETURN (CR). + */ + $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); + + $Element['attributes'] = array('class' => "language-$language"); + } + + $Block = array( + 'char' => $marker, + 'openerLength' => $openerLength, + 'element' => array( + 'name' => 'pre', + 'element' => $Element, + ), + ); + + return $Block; } protected function blockFencedCodeContinue($Line, $Block) @@ -472,31 +508,28 @@ class Parsedown if (isset($Block['interrupted'])) { - $Block['element']['text']['text'] .= "\n"; + $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); unset($Block['interrupted']); } - if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) - { - $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); + if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] + and chop(substr($Line['text'], $len), ' ') === '' + ) { + $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); $Block['complete'] = true; return $Block; } - $Block['element']['text']['text'] .= "\n".$Line['body']; + $Block['element']['element']['text'] .= "\n" . $Line['body']; return $Block; } protected function blockFencedCodeComplete($Block) { - $text = $Block['element']['text']['text']; - - $Block['element']['text']['text'] = $text; - return $Block; } @@ -505,71 +538,103 @@ class Parsedown protected function blockHeader($Line) { - if (isset($Line['text'][1])) + $level = strspn($Line['text'], '#'); + + if ($level > 6) { - $level = 1; - - while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') - { - $level ++; - } - - if ($level > 6) - { - return; - } - - $text = trim($Line['text'], '# '); - - $Block = array( - 'element' => array( - 'name' => 'h' . min(6, $level), - 'text' => $text, - 'handler' => 'line', - ), - ); - - return $Block; + return; } + + $text = trim($Line['text'], '#'); + + if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') + { + return; + } + + $text = trim($text, ' '); + + $Block = array( + 'element' => array( + 'name' => 'h' . $level, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $text, + 'destination' => 'elements', + ) + ), + ); + + return $Block; } # # List - protected function blockList($Line) + protected function blockList($Line, ?array $CurrentBlock = null) { - list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); + list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); - if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) + if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) { + $contentIndent = strlen($matches[2]); + + if ($contentIndent >= 5) + { + $contentIndent -= 1; + $matches[1] = substr($matches[1], 0, -$contentIndent); + $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; + } + elseif ($contentIndent === 0) + { + $matches[1] .= ' '; + } + + $markerWithoutWhitespace = strstr($matches[1], ' ', true); + $Block = array( 'indent' => $Line['indent'], 'pattern' => $pattern, + 'data' => array( + 'type' => $name, + 'marker' => $matches[1], + 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), + ), 'element' => array( 'name' => $name, - 'handler' => 'elements', + 'elements' => array(), ), ); + $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); - if($name === 'ol') + if ($name === 'ol') { - $listStart = stristr($matches[0], '.', true); + $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; - if($listStart !== '1') + if ($listStart !== '1') { + if ( + isset($CurrentBlock) + and $CurrentBlock['type'] === 'Paragraph' + and ! isset($CurrentBlock['interrupted']) + ) { + return; + } + $Block['element']['attributes'] = array('start' => $listStart); } } $Block['li'] = array( 'name' => 'li', - 'handler' => 'li', - 'text' => array( - $matches[2], - ), + 'handler' => array( + 'function' => 'li', + 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), + 'destination' => 'elements' + ) ); - $Block['element']['text'] []= & $Block['li']; + $Block['element']['elements'] []= & $Block['li']; return $Block; } @@ -577,11 +642,27 @@ class Parsedown protected function blockListContinue($Line, array $Block) { - if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) + if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) { + return null; + } + + $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); + + if ($Line['indent'] < $requiredIndent + and ( + ( + $Block['data']['type'] === 'ol' + and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) or ( + $Block['data']['type'] === 'ul' + and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) + ) + ) + ) { if (isset($Block['interrupted'])) { - $Block['li']['text'] []= ''; + $Block['li']['handler']['argument'] []= ''; $Block['loose'] = true; @@ -592,42 +673,54 @@ class Parsedown $text = isset($matches[1]) ? $matches[1] : ''; + $Block['indent'] = $Line['indent']; + $Block['li'] = array( 'name' => 'li', - 'handler' => 'li', - 'text' => array( - $text, - ), + 'handler' => array( + 'function' => 'li', + 'argument' => array($text), + 'destination' => 'elements' + ) ); - $Block['element']['text'] []= & $Block['li']; + $Block['element']['elements'] []= & $Block['li']; return $Block; } + elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) + { + return null; + } if ($Line['text'][0] === '[' and $this->blockReference($Line)) { return $Block; } - if ( ! isset($Block['interrupted'])) + if ($Line['indent'] >= $requiredIndent) { - $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); + if (isset($Block['interrupted'])) + { + $Block['li']['handler']['argument'] []= ''; - $Block['li']['text'] []= $text; + $Block['loose'] = true; + + unset($Block['interrupted']); + } + + $text = substr($Line['body'], $requiredIndent); + + $Block['li']['handler']['argument'] []= $text; return $Block; } - if ($Line['indent'] > 0) + if ( ! isset($Block['interrupted'])) { - $Block['li']['text'] []= ''; + $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); - $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); - - $Block['li']['text'] []= $text; - - unset($Block['interrupted']); + $Block['li']['handler']['argument'] []= $text; return $Block; } @@ -637,11 +730,11 @@ class Parsedown { if (isset($Block['loose'])) { - foreach ($Block['element']['text'] as &$li) + foreach ($Block['element']['elements'] as &$li) { - if (end($li['text']) !== '') + if (end($li['handler']['argument']) !== '') { - $li['text'] []= ''; + $li['handler']['argument'] []= ''; } } } @@ -654,13 +747,16 @@ class Parsedown protected function blockQuote($Line) { - if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) { $Block = array( 'element' => array( 'name' => 'blockquote', - 'handler' => 'lines', - 'text' => (array) $matches[1], + 'handler' => array( + 'function' => 'linesElements', + 'argument' => (array) $matches[1], + 'destination' => 'elements', + ) ), ); @@ -670,23 +766,21 @@ class Parsedown protected function blockQuoteContinue($Line, array $Block) { - if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) + if (isset($Block['interrupted'])) { - if (isset($Block['interrupted'])) - { - $Block['element']['text'] []= ''; + return; + } - unset($Block['interrupted']); - } - - $Block['element']['text'] []= $matches[1]; + if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) + { + $Block['element']['handler']['argument'] []= $matches[1]; return $Block; } if ( ! isset($Block['interrupted'])) { - $Block['element']['text'] []= $Line['text']; + $Block['element']['handler']['argument'] []= $Line['text']; return $Block; } @@ -697,11 +791,13 @@ class Parsedown protected function blockRule($Line) { - if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) + $marker = $Line['text'][0]; + + if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') { $Block = array( 'element' => array( - 'name' => 'hr' + 'name' => 'hr', ), ); @@ -712,14 +808,14 @@ class Parsedown # # Setext - protected function blockSetextHeader($Line, array $Block = null) + protected function blockSetextHeader($Line, ?array $Block = null) { - if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { return; } - if (chop($Line['text'], $Line['text'][0]) === '') + if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') { $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; @@ -737,7 +833,7 @@ class Parsedown return; } - if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) + if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) { $element = strtolower($matches[1]); @@ -748,72 +844,24 @@ class Parsedown $Block = array( 'name' => $matches[1], - 'depth' => 0, - 'markup' => $Line['text'], + 'element' => array( + 'rawHtml' => $Line['text'], + 'autobreak' => true, + ), ); - $length = strlen($matches[0]); - - $remainder = substr($Line['text'], $length); - - if (trim($remainder) === '') - { - if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) - { - $Block['closed'] = true; - - $Block['void'] = true; - } - } - else - { - if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) - { - return; - } - - if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) - { - $Block['closed'] = true; - } - } - return $Block; } } protected function blockMarkupContinue($Line, array $Block) { - if (isset($Block['closed'])) + if (isset($Block['closed']) or isset($Block['interrupted'])) { return; } - if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open - { - $Block['depth'] ++; - } - - if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close - { - if ($Block['depth'] > 0) - { - $Block['depth'] --; - } - else - { - $Block['closed'] = true; - } - } - - if (isset($Block['interrupted'])) - { - $Block['markup'] .= "\n"; - - unset($Block['interrupted']); - } - - $Block['markup'] .= "\n".$Line['body']; + $Block['element']['rawHtml'] .= "\n" . $Line['body']; return $Block; } @@ -823,24 +871,20 @@ class Parsedown protected function blockReference($Line) { - if (preg_match('/^\[(.+?)\]:[ ]*?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) - { + if (strpos($Line['text'], ']') !== false + and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) + ) { $id = strtolower($matches[1]); $Data = array( 'url' => $matches[2], - 'title' => null, + 'title' => isset($matches[3]) ? $matches[3] : null, ); - if (isset($matches[3])) - { - $Data['title'] = $matches[3]; - } - $this->DefinitionData['Reference'][$id] = $Data; $Block = array( - 'hidden' => true, + 'element' => array(), ); return $Block; @@ -850,111 +894,127 @@ class Parsedown # # Table - protected function blockTable($Line, array $Block = null) + protected function blockTable($Line, ?array $Block = null) { - if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) + if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) { return; } - if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') - { - $alignments = array(); - - $divider = $Line['text']; - - $divider = trim($divider); - $divider = trim($divider, '|'); - - $dividerCells = explode('|', $divider); - - foreach ($dividerCells as $dividerCell) - { - $dividerCell = trim($dividerCell); - - if ($dividerCell === '') - { - continue; - } - - $alignment = null; - - if ($dividerCell[0] === ':') - { - $alignment = 'left'; - } - - if (substr($dividerCell, - 1) === ':') - { - $alignment = $alignment === 'left' ? 'center' : 'right'; - } - - $alignments []= $alignment; - } - - # ~ - - $HeaderElements = array(); - - $header = $Block['element']['text']; - - $header = trim($header); - $header = trim($header, '|'); - - $headerCells = explode('|', $header); - - foreach ($headerCells as $index => $headerCell) - { - $headerCell = trim($headerCell); - - $HeaderElement = array( - 'name' => 'th', - 'text' => $headerCell, - 'handler' => 'line', - ); - - if (isset($alignments[$index])) - { - $alignment = $alignments[$index]; - - $HeaderElement['attributes'] = array( - 'style' => 'text-align: '.$alignment.';', - ); - } - - $HeaderElements []= $HeaderElement; - } - - # ~ - - $Block = array( - 'alignments' => $alignments, - 'identified' => true, - 'element' => array( - 'name' => 'table', - 'handler' => 'elements', - ), - ); - - $Block['element']['text'] []= array( - 'name' => 'thead', - 'handler' => 'elements', - ); - - $Block['element']['text'] []= array( - 'name' => 'tbody', - 'handler' => 'elements', - 'text' => array(), - ); - - $Block['element']['text'][0]['text'] []= array( - 'name' => 'tr', - 'handler' => 'elements', - 'text' => $HeaderElements, - ); - - return $Block; + if ( + strpos($Block['element']['handler']['argument'], '|') === false + and strpos($Line['text'], '|') === false + and strpos($Line['text'], ':') === false + or strpos($Block['element']['handler']['argument'], "\n") !== false + ) { + return; } + + if (chop($Line['text'], ' -:|') !== '') + { + return; + } + + $alignments = array(); + + $divider = $Line['text']; + + $divider = trim($divider); + $divider = trim($divider, '|'); + + $dividerCells = explode('|', $divider); + + foreach ($dividerCells as $dividerCell) + { + $dividerCell = trim($dividerCell); + + if ($dividerCell === '') + { + return; + } + + $alignment = null; + + if ($dividerCell[0] === ':') + { + $alignment = 'left'; + } + + if (substr($dividerCell, - 1) === ':') + { + $alignment = $alignment === 'left' ? 'center' : 'right'; + } + + $alignments []= $alignment; + } + + # ~ + + $HeaderElements = array(); + + $header = $Block['element']['handler']['argument']; + + $header = trim($header); + $header = trim($header, '|'); + + $headerCells = explode('|', $header); + + if (count($headerCells) !== count($alignments)) + { + return; + } + + foreach ($headerCells as $index => $headerCell) + { + $headerCell = trim($headerCell); + + $HeaderElement = array( + 'name' => 'th', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $headerCell, + 'destination' => 'elements', + ) + ); + + if (isset($alignments[$index])) + { + $alignment = $alignments[$index]; + + $HeaderElement['attributes'] = array( + 'style' => "text-align: $alignment;", + ); + } + + $HeaderElements []= $HeaderElement; + } + + # ~ + + $Block = array( + 'alignments' => $alignments, + 'identified' => true, + 'element' => array( + 'name' => 'table', + 'elements' => array(), + ), + ); + + $Block['element']['elements'] []= array( + 'name' => 'thead', + ); + + $Block['element']['elements'] []= array( + 'name' => 'tbody', + 'elements' => array(), + ); + + $Block['element']['elements'][0]['elements'] []= array( + 'name' => 'tr', + 'elements' => $HeaderElements, + ); + + return $Block; } protected function blockTableContinue($Line, array $Block) @@ -964,7 +1024,7 @@ class Parsedown return; } - if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) + if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) { $Elements = array(); @@ -973,22 +1033,27 @@ class Parsedown $row = trim($row); $row = trim($row, '|'); - preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); + preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); - foreach ($matches[0] as $index => $cell) + $cells = array_slice($matches[0], 0, count($Block['alignments'])); + + foreach ($cells as $index => $cell) { $cell = trim($cell); $Element = array( 'name' => 'td', - 'handler' => 'line', - 'text' => $cell, + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $cell, + 'destination' => 'elements', + ) ); if (isset($Block['alignments'][$index])) { $Element['attributes'] = array( - 'style' => 'text-align: '.$Block['alignments'][$index].';', + 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', ); } @@ -997,11 +1062,10 @@ class Parsedown $Element = array( 'name' => 'tr', - 'handler' => 'elements', - 'text' => $Elements, + 'elements' => $Elements, ); - $Block['element']['text'][1]['text'] []= $Element; + $Block['element']['elements'][1]['elements'] []= $Element; return $Block; } @@ -1013,13 +1077,27 @@ class Parsedown protected function paragraph($Line) { - $Block = array( + return array( + 'type' => 'Paragraph', 'element' => array( 'name' => 'p', - 'text' => $Line['text'], - 'handler' => 'line', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $Line['text'], + 'destination' => 'elements', + ), ), ); + } + + protected function paragraphContinue($Line, array $Block) + { + if (isset($Block['interrupted'])) + { + return; + } + + $Block['element']['handler']['argument'] .= "\n".$Line['text']; return $Block; } @@ -1029,13 +1107,11 @@ class Parsedown # protected $InlineTypes = array( - '"' => array('SpecialCharacter'), '!' => array('Image'), '&' => array('SpecialCharacter'), '*' => array('Emphasis'), ':' => array('Url'), - '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), - '>' => array('SpecialCharacter'), + '<' => array('UrlTag', 'EmailTag', 'Markup'), '[' => array('Link'), '_' => array('Emphasis'), '`' => array('Code'), @@ -1045,15 +1121,28 @@ class Parsedown # ~ - protected $inlineMarkerList = '!"*_&[:<>`~\\'; + protected $inlineMarkerList = '!*_&[:<`~\\'; # # ~ # - public function line($text, $nonNestables=array()) + public function line($text, $nonNestables = array()) { - $markup = ''; + return $this->elements($this->lineElements($text, $nonNestables)); + } + + protected function lineElements($text, $nonNestables = array()) + { + # standardize line breaks + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + $Elements = array(); + + $nonNestables = (empty($nonNestables) + ? array() + : array_combine($nonNestables, $nonNestables) + ); # $excerpt is based on the first occurrence of a marker @@ -1061,7 +1150,7 @@ class Parsedown { $marker = $excerpt[0]; - $markerPosition = strpos($text, $marker); + $markerPosition = strlen($text) - strlen($excerpt); $Excerpt = array('text' => $excerpt, 'context' => $text); @@ -1069,12 +1158,12 @@ class Parsedown { # check to see if the current inline type is nestable in the current context - if ( ! empty($nonNestables) and in_array($inlineType, $nonNestables)) + if (isset($nonNestables[$inlineType])) { continue; } - $Inline = $this->{'inline'.$inlineType}($Excerpt); + $Inline = $this->{"inline$inlineType"}($Excerpt); if ( ! isset($Inline)) { @@ -1097,19 +1186,21 @@ class Parsedown # cause the new element to 'inherit' our non nestables - foreach ($nonNestables as $non_nestable) - { - $Inline['element']['nonNestables'][] = $non_nestable; - } + + $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) + ? array_merge($Inline['element']['nonNestables'], $nonNestables) + : $nonNestables + ; # the text that comes before the inline $unmarkedText = substr($text, 0, $Inline['position']); # compile the unmarked text - $markup .= $this->unmarkedText($unmarkedText); + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; # compile the inline - $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); + $Elements[] = $this->extractElement($Inline); # remove the examined text $text = substr($text, $Inline['position'] + $Inline['extent']); @@ -1121,28 +1212,57 @@ class Parsedown $unmarkedText = substr($text, 0, $markerPosition + 1); - $markup .= $this->unmarkedText($unmarkedText); + $InlineText = $this->inlineText($unmarkedText); + $Elements[] = $InlineText['element']; $text = substr($text, $markerPosition + 1); } - $markup .= $this->unmarkedText($text); + $InlineText = $this->inlineText($text); + $Elements[] = $InlineText['element']; - return $markup; + foreach ($Elements as &$Element) + { + if ( ! isset($Element['autobreak'])) + { + $Element['autobreak'] = false; + } + } + + return $Elements; } # # ~ # + protected function inlineText($text) + { + $Inline = array( + 'extent' => strlen($text), + 'element' => array(), + ); + + $Inline['element']['elements'] = self::pregReplaceElements( + $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', + array( + array('name' => 'br'), + array('text' => "\n"), + ), + $text + ); + + return $Inline; + } + protected function inlineCode($Excerpt) { $marker = $Excerpt['text'][0]; - if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(? strlen($matches[0]), @@ -1156,13 +1276,19 @@ class Parsedown protected function inlineEmailTag($Excerpt) { - if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) - { + $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; + + $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' + . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; + + if (strpos($Excerpt['text'], '>') !== false + and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) + ){ $url = $matches[1]; if ( ! isset($matches[2])) { - $url = 'mailto:' . $url; + $url = "mailto:$url"; } return array( @@ -1204,8 +1330,11 @@ class Parsedown 'extent' => strlen($matches[0]), 'element' => array( 'name' => $emphasis, - 'handler' => 'line', - 'text' => $matches[1], + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) ), ); } @@ -1215,7 +1344,7 @@ class Parsedown if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) { return array( - 'markup' => $Excerpt['text'][1], + 'element' => array('rawHtml' => $Excerpt['text'][1]), 'extent' => 2, ); } @@ -1243,8 +1372,9 @@ class Parsedown 'name' => 'img', 'attributes' => array( 'src' => $Link['element']['attributes']['href'], - 'alt' => $Link['element']['text'], + 'alt' => $Link['element']['handler']['argument'], ), + 'autobreak' => true, ), ); @@ -1259,9 +1389,12 @@ class Parsedown { $Element = array( 'name' => 'a', - 'handler' => 'line', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => null, + 'destination' => 'elements', + ), 'nonNestables' => array('Url', 'Link'), - 'text' => null, 'attributes' => array( 'href' => null, 'title' => null, @@ -1274,7 +1407,7 @@ class Parsedown if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) { - $Element['text'] = $matches[1]; + $Element['handler']['argument'] = $matches[1]; $extent += strlen($matches[0]); @@ -1285,7 +1418,7 @@ class Parsedown return; } - if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) + if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) { $Element['attributes']['href'] = $matches[1]; @@ -1300,14 +1433,14 @@ class Parsedown { if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) { - $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; + $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; $definition = strtolower($definition); $extent += strlen($matches[0]); } else { - $definition = strtolower($Element['text']); + $definition = strtolower($Element['handler']['argument']); } if ( ! isset($this->DefinitionData['Reference'][$definition])) @@ -1334,26 +1467,26 @@ class Parsedown return; } - if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) + if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) { return array( - 'markup' => $matches[0], + 'element' => array('rawHtml' => $matches[0]), 'extent' => strlen($matches[0]), ); } - if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) + if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) { return array( - 'markup' => $matches[0], + 'element' => array('rawHtml' => $matches[0]), 'extent' => strlen($matches[0]), ); } - if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) + if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) { return array( - 'markup' => $matches[0], + 'element' => array('rawHtml' => $matches[0]), 'extent' => strlen($matches[0]), ); } @@ -1361,23 +1494,16 @@ class Parsedown protected function inlineSpecialCharacter($Excerpt) { - if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) - { + if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false + and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) + ) { return array( - 'markup' => '&', - 'extent' => 1, + 'element' => array('rawHtml' => '&' . $matches[1] . ';'), + 'extent' => strlen($matches[0]), ); } - $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); - - if (isset($SpecialCharacter[$Excerpt['text'][0]])) - { - return array( - 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', - 'extent' => 1, - ); - } + return; } protected function inlineStrikethrough($Excerpt) @@ -1393,8 +1519,11 @@ class Parsedown 'extent' => strlen($matches[0]), 'element' => array( 'name' => 'del', - 'text' => $matches[1], - 'handler' => 'line', + 'handler' => array( + 'function' => 'lineElements', + 'argument' => $matches[1], + 'destination' => 'elements', + ) ), ); } @@ -1407,8 +1536,9 @@ class Parsedown return; } - if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) - { + if (strpos($Excerpt['context'], 'http') !== false + and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) + ) { $url = $matches[0][0]; $Inline = array( @@ -1429,7 +1559,7 @@ class Parsedown protected function inlineUrlTag($Excerpt) { - if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) + if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) { $url = $matches[1]; @@ -1450,23 +1580,112 @@ class Parsedown protected function unmarkedText($text) { - if ($this->breaksEnabled) - { - $text = preg_replace('/[ ]*\n/', "
\n", $text); - } - else - { - $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "
\n", $text); - $text = str_replace(" \n", "\n", $text); - } - - return $text; + $Inline = $this->inlineText($text); + return $this->element($Inline['element']); } # # Handlers # + protected function handle(array $Element) + { + if (isset($Element['handler'])) + { + if (!isset($Element['nonNestables'])) + { + $Element['nonNestables'] = array(); + } + + if (is_string($Element['handler'])) + { + $function = $Element['handler']; + $argument = $Element['text']; + unset($Element['text']); + $destination = 'rawHtml'; + } + else + { + $function = $Element['handler']['function']; + $argument = $Element['handler']['argument']; + $destination = $Element['handler']['destination']; + } + + $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); + + if ($destination === 'handler') + { + $Element = $this->handle($Element); + } + + unset($Element['handler']); + } + + return $Element; + } + + protected function handleElementRecursive(array $Element) + { + return $this->elementApplyRecursive(array($this, 'handle'), $Element); + } + + protected function handleElementsRecursive(array $Elements) + { + return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); + } + + protected function elementApplyRecursive($closure, array $Element) + { + $Element = call_user_func($closure, $Element); + + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); + } + + return $Element; + } + + protected function elementApplyRecursiveDepthFirst($closure, array $Element) + { + if (isset($Element['elements'])) + { + $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); + } + elseif (isset($Element['element'])) + { + $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); + } + + $Element = call_user_func($closure, $Element); + + return $Element; + } + + protected function elementsApplyRecursive($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursive($closure, $Element); + } + + return $Elements; + } + + protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) + { + foreach ($Elements as &$Element) + { + $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); + } + + return $Elements; + } + protected function element(array $Element) { if ($this->safeMode) @@ -1474,18 +1693,28 @@ class Parsedown $Element = $this->sanitiseElement($Element); } - $markup = '<'.$Element['name']; + # identity map if element has no handler + $Element = $this->handle($Element); - if (isset($Element['attributes'])) + $hasName = isset($Element['name']); + + $markup = ''; + + if ($hasName) { - foreach ($Element['attributes'] as $name => $value) - { - if ($value === null) - { - continue; - } + $markup .= '<' . $Element['name']; - $markup .= ' '.$name.'="'.self::escape($value).'"'; + if (isset($Element['attributes'])) + { + foreach ($Element['attributes'] as $name => $value) + { + if ($value === null) + { + continue; + } + + $markup .= " $name=\"".self::escape($value).'"'; + } } } @@ -1500,35 +1729,40 @@ class Parsedown elseif (isset($Element['rawHtml'])) { $text = $Element['rawHtml']; + $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; } - if (isset($text)) + $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); + + if ($hasContent) { - $markup .= '>'; + $markup .= $hasName ? '>' : ''; - if (!isset($Element['nonNestables'])) + if (isset($Element['elements'])) { - $Element['nonNestables'] = array(); + $markup .= $this->elements($Element['elements']); } - - if (isset($Element['handler'])) + elseif (isset($Element['element'])) { - $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']); - } - elseif (!$permitRawHtml) - { - $markup .= self::escape($text, true); + $markup .= $this->element($Element['element']); } else { - $markup .= $text; + if (!$permitRawHtml) + { + $markup .= self::escape($text, true); + } + else + { + $markup .= $text; + } } - $markup .= ''; + $markup .= $hasName ? '' : ''; } - else + elseif ($hasName) { $markup .= ' />'; } @@ -1540,12 +1774,26 @@ class Parsedown { $markup = ''; + $autoBreak = true; + foreach ($Elements as $Element) { - $markup .= "\n" . $this->element($Element); + if (empty($Element)) + { + continue; + } + + $autoBreakNext = (isset($Element['autobreak']) + ? $Element['autobreak'] : isset($Element['name']) + ); + // (autobreak === false) covers both sides of an element + $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; + + $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); + $autoBreak = $autoBreakNext; } - $markup .= "\n"; + $markup .= $autoBreak ? "\n" : ''; return $markup; } @@ -1554,21 +1802,49 @@ class Parsedown protected function li($lines) { - $markup = $this->lines($lines); + $Elements = $this->linesElements($lines); - $trimmedMarkup = trim($markup); - - if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '

') - { - $markup = $trimmedMarkup; - $markup = substr($markup, 3); - - $position = strpos($markup, "

"); - - $markup = substr_replace($markup, '', $position, 4); + if ( ! in_array('', $lines) + and isset($Elements[0]) and isset($Elements[0]['name']) + and $Elements[0]['name'] === 'p' + ) { + unset($Elements[0]['name']); } - return $markup; + return $Elements; + } + + # + # AST Convenience + # + + /** + * Replace occurrences $regexp with $Elements in $text. Return an array of + * elements representing the replacement. + */ + protected static function pregReplaceElements($regexp, $Elements, $text) + { + $newElements = array(); + + while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) + { + $offset = $matches[0][1]; + $before = substr($text, 0, $offset); + $after = substr($text, $offset + strlen($matches[0][0])); + + $newElements[] = array('text' => $before); + + foreach ($Elements as $Element) + { + $newElements[] = $Element; + } + + $text = $after; + } + + $newElements[] = array('text' => $text); + + return $newElements; } # @@ -1590,6 +1866,12 @@ class Parsedown 'img' => 'src', ); + if ( ! isset($Element['name'])) + { + unset($Element['attributes']); + return $Element; + } + if (isset($safeUrlNameToAtt[$Element['name']])) { $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); @@ -1679,12 +1961,12 @@ class Parsedown # Read-Only protected $specialCharacters = array( - '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', + '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' ); protected $StrongRegex = array( - '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', - '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', + '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', + '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', ); protected $EmRegex = array( @@ -1692,7 +1974,7 @@ class Parsedown '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', ); - protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; + protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; protected $voidElements = array( 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', diff --git a/vendor/erusev/parsedown/README.md b/vendor/erusev/parsedown/README.md index b5d9ed2e..b9df3a20 100644 --- a/vendor/erusev/parsedown/README.md +++ b/vendor/erusev/parsedown/README.md @@ -1,71 +1,79 @@ -> I also make [Caret](https://caret.io?ref=parsedown) - a Markdown editor for Mac and PC. +# Parsedown -## Parsedown +[![Total Downloads](https://poser.pugx.org/erusev/parsedown/d/total.svg)](https://packagist.org/packages/erusev/parsedown) +[![Version](https://poser.pugx.org/erusev/parsedown/v/stable.svg)](https://packagist.org/packages/erusev/parsedown) +[![License](https://poser.pugx.org/erusev/parsedown/license.svg)](https://packagist.org/packages/erusev/parsedown) -[![Build Status](https://img.shields.io/travis/erusev/parsedown/master.svg?style=flat-square)](https://travis-ci.org/erusev/parsedown) - +Better Markdown Parser in PHP — demo -Better Markdown Parser in PHP +## Features -[Demo](http://parsedown.org/demo) | -[Benchmarks](http://parsedown.org/speed) | -[Tests](http://parsedown.org/tests/) | -[Documentation](https://github.com/erusev/parsedown/wiki/) +- One file +- No dependencies +- [Super fast](http://parsedown.org/speed) +- Extensible +- [GitHub flavored](https://github.github.com/gfm) +- [Tested](http://parsedown.org/tests/) in 5.3 to 7.3 +- [Markdown Extra extension](https://github.com/erusev/parsedown-extra) -### Features +## Installation -* One File -* No Dependencies -* Super Fast -* Extensible -* [GitHub flavored](https://help.github.com/articles/github-flavored-markdown) -* Tested in 5.3 to 7.1 and in HHVM -* [Markdown Extra extension](https://github.com/erusev/parsedown-extra) +Install the [composer package]: -### Installation +```sh +composer require erusev/parsedown +``` -Include `Parsedown.php` or install [the composer package](https://packagist.org/packages/erusev/parsedown). +Or download the [latest release] and include `Parsedown.php` -### Example +[composer package]: https://packagist.org/packages/erusev/parsedown "The Parsedown package on packagist.org" +[latest release]: https://github.com/erusev/parsedown/releases/latest "The latest release of Parsedown" -``` php +## Example + +```php $Parsedown = new Parsedown(); echo $Parsedown->text('Hello _Parsedown_!'); # prints:

Hello Parsedown!

``` +You can also parse inline markdown only: + +```php +echo $Parsedown->line('Hello _Parsedown_!'); # prints: Hello Parsedown! +``` + More examples in [the wiki](https://github.com/erusev/parsedown/wiki/) and in [this video tutorial](http://youtu.be/wYZBY8DEikI). -### Security +## Security Parsedown is capable of escaping user-input within the HTML that it generates. Additionally Parsedown will apply sanitisation to additional scripting vectors (such as scripting link destinations) that are introduced by the markdown syntax itself. To tell Parsedown that it is processing untrusted user-input, use the following: + ```php -$parsedown = new Parsedown; -$parsedown->setSafeMode(true); +$Parsedown->setSafeMode(true); ``` If instead, you wish to allow HTML within untrusted user-input, but still want output to be free from XSS it is recommended that you make use of a HTML sanitiser that allows HTML tags to be whitelisted, like [HTML Purifier](http://htmlpurifier.org/). In both cases you should strongly consider employing defence-in-depth measures, like [deploying a Content-Security-Policy](https://scotthelme.co.uk/content-security-policy-an-introduction/) (a browser security feature) so that your page is likely to be safe even if an attacker finds a vulnerability in one of the first lines of defence above. -#### Security of Parsedown Extensions - Safe mode does not necessarily yield safe results when using extensions to Parsedown. Extensions should be evaluated on their own to determine their specific safety against XSS. -### Escaping HTML -> ⚠️  **WARNING:** This method isn't safe from XSS! +## Escaping HTML + +> WARNING: This method is not safe from XSS! + +If you wish to escape HTML in trusted input, you can use the following: -If you wish to escape HTML **in trusted input**, you can use the following: ```php -$parsedown = new Parsedown; -$parsedown->setMarkupEscaped(true); +$Parsedown->setMarkupEscaped(true); ``` -Beware that this still allows users to insert unsafe scripting vectors, such as links like `[xss](javascript:alert%281%29)`. +Beware that this still allows users to insert unsafe scripting vectors, ex: `[xss](javascript:alert%281%29)`. -### Questions +## Questions **How does Parsedown work?** @@ -79,8 +87,12 @@ It passes most of the CommonMark tests. Most of the tests that don't pass deal w **Who uses it?** -[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony demo](https://github.com/symfony/symfony-demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents). +[Laravel Framework](https://laravel.com/), [Bolt CMS](http://bolt.cm/), [Grav CMS](http://getgrav.org/), [Herbie CMS](http://www.getherbie.org/), [Kirby CMS](http://getkirby.com/), [October CMS](http://octobercms.com/), [Pico CMS](http://picocms.org), [Statamic CMS](http://www.statamic.com/), [phpDocumentor](http://www.phpdoc.org/), [RaspberryPi.org](http://www.raspberrypi.org/), [Symfony Demo](https://github.com/symfony/demo) and [more](https://packagist.org/packages/erusev/parsedown/dependents). **How can I help?** Use it, star it, share it and if you feel generous, [donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=528P3NZQMP8N2). + +**What else should I know?** + +I also make [Nota](https://nota.md/) — a notes app designed for local Markdown files. diff --git a/vendor/erusev/parsedown/composer.json b/vendor/erusev/parsedown/composer.json index f8b40f8c..b145680a 100644 --- a/vendor/erusev/parsedown/composer.json +++ b/vendor/erusev/parsedown/composer.json @@ -13,11 +13,11 @@ } ], "require": { - "php": ">=5.3.0", + "php": ">=7.1", "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.35" + "phpunit/phpunit": "^7.5|^8.5|^9.6" }, "autoload": { "psr-0": {"Parsedown": ""}