3 namespace cebe\markdown;
5 use cebe\markdown\block\TableTrait;
7 // work around https://github.com/facebook/hhvm/issues/1120
8 defined('ENT_HTML401') || define('ENT_HTML401', 0);
11 * Markdown parser for the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) flavor.
13 * @author Carsten Brandt <mail@cebe.cc>
14 * @license https://github.com/cebe/markdown/blob/master/LICENSE
15 * @link https://github.com/cebe/markdown#readme
17 class MarkdownExtra extends Markdown
19 // include block element parsing using traits
21 use block\FencedCodeTrait;
23 // include inline element parsing using traits
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.
30 public $codeAttributesOnPre = false;
35 protected $escapeCharacters = [
41 '{', '}', // curly braces
42 '[', ']', // square brackets
43 '(', ')', // parentheses
46 '-', // minus sign (hyphen)
48 '!', // exclamation mark
50 // added by MarkdownExtra
55 private $_specialAttributesRegex = '\{(([#\.][A-z0-9-_]+\s*)+)\}';
57 // TODO allow HTML intended 3 spaces
59 // TODO add markdown inside HTML blocks
61 // TODO implement definition lists
63 // TODO implement footnotes
65 // TODO implement Abbreviations
70 protected function identifyReference($line)
72 return ($line[0] === ' ' || $line[0] === '[') && preg_match('/^ {0,3}\[(.+?)\]:\s*([^\s]+?)(?:\s+[\'"](.+?)[\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $line);
76 * Consume link references
78 protected function consumeReference($lines, $current)
80 while (isset($lines[$current]) && preg_match('/^ {0,3}\[(.+?)\]:\s*(.+?)(?:\s+[\(\'"](.+?)[\)\'"])?\s*('.$this->_specialAttributesRegex.')?\s*$/', $lines[$current], $matches)) {
81 $label = strtolower($matches[1]);
83 $this->references[$label] = [
86 if (isset($matches[3])) {
87 $this->references[$label]['title'] = $matches[3];
89 // title may be on the next line
90 if (isset($lines[$current + 1]) && preg_match('/^\s+[\(\'"](.+?)[\)\'"]\s*$/', $lines[$current + 1], $matches)) {
91 $this->references[$label]['title'] = $matches[1];
95 if (isset($matches[5])) {
96 $this->references[$label]['attributes'] = $matches[5];
100 return [false, --$current];
104 * Consume lines for a fenced code block
106 protected function consumeFencedCode($lines, $current)
112 $line = rtrim($lines[$current]);
113 if (($pos = strrpos($line, '`')) === false) {
114 $pos = strrpos($line, '~');
116 $fence = substr($line, 0, $pos + 1);
117 $block['attributes'] = substr($line, $pos);
119 for($i = $current + 1, $count = count($lines); $i < $count; $i++) {
120 if (rtrim($line = $lines[$i]) !== $fence) {
126 $block['content'] = implode("\n", $content);
130 protected function renderCode($block)
132 $attributes = $this->renderAttributes($block);
133 return ($this->codeAttributesOnPre ? "<pre$attributes><code>" : "<pre><code$attributes>")
134 . htmlspecialchars($block['content'] . "\n", ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8')
141 protected function renderHeadline($block)
143 foreach($block['content'] as $i => $element) {
144 if ($element[0] === 'specialAttributes') {
145 unset($block['content'][$i]);
146 $block['attributes'] = $element[1];
149 $tag = 'h' . $block['level'];
150 $attributes = $this->renderAttributes($block);
151 return "<$tag$attributes>" . rtrim($this->renderAbsy($block['content']), "# \t") . "</$tag>\n";
154 protected function renderAttributes($block)
157 if (isset($block['attributes'])) {
158 $attributes = preg_split('/\s+/', $block['attributes'], -1, PREG_SPLIT_NO_EMPTY);
159 foreach($attributes as $attribute) {
160 if ($attribute[0] === '#') {
161 $html['id'] = substr($attribute, 1);
163 $html['class'][] = substr($attribute, 1);
168 foreach($html as $attr => $value) {
169 if (is_array($value)) {
170 $value = trim(implode(' ', $value));
172 if (!empty($value)) {
173 $result .= " $attr=\"$value\"";
186 protected function parseSpecialAttributes($text)
188 if (preg_match("~$this->_specialAttributesRegex~", $text, $matches)) {
189 return [['specialAttributes', $matches[1]], strlen($matches[0])];
191 return [['text', '{'], 1];
194 protected function renderSpecialAttributes($block)
196 return '{' . $block[1] . '}';
199 protected function parseInline($text)
201 $elements = parent::parseInline($text);
202 // merge special attribute elements to links and images as they are not part of the final absy later
203 $relatedElement = null;
204 foreach($elements as $i => $element) {
205 if ($element[0] === 'link' || $element[0] === 'image') {
206 $relatedElement = $i;
207 } elseif ($element[0] === 'specialAttributes') {
208 if ($relatedElement !== null) {
209 $elements[$relatedElement]['attributes'] = $element[1];
210 unset($elements[$i]);
212 $relatedElement = null;
214 $relatedElement = null;
220 protected function renderLink($block)
222 if (isset($block['refkey'])) {
223 if (($ref = $this->lookupReference($block['refkey'])) !== false) {
224 $block = array_merge($block, $ref);
226 return $block['orig'];
229 $attributes = $this->renderAttributes($block);
230 return '<a href="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
231 . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
232 . $attributes . '>' . $this->renderAbsy($block['text']) . '</a>';
235 protected function renderImage($block)
237 if (isset($block['refkey'])) {
238 if (($ref = $this->lookupReference($block['refkey'])) !== false) {
239 $block = array_merge($block, $ref);
241 return $block['orig'];
244 $attributes = $this->renderAttributes($block);
245 return '<img src="' . htmlspecialchars($block['url'], ENT_COMPAT | ENT_HTML401, 'UTF-8') . '"'
246 . ' alt="' . htmlspecialchars($block['text'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"'
247 . (empty($block['title']) ? '' : ' title="' . htmlspecialchars($block['title'], ENT_COMPAT | ENT_HTML401 | ENT_SUBSTITUTE, 'UTF-8') . '"')
248 . $attributes . ($this->html5 ? '>' : ' />');