Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
namespace cebe\markdown;
4
5
use cebe\markdown\block\TableTrait;
6
7
// work around https://github.com/facebook/hhvm/issues/1120
8 1
defined('ENT_HTML401') || define('ENT_HTML401', 0);
9
10
/**
11
 * Markdown parser for the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) flavor.
12
 *
13
 * @author Carsten Brandt <[email protected]>
14
 * @license https://github.com/cebe/markdown/blob/master/LICENSE
15
 * @link https://github.com/cebe/markdown#readme
16
 */
17
class MarkdownExtra extends Markdown
18
{
19
	// include block element parsing using traits
20
	use block\TableTrait;
21
	use block\FencedCodeTrait;
22
23
	// include inline element parsing using traits
24
	// TODO
25
26
	/**
27
	 * @var bool whether special attributes on code blocks should be applied on the `<pre>` element.
28
	 * The default behavior is to put them on the `<code>` element.
29
	 */
30
	public $codeAttributesOnPre = false;
31
32
	/**
33
	 * @inheritDoc
34
	 */
35
	protected $escapeCharacters = [
36
		// from Markdown
37
		'\\', // backslash
38
		'`', // backtick
39
		'*', // asterisk
40
		'_', // underscore
41
		'{', '}', // curly braces
42
		'[', ']', // square brackets
43
		'(', ')', // parentheses
44
		'#', // hash mark
45
		'+', // plus sign
46
		'-', // minus sign (hyphen)
47
		'.', // dot
48
		'!', // exclamation mark
49
		'<', '>',
50
		// added by MarkdownExtra
51
		':', // colon
52
		'|', // pipe
53
	];
54
55
	private $_specialAttributesRegex = '\{(([#\.][A-z0-9-_]+\s*)+)\}';
56
57
	// TODO allow HTML intended 3 spaces
58
59
	// TODO add markdown inside HTML blocks
60
61
	// TODO implement definition lists
62
63
	// TODO implement footnotes
64
65
	// TODO implement Abbreviations
66
67
68
	// block parsing
69
70 62
	protected function identifyReference($line)
71
	{
72 62
		return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[[^\[](.*?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $line);
73
	}
74
75
	/**
76
	 * Consume link references
77
	 */
78 10
	protected function consumeReference($lines, $current)
79
	{
80 10
		while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $lines[$current], $matches)) {
81 10
			$label = strtolower($matches[1]);
82
83 10
			$this->references[$label] = [
84 10
				'url' => $this->replaceEscape($matches[2]),
85
			];
86 10 View Code Duplication
			if (isset($matches[3])) {
87 5
				$this->references[$label]['title'] = $matches[3];
88
			} else {
89
				// title may be on the next line
90 8
				if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
91 1
					$this->references[$label]['title'] = $matches[1];
92 1
					$current++;
93
				}
94
			}
95 10
			if (isset($matches[5])) {
96 1
				$this->references[$label]['attributes'] = $matches[5];
97
			}
98 10
			$current++;
99
		}
100 10
		return [false, --$current];
101
	}
102
103
	/**
104
	 * Consume lines for a fenced code block
105
	 */
106 3 View Code Duplication
	protected function consumeFencedCode($lines, $current)
107
	{
108
		// consume until ```
109
		$block = [
110 3
			'code',
111
		];
112 3
		$line = trim($lines[$current]);
113 3
		if (($pos = strrpos($line, '`')) === false) {
114 1
			$pos = strrpos($line, '~');
115
		}
116 3
		$fence = substr($line, 0, $pos + 1);
117 3
		$block['attributes'] = substr($line, $pos);
118 3
		$content = [];
119 3
		for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
120 3
			if (($pos = strpos($line = $lines[$i], $fence)) === false || $pos > 3) {
121 3
				$content[] = $line;
122
			} else {
123 3
				break;
124
			}
125
		}
126 3
		$block['content'] = implode("\n", $content);
127 3
		return [$block, $i];
128
	}
129
130 19
	protected function renderCode($block)
131
	{
132 19
		$attributes = $this->renderAttributes($block);
133 19
		return ($this->codeAttributesOnPre ? "<pre$attributes><code>" : "<pre><code$attributes>")
134 19
			. htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8')
135 19
			. "</code></pre>\n";
136
	}
137
138
	/**
139
	 * Renders a headline
140
	 */
141 12
	protected function renderHeadline($block)
142
	{
143 12
		foreach($block['content'] as $i => $element) {
144 12
			if ($element[0] === 'specialAttributes') {
145 1
				unset($block['content'][$i]);
146 12
				$block['attributes'] = $element[1];
147
			}
148
		}
149 12
		$tag = 'h' . $block['level'];
150 12
		$attributes = $this->renderAttributes($block);
151 12
		return "<$tag$attributes>" . rtrim($this->renderAbsy($block['content']), "# \t") . "</$tag>\n";
152
	}
153
154 34
	protected function renderAttributes($block)
155
	{
156 34
		$html = [];
157 34
		if (isset($block['attributes'])) {
158 4
			$attributes = preg_split('/\s+/', $block['attributes'], -1, PREG_SPLIT_NO_EMPTY);
159 4
			foreach($attributes as $attribute) {
160 4
				if ($attribute[0] === '#') {
161 2
					$html['id'] = substr($attribute, 1);
162
				} else {
163 4
					$html['class'][] = substr($attribute, 1);
164
				}
165
			}
166
		}
167 34
		$result = '';
168 34
		foreach($html as $attr => $value) {
169 4
			if (is_array($value)) {
170 4
				$value = trim(implode(' ', $value));
171
			}
172 4
			if (!empty($value)) {
173 4
				$result .= " $attr=\"$value\"";
174
			}
175
		}
176 34
		return $result;
177
	}
178
179
180
	// inline parsing
181
182
183
	/**
184
	 * @marker {
185
	 */
186 1 View Code Duplication
	protected function parseSpecialAttributes($text)
187
	{
188 1
		if (preg_match("~$this->_specialAttributesRegex~", $text, $matches)) {
189 1
			return [['specialAttributes', $matches[1]], strlen($matches[0])];
190
		}
191 1
		return [['text', '{'], 1];
192
	}
193
194 1
	protected function renderSpecialAttributes($block)
195
	{
196 1
		return '{' . $block[1] . '}';
197
	}
198
199 62
	protected function parseInline($text)
200
	{
201 62
		$elements = parent::parseInline($text);
202
		// merge special attribute elements to links and images as they are not part of the final absy later
203 62
		$relatedElement = null;
204 62
		foreach($elements as $i => $element) {
205 62
			if ($element[0] === 'link' || $element[0] === 'image') {
206 18
				$relatedElement = $i;
207 62
			} elseif ($element[0] === 'specialAttributes') {
208 1
				if ($relatedElement !== null) {
209 1
					$elements[$relatedElement]['attributes'] = $element[1];
210 1
					unset($elements[$i]);
211
				}
212 1
				$relatedElement = null;
213
			} else {
214 62
				$relatedElement = null;
215
			}
216
		}
217 62
		return $elements;
218
	}
219
220 18
	protected function renderLink($block)
221
	{
222 18 View Code Duplication
		if (isset($block['refkey'])) {
223 15
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
224 10
				$block = array_merge($block, $ref);
225
			} else {
226 8
				if (strncmp($block['orig'], '[', 1) === 0) {
227 8
					return '[' . $this->renderAbsy($this->parseInline(substr($block['orig'], 1)));
228
				}
229
				return $block['orig'];
230
			}
231
		}
232 17
		$attributes = $this->renderAttributes($block);
233 17
		return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
234 17
			. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
235 17
			. $attributes . '>' . $this->renderAbsy($block['text']) . '</a>';
236
	}
237
238 4
	protected function renderImage($block)
239
	{
240 4 View Code Duplication
		if (isset($block['refkey'])) {
241 3
			if (($ref = $this->lookupReference($block['refkey'])) !== false) {
242 1
				$block = array_merge($block, $ref);
243
			} else {
244 2
				if (strncmp($block['orig'], '![', 2) === 0) {
245 2
					return '![' . $this->renderAbsy($this->parseInline(substr($block['orig'], 2)));
246
				}
247
				return $block['orig'];
248
			}
249
		}
250 3
		$attributes = $this->renderAttributes($block);
251 3
		return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
252 3
			. ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
253 3
			. (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
254 3
			. $attributes . ($this->html5 ? '>' : ' />');
255
	}
256
}