Parser::renderText()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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;
9
use ReflectionMethod;
10
11
/**
12
 * A generic parser for markdown-like languages.
13
 *
14
 * @author Carsten Brandt <[email protected]>
15
 */
16
abstract class Parser
17
{
18
	/**
19
	 * @var integer the maximum nesting level for language elements.
20
	 */
21
	public $maximumNestingLevel = 32;
22
23
	/**
24
	 * @var array the current context the parser is in.
25
	 * TODO remove in favor of absy
26
	 */
27
	protected $context = [];
28
	/**
29
	 * @var array these are "escapeable" characters. When using one of these prefixed with a
30
	 * backslash, the character will be outputted without the backslash and is not interpreted
31
	 * as markdown.
32
	 */
33
	protected $escapeCharacters = [
34
		'\\', // backslash
35
	];
36
37
	private $_depth = 0;
38
39
40
	/**
41
	 * Parses the given text considering the full language.
42
	 *
43
	 * This includes parsing block elements as well as inline elements.
44
	 *
45
	 * @param string $text the text to parse
46
	 * @return string parsed markup
47
	 */
48 208 View Code Duplication
	public function parse($text)
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...
49
	{
50 208
		$this->prepare();
51
52 208
		if (ltrim($text) === '') {
53
			return '';
54
		}
55
56 208
		$text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
57
58 208
		$this->prepareMarkers($text);
59
60 208
		$absy = $this->parseBlocks(explode("\n", $text));
61 208
		$markup = $this->renderAbsy($absy);
62
63 208
		$this->cleanup();
64 208
		return $markup;
65
	}
66
67
	/**
68
	 * Parses a paragraph without block elements (block elements are ignored).
69
	 *
70
	 * @param string $text the text to parse
71
	 * @return string parsed markup
72
	 */
73 48 View Code Duplication
	public function parseParagraph($text)
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...
74
	{
75 48
		$this->prepare();
76
77 48
		if (ltrim($text) === '') {
78
			return '';
79
		}
80
81 48
		$text = str_replace(["\r\n", "\n\r", "\r"], "\n", $text);
82
83 48
		$this->prepareMarkers($text);
84
85 48
		$absy = $this->parseInline($text);
86 48
		$markup = $this->renderAbsy($absy);
87
88 48
		$this->cleanup();
89 48
		return $markup;
90
	}
91
92
	/**
93
	 * This method will be called before `parse()` and `parseParagraph()`.
94
	 * You can override it to do some initialization work.
95
	 */
96 3
	protected function prepare()
97
	{
98 3
	}
99
100
	/**
101
	 * This method will be called after `parse()` and `parseParagraph()`.
102
	 * You can override it to do cleanup.
103
	 */
104 211
	protected function cleanup()
105
	{
106 211
	}
107
108
109
	// block parsing
110
111
	private $_blockTypes;
112
113
	/**
114
	 * @return array a list of block element types available.
115
	 */
116 208
	protected function blockTypes()
117
	{
118 208
		if ($this->_blockTypes === null) {
119
			// detect block types via "identify" functions
120 208
			$reflection = new \ReflectionClass($this);
121 208
			$this->_blockTypes = array_filter(array_map(function($method) {
122 208
				$name = $method->getName();
123 208
				return strncmp($name, 'identify', 8) === 0 ? strtolower(substr($name, 8)) : false;
124 208
			}, $reflection->getMethods(ReflectionMethod::IS_PROTECTED)));
125
126 208
			sort($this->_blockTypes);
127
		}
128 208
		return $this->_blockTypes;
129
	}
130
131
	/**
132
	 * Given a set of lines and an index of a current line it uses the registed block types to
133
	 * detect the type of this line.
134
	 * @param array $lines
135
	 * @param integer $current
136
	 * @return string name of the block type in lower case
137
	 */
138 208
	protected function detectLineType($lines, $current)
139
	{
140 208
		$line = $lines[$current];
141 208
		$blockTypes = $this->blockTypes();
142 208
		foreach($blockTypes as $blockType) {
143 206
			if ($this->{'identify' . $blockType}($line, $lines, $current)) {
144 206
				return $blockType;
145
			}
146
		}
147
		// consider the line a normal paragraph if no other block type matches
148 207
		return 'paragraph';
149
	}
150
151
	/**
152
	 * Parse block elements by calling `detectLineType()` to identify them
153
	 * and call consume function afterwards.
154
	 */
155 208
	protected function parseBlocks($lines)
156
	{
157 208
		if ($this->_depth >= $this->maximumNestingLevel) {
158
			// maximum depth is reached, do not parse input
159
			return [['text', implode("\n", $lines)]];
160
		}
161 208
		$this->_depth++;
162
163 208
		$blocks = [];
164
165
		// convert lines to blocks
166 208
		for ($i = 0, $count = count($lines); $i < $count; $i++) {
167 208
			$line = $lines[$i];
168 208
			if ($line !== '' && rtrim($line) !== '') { // skip empty lines
169
				// identify a blocks beginning and parse the content
170 208
				list($block, $i) = $this->parseBlock($lines, $i);
171 208
				if ($block !== false) {
172 208
					$blocks[] = $block;
173
				}
174
			}
175
		}
176
177 208
		$this->_depth--;
178
179 208
		return $blocks;
180
	}
181
182
	/**
183
	 * Parses the block at current line by identifying the block type and parsing the content
184
	 * @param $lines
185
	 * @param $current
186
	 * @return array Array of two elements, the first element contains the block,
187
	 * the second contains the next line index to be parsed.
188
	 */
189 208
	protected function parseBlock($lines, $current)
190
	{
191
		// identify block type for this line
192 208
		$blockType = $this->detectLineType($lines, $current);
193
194
		// call consume method for the detected block type to consume further lines
195 208
		return $this->{'consume' . $blockType}($lines, $current);
196
	}
197
198 211
	protected function renderAbsy($blocks)
199
	{
200 211
		$output = '';
201 211
		foreach ($blocks as $block) {
202 211
			array_unshift($this->context, $block[0]);
203 211
			$output .= $this->{'render' . $block[0]}($block);
204 211
			array_shift($this->context);
205
		}
206 211
		return $output;
207
	}
208
209
	/**
210
	 * Consume lines for a paragraph
211
	 *
212
	 * @param $lines
213
	 * @param $current
214
	 * @return array
215
	 */
216 2
	protected function consumeParagraph($lines, $current)
217
	{
218
		// consume until newline
219 2
		$content = [];
220 2 View Code Duplication
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
221 2
			if (ltrim($lines[$i]) !== '') {
222 2
				$content[] = $lines[$i];
223
			} else {
224
				break;
225
			}
226
		}
227
		$block = [
228 2
			'paragraph',
229 2
			'content' => $this->parseInline(implode("\n", $content)),
230
		];
231 2
		return [$block, --$i];
232
	}
233
234
	/**
235
	 * Render a paragraph block
236
	 *
237
	 * @param $block
238
	 * @return string
239
	 */
240 203
	protected function renderParagraph($block)
241
	{
242 203
		return '<p>' . $this->renderAbsy($block['content']) . "</p>\n";
243
	}
244
245
246
	// inline parsing
247
248
249
	/**
250
	 * @var array the set of inline markers to use in different contexts.
251
	 */
252
	private $_inlineMarkers = [];
253
254
	/**
255
	 * Returns a map of inline markers to the corresponding parser methods.
256
	 *
257
	 * This array defines handler methods for inline markdown markers.
258
	 * When a marker is found in the text, the handler method is called with the text
259
	 * starting at the position of the marker.
260
	 *
261
	 * Note that markers starting with whitespace may slow down the parser,
262
	 * you may want to use [[renderText]] to deal with them.
263
	 *
264
	 * You may override this method to define a set of markers and parsing methods.
265
	 * The default implementation looks for protected methods starting with `parse` that
266
	 * also have an `@marker` annotation in PHPDoc.
267
	 *
268
	 * @return array a map of markers to parser methods
269
	 */
270 208
	protected function inlineMarkers()
271
	{
272 208
		$markers = [];
273
		// detect "parse" functions
274 208
		$reflection = new \ReflectionClass($this);
275 208
		foreach($reflection->getMethods(ReflectionMethod::IS_PROTECTED) as $method) {
276 208
			$methodName = $method->getName();
277 208
			if (strncmp($methodName, 'parse', 5) === 0) {
278 208
				preg_match_all('/@marker ([^\s]+)/', $method->getDocComment(), $matches);
279 208
				foreach($matches[1] as $match) {
280 208
					$markers[$match] = $methodName;
281
				}
282
			}
283
		}
284 208
		return $markers;
285
	}
286
287
	/**
288
	 * Prepare markers that are used in the text to parse
289
	 *
290
	 * Add all markers that are present in markdown.
291
	 * Check is done to avoid iterations in parseInline(), good for huge markdown files
292
	 * @param string $text
293
	 */
294 211
	protected function prepareMarkers($text)
295
	{
296 211
		$this->_inlineMarkers = [];
297 211
		foreach ($this->inlineMarkers() as $marker => $method) {
298 210
			if (strpos($text, $marker) !== false) {
299 152
				$m = $marker[0];
300
				// put the longest marker first
301 152
				if (isset($this->_inlineMarkers[$m])) {
302 1
					reset($this->_inlineMarkers[$m]);
303 1
					if (strlen($marker) > strlen(key($this->_inlineMarkers[$m]))) {
304 1
						$this->_inlineMarkers[$m] = array_merge([$marker => $method], $this->_inlineMarkers[$m]);
305 1
						continue;
306
					}
307
				}
308 210
				$this->_inlineMarkers[$m][$marker] = $method;
309
			}
310
		}
311 211
	}
312
313
	/**
314
	 * Parses inline elements of the language.
315
	 *
316
	 * @param string $text the inline text to parse.
317
	 * @return array
318
	 */
319 210
	protected function parseInline($text)
320
	{
321 210
		if ($this->_depth >= $this->maximumNestingLevel) {
322
			// maximum depth is reached, do not parse input
323 1
			return [['text', $text]];
324
		}
325 210
		$this->_depth++;
326
327 210
		$markers = implode('', array_keys($this->_inlineMarkers));
328
329 210
		$paragraph = [];
330
331 210
		while (!empty($markers) && ($found = strpbrk($text, $markers)) !== false) {
332
333 103
			$pos = strpos($text, $found);
334
335
			// add the text up to next marker to the paragraph
336 103 View Code Duplication
			if ($pos !== 0) {
337 87
				$paragraph[] = ['text', substr($text, 0, $pos)];
338
			}
339 103
			$text = $found;
340
341 103
			$parsed = false;
342 103
			foreach ($this->_inlineMarkers[$text[0]] as $marker => $method) {
343 103
				if (strncmp($text, $marker, strlen($marker)) === 0) {
344
					// parse the marker
345 103
					array_unshift($this->context, $method);
346 103
					list($output, $offset) = $this->$method($text);
347 103
					array_shift($this->context);
348
349 103
					$paragraph[] = $output;
350 103
					$text = substr($text, $offset);
351 103
					$parsed = true;
352 103
					break;
353
				}
354
			}
355 103 View Code Duplication
			if (!$parsed) {
356 16
				$paragraph[] = ['text', substr($text, 0, 1)];
357 16
				$text = substr($text, 1);
358
			}
359
		}
360
361 210
		$paragraph[] = ['text', $text];
362
363 210
		$this->_depth--;
364
365 210
		return $paragraph;
366
	}
367
368
	/**
369
	 * Parses escaped special characters.
370
	 * @marker \
371
	 */
372 21
	protected function parseEscape($text)
373
	{
374 21
		if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
375 16
			return [['text', $text[1]], 2];
376
		}
377 8
		return [['text', $text[0]], 1];
378
	}
379
380
	/**
381
	 * This function renders plain text sections in the markdown text.
382
	 * It can be used to work on normal text sections for example to highlight keywords or
383
	 * do special escaping.
384
	 */
385 3
	protected function renderText($block)
386
	{
387 3
		return $block[1];
388
	}
389
}
390