Completed
Pull Request — master (#13)
by
unknown
03:02
created

Markdown::escapeLatex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 2
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
140 3
		$content = $block['content'];
141
		// replace No-Break Space characters in code block, which do not render in LaTeX
142 3
		$content = preg_replace("/[\x{00a0}\x{202f}]/u", ' ', $content);
143
144 3
		return "$language\\begin{lstlisting}\n{$content}\n\\end{lstlisting}\n";
145
	}
146
147
	/**
148
	 * @inheritdoc
149
	 */
150 2
	protected function renderList($block)
151
	{
152 2
		$type = ($block['list'] === 'ol') ? 'enumerate' : 'itemize';
153 2
		$output = "\\begin{{$type}}\n";
154
155 2
		foreach ($block['items'] as $item => $itemLines) {
156 2
			$output .= '\item ' . $this->renderAbsy($itemLines). "\n";
157 2
		}
158
159 2
		return "$output\\end{{$type}}\n";
160
	}
161
162
	/**
163
	 * @inheritdoc
164
	 */
165 3
	protected function renderHeadline($block)
166
	{
167 3
		$content = $this->renderAbsy($block['content']);
168 3
		switch($block['level']) {
169 3
			case 1: return "\\section{{$content}}\n";
170 3
			case 2: return "\\subsection{{$content}}\n";
171 2
			case 3: return "\\subsubsection{{$content}}\n";
172 2
			default: return "\\paragraph{{$content}}\n";
173 2
		}
174
	}
175
176
	/**
177
	 * @inheritdoc
178
	 */
179 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...
180
	{
181 2
		return "\n\\noindent\\rule{\\textwidth}{0.4pt}\n";
182
	}
183
184
	/**
185
	 * @inheritdoc
186
	 */
187 2
	protected function renderLink($block)
188
	{
189 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...
190
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
191
				$block = array_merge($block, $ref);
192
			} else {
193
				return $block['orig'];
194
			}
195
		}
196
197 2
		$url = $block['url'];
198 2
		$text = $this->renderAbsy($block['text']);
199 2
		if (strpos($url, '://') === false) {
200
			// consider all non absolute links as relative in the document
201
			// $title is ignored in this case.
202
			if (isset($url[0]) && $url[0] === '#') {
203
				$url = $this->labelPrefix . $url;
204
			}
205
			return '\hyperref['.str_replace('#', '::', $url).']{' . $text . '}';
206
		} else {
207 2
			return $text . '\\footnote{' . (empty($block['title']) ? '' : $this->escapeLatex($block['title']) . ': ') . '\url{' . $this->escapeLatex($url) . '}}';
208
		}
209
	}
210
211
	/**
212
	 * @inheritdoc
213
	 */
214
	protected function renderImage($block)
215
	{
216 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...
217
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
218
				$block = array_merge($block, $ref);
219
			} else {
220
				return $block['orig'];
221
			}
222
		}
223
224
		// TODO create figure with caption with title
225
		$url = $this->escapeLatex($block['url']);
226
		return "\\noindent\\includegraphics[width=\\textwidth]{{$url}}";
227
	}
228
229
	/**
230
	 * Parses <a name="..."></a> tags as reference labels
231
	 */
232 2
	private function parseInlineHtml($text)
233
	{
234 2
		if (strpos($text, '>') !== false) {
235
			// convert a name markers to \labels
236 2
			if (preg_match('~^<((a|span)) (name|id)="(.*?)">.*?</\1>~i', $text, $matches)) {
237
				return [
238 2
					['label', 'name' => str_replace('#', '::', $this->labelPrefix . $matches[4])],
239 2
					strlen($matches[0])
240 2
				];
241
			}
242
		}
243
		return [['text', '<'], 1];
244
	}
245
246
	/**
247
	 * renders a reference label
248
	 */
249 2
	protected function renderLabel($block)
250
	{
251 2
		return "\\label{{$block['name']}}";
252
	}
253
254
	/**
255
	 * @inheritdoc
256
	 */
257 2
	protected function renderEmail($block)
258
	{
259 2
		$email = $this->escapeLatex($block[1]);
260 2
		return "\\href{mailto:{$email}}{{$email}}";
261
	}
262
263
	/**
264
	 * @inheritdoc
265
	 */
266 2
	protected function renderUrl($block)
267
	{
268 2
		return '\url{' . $this->escapeLatex($block[1]) . '}';
269
	}
270
271
	/**
272
	 * @inheritdoc
273
	 */
274 1
	protected function renderInlineCode($block)
275
	{
276
		// replace No-Break Space characters in code block, which do not render in LaTeX
277 1
		$content = preg_replace("/[\x{00a0}\x{202f}]/u", ' ', $block[1]);
278
279 1
		if (strpos($content, '|') !== false) {
280 1
			return '\\lstinline`' . str_replace("\n", ' ', $content) . '`'; // TODO make this more robust against code containing backticks
281
		} else {
282
			return '\\lstinline|' . str_replace("\n", ' ', $content) . '|';
283
		}
284
	}
285
286
	/**
287
	 * @inheritdoc
288
	 */
289 1
	protected function renderStrong($block)
290
	{
291 1
		return '\textbf{' . $this->renderAbsy($block[1]) . '}';
292
	}
293
294
	/**
295
	 * @inheritdoc
296
	 */
297 1
	protected function renderEmph($block)
298
	{
299 1
		return '\textit{' . $this->renderAbsy($block[1]) . '}';
300
	}
301
302
	/**
303
	 * Parses escaped special characters.
304
	 * This allow a backslash to be interpreted as LaTeX
305
	 * @marker \
306
	 */
307 1
	protected function parseEscape($text)
308
	{
309 1
		if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
310 1
			if ($text[1] === '\\') {
311
				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...
312
			}
313 1
			return [['text', $text[1]], 2];
314
		}
315
		return [['text', $text[0]], 1];
316
	}
317
318
	protected function renderBackslash()
319
	{
320
		return '\\';
321
	}
322
323
	private $_escaper;
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
		$output = $this->_escaper->convert($string);
334 19
		$output = str_replace('%', '\\%', $output);
335 19
		return $output;
336
	}
337
338
	/**
339
	 * @inheritdocs
340
	 *
341
	 * Parses a newline indicated by two spaces on the end of a markdown line.
342
	 */
343 19
	protected function renderText($text)
344
	{
345 19
		$output = str_replace("  \n", "\\\\\n", $this->escapeLatex($text[1]));
346
		// support No-Break Space in LaTeX
347 19
		$output = preg_replace("/\x{00a0}/u", '~', $output);
348
		// support Narrow No-Break Space spaces in LaTeX
349
		// http://unicode-table.com/en/202F/
350
		// http://tex.stackexchange.com/questions/76132/how-to-typeset-a-small-non-breaking-space
351 19
		$output = preg_replace("/\x{202f}/u", '\nobreak\hspace{.16667em plus .08333em}', $output);
352 19
		return $output;
353
	}
354
}
355