1<?php 2 3# 4# 5# Parsedown Extra 6# https://github.com/erusev/parsedown-extra 7# 8# (c) Emanuil Rusev 9# http://erusev.com 10# 11# For the full license information, view the LICENSE file that was distributed 12# with this source code. 13# 14# 15 16class ParsedownExtra extends Parsedown 17{ 18 # ~ 19 20 const version = '0.7.0'; 21 22 # ~ 23 24 function __construct() 25 { 26 if (parent::version < '1.5.0') 27 { 28 throw new Exception('ParsedownExtra requires a later version of Parsedown'); 29 } 30 31 $this->BlockTypes[':'] []= 'DefinitionList'; 32 $this->BlockTypes['*'] []= 'Abbreviation'; 33 34 # identify footnote definitions before reference definitions 35 array_unshift($this->BlockTypes['['], 'Footnote'); 36 37 # identify footnote markers before before links 38 array_unshift($this->InlineTypes['['], 'FootnoteMarker'); 39 } 40 41 # 42 # ~ 43 44 function text($text) 45 { 46 $markup = parent::text($text); 47 48 # merge consecutive dl elements 49 50 $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup); 51 52 # add footnotes 53 54 if (isset($this->DefinitionData['Footnote'])) 55 { 56 $Element = $this->buildFootnoteElement(); 57 58 $markup .= "\n" . $this->element($Element); 59 } 60 61 return $markup; 62 } 63 64 # 65 # Blocks 66 # 67 68 # 69 # Abbreviation 70 71 protected function blockAbbreviation($Line) 72 { 73 if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches)) 74 { 75 $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2]; 76 77 $Block = array( 78 'hidden' => true, 79 ); 80 81 return $Block; 82 } 83 } 84 85 # 86 # Footnote 87 88 protected function blockFootnote($Line) 89 { 90 if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches)) 91 { 92 $Block = array( 93 'label' => $matches[1], 94 'text' => $matches[2], 95 'hidden' => true, 96 ); 97 98 return $Block; 99 } 100 } 101 102 protected function blockFootnoteContinue($Line, $Block) 103 { 104 if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text'])) 105 { 106 return; 107 } 108 109 if (isset($Block['interrupted'])) 110 { 111 if ($Line['indent'] >= 4) 112 { 113 $Block['text'] .= "\n\n" . $Line['text']; 114 115 return $Block; 116 } 117 } 118 else 119 { 120 $Block['text'] .= "\n" . $Line['text']; 121 122 return $Block; 123 } 124 } 125 126 protected function blockFootnoteComplete($Block) 127 { 128 $this->DefinitionData['Footnote'][$Block['label']] = array( 129 'text' => $Block['text'], 130 'count' => null, 131 'number' => null, 132 ); 133 134 return $Block; 135 } 136 137 # 138 # Definition List 139 140 protected function blockDefinitionList($Line, $Block) 141 { 142 if ( ! isset($Block) or isset($Block['type'])) 143 { 144 return; 145 } 146 147 $Element = array( 148 'name' => 'dl', 149 'handler' => 'elements', 150 'text' => array(), 151 ); 152 153 $terms = explode("\n", $Block['element']['text']); 154 155 foreach ($terms as $term) 156 { 157 $Element['text'] []= array( 158 'name' => 'dt', 159 'handler' => 'line', 160 'text' => $term, 161 ); 162 } 163 164 $Block['element'] = $Element; 165 166 $Block = $this->addDdElement($Line, $Block); 167 168 return $Block; 169 } 170 171 protected function blockDefinitionListContinue($Line, array $Block) 172 { 173 if ($Line['text'][0] === ':') 174 { 175 $Block = $this->addDdElement($Line, $Block); 176 177 return $Block; 178 } 179 else 180 { 181 if (isset($Block['interrupted']) and $Line['indent'] === 0) 182 { 183 return; 184 } 185 186 if (isset($Block['interrupted'])) 187 { 188 $Block['dd']['handler'] = 'text'; 189 $Block['dd']['text'] .= "\n\n"; 190 191 unset($Block['interrupted']); 192 } 193 194 $text = substr($Line['body'], min($Line['indent'], 4)); 195 196 $Block['dd']['text'] .= "\n" . $text; 197 198 return $Block; 199 } 200 } 201 202 # 203 # Header 204 205 protected function blockHeader($Line) 206 { 207 $Block = parent::blockHeader($Line); 208 209 if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) 210 { 211 $attributeString = $matches[1][0]; 212 213 $Block['element']['attributes'] = $this->parseAttributeData($attributeString); 214 215 $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]); 216 } 217 218 return $Block; 219 } 220 221 # 222 # Markup 223 224 protected function blockMarkupComplete($Block) 225 { 226 if ( ! isset($Block['void'])) 227 { 228 $Block['markup'] = $this->processTag($Block['markup']); 229 } 230 231 return $Block; 232 } 233 234 # 235 # Setext 236 237 protected function blockSetextHeader($Line, array $Block = null) 238 { 239 $Block = parent::blockSetextHeader($Line, $Block); 240 241 if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE)) 242 { 243 $attributeString = $matches[1][0]; 244 245 $Block['element']['attributes'] = $this->parseAttributeData($attributeString); 246 247 $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]); 248 } 249 250 return $Block; 251 } 252 253 # 254 # Inline Elements 255 # 256 257 # 258 # Footnote Marker 259 260 protected function inlineFootnoteMarker($Excerpt) 261 { 262 if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches)) 263 { 264 $name = $matches[1]; 265 266 if ( ! isset($this->DefinitionData['Footnote'][$name])) 267 { 268 return; 269 } 270 271 $this->DefinitionData['Footnote'][$name]['count'] ++; 272 273 if ( ! isset($this->DefinitionData['Footnote'][$name]['number'])) 274 { 275 $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » & 276 } 277 278 $Element = array( 279 'name' => 'sup', 280 'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name), 281 'handler' => 'element', 282 'text' => array( 283 'name' => 'a', 284 'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'), 285 'text' => $this->DefinitionData['Footnote'][$name]['number'], 286 ), 287 ); 288 289 return array( 290 'extent' => strlen($matches[0]), 291 'element' => $Element, 292 ); 293 } 294 } 295 296 private $footnoteCount = 0; 297 298 # 299 # Link 300 301 protected function inlineLink($Excerpt) 302 { 303 $Link = parent::inlineLink($Excerpt); 304 305 $remainder = substr($Excerpt['text'], $Link['extent']); 306 307 if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) 308 { 309 $Link['element']['attributes'] += $this->parseAttributeData($matches[1]); 310 311 $Link['extent'] += strlen($matches[0]); 312 } 313 314 return $Link; 315 } 316 317 # 318 # ~ 319 # 320 321 protected function unmarkedText($text) 322 { 323 $text = parent::unmarkedText($text); 324 325 if (isset($this->DefinitionData['Abbreviation'])) 326 { 327 foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning) 328 { 329 $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/'; 330 331 $text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text); 332 } 333 } 334 335 return $text; 336 } 337 338 # 339 # Util Methods 340 # 341 342 protected function addDdElement(array $Line, array $Block) 343 { 344 $text = substr($Line['text'], 1); 345 $text = trim($text); 346 347 unset($Block['dd']); 348 349 $Block['dd'] = array( 350 'name' => 'dd', 351 'handler' => 'line', 352 'text' => $text, 353 ); 354 355 if (isset($Block['interrupted'])) 356 { 357 $Block['dd']['handler'] = 'text'; 358 359 unset($Block['interrupted']); 360 } 361 362 $Block['element']['text'] []= & $Block['dd']; 363 364 return $Block; 365 } 366 367 protected function buildFootnoteElement() 368 { 369 $Element = array( 370 'name' => 'div', 371 'attributes' => array('class' => 'footnotes'), 372 'handler' => 'elements', 373 'text' => array( 374 array( 375 'name' => 'hr', 376 ), 377 array( 378 'name' => 'ol', 379 'handler' => 'elements', 380 'text' => array(), 381 ), 382 ), 383 ); 384 385 uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes'); 386 387 foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) 388 { 389 if ( ! isset($DefinitionData['number'])) 390 { 391 continue; 392 } 393 394 $text = $DefinitionData['text']; 395 396 $text = parent::text($text); 397 398 $numbers = range(1, $DefinitionData['count']); 399 400 $backLinksMarkup = ''; 401 402 foreach ($numbers as $number) 403 { 404 $backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">↩</a>'; 405 } 406 407 $backLinksMarkup = substr($backLinksMarkup, 1); 408 409 if (substr($text, - 4) === '</p>') 410 { 411 $backLinksMarkup = ' '.$backLinksMarkup; 412 413 $text = substr_replace($text, $backLinksMarkup.'</p>', - 4); 414 } 415 else 416 { 417 $text .= "\n".'<p>'.$backLinksMarkup.'</p>'; 418 } 419 420 $Element['text'][1]['text'] []= array( 421 'name' => 'li', 422 'attributes' => array('id' => 'fn:'.$definitionId), 423 'text' => "\n".$text."\n", 424 ); 425 } 426 427 return $Element; 428 } 429 430 # ~ 431 432 protected function parseAttributeData($attributeString) 433 { 434 $Data = array(); 435 436 $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY); 437 438 foreach ($attributes as $attribute) 439 { 440 if ($attribute[0] === '#') 441 { 442 $Data['id'] = substr($attribute, 1); 443 } 444 else # "." 445 { 446 $classes []= substr($attribute, 1); 447 } 448 } 449 450 if (isset($classes)) 451 { 452 $Data['class'] = implode(' ', $classes); 453 } 454 455 return $Data; 456 } 457 458 # ~ 459 460 protected function processTag($elementMarkup) # recursive 461 { 462 # http://stackoverflow.com/q/1148928/200145 463 libxml_use_internal_errors(true); 464 465 $DOMDocument = new DOMDocument; 466 467 # http://stackoverflow.com/q/11309194/200145 468 $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); 469 470 # http://stackoverflow.com/q/4879946/200145 471 $DOMDocument->loadHTML($elementMarkup); 472 $DOMDocument->removeChild($DOMDocument->doctype); 473 $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild); 474 475 $elementText = ''; 476 477 if ($DOMDocument->documentElement->getAttribute('markdown') === '1') 478 { 479 foreach ($DOMDocument->documentElement->childNodes as $Node) 480 { 481 $elementText .= $DOMDocument->saveHTML($Node); 482 } 483 484 $DOMDocument->documentElement->removeAttribute('markdown'); 485 486 $elementText = "\n".$this->text($elementText)."\n"; 487 } 488 else 489 { 490 foreach ($DOMDocument->documentElement->childNodes as $Node) 491 { 492 $nodeMarkup = $DOMDocument->saveHTML($Node); 493 494 if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements)) 495 { 496 $elementText .= $this->processTag($nodeMarkup); 497 } 498 else 499 { 500 $elementText .= $nodeMarkup; 501 } 502 } 503 } 504 505 # because we don't want for markup to get encoded 506 $DOMDocument->documentElement->nodeValue = 'placeholder\x1A'; 507 508 $markup = $DOMDocument->saveHTML($DOMDocument->documentElement); 509 $markup = str_replace('placeholder\x1A', $elementText, $markup); 510 511 return $markup; 512 } 513 514 # ~ 515 516 protected function sortFootnotes($A, $B) # callback 517 { 518 return $A['number'] - $B['number']; 519 } 520 521 # 522 # Fields 523 # 524 525 protected $regexAttribute = '(?:[#.][-\w]+[ ]*)'; 526} 527