/** * @param Text $text * * @return array */ protected function parseAttributes(Text $text) { $patterns = ['id' => '/^#([a-zA-Z0-9_-]+)/', 'class' => '/^\\.([a-zA-Z0-9_-]+)/', 'attr' => '/^\\[([^\\]]+)\\]/', 'ident' => '/^(.)/']; $tokens = ['id' => [], 'class' => [], 'attr' => [], 'ident' => []]; while (!$text->isEmpty()) { foreach ($patterns as $name => $pattern) { if ($text->match($pattern, $matches)) { $tokens[$name][] = $matches[1]; $text->setString(substr($text->getString(), strlen($matches[0]))); break; } } } $attributes = array(); if (count($tokens['id'])) { $attributes['id'] = array_pop($tokens['id']); } if (count($tokens['class'])) { $attributes['class'] = implode(' ', $tokens['class']); } if (count($tokens['attr'])) { foreach ($tokens['attr'] as $raw) { $items = explode(' ', $raw); foreach ($items as $item) { if (strpos($item, '=') !== false) { list($key, $value) = explode('=', $item); $attributes[$key] = trim($value, '"'); } else { $attributes[$item] = $item; } } } } return $attributes; }
/** * {@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 * @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 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 */ 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 . ')'; }); }
/** * 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); }); }
/** * @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); }); }
public function testTrim() { $text = new Text(' #Test## '); $this->assertEquals('#Test##', $text->trim()); $this->assertEquals('Test', $text->trim('#')); $text = new Text('Test## '); $this->assertEquals('Test##', $text->rtrim()); $this->assertEquals('Test', $text->rtrim('#')); }
/** * @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); }); }
/** * 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 * * @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; }); }
/** * 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]; }); }
/** * @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"; }); }
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]; }); }
/** * @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); }); }
/** * {@inheritdoc} */ public function renderList($content, array $options = array()) { if (!$content instanceof Text) { $content = new Text($content); } $options = $this->createResolver()->setRequired(array('type'))->setAllowedValues(array('type' => array('ul', 'ol')))->setDefaults(array('type' => 'ul'))->resolve($options); $tag = new Tag($options['type']); $tag->setText($content->prepend("\n")); $tag->setAttributes($options['attr']); return $tag->render(); }
/** * @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"; }); }
/** * @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 unhashHtmlBlocks(Text $text) { foreach ($this->markdown->getHashRegistry() as $hash => $html) { $text->replaceString($hash, trim($html)); } }
/** * @param Text $text */ public function unescapeSpecialChars(Text $text) { foreach ($this->hashes as $char => $hash) { $text->replaceString($hash, $char); } }
/** * 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]); }); }
public function processNewLines(Text $text) { $text->replace('/\\n/', '<br>'); return $text; }
/** * @param Text $text */ protected function unescapePipes(Text $text) { $text->replaceString($this->hash, '|'); }
/** * @param Text $text */ public function processComment(Text $text) { $text->replace('/^###\\.[ \\t]*(.+?)\\n{2,}/m', "\n\n"); }
/** * 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"; }); }