Completed
Push — master ( 9ffe58...cd3227 )
by Carsten
03:21 queued 01:21
created

LinkTrait::parseLink()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 26
Code Lines 18

Duplication

Lines 10
Ratio 38.46 %

Code Coverage

Tests 15
CRAP Score 5.1158

Importance

Changes 0
Metric Value
dl 10
loc 26
ccs 15
cts 18
cp 0.8333
rs 8.439
c 0
b 0
f 0
cc 5
eloc 18
nc 3
nop 1
crap 5.1158
1
<?php
2
/**
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
6
 */
7
8
namespace cebe\markdown\inline;
9
10
// work around https://github.com/facebook/hhvm/issues/1120
11 1
defined('ENT_HTML401') || define('ENT_HTML401', 0);
12
13
/**
14
 * Addes links and images as well as url markers.
15
 *
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:
19
 *
20
 * ```php
21
 * use block\HtmlTrait {
22
 *     parseInlineHtml as private parseInlineHtml;
23
 * }
24
 * ```
25
 *
26
 * If the method exists it is called internally by this trait.
27
 *
28
 * Also make sure to reset references on prepare():
29
 *
30
 * ```php
31
 * protected function prepare()
32
 * {
33
 *     // reset references
34
 *     $this->references = [];
35
 * }
36
 * ```
37
 */
38
trait LinkTrait
39
{
40
	/**
41
	 * @var array a list of defined references in this document.
42
	 */
43
	protected $references = [];
44
45
	/**
46
	 * Remove backslash from escaped characters
47
	 * @param $text
48
	 * @return string
49
	 */
50 52
	protected function replaceEscape($text)
51
	{
52 52
		$strtr = [];
53 52
		foreach($this->escapeCharacters as $char) {
54 52
			$strtr["\\$char"] = $char;
55 52
		}
56 52
		return strtr($text, $strtr);
57
	}
58
59
	/**
60
	 * Parses a link indicated by `[`.
61
	 * @marker [
62
	 */
63 50
	protected function parseLink($markdown)
64
	{
65 50
		if (!in_array('parseLink', array_slice($this->context, 1)) && ($parts = $this->parseLinkOrImage($markdown)) !== false) {
66 50
			list($text, $url, $title, $offset, $key) = $parts;
67
			return [
68
				[
69 50
					'link',
70 50
					'text' => $this->parseInline($text),
71 50
					'url' => $url,
72 50
					'title' => $title,
73 50
					'refkey' => $key,
74 50
					'orig' => substr($markdown, 0, $offset),
75 50
				],
76
				$offset
77 50
			];
78 View Code Duplication
		} else {
79
			// remove all starting [ markers to avoid next one to be parsed as link
80 11
			$result = '[';
81 11
			$i = 1;
82 11
			while (isset($markdown[$i]) && $markdown[$i] == '[') {
83
				$result .= '[';
84
				$i++;
85
			}
86 11
			return [['text', $result], $i];
87
		}
88
	}
89
90
	/**
91
	 * Parses an image indicated by `![`.
92
	 * @marker ![
93
	 */
94 7
	protected function parseImage($markdown)
95
	{
96 7
		if (($parts = $this->parseLinkOrImage(substr($markdown, 1))) !== false) {
97 7
			list($text, $url, $title, $offset, $key) = $parts;
98
99
			return [
100
				[
101 7
					'image',
102 7
					'text' => $text,
103 7
					'url' => $url,
104 7
					'title' => $title,
105 7
					'refkey' => $key,
106 7
					'orig' => substr($markdown, 0, $offset + 1),
107 7
				],
108
				$offset + 1
109 7
			];
110 View Code Duplication
		} else {
111
			// remove all starting [ markers to avoid next one to be parsed as link
112 3
			$result = '!';
113 3
			$i = 1;
114 3
			while (isset($markdown[$i]) && $markdown[$i] == '[') {
115 3
				$result .= '[';
116 3
				$i++;
117 3
			}
118 3
			return [['text', $result], $i];
119
		}
120
	}
121
122 50
	protected function parseLinkOrImage($markdown)
123
	{
124 50
		if (strpos($markdown, ']') !== false && preg_match('/\[((?>[^\]\[]+|(?R))*)\]/', $markdown, $textMatches)) { // TODO improve bracket regex
125 50
			$text = $textMatches[1];
126 50
			$offset = strlen($textMatches[0]);
127 50
			$markdown = substr($markdown, $offset);
128
129
			$pattern = <<<REGEXP
130
				/(?(R) # in case of recursion match parentheses
131
					 \(((?>[^\s()]+)|(?R))*\)
132
				|      # else match a link with title
133
					^\(\s*(((?>[^\s()]+)|(?R))*)(\s+"(.*?)")?\s*\)
134
				)/x
135 50
REGEXP;
136 50
			if (preg_match($pattern, $markdown, $refMatches)) {
137
				// inline link
138
				return [
139 33
					$text,
140 33
					isset($refMatches[2]) ? $this->replaceEscape($refMatches[2]) : '', // url
141 33
					empty($refMatches[5]) ? null: $refMatches[5], // title
142 33
					$offset + strlen($refMatches[0]), // offset
143 33
					null, // reference key
144 33
				];
145 40
			} elseif (preg_match('/^([ \n]?\[(.*?)\])?/s', $markdown, $refMatches)) {
146
				// reference style link
147 40
				if (empty($refMatches[2])) {
148 33
					$key = strtolower($text);
149 33
				} else {
150 17
					$key = strtolower($refMatches[2]);
151
				}
152
				return [
153 40
					$text,
154 40
					null, // url
155 40
					null, // title
156 40
					$offset + strlen($refMatches[0]), // offset
157 40
					$key,
158 40
				];
159
			}
160
		}
161 3
		return false;
162
	}
163
164
	/**
165
	 * Parses inline HTML.
166
	 * @marker <
167
	 */
168 30
	protected function parseLt($text)
169
	{
170 30
		if (strpos($text, '>') !== false) {
171 26
			if (!in_array('parseLink', $this->context)) { // do not allow links in links
172 26 View Code Duplication
				if (preg_match('/^<([^\s]*?@[^\s]*?\.\w+?)>/', $text, $matches)) {
173
					// email address
174
					return [
175 3
						['email', $this->replaceEscape($matches[1])],
176 3
						strlen($matches[0])
177 3
					];
178 26
				} elseif (preg_match('/^<([a-z]{3,}:\/\/[^\s]+?)>/', $text, $matches)) {
179
					// URL
180
					return [
181 10
						['url', $this->replaceEscape($matches[1])],
182 10
						strlen($matches[0])
183 10
					];
184
				}
185 16
			}
186
			// try inline HTML if it was neither a URL nor email if HtmlTrait is included.
187 16
			if (method_exists($this, 'parseInlineHtml')) {
188 16
				return $this->parseInlineHtml($text);
189
			}
190
		}
191 11
		return [['text', '&lt;'], 1];
192
	}
193
194 3
	protected function renderEmail($block)
195
	{
196 3
		$email = htmlspecialchars($block[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
197 3
		return "<a href=\"mailto:$email\">$email</a>";
198
	}
199
200 10 View Code Duplication
	protected function renderUrl($block)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
201
	{
202 10
		$url = htmlspecialchars($block[1], ENT_COMPAT | ENT_HTML401, 'UTF-8');
203 10
		$decodedUrl = urldecode($block[1]);
204 10
		$secureUrlText = preg_match('//u', $decodedUrl) ? $decodedUrl : $block[1];
205 10
		$text = htmlspecialchars($secureUrlText, ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
206 10
		return "<a href=\"$url\">$text</a>";
207
	}
208
209 40
	protected function lookupReference($key)
210
	{
211 40
		$normalizedKey = preg_replace('/\s+/', ' ', $key);
212 40
		if (isset($this->references[$key]) || isset($this->references[$key = $normalizedKey])) {
213 29
			return $this->references[$key];
214
		}
215 20
		return false;
216
	}
217
218 33
	protected function renderLink($block)
219
	{
220 33 View Code Duplication
		if (isset($block['refkey'])) {
221 24
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
222 19
				$block = array_merge($block, $ref);
223 19
			} else {
224 11
				return $block['orig'];
225
			}
226 19
		}
227 32
		return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
228 32
			. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
229 32
			. '>' . $this->renderAbsy($block['text']) . '</a>';
230
	}
231
232 4
	protected function renderImage($block)
233
	{
234 4 View Code Duplication
		if (isset($block['refkey'])) {
235 2
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
236
				$block = array_merge($block, $ref);
237
			} else {
238 2
				return $block['orig'];
239
			}
240
		}
241 4
		return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
242 4
			. ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
243 4
			. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
244 4
			. ($this->html5 ? '>' : ' />');
245
	}
246
247
	// references
248
249 141
	protected function identifyReference($line)
250
	{
251 141
		return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*$/', $line);
252
	}
253
254
	/**
255
	 * Consume link references
256
	 */
257 19
	protected function consumeReference($lines, $current)
258
	{
259 19
		while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*$/', $lines[$current], $matches)) {
260 19
			$label = strtolower($matches[1]);
261
262 19
			$this->references[$label] = [
263 19
				'url' => $this->replaceEscape($matches[2]),
264
			];
265 19 View Code Duplication
			if (isset($matches[3])) {
266 9
				$this->references[$label]['title'] = $matches[3];
267 9
			} else {
268
				// title may be on the next line
269 16
				if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
270 2
					$this->references[$label]['title'] = $matches[1];
271 2
					$current++;
272 2
				}
273
			}
274 19
			$current++;
275 19
		}
276 19
		return [false, --$current];
277
	}
278
}
279