Completed
Push — master ( b3baf4...febd9e )
by Carsten
03:34
created

Markdown::parseEscape()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.5923

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 10
ccs 4
cts 6
cp 0.6667
rs 9.2
cc 4
eloc 6
nc 3
nop 1
crap 4.5923
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\latex;
9
10
use cebe\markdown\block\CodeTrait;
11
use cebe\markdown\block\HeadlineTrait;
12
use cebe\markdown\block\ListTrait;
13
use cebe\markdown\block\QuoteTrait;
14
use cebe\markdown\block\RuleTrait;
15
16
use cebe\markdown\inline\CodeTrait as InlineCodeTrait;
17
use cebe\markdown\inline\EmphStrongTrait;
18
use cebe\markdown\inline\LinkTrait;
19
20
use MikeVanRiel\TextToLatex;
21
22
/**
23
 * Markdown parser for the [initial markdown spec](http://daringfireball.net/projects/markdown/syntax).
24
 *
25
 * @author Carsten Brandt <[email protected]>
26
 */
27
class Markdown extends \cebe\markdown\Parser
28
{
29
	// include block element parsing using traits
30
	use CodeTrait;
31
	use HeadlineTrait;
32
	use ListTrait {
33
		// Check Ul List before headline
34
		identifyUl as protected identifyBUl;
35
		consumeUl as protected consumeBUl;
36
	}
37
	use QuoteTrait;
38
	use RuleTrait {
39
		// Check Hr before checking lists
40
		identifyHr as protected identifyAHr;
41
		consumeHr as protected consumeAHr;
42
	}
43
44
	// include inline element parsing using traits
45
	use InlineCodeTrait;
46
	use EmphStrongTrait;
47
	use LinkTrait;
48
49
	/**
50
	 * @var string this string will be prefixed to all auto generated labels.
51
	 * This can be used to disambiguate labels when combining multiple markdown files into one document.
52
	 */
53
	public $labelPrefix = '';
54
55
	/**
56
	 * @var array these are "escapeable" characters. When using one of these prefixed with a
57
	 * backslash, the character will be outputted without the backslash and is not interpreted
58
	 * as markdown.
59
	 */
60
	protected $escapeCharacters = [
61
		'\\', // backslash
62
		'`', // backtick
63
		'*', // asterisk
64
		'_', // underscore
65
		'{', '}', // curly braces
66
		'[', ']', // square brackets
67
		'(', ')', // parentheses
68
		'#', // hash mark
69
		'+', // plus sign
70
		'-', // minus sign (hyphen)
71
		'.', // dot
72
		'!', // exclamation mark
73
		'<', '>',
74
	];
75
76
77
	/**
78
	 * @inheritDoc
79
	 */
80 21
	protected function prepare()
81
	{
82
		// reset references
83 21
		$this->references = [];
84 21
	}
85
86
	/**
87
	 * Consume lines for a paragraph
88
	 *
89
	 * Allow headlines and code to break paragraphs
90
	 */
91 7
	protected function consumeParagraph($lines, $current)
92
	{
93
		// consume until newline
94 7
		$content = [];
95 7
		for ($i = $current, $count = count($lines); $i < $count; $i++) {
96 7
			$line = $lines[$i];
97 7
			if (!empty($line) && ltrim($line) !== '' &&
98 7
				!($line[0] === "\t" || $line[0] === " " && strncmp($line, '    ', 4) === 0) &&
99 7
				!$this->identifyHeadline($line, $lines, $i))
100 7
			{
101 7
				$content[] = $line;
102 7
			} else {
103 6
				break;
104
			}
105 7
		}
106
		$block = [
107 7
			'paragraph',
108 7
			'content' => $this->parseInline(implode("\n", $content)),
109 7
		];
110 7
		return [$block, --$i];
111
	}
112
113
114
	// rendering adjusted for LaTeX output
115
116
117
	/**
118
	 * @inheritdoc
119
	 */
120 19
	protected function renderParagraph($block)
121
	{
122 19
		return $this->renderAbsy($block['content']) . "\n\n";
123
	}
124
125
	/**
126
	 * @inheritdoc
127
	 */
128 2
	protected function renderQuote($block)
129
	{
130 2
		return '\begin{quote}' . $this->renderAbsy($block['content']) . "\\end{quote}\n";
131
	}
132
133
	/**
134
	 * @inheritdoc
135
	 */
136 3
	protected function renderCode($block)
137
	{
138 3
		$language = isset($block['language']) ? "\\lstset{language={$block['language']}}" : '\lstset{language={}}';
139 3
		return "$language\\begin{lstlisting}\n{$block['content']}\n\\end{lstlisting}\n";
140
	}
141
142
	/**
143
	 * @inheritdoc
144
	 */
145 2
	protected function renderList($block)
146
	{
147 2
		$type = ($block['list'] === 'ol') ? 'enumerate' : 'itemize';
148 2
		$output = "\\begin{{$type}}\n";
149
150 2
		foreach ($block['items'] as $item => $itemLines) {
151 2
			$output .= '\item ' . $this->renderAbsy($itemLines). "\n";
152 2
		}
153
154 2
		return "$output\\end{{$type}}\n";
155
	}
156
157
	/**
158
	 * @inheritdoc
159
	 */
160 3
	protected function renderHeadline($block)
161
	{
162 3
		$content = $this->renderAbsy($block['content']);
163 3
		switch($block['level']) {
164 3
			case 1: return "\\section{{$content}}\n";
165 3
			case 2: return "\\subsection{{$content}}\n";
166 2
			case 3: return "\\subsubsection{{$content}}\n";
167 2
			default: return "\\paragraph{{$content}}\n";
168 2
		}
169
	}
170
171
	/**
172
	 * @inheritdoc
173
	 */
174 2
	protected function renderHr($block)
0 ignored issues
show
Unused Code introduced by
The parameter $block is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
175
	{
176 2
		return "\n\\noindent\\rule{\\textwidth}{0.4pt}\n";
177
	}
178
179
	/**
180
	 * @inheritdoc
181
	 */
182 2
	protected function renderLink($block)
183
	{
184 2 View Code Duplication
		if (isset($block['refkey'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
185
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
186
				$block = array_merge($block, $ref);
187
			} else {
188
				return $block['orig'];
189
			}
190
		}
191
192 2
		$url = $block['url'];
193 2
		$text = $this->renderAbsy($block['text']);
194 2
		if (strpos($url, '://') === false) {
195
			// consider all non absolute links as relative in the document
196
			// $title is ignored in this case.
197
			if (isset($url[0]) && $url[0] === '#') {
198
				$url = $this->labelPrefix . $url;
199
			}
200
			return '\hyperref['.str_replace('#', '::', $url).']{' . $text . '}';
201
		} else {
202 2
			return $text . '\\footnote{' . (empty($block['title']) ? '' : $this->escapeLatex($block['title']) . ': ') . '\url{' . $this->escapeUrl($url) . '}}';
203
		}
204
	}
205
206
	/**
207
	 * @inheritdoc
208
	 */
209
	protected function renderImage($block)
210
	{
211 View Code Duplication
		if (isset($block['refkey'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
212
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
213
				$block = array_merge($block, $ref);
214
			} else {
215
				return $block['orig'];
216
			}
217
		}
218
219
		// TODO create figure with caption with title
220
		$url = $this->escapeUrl($block['url']);
221
		return "\\noindent\\includegraphics[width=\\textwidth]{{$url}}";
222
	}
223
224
	/**
225
	 * Parses <a name="..."></a> tags as reference labels
226
	 */
227 2
	private function parseInlineHtml($text)
228
	{
229 2
		if (strpos($text, '>') !== false) {
230
			// convert a name markers to \labels
231 2
			if (preg_match('~^<((a|span)) (name|id)="(.*?)">.*?</\1>~i', $text, $matches)) {
232
				return [
233 2
					['label', 'name' => str_replace('#', '::', $this->labelPrefix . $matches[4])],
234 2
					strlen($matches[0])
235 2
				];
236
			}
237
		}
238
		return [['text', '<'], 1];
239
	}
240
241
	/**
242
	 * renders a reference label
243
	 */
244 2
	protected function renderLabel($block)
245
	{
246 2
		return "\\label{{$block['name']}}";
247
	}
248
249
	/**
250
	 * @inheritdoc
251
	 */
252 2
	protected function renderEmail($block)
253
	{
254 2
		$email = $this->escapeUrl($block[1]);
255 2
		return "\\href{mailto:{$email}}{{$email}}";
256
	}
257
258
	/**
259
	 * @inheritdoc
260
	 */
261 2
	protected function renderUrl($block)
262
	{
263 2
		return '\url{' . $this->escapeUrl($block[1]) . '}';
264
	}
265
266
	/**
267
	 * @inheritdoc
268
	 */
269 1
	protected function renderInlineCode($block)
270
	{
271 1
		if (strpos($block[1], '|') !== false) {
272 1
			return '\\lstinline`' . str_replace("\n", ' ', $block[1]) . '`'; // TODO make this more robust against code containing backticks
273
		} else {
274
			return '\\lstinline|' . str_replace("\n", ' ', $block[1]) . '|';
275
		}
276
	}
277
278
	/**
279
	 * @inheritdoc
280
	 */
281 1
	protected function renderStrong($block)
282
	{
283 1
		return '\textbf{' . $this->renderAbsy($block[1]) . '}';
284
	}
285
286
	/**
287
	 * @inheritdoc
288
	 */
289 1
	protected function renderEmph($block)
290
	{
291 1
		return '\textit{' . $this->renderAbsy($block[1]) . '}';
292
	}
293
294
	/**
295
	 * Parses escaped special characters.
296
	 * This allow a backslash to be interpreted as LaTeX
297
	 * @marker \
298
	 */
299 1
	protected function parseEscape($text)
300
	{
301 1
		if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
302 1
			if ($text[1] === '\\') {
303
				return [['backslash'], 2];
0 ignored issues
show
Best Practice introduced by
The expression return array(array('backslash'), 2); seems to be an array, but some of its elements' types (string[]) are incompatible with the return type of the parent method cebe\markdown\Parser::parseEscape of type array<array|integer>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
304
			}
305 1
			return [['text', $text[1]], 2];
306
		}
307
		return [['text', $text[0]], 1];
308
	}
309
310
	protected function renderBackslash()
311
	{
312
		return '\\';
313
	}
314
315
	private $_escaper;
316
317
	/**
318
	 * Escape special characters in URLs
319
	 */
320 4
	protected function escapeUrl($string)
321
	{
322 4
		return str_replace('%', '\\%', $this->escapeLatex($string));
323
	}
324
325
	/**
326
	 * Escape special LaTeX characters
327
	 */
328 19
	protected function escapeLatex($string)
329
	{
330 19
		if ($this->_escaper === null) {
331 19
			$this->_escaper = new TextToLatex();
332 19
		}
333 19
		return $this->_escaper->convert($string);
334
	}
335
336
	/**
337
	 * @inheritdocs
338
	 *
339
	 * Parses a newline indicated by two spaces on the end of a markdown line.
340
	 */
341 19
	protected function renderText($text)
342
	{
343 19
		$output = str_replace("  \n", "\\\\\n", $this->escapeLatex($text[1]));
344
		// support No-Break Space in LaTeX
345 19
		$output = preg_replace("/\x{00a0}/u", '~', $output);
346
		// support Narrow No-Break Space spaces in LaTeX
347
		// http://unicode-table.com/en/202F/
348
		// http://tex.stackexchange.com/questions/76132/how-to-typeset-a-small-non-breaking-space
349 19
		$output = preg_replace("/\x{202f}/u", '\nobreak\hspace{.16667em plus .08333em}', $output);
350 19
		return $output;
351
	}
352
}
353