/** * {@inheritdoc} */ public function processListItems(Text $list, array $options = array(), $level = 0) { $list->replace('/\\n{2,}\\z/', "\n"); /** @noinspection PhpUnusedParameterInspection */ $list->replace('{ (\\n)? # leading line = $1 (^[ \\t]*) # leading whitespace = $2 (' . $this->getPattern() . ') [ \\t]+ # list marker = $3 (([ ]*(\\[([ ]|x)\\]) [ \\t]+)?(?s:.+?) # list item text = $4, checkbox = $5, checked = %6 (\\n{1,2})) (?= \\n* (\\z | \\2 (' . $this->getPattern() . ') [ \\t]+)) }mx', function (Text $w, Text $leadingLine, Text $ls, Text $m, Text $item, Text $checkbox, Text $check) use($options, $level) { if (!$checkbox->isEmpty()) { $item->replace('/^\\[( |x)\\]/', function (Text $w, Text $check) { $attr = array('type' => 'checkbox'); if ($check == 'x') { $attr['checked'] = 'checked'; } return $this->getRenderer()->renderTag('input', new Text(), Tag::TYPE_INLINE, array('attr' => $attr)); }); } if ((string) $leadingLine || $item->match('/\\n{2,}/')) { $this->getEmitter()->emit('outdent', array($item)); $this->getEmitter()->emit('block', array($item)); } else { $this->getEmitter()->emit('outdent', array($item)); $this->processList($item, $options, ++$level); $item->rtrim(); $this->getEmitter()->emit('inline', array($item)); } return $this->getRenderer()->renderListItem($item) . "\n"; }); }
/** * @param Text $text */ public function escapeAmpsAndBrackets(Text $text) { if ($text->contains('&')) { $text->replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\\w+);)/', '&'); } if ($text->contains('<')) { $text->replace('{<(?![a-z/?\\$!])}i', '<'); } }
/** * Turn standard URL into markdown URL * * @param Text $text */ public function processStandardUrl(Text $text) { $hashes = []; // escape <code> $text->replace('{<code>.*?</code>}m', function (Text $w) use(&$hashes) { $md5 = md5($w); $hashes[$md5] = $w; return "{gfm-extraction-{$md5}}"; }); $text->replace('{(?<!]\\(|"|<|\\[)((?:https?|ftp)://[^\'"\\)>\\s]+)(?!>|\\"|\\])}', '<\\1>'); /** @noinspection PhpUnusedParameterInspection */ $text->replace('/\\{gfm-extraction-([0-9a-f]{32})\\}/m', function (Text $w, Text $md5) use(&$hashes) { return $hashes[(string) $md5]; }); }
/** * Strike-through `~~word~~` to `<del>word</del>` * * @param Text $text */ public function processStrikeThrough(Text $text) { /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ (~~) (?=\\S) (.+?) (?<=\\S) \\1 }sx', function (Text $w, Text $a, Text $target) { return $this->getRenderer()->renderTag('del', $target, Tag::TYPE_INLINE); }); }
public function testReplaceCallback() { $text = new Text('<tag></tag>'); $this->assertEquals('<div></div>', $text->replace('/[a-z]+/', function () { return 'div'; })); }
/** * @param Text $text */ public function processHorizontalRule(Text $text) { $marks = array('\\*', '-', '_'); foreach ($marks as $mark) { $text->replace('/^[ ]{0,2}([ ]?' . $mark . '[ ]?){3,}[ \\t]*$/m', $this->getRenderer()->renderHorizontalRule() . "\n\n"); } }
/** * @param Text $text */ public function processMentions(Text $text) { // Turn @username into [@username](http://example.com/user/username) $text->replace('/(?:^|[^a-zA-Z0-9.])@([A-Za-z0-9]+)/', function (Text $w, Text $username) { return ' [@' . $username . '](' . $this->config->site->url . '/user/0/' . $username . ')'; }); }
/** * @param Text $text * @param array $options */ public function processFencedCodeBlock(Text $text, array $options = []) { /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ (?:\\n\\n|\\A) (?: ([`~]{3})[ ]* #1 fence ` or ~ ([a-zA-Z0-9]*?)? #2 language [optional] \\n+ (.*?)\\n #3 code block \\1 # matched #1 ) }smx', function (Text $w, Text $fence, Text $lang, Text $code) use($options) { $rendererOptions = []; if (!$lang->isEmpty()) { if ($options['pygments'] && class_exists('KzykHys\\Pygments\\Pygments')) { $pygments = new Pygments(); $html = $pygments->highlight($code, $lang, 'html'); return "\n\n" . $html . "\n\n"; } $rendererOptions = ['attr' => ['class' => 'prettyprint lang-' . $lang->lower()]]; } $code->escapeHtml(ENT_NOQUOTES); $this->markdown->emit('detab', array($code)); $code->replace('/\\A\\n+/', ''); $code->replace('/\\s+\\z/', ''); return "\n\n" . $this->getRenderer()->renderCodeBlock($code, $rendererOptions) . "\n\n"; }); }
/** * @param Text $text */ public function processMentions(Text $text) { // Turn @username into [@username](http://example.com/user/username) $text->replace('/(?:^|[^a-zA-Z0-9.])@([A-Za-z]+[A-Za-z0-9]+)/', function (Text $w, Text $username) { return ' [@' . $username . '](http://forum.phalconphp.com/user/0/' . $username . ')'; }); }
public function processTest(Text $text) { $hashes = []; // escape <code> $text->replace('{<code>.*?</code>}m', function (Text $w) use(&$hashes) { $md5 = md5($w); $hashes[$md5] = $w; return "{gfm-extraction-{$md5}}"; }); $text->replace('#(?:(?<=[href|src]=\\"|\')(?:[a-z0-9]+:\\/\\/)?|(?:[a-z0-9]+:\\/\\/))([^\'">\\s]+_+[^\'">\\s]*)+#i', function (Text $w) { $w->replaceString('_', '%5'); return $w; }); /** @noinspection PhpUnusedParameterInspection */ $text->replace('/\\{gfm-extraction-([0-9a-f]{32})\\}/m', function (Text $w, Text $md5) use(&$hashes) { return $hashes[(string) $md5]; }); }
/** * Newlines * * The biggest difference that GFM introduces is in the handling of line breaks. * With SM you can hard wrap paragraphs of text and they will be combined into a single paragraph. * We find this to be the cause of a huge number of unintentional formatting errors. * GFM treats newlines in paragraph-like content as real line breaks, which is probably what you intended. * * @param Text $text */ public function processHardBreak(Text $text) { $text->replace('/^[\\S\\<][^\\n]*\\n+(?!( |\\t)*<)/m', function (Text $w) { if ($w->match('/\\n{2}/') || $w->match('/ \\n/')) { return $w; } return $w->trim()->append(" \n"); }); }
/** * @param Text $text */ public function processMentions(Text $text) { /** * Turn @username into [@username](http://example.com/user/username) */ $text->replace('/(?:^|[^a-zA-Z0-9.])@([A-Za-z]+[A-Za-z0-9]+)/', function (Text $w, Text $username) { return sprintf('[@%s](https://github.com/%s)', $username, $username); }); }
/** * @param Text $text */ public function processIssues(Text $text) { /** * Turn the token to a github issue URL */ $text->replace('(\\[GI:(\\d+)\\])', function (Text $w, Text $issue) { return sprintf($this->issueUrl, $issue, $issue); }); }
/** * @param Text $text * * @throws \Exception */ public function processPullRequest(Text $text) { if (true === empty($this->accountName) || true === empty($this->projectName)) { throw new \Exception('Github account name or project are not set'); } /** * Turn the token to a github issue URL */ $text->replace('(\\[GPR:(\\d+)\\])', function (Text $w, Text $issue) { return sprintf($this->issueUrl, $issue, $this->accountName, $this->projectName, $issue); }); }
/** * @param Text $text */ public function processItalic(Text $text) { if (!$text->contains('*') && !$text->contains('_')) { return; } /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ ([^\\*_\\s]?) (\\*|_) (?=\\S) (.+?) (?<=\\S) \\2 ([^\\*_\\s]?) }sx', function (Text $w, Text $prevChar, Text $a, Text $target, Text $nextChar) { if (!$prevChar->isEmpty() && !$nextChar->isEmpty() && $target->contains(' ')) { $this->getEmitter()->emit('escape.special_chars', [$w->replaceString(['*', '_'], ['\\*', '\\_'])]); return $w; } return $prevChar . $this->getRenderer()->renderItalicText($target) . $nextChar; }); }
/** * @param Text $text */ public function processAtxHeader(Text $text) { /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ ^(\\#{1,6}) # $1 = string of #\'s [ \\t]* (.+?) # $2 = Header text [ \\t]* \\#* # optional closing #\'s (not counted) \\n+ }mx', function (Text $whole, Text $marks, Text $content) { $level = strlen($marks); $this->markdown->emit('inline', array($content)); return $this->getRenderer()->renderHeader($content, array('level' => $level)) . "\n\n"; }); }
/** * @param Text $text */ public function buildParagraph(Text $text) { $parts = $text->replace('/\\A\\n+/', '')->replace('/\\n+\\z/', '')->split('/\\n{2,}/', PREG_SPLIT_NO_EMPTY); $parts->apply(function (Text $part) { if (!$this->markdown->getHashRegistry()->exists($part)) { $this->markdown->emit('inline', array($part)); $part->replace('/^([ \\t]*)/', ''); $part->setString($this->getRenderer()->renderParagraph((string) $part)); } return $part; }); $parts->apply(function (Text $part) { if ($this->markdown->getHashRegistry()->exists($part)) { $part->setString(trim($this->markdown->getHashRegistry()->get($part))); } return $part; }); $text->setString($parts->join("\n\n")); }
/** * @param Text $text */ public function processCodeSpan(Text $text) { if (!$text->contains('`')) { return; } $chars = ['\\\\', '`', '\\*', '_', '{', '}', '\\[', '\\]', '\\(', '\\)', '>', '#', '\\+', '\\-', '\\.', '!']; $chars = implode('|', $chars); /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ (`+) # $1 = Opening run of ` (.+?) # $2 = The code block (?<!`) \\1 # Matching closer (?!`) }x', function (Text $w, Text $b, Text $code) use($chars) { $code->trim()->escapeHtml(ENT_NOQUOTES); $code->replace(sprintf('/(?<!\\\\)(%s)/', $chars), '\\\\${1}'); return $this->getRenderer()->renderCodeSpan($code); }); }
/** * @param Text $text */ public function processBlockQuote(Text $text) { $text->replace('{ (?: (?: ^[ \\t]*>[ \\t]? # > at the start of a line .+\\n # rest of the first line (?:.+\\n)* # subsequent consecutive lines \\n* # blanks )+ ) }mx', function (Text $bq) { $bq->replace('/^[ \\t]*>[ \\t]?/m', ''); $bq->replace('/^[ \\t]+$/m', ''); $this->markdown->emit('block', [$bq]); $bq->replace('|\\s*<pre>.+?</pre>|s', function (Text $pre) { return $pre->replace('/^ /m', ''); }); return $this->getRenderer()->renderBlockQuote($bq) . "\n\n"; }); }
/** * @param Text $text */ public function processHeader(Text $text) { $text->replace('{ ^h([1-6]) #1 Level (|=|>)\\. #2 Align marker [ \\t]* (.+) [ \\t]*\\n+ }mx', function (Text $w, Text $level, Text $mark, Text $header) { $attributes = []; switch ((string) $mark) { case '>': $attributes['align'] = 'right'; break; case '=': $attributes['align'] = 'center'; break; } return $this->getRenderer()->renderHeader($header, ['level' => (int) $level->getString(), 'attr' => $attributes]) . "\n\n"; }); }
/** * @param Text $text */ public function processFencedCodeBlock(Text $text) { /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ (?:\\n\\n|\\A) (?: ([`~]{3})[ ]* #1 fence ` or ~ ([a-zA-Z0-9]*?)? #2 language [optional] \\n+ (.*?)\\n #3 code block \\1 # matched #1 ) }smx', function (Text $w, Text $fence, Text $lang, Text $code) { $options = array(); if (!$lang->isEmpty()) { $options = array('attr' => array('class' => 'prettyprint lang-' . $lang->lower())); } $code->escapeHtml(ENT_NOQUOTES); $this->markdown->emit('detab', array($code)); $code->replace('/\\A\\n+/', ''); $code->replace('/\\s+\\z/', ''); return "\n\n" . $this->getRenderer()->renderCodeBlock($code, $options) . "\n\n"; }); }
/** * Gfm tables * * @param Text $text * @param array $options */ public function processTable(Text $text, array $options = array()) { $lessThanTab = $options['tabWidth'] - 1; $text->replace('/ (?:\\n\\n|\\A) (?:[ ]{0,' . $lessThanTab . '} # table header (?:\\|?) # optional outer pipe ([^\\n]*?\\|[^\\n]*?) #1 table header (?:\\|?) # optional outer pipe )\\n (?:[ ]{0,' . $lessThanTab . '} # second line (?:\\|?) # optional outer pipe ([-:\\| ]+?\\|[-:\\| ]+?) #2 dashes and pipes (?:\\|?) # optional outer pipe )\\n (.*?)\\n{2,} #3 table body /smx', function (Text $w, Text $header, Text $rule, Text $body) use($options) { // Escape pipe to hash, so you can include pipe in cells by escaping it like this: `\\|` $this->escapePipes($header); $this->escapePipes($rule); $this->escapePipes($body); try { $baseTags = $this->createBaseTags($rule->split('/\\|/')); $headerCells = $this->parseHeader($header, $baseTags); $bodyRows = $this->parseBody($body, $baseTags); } catch (SyntaxError $e) { if ($options['strict']) { throw $e; } return $w; } $html = $this->createView($headerCells, $bodyRows); $this->unescapePipes($html); return "\n\n" . $html . "\n\n"; }); }
/** * @param Text $text */ public function processWikiDefinitionList(Text $text) { $text->replace('{ (^ ;[ \\t]*.+\\n (:[ \\t]*.+\\n){1,} ){1,} \\n+ }mx', function (Text $w) { $w->replace('/^;[ \\t]*(.+)\\n((:[ \\t]*.+\\n){1,})/m', function (Text $w, Text $item, Text $content) { $dt = Tag::create('dt')->setText($item); $lines = $content->trim()->ltrim(':')->split('/\\n?^:[ \\t]*/m', PREG_SPLIT_NO_EMPTY); if (count($lines) > 1) { $dd = Tag::create('dd')->setText(Tag::create('p')->setText(trim($lines->join($this->getRenderer()->renderLineBreak() . "\n")))); } else { $dd = Tag::create('dd')->setText($content->trim()); } return $dt->render() . "\n" . $dd->render() . "\n"; }); $tag = Tag::create('dl')->setText("\n" . $w->trim() . "\n"); return $tag->render(); }); }
/** * @param Text $text */ public function processHardBreak(Text $text) { $text->replace('/ {2,}\\n/', $this->getRenderer()->renderLineBreak() . "\n"); }
/** * handle inline images: ![alt text](url "optional title") * * @param Text $text * @param array $options */ public function processInlineImage(Text $text, array $options = array()) { if (!$text->contains('![')) { return; } /** @xnoinspection PhpUnusedParameterInspection */ $text->replace('{ ( # wrap whole match in $1 !\\[ (.*?) # alt text = $2 \\] \\( # literal paren [ \\t]* <?(\\S+?)>? # src url = $3 [ \\t]* ( # $4 ([\'"]) # quote char = $5 (.*?) # title = $6 \\5 # matching quote [ \\t]* )? # title is optional \\) ) }xs', function (Text $w, Text $whole, Text $alt, Text $url, Text $a = null, Text $q = null, Text $title = null) use($options) { $attr = array('alt' => $alt->replace('/"/', '"')); $this->markdown->emit('escape.special_chars', [$url->replace('/(?<!\\\\)_/', '\\\\_')]); $url->escapeHtml(); if ($title) { $attr['title'] = $title->replace('/"/', '"')->escapeHtml(); } return $this->getRenderer()->renderImage($url, array('attr' => $attr)); }); }
public function processNewLines(Text $text) { $text->replace('/\\n/', '<br>'); return $text; }
/** * Process the contents of a single ordered or unordered list, splitting it into individual list items. * * @param Text $list * @param array $options * @param int $level */ public function processListItems(Text $list, array $options = array(), $level = 0) { $list->replace('/\\n{2,}\\z/', "\n"); /** @noinspection PhpUnusedParameterInspection */ $list->replace('{ (\\n)? # leading line = $1 (^[ \\t]*) # leading whitespace = $2 (' . $this->getPattern() . ') [ \\t]+ # list marker = $3 ((?s:.+?) # list item text = $4 (\\n{1,2})) (?= \\n* (\\z | \\2 (' . $this->getPattern() . ') [ \\t]+)) }mx', function (Text $w, Text $leadingLine, Text $ls, Text $m, Text $item) use($options, $level) { if ((string) $leadingLine || $item->match('/\\n{2,}/')) { $this->markdown->emit('outdent', array($item)); $this->markdown->emit('block', array($item)); } else { $this->markdown->emit('outdent', array($item)); $this->processList($item, $options, ++$level); $item->rtrim(); $this->markdown->emit('inline', array($item)); } return $this->getRenderer()->renderListItem($item) . "\n"; }); }
/** * @param Text $text * @param array $options */ public function hashHtmlBlocks(Text $text, array $options = array()) { $lessThanTab = $options['tabWidth'] - 1; /* * Original source code from PHP Markdown * * > PHP Markdown Lib Copyright (c) 2004-2013 Michel Fortin * > <http://michelf.ca/> */ $nestedHtml = str_repeat('(?>[^<]+|<\\2.*?(?>/>|>', $options['nestedTagLevel']) . '.*?' . str_repeat('</\\2\\s*>)|<(?!/\\2\\s*>))*', $options['nestedTagLevel']); /** @noinspection PhpUnusedParameterInspection */ $callback = function ($whole, $html, $tag) { $hash = $this->markdown->getHashRegistry()->register($html); return "\n\n" . $hash . "\n\n"; }; $text->replace('{ ( # save in $1 ^ # start of line (with /m) <(' . $this->tagsA . ') # start tag = $2 #\\b # word break .*?>' . $nestedHtml . ' # any number of lines, **NOT** minimally matching </\\2> # the matching end tag [ \\t]* # trailing spaces/tabs (?=\\n+|\\Z) # followed by a newline or end of document ) }mx', $callback); $text->replace('{ ( # save in $1 ^ # start of line (with /m) <(' . $this->tagsB . ') # start tag = $2 \\b # word break (.*\\n)*? # any number of lines, **NOT** minimally matching .*</\\2> # the matching end tag [ \\t]* # trailing spaces/tabs (?=\\n+|\\Z) # followed by a newline or end of document ) }mx', $callback); $text->replace('{ (?: (?<=\\n\\n) # Starting after a blank line | # or \\A\\n? # the beginning of the doc ) ( # save in $1 [ ]{0,' . $lessThanTab . '} <(hr) # start tag = $2 \\b # word break ([^<>])*? # /?> # the matching end tag [ \\t]* (?=\\n{2,}|\\Z) # followed by a blank line or end of document ) }x', $callback); $text->replace('{ (?: (?<=\\n\\n) # Starting after a blank line | # or \\A\\n? # the beginning of the doc ) ( # save in $1 [ ]{0,' . $lessThanTab . '} (?s: <! (--.*?--\\s*)+ > ) [ \\t]* (?=\\n{2,}|\\Z) # followed by a blank line or end of document ) }x', $callback); }
/** * Make links out of things like `<http://example.com/>` * * @param Text $text */ public function processAutoLink(Text $text) { if (!$text->contains('<')) { return; } $text->replace('{<((?:https?|ftp):[^\'">\\s]+)>}', function (Text $w, Text $url) { $this->markdown->emit('escape.special_chars', [$url->replace('/(?<!\\\\)_/', '\\\\_')]); return $this->getRenderer()->renderLink($url, ['href' => $url->getString()]); }); /** @noinspection PhpUnusedParameterInspection */ $text->replace('{ < (?:mailto:)? ( [-.\\w]+ \\@ [-a-z0-9]+(\\.[-a-z0-9]+)*\\.[a-z]+ ) > }ix', function (Text $w, Text $address) { $address = "mailto:" . $address; $encode = array(function ($char) { return '&#' . ord($char) . ';'; }, function ($char) { return '&#x' . dechex(ord($char)) . ';'; }, function ($char) { return $char; }); $chars = new Collection(str_split($address)); $chars->apply(function ($char) use($encode) { if ($char == '@') { return $encode[rand(0, 1)]($char); } elseif ($char != ':') { $rand = rand(0, 100); $key = $rand > 90 ? 2 : ($rand < 45 ? 0 : 1); return $encode[$key]($char); } return $char; }); $address = $chars->join(); $text = $chars->slice(7)->join(); return $this->getRenderer()->renderLink($text, ['href' => $address]); }); }
/** * @param Text $text */ public function processComment(Text $text) { $text->replace('/^###\\.[ \\t]*(.+?)\\n{2,}/m', "\n\n"); }