3 * @copyright Copyright (c) 2014 Carsten Brandt
4 * @license https://github.com/cebe/markdown/blob/master/LICENSE
5 * @link https://github.com/cebe/markdown#readme
8 namespace cebe\markdown\inline;
10 // work around https://github.com/facebook/hhvm/issues/1120
11 defined('ENT_HTML401') || define('ENT_HTML401', 0);
14 * Addes links and images as well as url markers.
16 * This trait conflicts with the HtmlTrait. If both are used together,
17 * you have to define a resolution, by defining the HtmlTrait::parseInlineHtml
18 * as private so it is not used directly:
21 * use block\HtmlTrait {
22 * parseInlineHtml as private parseInlineHtml;
26 * If the method exists it is called internally by this trait.
28 * Also make sure to reset references on prepare():
31 * protected function prepare()
34 * $this->references = [];
41 * @var array a list of defined references in this document.
43 protected $references = [];
46 * Parses a link indicated by `[`.
49 protected function parseLink($markdown)
51 if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
52 list($text, $url, $title, $offset, $key) = $parts;
56 'text' => $this->parseInline($text),
60 'orig' => substr($markdown, 0, $offset),
65 // remove all starting [ markers to avoid next one to be parsed as link
68 while (isset($markdown[$i]) && $markdown[$i] == '[') {
72 return [['text', $result], $i];
77 * Parses an image indicated by `![`.
80 protected function parseImage($markdown)
82 if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
83 list($text, $url, $title, $offset, $key) = $parts;
92 'orig' => substr($markdown, 0, $offset + 1),
97 // remove all starting [ markers to avoid next one to be parsed as link
100 while (isset($markdown[$i]) && $markdown[$i] == '[') {
104 return [['text', $result], $i];
108 protected function parseLinkOrImage($markdown)
110 if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
111 $text = $textMatches[1];
112 $offset = strlen($textMatches[0]);
113 $markdown = substr($markdown, $offset);
116 /(?(R) # in case of recursion match parentheses
117 \(((?>[^\s()]+)|(?R))*\)
118 | # else match a link with title
119 ^\((((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\)
122 if (preg_match($pattern, $markdown, $refMatches)) {
126 isset($refMatches[2]) ? $refMatches[2] : '', // url
127 empty($refMatches[5]) ? null: $refMatches[5], // title
128 $offset + strlen($refMatches[0]), // offset
129 null, // reference key
131 } elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
132 // reference style link
133 if (empty($refMatches[2])) {
134 $key = strtolower($text);
136 $key = strtolower($refMatches[2]);
142 $offset + strlen($refMatches[0]), // offset
151 * Parses inline HTML.
154 protected function parseLt($text)
156 if (strpos($text, '>') !== false) {
157 if (!in_array('parseLink', $this->context)) { // do not allow links in links
158 if (preg_match('/^<([^\s]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
161 ['email', $matches[1]],
164 } elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
167 ['url', $matches[1]],
172 // try inline HTML if it was neither a URL nor email if HtmlTrait is included.
173 if (method_exists($this, 'parseInlineHtml')) {
174 return $this->parseInlineHtml($text);
177 return [['text', '<'], 1];
180 protected function renderEmail($block)
182 $email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
183 return "<a href=\"mailto:$email\">$email</a>";
186 protected function renderUrl($block)
188 $url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
189 $text = htmlspecialchars(urldecode($block[1]), ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
190 return "<a href=\"$url\">$text</a>";
193 protected function lookupReference($key)
195 $normalizedKey = preg_replace('/\s+/', ' ', $key);
196 if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
197 return $this->references[$key];
202 protected function renderLink($block)
204 if (isset($block['refkey'])) {
205 if (($ref = $this->lookupReference($block['refkey'])) !== false) {
206 $block = array_merge($block, $ref);
208 return $block['orig'];
211 return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
212 . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
213 . '>' . $this->renderAbsy($block['text']) . '</a>';
216 protected function renderImage($block)
218 if (isset($block['refkey'])) {
219 if (($ref = $this->lookupReference($block['refkey'])) !== false) {
220 $block = array_merge($block, $ref);
222 return $block['orig'];
225 return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
226 . ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
227 . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
228 . ($this->html5 ? '>' : ' />');
233 protected function identifyReference($line)
235 return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
239 * Consume link references
241 protected function consumeReference($lines, $current)
243 while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
244 $label = strtolower($matches[1]);
246 $this->references[$label] = [
247 'url' => $matches[2],
249 if (isset($matches[3])) {
250 $this->references[$label]['title'] = $matches[3];
252 // title may be on the next line
253 if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
254 $this->references[$label]['title'] = $matches[1];
260 return [false, --$current];