├── .htaccess ├── LICENSE ├── README.md └── pagenode.php /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine on 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^(.*)$ index.php 6 | 7 | 8 | # Prevent file browsing 9 | Options -Indexes -MultiViews 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dominic Szablewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pagenode – No Bullshit Content Management 2 | 3 | Please see http://pagenode.org for more info and documentation. 4 | 5 | MIT Licensed 6 | -------------------------------------------------------------------------------- /pagenode.php: -------------------------------------------------------------------------------- 1 | path = realpath('./'.$path.'/'); 59 | if (!$this->path || strstr($path, '..') !== false) { 60 | header("HTTP/1.1 500 Internal Error"); 61 | echo 'select("'.htmlSpecialChars($path).'") does not exist.'; 62 | exit(); 63 | } 64 | $this->indexPath = 65 | (PN_CACHE_INDEX_PATH ?? sys_get_temp_dir()). 66 | '/pagenode-index-'.md5($this->path).'.json'; 67 | } 68 | 69 | protected function rebuildIndex() { 70 | $index = []; 71 | foreach (glob($this->path.'/*.md') as $path) { 72 | $meta = $this->loadMetaFromFile($path); 73 | if ($meta['active'] !== false) { 74 | $keyword = pathInfo($path, PATHINFO_FILENAME); 75 | $index[$keyword] = $meta; 76 | } 77 | } 78 | 79 | if (empty($index)) { 80 | return $index; 81 | } 82 | 83 | uasort($index, function ($a, $b) { 84 | return $b['date'] <=> $a['date']; 85 | }); 86 | 87 | $jsonOpts = JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES; 88 | $json = json_encode($index, $jsonOpts); 89 | file_put_contents($this->indexPath, $json); 90 | 91 | return $index; 92 | } 93 | 94 | protected function indexIsCurrent() { 95 | if (!file_exists($this->indexPath)) { 96 | return false; 97 | } 98 | 99 | $indexTime = filemtime($this->indexPath); 100 | if ( 101 | PN_CACHE_USE_INDICATOR_FILE && 102 | file_exists(PN_CACHE_INDICATOR_FILE) 103 | ) { 104 | return $indexTime > filemtime(PN_CACHE_INDICATOR_FILE); 105 | } 106 | 107 | $lastFileTime = 0; 108 | foreach (glob($this->path.'/*.md') as $f) { 109 | $lastFileTime = max($lastFileTime, filemtime($f)); 110 | } 111 | return $indexTime > $lastFileTime; 112 | } 113 | 114 | protected function getIndex() { 115 | $timeStart = microtime(true); 116 | $didRebuild = false; 117 | 118 | if (!isset(self::$IndexCache[$this->path])) { 119 | if ($this->indexIsCurrent()) { 120 | $json = file_get_contents($this->indexPath); 121 | self::$IndexCache[$this->path] = json_decode($json, true); 122 | } 123 | else { 124 | self::$IndexCache[$this->path] = $this->rebuildIndex(); 125 | $didRebuild = true; 126 | } 127 | 128 | self::$DebugInfo[] = [ 129 | 'action' => 'loadIndex', 130 | 'path' => $this->path, 131 | 'indexPath' => $this->indexPath, 132 | 'ms' => round((microtime(true) - $timeStart)*1000, 3), 133 | 'didRebuild' => (int)$didRebuild, 134 | 'cacheMethod' => PN_CACHE_USE_INDICATOR_FILE 135 | ? 'INDICATOR_FILE' 136 | : 'NODE_LAST_MODIFIED' 137 | ]; 138 | } 139 | 140 | return self::$IndexCache[$this->path] ?? []; 141 | } 142 | 143 | protected function loadMetaFromFile($path) { 144 | return PN_FileReader::ReadMeta($path); 145 | } 146 | 147 | 148 | public static function FoundNodes() { 149 | return self::$FoundNodes; 150 | } 151 | 152 | public function one($params = [], $raw = false) { 153 | $nodes = $this->query('date', self::SORT_DESC, 1, $params, $raw); 154 | return !empty($nodes) ? $nodes[0] : null; 155 | } 156 | 157 | public function newest($count = 0, $params = [], $raw = false) { 158 | return $this->query('date', self::SORT_DESC, $count, $params, $raw); 159 | } 160 | 161 | public function oldest($count = 0, $params = [], $raw = false) { 162 | return $this->query('date', self::SORT_ASC, $count, $params, $raw); 163 | } 164 | 165 | public function query($sort, $order, $count, $params, $raw = false) { 166 | if (!$this->path) { 167 | return []; 168 | } 169 | 170 | $index = $this->getIndex(); 171 | 172 | $timeStart = microtime(true); 173 | $scannedNodes = count($index); 174 | 175 | // Filter by keyword. Since keywords are unique, we can simply index 176 | // by it, returning only one node. 177 | 178 | if (!empty($params['keyword'])) { 179 | $index = !empty($index[$params['keyword']]) 180 | ? [$params['keyword'] => $index[$params['keyword']]] 181 | : []; 182 | } 183 | 184 | 185 | // Filter by date. Allow to become more granual by specifying either 186 | // just year, year & month or year & month & day. 187 | 188 | if (!empty($params['date'])) { 189 | $y = (int)($params['date'][0] ?? $params['date']); 190 | $m = (int)($params['date'][1] ?? 0); 191 | $d = (int)($params['date'][2] ?? 0); 192 | if (preg_match('/(\d{4}).(\d{2}).(\d{2})/', $y, $match)) { 193 | $y = $match[1]; 194 | $m = $match[2]; 195 | $d = $match[3]; 196 | } 197 | $start = mktime(0, 0, 0, ($m ? $m : 1), ($d ? $d : 1), $y); 198 | $end = mktime(23, 59, 59, ($m ? $m : 12), ($d ? $d : 31), $y); 199 | 200 | $index = array_filter($index, function($n) use ($start, $end) { 201 | return $n['date'] >= $start && $n['date'] <= $end; 202 | }); 203 | } 204 | 205 | 206 | // Filter by tags. Only return nodes that match all given tags. 207 | 208 | if (!empty($params['tags'])) { 209 | $tags = !is_array($params['tags']) 210 | ? array_map('trim', explode(',', $params['tags'])) 211 | : $params['tags']; 212 | $index = array_filter($index, function($n) use ($tags) { 213 | return !array_udiff($tags, $n['tags'], 'strcasecmp'); 214 | }); 215 | } 216 | 217 | 218 | // Filter by arbitrary properties 219 | 220 | if (!empty($params['meta'])) { 221 | $meta = $params['meta']; 222 | $index = array_filter($index, function($n) use ($meta) { 223 | foreach ($meta as $key => $value) { 224 | if (!isset($n[$key]) || $n[$key] !== $value) { 225 | return false; 226 | } 227 | } 228 | return true; 229 | }); 230 | } 231 | 232 | // Filter using a custom filter function 233 | 234 | if (!empty($params['filter']) && is_callable($params['filter'])) { 235 | $index = array_filter($index, $params['filter']); 236 | } 237 | 238 | 239 | // Sort by any property 240 | 241 | if ($sort === 'date' && $order === self::SORT_DESC) { 242 | // Nothing to do here; index is sorted by date, desc by default 243 | } 244 | else { 245 | if ($order === self::SORT_ASC) { 246 | uasort($index, function ($a, $b) use ($sort) { 247 | return ($a[$sort] ?? INF) <=> ($b[$sort] ?? INF); 248 | }); 249 | } 250 | else { 251 | uasort($index, function ($a, $b) use ($sort) { 252 | return ($b[$sort] ?? 0) <=> ($a[$sort] ?? 0); 253 | }); 254 | } 255 | } 256 | 257 | // Keep track of the total nodes found with the given filter params 258 | 259 | self::$FoundNodes = count($index); 260 | 261 | // Slice and Paginate 262 | 263 | if ($count) { 264 | $offset = ($params['page'] ?? 0) * $count; 265 | $index = array_slice($index, $offset, $count, true); 266 | } 267 | 268 | 269 | // Create Nodes 270 | 271 | $nodes = []; 272 | foreach ($index as $keyword => $meta) { 273 | $nodePath = $this->path.'/'.$keyword.'.md'; 274 | $nodes[] = new PN_Node($nodePath, $keyword, $meta, $raw); 275 | } 276 | 277 | self::$DebugInfo[] = [ 278 | 'action' => 'query', 279 | 'path' => $this->path, 280 | 'ms' => round((microtime(true) - $timeStart)*1000, 3), 281 | 'scanned' => $scannedNodes, 282 | 'returned' => count($nodes), 283 | 'params' => $params 284 | ]; 285 | 286 | return $nodes; 287 | } 288 | } 289 | 290 | 291 | // ----------------------------------------------------------------------------- 292 | // DateTime class - a simple wrapper for timestamps 293 | 294 | class PN_DateTime { 295 | protected $timestamp; 296 | public function __construct($timestamp) { 297 | $this->timestamp = $timestamp; 298 | } 299 | 300 | public function format($format = PN_DATE_FORMAT) { 301 | return htmlSpecialChars(date($format, $this->timestamp)); 302 | } 303 | 304 | public function __toString() { 305 | return $this->format(); 306 | } 307 | } 308 | 309 | 310 | // ----------------------------------------------------------------------------- 311 | // Node Class - each Node instance represents a single file 312 | 313 | class PN_Node { 314 | public static $DebugOpenedNodes = []; 315 | 316 | public $keyword, $tags = [], $date; 317 | protected $path, $meta = [], $body = null, $raw = false; 318 | 319 | public function __construct($path, $keyword, $meta, $raw = false) { 320 | $this->raw = $raw; 321 | $this->path = $path; 322 | $this->keyword = pathInfo($path, PATHINFO_FILENAME); 323 | $this->date = $raw ? $meta['date'] : new PN_DateTime($meta['date']); 324 | $this->meta = $meta; 325 | 326 | if (!$raw) { 327 | foreach ($meta['tags'] as $t) { 328 | $this->tags[] = htmlSpecialChars($t); 329 | } 330 | } 331 | else { 332 | $this->tags = $meta['tags']; 333 | } 334 | } 335 | 336 | protected function loadBody() { 337 | self::$DebugOpenedNodes[] = $this->path; 338 | $markdown = PN_FileReader::ReadContent($this->path); 339 | 340 | if ($this->raw) { 341 | return $markdown; 342 | } 343 | else { 344 | return !empty(PN_SYNTAX_HIGHLIGHT_LANGS) 345 | ? PN_ParsedownSyntaxHighlight::instance()->text($markdown) 346 | : Parsedown::instance()->text($markdown); 347 | } 348 | } 349 | 350 | public function hasTag($tag) { 351 | return in_array($tag, $this->meta['tags']); 352 | } 353 | 354 | public function __get($name) { 355 | if ($name === 'body') { 356 | if (!$this->body) { 357 | $this->body = $this->loadBody(); 358 | } 359 | return $this->body; 360 | } 361 | else if (isset($this->meta[$name])) { 362 | return $this->raw 363 | ? $this->meta[$name] 364 | : htmlSpecialChars($this->meta[$name]); 365 | } 366 | 367 | return null; 368 | } 369 | } 370 | 371 | class PN_FileReader 372 | { 373 | public static function ReadMeta($path) 374 | { 375 | return self::ReadFile($path)[0]; 376 | } 377 | 378 | public static function ReadContent($path) 379 | { 380 | return self::ReadFile($path)[1]; 381 | } 382 | 383 | private static function ReadFile($path) 384 | { 385 | $meta = []; 386 | $file = file_get_contents($path); 387 | 388 | $lines = preg_split('/\R/', $file); 389 | 390 | if ($lines[0] === '---') { 391 | array_shift($lines); 392 | $file = implode("\n", $lines); 393 | } 394 | 395 | if (preg_match('/(.*?)^---\s*$/ms', $file, $metaSection)) { 396 | preg_match_all('/^(\w+):(.*)$/m', $metaSection[1], $metaAttribs); 397 | foreach ($metaAttribs[1] as $i => $key) { 398 | $meta[$key] = trim($metaAttribs[2][$i]); 399 | } 400 | } 401 | 402 | $meta['tags'] = !empty($meta['tags']) 403 | ? array_map('trim', explode(',', $meta['tags'])) 404 | : []; 405 | 406 | if ( 407 | !empty($meta['date']) && 408 | preg_match( 409 | '/(\d{4})[\.\-](\d{2})[\.\-](\d{2})( (\d{2}):(\d{2}))?/', 410 | $meta['date'], 411 | $dateMatch 412 | ) 413 | ) { 414 | $y = $dateMatch[1]; 415 | $m = $dateMatch[2]; 416 | $d = $dateMatch[3]; 417 | $h = !empty($dateMatch[5]) ? $dateMatch[5] : 0; 418 | $i = !empty($dateMatch[6]) ? $dateMatch[6] : 0; 419 | $meta['date'] = mktime($h, $i, 0, $m, $d, $y); 420 | } else { 421 | $meta['date'] = filemtime($path); 422 | } 423 | 424 | $meta['active'] = empty($meta['active']) || $meta['active'] !== 'false'; 425 | 426 | $markdown = (preg_match('/^---\s*$(.*)/ms', $file, $m)) 427 | ? $m[1] 428 | : $file; 429 | 430 | return [$meta, $markdown]; 431 | } 432 | } 433 | 434 | // ----------------------------------------------------------------------------- 435 | // Router Class - handles routes and dispatch 436 | 437 | class PN_Router { 438 | public static $Routes = []; 439 | 440 | public static function AddRoute($path, $resolver) { 441 | $r = str_replace('/', '\\/', $path); 442 | $r = str_replace('*', '.*?', $r); 443 | $r = preg_replace('/{(\w+)}/', '(?<$1>[^\\/]+?)', $r); 444 | $regexp = '/^'.$r.'$/'; 445 | 446 | self::$Routes[$path] = [ 447 | 'regexp' => $regexp, 448 | 'resolver' => $resolver 449 | ]; 450 | } 451 | 452 | public static function Dispatch($request) { 453 | foreach (self::$Routes as $path => $r) { 454 | if (preg_match($r['regexp'], $request, $m)) { 455 | $found = self::Resolve($r['resolver'], $m); 456 | return ($found && $path !== '/*'); 457 | } 458 | } 459 | return self::ErrorNotFound(); 460 | } 461 | 462 | public static function Resolve($resolver, $regexpMatch, $recurse = true) { 463 | $params = array_filter($regexpMatch, function($key) { 464 | return !is_int($key); 465 | }, ARRAY_FILTER_USE_KEY); 466 | 467 | if (call_user_func_array($resolver, array_values($params)) !== false) { 468 | return true; 469 | }; 470 | 471 | return self::ErrorNotFound($recurse); 472 | } 473 | 474 | public static function ErrorNotFound($recurse = true) { 475 | if ($recurse && !empty(self::$Routes['/*'])) { 476 | self::Resolve(self::$Routes['/*']['resolver'], [], false); 477 | } 478 | else { 479 | header("HTTP/1.1 404 Not Found"); 480 | echo "Not Found"; 481 | } 482 | return false; 483 | } 484 | } 485 | 486 | 487 | // ----------------------------------------------------------------------------- 488 | // Generic Syntax Highlighting extension for Parsedown 489 | 490 | class PN_ParsedownSyntaxHighlight extends Parsedown { 491 | public static function SyntaxHighlight($s) { 492 | $s = htmlSpecialChars($s, ENT_COMPAT)."\n"; 493 | $s = str_replace('\\\\','\\\\', $s); // break escaped backslashes 494 | 495 | $tokens = []; 496 | $transforms = [ 497 | // Insert helpers to find regexps 498 | '/ 499 | ([\[({=:+,]\s*) 500 | \/ 501 | (?![\/\*]) 502 | /x' 503 | => '$1/', 504 | 505 | // Extract Comments, Strings & Regexps, insert them into $tokens 506 | // and return the index 507 | '/( 508 | \/\*.*?\*\/| 509 | \/\/.*?\n| 510 | \#.*?\n| 511 | --.*?\n| 512 | (?\/.+?(? function($m) use (&$tokens) { 517 | $id = ''; 518 | $block = $m[1]; 519 | 520 | if ($block[0] === '&' || $block[0] === "'") { 521 | $type = 'string'; 522 | } 523 | else if ($block[0] === '<') { 524 | $type = 'regexp'; 525 | } 526 | else { 527 | $type = 'comment'; 528 | } 529 | $tokens[$id] = ''.$block.''; 530 | return $id; 531 | }, 532 | 533 | // Punctuation 534 | '/(( 535 | &\w+;| 536 | [-\/+*=?:.,;()\[\]{}|%^!] 537 | )+)/x' 538 | => '$1', 539 | 540 | // Numbers (also look for Hex encoding) 541 | '/(? '$1', 546 | 547 | // Keywords 548 | '/(? '$1', 560 | 561 | // PHP-Style Vars: $var 562 | '/(? '$1', 566 | 567 | // Make the bold assumption that an all uppercase word has a 568 | // special meaning 569 | '/(?)( 570 | [A-Z_][A-Z_0-9]+ 571 | )(?!\w)/x' 572 | => '$1' 573 | ]; 574 | 575 | foreach ($transforms as $search => $replace) { 576 | $s = is_string($replace) 577 | ? preg_replace($search, $replace, $s) 578 | : preg_replace_callback($search, $replace, $s); 579 | } 580 | 581 | // Paste the comments and strings back in again 582 | $s = strtr($s, $tokens); 583 | 584 | // Delete the escaped backslash breaker and replace tabs with 4 spaces 585 | $s = str_replace(['', '', "\t" ], ['', '', ' '], $s); 586 | 587 | return trim($s, "\n\r"); 588 | } 589 | 590 | protected function blockFencedCodeComplete($Block) { 591 | $class = $Block['element']['element']['attributes']['class'] ?? null; 592 | $re = '/^language-('.PN_SYNTAX_HIGHLIGHT_LANGS.')$/'; 593 | if (empty($class) || !preg_match($re, $class)) { 594 | return $Block; 595 | } 596 | 597 | $text = $Block['element']['element']['text']; 598 | unset($Block['element']['element']['text']); 599 | $Block['element']['element']['rawHtml'] = self::SyntaxHighlight($text); 600 | $Block['element']['element']['allowRawHtmlInSafeMode'] = true; 601 | return $Block; 602 | } 603 | } 604 | 605 | 606 | // ----------------------------------------------------------------------------- 607 | // mb_strlen polyfill for Parsedown when mbstring extension is not installed 608 | 609 | if (!function_exists('mb_strlen')) { 610 | function mb_strlen($s) { 611 | $byteLength = strlen($s); 612 | for ($q = 0, $i = 0; $i < $byteLength; $i++, $q++) { 613 | $c = ord($s[$i]); 614 | if ($c >= 0 && $c <= 127) { $i += 0; } 615 | else if (($c & 0xE0) == 0xC0) { $i += 1; } 616 | else if (($c & 0xF0) == 0xE0) { $i += 2; } 617 | else if (($c & 0xF8) == 0xF0) { $i += 3; } 618 | else return $byteLength; //invalid utf8 619 | } 620 | return $q; 621 | } 622 | } 623 | 624 | 625 | // ----------------------------------------------------------------------------- 626 | // PAGENODE Public API 627 | 628 | function select($path = '') { 629 | return new PN_Selector($path); 630 | } 631 | 632 | function foundNodes() { 633 | return PN_Selector::FoundNodes(); 634 | } 635 | 636 | function route($path, $resolver = null) { 637 | PN_Router::AddRoute($path, $resolver); 638 | } 639 | 640 | function reroute($source, $target) { 641 | route($source, function() use ($target) { 642 | $args = func_get_args(); 643 | $target = preg_replace_callback( 644 | '/{(\w+)}/', 645 | function($m) use ($args) { return $args[$m[1] - 1] ?? ''; }, 646 | $target 647 | ); 648 | dispatch($target); 649 | }); 650 | } 651 | 652 | function redirect($path = '/', $params = []) { 653 | $query = !empty($params) 654 | ? '?'.http_build_query($params) 655 | : ''; 656 | header('Location: '.$path.$query); 657 | exit(); 658 | } 659 | 660 | function dispatch($request = null) { 661 | if ($request === null) { 662 | $request = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); 663 | $request = '/'.substr($request, strlen(PN_ABS)); 664 | } 665 | 666 | $found = PN_Router::Dispatch($request); 667 | } 668 | 669 | function getDebugInfo() { 670 | global $PN_TimeStart; 671 | return [ 672 | 'totalRuntime' => (microtime(true) - $PN_TimeStart)*1000, 673 | 'selctorInfo' => PN_Selector::$DebugInfo, 674 | 'openedNodes' => PN_Node::$DebugOpenedNodes 675 | ]; 676 | } 677 | 678 | function printDebugInfo() { 679 | echo "
\n".htmlSpecialChars(print_r(getDebugInfo(), true))."
"; 680 | } 681 | 682 | 683 | // ----------------------------------------------------------------------------- 684 | // PAGENODE JSON Route, disabled by default 685 | 686 | if (defined('PN_JSON_API_PATH')) { 687 | route(PN_JSON_API_PATH, function(){ 688 | $nodes = select($_GET['path'] ?? '')->query( 689 | $_GET['sort'] ?? 'date', 690 | $_GET['order'] ?? 'desc', 691 | $_GET['count'] ?? 0, 692 | [ 693 | 'keyword' => $_GET['keyword'] ?? null, 694 | 'date' => $_GET['date'] ?? null, 695 | 'tags' => $_GET['tags'] ?? null, 696 | 'meta' => $_GET['meta'] ?? null, 697 | 'page' => $_GET['page'] ?? null 698 | ], 699 | true 700 | ); 701 | 702 | $fields = !empty($_GET['fields']) 703 | ? array_map('trim', explode(',', $_GET['fields'])) 704 | : ['keyword']; 705 | 706 | header('Content-type: application/json; charset=UTF-8'); 707 | echo json_encode([ 708 | 'nodes' => array_map(function($n) use ($fields) { 709 | $ret = []; 710 | foreach ($fields as $f) { 711 | $ret[$f] = $n->$f; 712 | } 713 | return $ret; 714 | }, $nodes), 715 | 'info' => PN_JSON_API_FULL_DEBUG_INFO 716 | ? getDebugInfo() 717 | : ['totalRuntime' => getDebugInfo()['totalRuntime']] 718 | ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT); 719 | }); 720 | } 721 | 722 | 723 | 724 | 725 | // ----------------------------------------------------------------------------- 726 | // Parsedown Library 727 | 728 | # 729 | # 730 | # Parsedown 731 | # http://parsedown.org 732 | # 733 | # (c) Emanuil Rusev 734 | # http://erusev.com 735 | # 736 | # For the full license information, view the LICENSE file that was distributed 737 | # with this source code. 738 | # 739 | # 740 | 741 | class Parsedown 742 | { 743 | # ~ 744 | 745 | const version = '1.8.0-beta-5'; 746 | 747 | # ~ 748 | 749 | function text($text) 750 | { 751 | $Elements = $this->textElements($text); 752 | 753 | # convert to markup 754 | $markup = $this->elements($Elements); 755 | 756 | # trim line breaks 757 | $markup = trim($markup, "\n"); 758 | 759 | return $markup; 760 | } 761 | 762 | protected function textElements($text) 763 | { 764 | # make sure no definitions are set 765 | $this->DefinitionData = array(); 766 | 767 | # standardize line breaks 768 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 769 | 770 | # remove surrounding line breaks 771 | $text = trim($text, "\n"); 772 | 773 | # split text into lines 774 | $lines = explode("\n", $text); 775 | 776 | # iterate through lines to identify blocks 777 | return $this->linesElements($lines); 778 | } 779 | 780 | # 781 | # Setters 782 | # 783 | 784 | function setBreaksEnabled($breaksEnabled) 785 | { 786 | $this->breaksEnabled = $breaksEnabled; 787 | 788 | return $this; 789 | } 790 | 791 | protected $breaksEnabled; 792 | 793 | function setMarkupEscaped($markupEscaped) 794 | { 795 | $this->markupEscaped = $markupEscaped; 796 | 797 | return $this; 798 | } 799 | 800 | protected $markupEscaped; 801 | 802 | function setUrlsLinked($urlsLinked) 803 | { 804 | $this->urlsLinked = $urlsLinked; 805 | 806 | return $this; 807 | } 808 | 809 | protected $urlsLinked = true; 810 | 811 | function setSafeMode($safeMode) 812 | { 813 | $this->safeMode = (bool) $safeMode; 814 | 815 | return $this; 816 | } 817 | 818 | protected $safeMode; 819 | 820 | function setStrictMode($strictMode) 821 | { 822 | $this->strictMode = (bool) $strictMode; 823 | 824 | return $this; 825 | } 826 | 827 | protected $strictMode; 828 | 829 | protected $safeLinksWhitelist = array( 830 | 'http://', 831 | 'https://', 832 | 'ftp://', 833 | 'ftps://', 834 | 'mailto:', 835 | 'data:image/png;base64,', 836 | 'data:image/gif;base64,', 837 | 'data:image/jpeg;base64,', 838 | 'irc:', 839 | 'ircs:', 840 | 'git:', 841 | 'ssh:', 842 | 'news:', 843 | 'steam:', 844 | ); 845 | 846 | # 847 | # Lines 848 | # 849 | 850 | protected $BlockTypes = array( 851 | '#' => array('Header'), 852 | '*' => array('Rule', 'List'), 853 | '+' => array('List'), 854 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 855 | '0' => array('List'), 856 | '1' => array('List'), 857 | '2' => array('List'), 858 | '3' => array('List'), 859 | '4' => array('List'), 860 | '5' => array('List'), 861 | '6' => array('List'), 862 | '7' => array('List'), 863 | '8' => array('List'), 864 | '9' => array('List'), 865 | ':' => array('Table'), 866 | '<' => array('Comment', 'Markup'), 867 | '=' => array('SetextHeader'), 868 | '>' => array('Quote'), 869 | '[' => array('Reference'), 870 | '_' => array('Rule'), 871 | '`' => array('FencedCode'), 872 | '|' => array('Table'), 873 | '~' => array('FencedCode'), 874 | ); 875 | 876 | # ~ 877 | 878 | protected $unmarkedBlockTypes = array( 879 | 'Code', 880 | ); 881 | 882 | # 883 | # Blocks 884 | # 885 | 886 | protected function lines(array $lines) 887 | { 888 | return $this->elements($this->linesElements($lines)); 889 | } 890 | 891 | protected function linesElements(array $lines) 892 | { 893 | $Elements = array(); 894 | $CurrentBlock = null; 895 | 896 | foreach ($lines as $line) 897 | { 898 | if (chop($line) === '') 899 | { 900 | if (isset($CurrentBlock)) 901 | { 902 | $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) 903 | ? $CurrentBlock['interrupted'] + 1 : 1 904 | ); 905 | } 906 | 907 | continue; 908 | } 909 | 910 | while (($beforeTab = strstr($line, "\t", true)) !== false) 911 | { 912 | $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; 913 | 914 | $line = $beforeTab 915 | . str_repeat(' ', $shortage) 916 | . substr($line, strlen($beforeTab) + 1) 917 | ; 918 | } 919 | 920 | $indent = strspn($line, ' '); 921 | 922 | $text = $indent > 0 ? substr($line, $indent) : $line; 923 | 924 | # ~ 925 | 926 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 927 | 928 | # ~ 929 | 930 | if (isset($CurrentBlock['continuable'])) 931 | { 932 | $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; 933 | $Block = $this->$methodName($Line, $CurrentBlock); 934 | 935 | if (isset($Block)) 936 | { 937 | $CurrentBlock = $Block; 938 | 939 | continue; 940 | } 941 | else 942 | { 943 | if ($this->isBlockCompletable($CurrentBlock['type'])) 944 | { 945 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 946 | $CurrentBlock = $this->$methodName($CurrentBlock); 947 | } 948 | } 949 | } 950 | 951 | # ~ 952 | 953 | $marker = $text[0]; 954 | 955 | # ~ 956 | 957 | $blockTypes = $this->unmarkedBlockTypes; 958 | 959 | if (isset($this->BlockTypes[$marker])) 960 | { 961 | foreach ($this->BlockTypes[$marker] as $blockType) 962 | { 963 | $blockTypes []= $blockType; 964 | } 965 | } 966 | 967 | # 968 | # ~ 969 | 970 | foreach ($blockTypes as $blockType) 971 | { 972 | $Block = $this->{"block$blockType"}($Line, $CurrentBlock); 973 | 974 | if (isset($Block)) 975 | { 976 | $Block['type'] = $blockType; 977 | 978 | if ( ! isset($Block['identified'])) 979 | { 980 | if (isset($CurrentBlock)) 981 | { 982 | $Elements[] = $this->extractElement($CurrentBlock); 983 | } 984 | 985 | $Block['identified'] = true; 986 | } 987 | 988 | if ($this->isBlockContinuable($blockType)) 989 | { 990 | $Block['continuable'] = true; 991 | } 992 | 993 | $CurrentBlock = $Block; 994 | 995 | continue 2; 996 | } 997 | } 998 | 999 | # ~ 1000 | 1001 | if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') 1002 | { 1003 | $Block = $this->paragraphContinue($Line, $CurrentBlock); 1004 | } 1005 | 1006 | if (isset($Block)) 1007 | { 1008 | $CurrentBlock = $Block; 1009 | } 1010 | else 1011 | { 1012 | if (isset($CurrentBlock)) 1013 | { 1014 | $Elements[] = $this->extractElement($CurrentBlock); 1015 | } 1016 | 1017 | $CurrentBlock = $this->paragraph($Line); 1018 | 1019 | $CurrentBlock['identified'] = true; 1020 | } 1021 | } 1022 | 1023 | # ~ 1024 | 1025 | if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 1026 | { 1027 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 1028 | $CurrentBlock = $this->$methodName($CurrentBlock); 1029 | } 1030 | 1031 | # ~ 1032 | 1033 | if (isset($CurrentBlock)) 1034 | { 1035 | $Elements[] = $this->extractElement($CurrentBlock); 1036 | } 1037 | 1038 | # ~ 1039 | 1040 | return $Elements; 1041 | } 1042 | 1043 | protected function extractElement(array $Component) 1044 | { 1045 | if ( ! isset($Component['element'])) 1046 | { 1047 | if (isset($Component['markup'])) 1048 | { 1049 | $Component['element'] = array('rawHtml' => $Component['markup']); 1050 | } 1051 | elseif (isset($Component['hidden'])) 1052 | { 1053 | $Component['element'] = array(); 1054 | } 1055 | } 1056 | 1057 | return $Component['element']; 1058 | } 1059 | 1060 | protected function isBlockContinuable($Type) 1061 | { 1062 | return method_exists($this, 'block' . $Type . 'Continue'); 1063 | } 1064 | 1065 | protected function isBlockCompletable($Type) 1066 | { 1067 | return method_exists($this, 'block' . $Type . 'Complete'); 1068 | } 1069 | 1070 | # 1071 | # Code 1072 | 1073 | protected function blockCode($Line, $Block = null) 1074 | { 1075 | if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) 1076 | { 1077 | return; 1078 | } 1079 | 1080 | if ($Line['indent'] >= 4) 1081 | { 1082 | $text = substr($Line['body'], 4); 1083 | 1084 | $Block = array( 1085 | 'element' => array( 1086 | 'name' => 'pre', 1087 | 'element' => array( 1088 | 'name' => 'code', 1089 | 'text' => $text, 1090 | ), 1091 | ), 1092 | ); 1093 | 1094 | return $Block; 1095 | } 1096 | } 1097 | 1098 | protected function blockCodeContinue($Line, $Block) 1099 | { 1100 | if ($Line['indent'] >= 4) 1101 | { 1102 | if (isset($Block['interrupted'])) 1103 | { 1104 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 1105 | 1106 | unset($Block['interrupted']); 1107 | } 1108 | 1109 | $Block['element']['element']['text'] .= "\n"; 1110 | 1111 | $text = substr($Line['body'], 4); 1112 | 1113 | $Block['element']['element']['text'] .= $text; 1114 | 1115 | return $Block; 1116 | } 1117 | } 1118 | 1119 | protected function blockCodeComplete($Block) 1120 | { 1121 | return $Block; 1122 | } 1123 | 1124 | # 1125 | # Comment 1126 | 1127 | protected function blockComment($Line) 1128 | { 1129 | if ($this->markupEscaped or $this->safeMode) 1130 | { 1131 | return; 1132 | } 1133 | 1134 | if (strpos($Line['text'], '') !== false) 1144 | { 1145 | $Block['closed'] = true; 1146 | } 1147 | 1148 | return $Block; 1149 | } 1150 | } 1151 | 1152 | protected function blockCommentContinue($Line, array $Block) 1153 | { 1154 | if (isset($Block['closed'])) 1155 | { 1156 | return; 1157 | } 1158 | 1159 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 1160 | 1161 | if (strpos($Line['text'], '-->') !== false) 1162 | { 1163 | $Block['closed'] = true; 1164 | } 1165 | 1166 | return $Block; 1167 | } 1168 | 1169 | # 1170 | # Fenced Code 1171 | 1172 | protected function blockFencedCode($Line) 1173 | { 1174 | $marker = $Line['text'][0]; 1175 | 1176 | $openerLength = strspn($Line['text'], $marker); 1177 | 1178 | if ($openerLength < 3) 1179 | { 1180 | return; 1181 | } 1182 | 1183 | $infostring = trim(substr($Line['text'], $openerLength), "\t "); 1184 | 1185 | if (strpos($infostring, '`') !== false) 1186 | { 1187 | return; 1188 | } 1189 | 1190 | $Element = array( 1191 | 'name' => 'code', 1192 | 'text' => '', 1193 | ); 1194 | 1195 | if ($infostring !== '') 1196 | { 1197 | $Element['attributes'] = array('class' => "language-$infostring"); 1198 | } 1199 | 1200 | $Block = array( 1201 | 'char' => $marker, 1202 | 'openerLength' => $openerLength, 1203 | 'element' => array( 1204 | 'name' => 'pre', 1205 | 'element' => $Element, 1206 | ), 1207 | ); 1208 | 1209 | return $Block; 1210 | } 1211 | 1212 | protected function blockFencedCodeContinue($Line, $Block) 1213 | { 1214 | if (isset($Block['complete'])) 1215 | { 1216 | return; 1217 | } 1218 | 1219 | if (isset($Block['interrupted'])) 1220 | { 1221 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 1222 | 1223 | unset($Block['interrupted']); 1224 | } 1225 | 1226 | if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] 1227 | and chop(substr($Line['text'], $len), ' ') === '' 1228 | ) { 1229 | $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); 1230 | 1231 | $Block['complete'] = true; 1232 | 1233 | return $Block; 1234 | } 1235 | 1236 | $Block['element']['element']['text'] .= "\n" . $Line['body']; 1237 | 1238 | return $Block; 1239 | } 1240 | 1241 | protected function blockFencedCodeComplete($Block) 1242 | { 1243 | return $Block; 1244 | } 1245 | 1246 | # 1247 | # Header 1248 | 1249 | protected function blockHeader($Line) 1250 | { 1251 | $level = strspn($Line['text'], '#'); 1252 | 1253 | if ($level > 6) 1254 | { 1255 | return; 1256 | } 1257 | 1258 | $text = trim($Line['text'], '#'); 1259 | 1260 | if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') 1261 | { 1262 | return; 1263 | } 1264 | 1265 | $text = trim($text, ' '); 1266 | 1267 | $Block = array( 1268 | 'element' => array( 1269 | 'name' => 'h' . min(6, $level), 1270 | 'handler' => array( 1271 | 'function' => 'lineElements', 1272 | 'argument' => $text, 1273 | 'destination' => 'elements', 1274 | ) 1275 | ), 1276 | ); 1277 | 1278 | return $Block; 1279 | } 1280 | 1281 | # 1282 | # List 1283 | 1284 | protected function blockList($Line, array $CurrentBlock = null) 1285 | { 1286 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); 1287 | 1288 | if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) 1289 | { 1290 | $contentIndent = strlen($matches[2]); 1291 | 1292 | if ($contentIndent >= 5) 1293 | { 1294 | $contentIndent -= 1; 1295 | $matches[1] = substr($matches[1], 0, -$contentIndent); 1296 | $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; 1297 | } 1298 | elseif ($contentIndent === 0) 1299 | { 1300 | $matches[1] .= ' '; 1301 | } 1302 | 1303 | $markerWithoutWhitespace = strstr($matches[1], ' ', true); 1304 | 1305 | $Block = array( 1306 | 'indent' => $Line['indent'], 1307 | 'pattern' => $pattern, 1308 | 'data' => array( 1309 | 'type' => $name, 1310 | 'marker' => $matches[1], 1311 | 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), 1312 | ), 1313 | 'element' => array( 1314 | 'name' => $name, 1315 | 'elements' => array(), 1316 | ), 1317 | ); 1318 | $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); 1319 | 1320 | if ($name === 'ol') 1321 | { 1322 | $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; 1323 | 1324 | if ($listStart !== '1') 1325 | { 1326 | if ( 1327 | isset($CurrentBlock) 1328 | and $CurrentBlock['type'] === 'Paragraph' 1329 | and ! isset($CurrentBlock['interrupted']) 1330 | ) { 1331 | return; 1332 | } 1333 | 1334 | $Block['element']['attributes'] = array('start' => $listStart); 1335 | } 1336 | } 1337 | 1338 | $Block['li'] = array( 1339 | 'name' => 'li', 1340 | 'handler' => array( 1341 | 'function' => 'li', 1342 | 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), 1343 | 'destination' => 'elements' 1344 | ) 1345 | ); 1346 | 1347 | $Block['element']['elements'] []= & $Block['li']; 1348 | 1349 | return $Block; 1350 | } 1351 | } 1352 | 1353 | protected function blockListContinue($Line, array $Block) 1354 | { 1355 | if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) 1356 | { 1357 | return null; 1358 | } 1359 | 1360 | $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); 1361 | 1362 | if ($Line['indent'] < $requiredIndent 1363 | and ( 1364 | ( 1365 | $Block['data']['type'] === 'ol' 1366 | and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 1367 | ) or ( 1368 | $Block['data']['type'] === 'ul' 1369 | and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 1370 | ) 1371 | ) 1372 | ) { 1373 | if (isset($Block['interrupted'])) 1374 | { 1375 | $Block['li']['handler']['argument'] []= ''; 1376 | 1377 | $Block['loose'] = true; 1378 | 1379 | unset($Block['interrupted']); 1380 | } 1381 | 1382 | unset($Block['li']); 1383 | 1384 | $text = isset($matches[1]) ? $matches[1] : ''; 1385 | 1386 | $Block['indent'] = $Line['indent']; 1387 | 1388 | $Block['li'] = array( 1389 | 'name' => 'li', 1390 | 'handler' => array( 1391 | 'function' => 'li', 1392 | 'argument' => array($text), 1393 | 'destination' => 'elements' 1394 | ) 1395 | ); 1396 | 1397 | $Block['element']['elements'] []= & $Block['li']; 1398 | 1399 | return $Block; 1400 | } 1401 | elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) 1402 | { 1403 | return null; 1404 | } 1405 | 1406 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) 1407 | { 1408 | return $Block; 1409 | } 1410 | 1411 | if ($Line['indent'] >= $requiredIndent) 1412 | { 1413 | if (isset($Block['interrupted'])) 1414 | { 1415 | $Block['li']['handler']['argument'] []= ''; 1416 | 1417 | $Block['loose'] = true; 1418 | 1419 | unset($Block['interrupted']); 1420 | } 1421 | 1422 | $text = substr($Line['body'], $requiredIndent); 1423 | 1424 | $Block['li']['handler']['argument'] []= $text; 1425 | 1426 | return $Block; 1427 | } 1428 | 1429 | if ( ! isset($Block['interrupted'])) 1430 | { 1431 | $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); 1432 | 1433 | $Block['li']['handler']['argument'] []= $text; 1434 | 1435 | return $Block; 1436 | } 1437 | } 1438 | 1439 | protected function blockListComplete(array $Block) 1440 | { 1441 | if (isset($Block['loose'])) 1442 | { 1443 | foreach ($Block['element']['elements'] as &$li) 1444 | { 1445 | if (end($li['handler']['argument']) !== '') 1446 | { 1447 | $li['handler']['argument'] []= ''; 1448 | } 1449 | } 1450 | } 1451 | 1452 | return $Block; 1453 | } 1454 | 1455 | # 1456 | # Quote 1457 | 1458 | protected function blockQuote($Line) 1459 | { 1460 | if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 1461 | { 1462 | $Block = array( 1463 | 'element' => array( 1464 | 'name' => 'blockquote', 1465 | 'handler' => array( 1466 | 'function' => 'linesElements', 1467 | 'argument' => (array) $matches[1], 1468 | 'destination' => 'elements', 1469 | ) 1470 | ), 1471 | ); 1472 | 1473 | return $Block; 1474 | } 1475 | } 1476 | 1477 | protected function blockQuoteContinue($Line, array $Block) 1478 | { 1479 | if (isset($Block['interrupted'])) 1480 | { 1481 | return; 1482 | } 1483 | 1484 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 1485 | { 1486 | $Block['element']['handler']['argument'] []= $matches[1]; 1487 | 1488 | return $Block; 1489 | } 1490 | 1491 | if ( ! isset($Block['interrupted'])) 1492 | { 1493 | $Block['element']['handler']['argument'] []= $Line['text']; 1494 | 1495 | return $Block; 1496 | } 1497 | } 1498 | 1499 | # 1500 | # Rule 1501 | 1502 | protected function blockRule($Line) 1503 | { 1504 | $marker = $Line['text'][0]; 1505 | 1506 | if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') 1507 | { 1508 | $Block = array( 1509 | 'element' => array( 1510 | 'name' => 'hr', 1511 | ), 1512 | ); 1513 | 1514 | return $Block; 1515 | } 1516 | } 1517 | 1518 | # 1519 | # Setext 1520 | 1521 | protected function blockSetextHeader($Line, array $Block = null) 1522 | { 1523 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 1524 | { 1525 | return; 1526 | } 1527 | 1528 | if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') 1529 | { 1530 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 1531 | 1532 | return $Block; 1533 | } 1534 | } 1535 | 1536 | # 1537 | # Markup 1538 | 1539 | protected function blockMarkup($Line) 1540 | { 1541 | if ($this->markupEscaped or $this->safeMode) 1542 | { 1543 | return; 1544 | } 1545 | 1546 | if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) 1547 | { 1548 | $element = strtolower($matches[1]); 1549 | 1550 | if (in_array($element, $this->textLevelElements)) 1551 | { 1552 | return; 1553 | } 1554 | 1555 | $Block = array( 1556 | 'name' => $matches[1], 1557 | 'element' => array( 1558 | 'rawHtml' => $Line['text'], 1559 | 'autobreak' => true, 1560 | ), 1561 | ); 1562 | 1563 | return $Block; 1564 | } 1565 | } 1566 | 1567 | protected function blockMarkupContinue($Line, array $Block) 1568 | { 1569 | if (isset($Block['closed']) or isset($Block['interrupted'])) 1570 | { 1571 | return; 1572 | } 1573 | 1574 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 1575 | 1576 | return $Block; 1577 | } 1578 | 1579 | # 1580 | # Reference 1581 | 1582 | protected function blockReference($Line) 1583 | { 1584 | if (strpos($Line['text'], ']') !== false 1585 | and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) 1586 | ) { 1587 | $id = strtolower($matches[1]); 1588 | 1589 | $Data = array( 1590 | 'url' => $matches[2], 1591 | 'title' => isset($matches[3]) ? $matches[3] : null, 1592 | ); 1593 | 1594 | $this->DefinitionData['Reference'][$id] = $Data; 1595 | 1596 | $Block = array( 1597 | 'element' => array(), 1598 | ); 1599 | 1600 | return $Block; 1601 | } 1602 | } 1603 | 1604 | # 1605 | # Table 1606 | 1607 | protected function blockTable($Line, array $Block = null) 1608 | { 1609 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 1610 | { 1611 | return; 1612 | } 1613 | 1614 | if ( 1615 | strpos($Block['element']['handler']['argument'], '|') === false 1616 | and strpos($Line['text'], '|') === false 1617 | and strpos($Line['text'], ':') === false 1618 | or strpos($Block['element']['handler']['argument'], "\n") !== false 1619 | ) { 1620 | return; 1621 | } 1622 | 1623 | if (chop($Line['text'], ' -:|') !== '') 1624 | { 1625 | return; 1626 | } 1627 | 1628 | $alignments = array(); 1629 | 1630 | $divider = $Line['text']; 1631 | 1632 | $divider = trim($divider); 1633 | $divider = trim($divider, '|'); 1634 | 1635 | $dividerCells = explode('|', $divider); 1636 | 1637 | foreach ($dividerCells as $dividerCell) 1638 | { 1639 | $dividerCell = trim($dividerCell); 1640 | 1641 | if ($dividerCell === '') 1642 | { 1643 | return; 1644 | } 1645 | 1646 | $alignment = null; 1647 | 1648 | if ($dividerCell[0] === ':') 1649 | { 1650 | $alignment = 'left'; 1651 | } 1652 | 1653 | if (substr($dividerCell, - 1) === ':') 1654 | { 1655 | $alignment = $alignment === 'left' ? 'center' : 'right'; 1656 | } 1657 | 1658 | $alignments []= $alignment; 1659 | } 1660 | 1661 | # ~ 1662 | 1663 | $HeaderElements = array(); 1664 | 1665 | $header = $Block['element']['handler']['argument']; 1666 | 1667 | $header = trim($header); 1668 | $header = trim($header, '|'); 1669 | 1670 | $headerCells = explode('|', $header); 1671 | 1672 | if (count($headerCells) !== count($alignments)) 1673 | { 1674 | return; 1675 | } 1676 | 1677 | foreach ($headerCells as $index => $headerCell) 1678 | { 1679 | $headerCell = trim($headerCell); 1680 | 1681 | $HeaderElement = array( 1682 | 'name' => 'th', 1683 | 'handler' => array( 1684 | 'function' => 'lineElements', 1685 | 'argument' => $headerCell, 1686 | 'destination' => 'elements', 1687 | ) 1688 | ); 1689 | 1690 | if (isset($alignments[$index])) 1691 | { 1692 | $alignment = $alignments[$index]; 1693 | 1694 | $HeaderElement['attributes'] = array( 1695 | 'style' => "text-align: $alignment;", 1696 | ); 1697 | } 1698 | 1699 | $HeaderElements []= $HeaderElement; 1700 | } 1701 | 1702 | # ~ 1703 | 1704 | $Block = array( 1705 | 'alignments' => $alignments, 1706 | 'identified' => true, 1707 | 'element' => array( 1708 | 'name' => 'table', 1709 | 'elements' => array(), 1710 | ), 1711 | ); 1712 | 1713 | $Block['element']['elements'] []= array( 1714 | 'name' => 'thead', 1715 | ); 1716 | 1717 | $Block['element']['elements'] []= array( 1718 | 'name' => 'tbody', 1719 | 'elements' => array(), 1720 | ); 1721 | 1722 | $Block['element']['elements'][0]['elements'] []= array( 1723 | 'name' => 'tr', 1724 | 'elements' => $HeaderElements, 1725 | ); 1726 | 1727 | return $Block; 1728 | } 1729 | 1730 | protected function blockTableContinue($Line, array $Block) 1731 | { 1732 | if (isset($Block['interrupted'])) 1733 | { 1734 | return; 1735 | } 1736 | 1737 | if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) 1738 | { 1739 | $Elements = array(); 1740 | 1741 | $row = $Line['text']; 1742 | 1743 | $row = trim($row); 1744 | $row = trim($row, '|'); 1745 | 1746 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); 1747 | 1748 | $cells = array_slice($matches[0], 0, count($Block['alignments'])); 1749 | 1750 | foreach ($cells as $index => $cell) 1751 | { 1752 | $cell = trim($cell); 1753 | 1754 | $Element = array( 1755 | 'name' => 'td', 1756 | 'handler' => array( 1757 | 'function' => 'lineElements', 1758 | 'argument' => $cell, 1759 | 'destination' => 'elements', 1760 | ) 1761 | ); 1762 | 1763 | if (isset($Block['alignments'][$index])) 1764 | { 1765 | $Element['attributes'] = array( 1766 | 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', 1767 | ); 1768 | } 1769 | 1770 | $Elements []= $Element; 1771 | } 1772 | 1773 | $Element = array( 1774 | 'name' => 'tr', 1775 | 'elements' => $Elements, 1776 | ); 1777 | 1778 | $Block['element']['elements'][1]['elements'] []= $Element; 1779 | 1780 | return $Block; 1781 | } 1782 | } 1783 | 1784 | # 1785 | # ~ 1786 | # 1787 | 1788 | protected function paragraph($Line) 1789 | { 1790 | return array( 1791 | 'type' => 'Paragraph', 1792 | 'element' => array( 1793 | 'name' => 'p', 1794 | 'handler' => array( 1795 | 'function' => 'lineElements', 1796 | 'argument' => $Line['text'], 1797 | 'destination' => 'elements', 1798 | ), 1799 | ), 1800 | ); 1801 | } 1802 | 1803 | protected function paragraphContinue($Line, array $Block) 1804 | { 1805 | if (isset($Block['interrupted'])) 1806 | { 1807 | return; 1808 | } 1809 | 1810 | $Block['element']['handler']['argument'] .= "\n".$Line['text']; 1811 | 1812 | return $Block; 1813 | } 1814 | 1815 | # 1816 | # Inline Elements 1817 | # 1818 | 1819 | protected $InlineTypes = array( 1820 | '!' => array('Image'), 1821 | '&' => array('SpecialCharacter'), 1822 | '*' => array('Emphasis'), 1823 | ':' => array('Url'), 1824 | '<' => array('UrlTag', 'EmailTag', 'Markup'), 1825 | '[' => array('Link'), 1826 | '_' => array('Emphasis'), 1827 | '`' => array('Code'), 1828 | '~' => array('Strikethrough'), 1829 | '\\' => array('EscapeSequence'), 1830 | ); 1831 | 1832 | # ~ 1833 | 1834 | protected $inlineMarkerList = '!*_&[:<`~\\'; 1835 | 1836 | # 1837 | # ~ 1838 | # 1839 | 1840 | public function line($text, $nonNestables = array()) 1841 | { 1842 | return $this->elements($this->lineElements($text, $nonNestables)); 1843 | } 1844 | 1845 | protected function lineElements($text, $nonNestables = array()) 1846 | { 1847 | $Elements = array(); 1848 | 1849 | $nonNestables = (empty($nonNestables) 1850 | ? array() 1851 | : array_combine($nonNestables, $nonNestables) 1852 | ); 1853 | 1854 | # $excerpt is based on the first occurrence of a marker 1855 | 1856 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1857 | { 1858 | $marker = $excerpt[0]; 1859 | 1860 | $markerPosition = strlen($text) - strlen($excerpt); 1861 | 1862 | $Excerpt = array('text' => $excerpt, 'context' => $text); 1863 | 1864 | foreach ($this->InlineTypes[$marker] as $inlineType) 1865 | { 1866 | # check to see if the current inline type is nestable in the current context 1867 | 1868 | if (isset($nonNestables[$inlineType])) 1869 | { 1870 | continue; 1871 | } 1872 | 1873 | $Inline = $this->{"inline$inlineType"}($Excerpt); 1874 | 1875 | if ( ! isset($Inline)) 1876 | { 1877 | continue; 1878 | } 1879 | 1880 | # makes sure that the inline belongs to "our" marker 1881 | 1882 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1883 | { 1884 | continue; 1885 | } 1886 | 1887 | # sets a default inline position 1888 | 1889 | if ( ! isset($Inline['position'])) 1890 | { 1891 | $Inline['position'] = $markerPosition; 1892 | } 1893 | 1894 | # cause the new element to 'inherit' our non nestables 1895 | 1896 | 1897 | $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) 1898 | ? array_merge($Inline['element']['nonNestables'], $nonNestables) 1899 | : $nonNestables 1900 | ; 1901 | 1902 | # the text that comes before the inline 1903 | $unmarkedText = substr($text, 0, $Inline['position']); 1904 | 1905 | # compile the unmarked text 1906 | $InlineText = $this->inlineText($unmarkedText); 1907 | $Elements[] = $InlineText['element']; 1908 | 1909 | # compile the inline 1910 | $Elements[] = $this->extractElement($Inline); 1911 | 1912 | # remove the examined text 1913 | $text = substr($text, $Inline['position'] + $Inline['extent']); 1914 | 1915 | continue 2; 1916 | } 1917 | 1918 | # the marker does not belong to an inline 1919 | 1920 | $unmarkedText = substr($text, 0, $markerPosition + 1); 1921 | 1922 | $InlineText = $this->inlineText($unmarkedText); 1923 | $Elements[] = $InlineText['element']; 1924 | 1925 | $text = substr($text, $markerPosition + 1); 1926 | } 1927 | 1928 | $InlineText = $this->inlineText($text); 1929 | $Elements[] = $InlineText['element']; 1930 | 1931 | foreach ($Elements as &$Element) 1932 | { 1933 | if ( ! isset($Element['autobreak'])) 1934 | { 1935 | $Element['autobreak'] = false; 1936 | } 1937 | } 1938 | 1939 | return $Elements; 1940 | } 1941 | 1942 | # 1943 | # ~ 1944 | # 1945 | 1946 | protected function inlineText($text) 1947 | { 1948 | $Inline = array( 1949 | 'extent' => strlen($text), 1950 | 'element' => array(), 1951 | ); 1952 | 1953 | $Inline['element']['elements'] = self::pregReplaceElements( 1954 | $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', 1955 | array( 1956 | array('name' => 'br'), 1957 | array('text' => "\n"), 1958 | ), 1959 | $text 1960 | ); 1961 | 1962 | return $Inline; 1963 | } 1964 | 1965 | protected function inlineCode($Excerpt) 1966 | { 1967 | $marker = $Excerpt['text'][0]; 1968 | 1969 | if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), 1976 | 'element' => array( 1977 | 'name' => 'code', 1978 | 'text' => $text, 1979 | ), 1980 | ); 1981 | } 1982 | } 1983 | 1984 | protected function inlineEmailTag($Excerpt) 1985 | { 1986 | $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; 1987 | 1988 | $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' 1989 | . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; 1990 | 1991 | if (strpos($Excerpt['text'], '>') !== false 1992 | and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) 1993 | ){ 1994 | $url = $matches[1]; 1995 | 1996 | if ( ! isset($matches[2])) 1997 | { 1998 | $url = "mailto:$url"; 1999 | } 2000 | 2001 | return array( 2002 | 'extent' => strlen($matches[0]), 2003 | 'element' => array( 2004 | 'name' => 'a', 2005 | 'text' => $matches[1], 2006 | 'attributes' => array( 2007 | 'href' => $url, 2008 | ), 2009 | ), 2010 | ); 2011 | } 2012 | } 2013 | 2014 | protected function inlineEmphasis($Excerpt) 2015 | { 2016 | if ( ! isset($Excerpt['text'][1])) 2017 | { 2018 | return; 2019 | } 2020 | 2021 | $marker = $Excerpt['text'][0]; 2022 | 2023 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 2024 | { 2025 | $emphasis = 'strong'; 2026 | } 2027 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 2028 | { 2029 | $emphasis = 'em'; 2030 | } 2031 | else 2032 | { 2033 | return; 2034 | } 2035 | 2036 | return array( 2037 | 'extent' => strlen($matches[0]), 2038 | 'element' => array( 2039 | 'name' => $emphasis, 2040 | 'handler' => array( 2041 | 'function' => 'lineElements', 2042 | 'argument' => $matches[1], 2043 | 'destination' => 'elements', 2044 | ) 2045 | ), 2046 | ); 2047 | } 2048 | 2049 | protected function inlineEscapeSequence($Excerpt) 2050 | { 2051 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 2052 | { 2053 | return array( 2054 | 'element' => array('rawHtml' => $Excerpt['text'][1]), 2055 | 'extent' => 2, 2056 | ); 2057 | } 2058 | } 2059 | 2060 | protected function inlineImage($Excerpt) 2061 | { 2062 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 2063 | { 2064 | return; 2065 | } 2066 | 2067 | $Excerpt['text']= substr($Excerpt['text'], 1); 2068 | 2069 | $Link = $this->inlineLink($Excerpt); 2070 | 2071 | if ($Link === null) 2072 | { 2073 | return; 2074 | } 2075 | 2076 | $Inline = array( 2077 | 'extent' => $Link['extent'] + 1, 2078 | 'element' => array( 2079 | 'name' => 'img', 2080 | 'attributes' => array( 2081 | 'src' => $Link['element']['attributes']['href'], 2082 | 'alt' => $Link['element']['handler']['argument'], 2083 | ), 2084 | 'autobreak' => true, 2085 | ), 2086 | ); 2087 | 2088 | $Inline['element']['attributes'] += $Link['element']['attributes']; 2089 | 2090 | unset($Inline['element']['attributes']['href']); 2091 | 2092 | return $Inline; 2093 | } 2094 | 2095 | protected function inlineLink($Excerpt) 2096 | { 2097 | $Element = array( 2098 | 'name' => 'a', 2099 | 'handler' => array( 2100 | 'function' => 'lineElements', 2101 | 'argument' => null, 2102 | 'destination' => 'elements', 2103 | ), 2104 | 'nonNestables' => array('Url', 'Link'), 2105 | 'attributes' => array( 2106 | 'href' => null, 2107 | 'title' => null, 2108 | ), 2109 | ); 2110 | 2111 | $extent = 0; 2112 | 2113 | $remainder = $Excerpt['text']; 2114 | 2115 | if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 2116 | { 2117 | $Element['handler']['argument'] = $matches[1]; 2118 | 2119 | $extent += strlen($matches[0]); 2120 | 2121 | $remainder = substr($remainder, $extent); 2122 | } 2123 | else 2124 | { 2125 | return; 2126 | } 2127 | 2128 | if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) 2129 | { 2130 | $Element['attributes']['href'] = $matches[1]; 2131 | 2132 | if (isset($matches[2])) 2133 | { 2134 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); 2135 | } 2136 | 2137 | $extent += strlen($matches[0]); 2138 | } 2139 | else 2140 | { 2141 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 2142 | { 2143 | $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; 2144 | $definition = strtolower($definition); 2145 | 2146 | $extent += strlen($matches[0]); 2147 | } 2148 | else 2149 | { 2150 | $definition = strtolower($Element['handler']['argument']); 2151 | } 2152 | 2153 | if ( ! isset($this->DefinitionData['Reference'][$definition])) 2154 | { 2155 | return; 2156 | } 2157 | 2158 | $Definition = $this->DefinitionData['Reference'][$definition]; 2159 | 2160 | $Element['attributes']['href'] = $Definition['url']; 2161 | $Element['attributes']['title'] = $Definition['title']; 2162 | } 2163 | 2164 | return array( 2165 | 'extent' => $extent, 2166 | 'element' => $Element, 2167 | ); 2168 | } 2169 | 2170 | protected function inlineMarkup($Excerpt) 2171 | { 2172 | if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) 2173 | { 2174 | return; 2175 | } 2176 | 2177 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) 2178 | { 2179 | return array( 2180 | 'element' => array('rawHtml' => $matches[0]), 2181 | 'extent' => strlen($matches[0]), 2182 | ); 2183 | } 2184 | 2185 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) 2186 | { 2187 | return array( 2188 | 'element' => array('rawHtml' => $matches[0]), 2189 | 'extent' => strlen($matches[0]), 2190 | ); 2191 | } 2192 | 2193 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) 2194 | { 2195 | return array( 2196 | 'element' => array('rawHtml' => $matches[0]), 2197 | 'extent' => strlen($matches[0]), 2198 | ); 2199 | } 2200 | } 2201 | 2202 | protected function inlineSpecialCharacter($Excerpt) 2203 | { 2204 | if ($Excerpt['text'][1] !== ' ' and strpos($Excerpt['text'], ';') !== false 2205 | and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) 2206 | ) { 2207 | return array( 2208 | 'element' => array('rawHtml' => '&' . $matches[1] . ';'), 2209 | 'extent' => strlen($matches[0]), 2210 | ); 2211 | } 2212 | 2213 | return; 2214 | } 2215 | 2216 | protected function inlineStrikethrough($Excerpt) 2217 | { 2218 | if ( ! isset($Excerpt['text'][1])) 2219 | { 2220 | return; 2221 | } 2222 | 2223 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 2224 | { 2225 | return array( 2226 | 'extent' => strlen($matches[0]), 2227 | 'element' => array( 2228 | 'name' => 'del', 2229 | 'handler' => array( 2230 | 'function' => 'lineElements', 2231 | 'argument' => $matches[1], 2232 | 'destination' => 'elements', 2233 | ) 2234 | ), 2235 | ); 2236 | } 2237 | } 2238 | 2239 | protected function inlineUrl($Excerpt) 2240 | { 2241 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 2242 | { 2243 | return; 2244 | } 2245 | 2246 | if (strpos($Excerpt['context'], 'http') !== false 2247 | and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) 2248 | ) { 2249 | $url = $matches[0][0]; 2250 | 2251 | $Inline = array( 2252 | 'extent' => strlen($matches[0][0]), 2253 | 'position' => $matches[0][1], 2254 | 'element' => array( 2255 | 'name' => 'a', 2256 | 'text' => $url, 2257 | 'attributes' => array( 2258 | 'href' => $url, 2259 | ), 2260 | ), 2261 | ); 2262 | 2263 | return $Inline; 2264 | } 2265 | } 2266 | 2267 | protected function inlineUrlTag($Excerpt) 2268 | { 2269 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) 2270 | { 2271 | $url = $matches[1]; 2272 | 2273 | return array( 2274 | 'extent' => strlen($matches[0]), 2275 | 'element' => array( 2276 | 'name' => 'a', 2277 | 'text' => $url, 2278 | 'attributes' => array( 2279 | 'href' => $url, 2280 | ), 2281 | ), 2282 | ); 2283 | } 2284 | } 2285 | 2286 | # ~ 2287 | 2288 | protected function unmarkedText($text) 2289 | { 2290 | $Inline = $this->inlineText($text); 2291 | return $this->element($Inline['element']); 2292 | } 2293 | 2294 | # 2295 | # Handlers 2296 | # 2297 | 2298 | protected function handle(array $Element) 2299 | { 2300 | if (isset($Element['handler'])) 2301 | { 2302 | if (!isset($Element['nonNestables'])) 2303 | { 2304 | $Element['nonNestables'] = array(); 2305 | } 2306 | 2307 | if (is_string($Element['handler'])) 2308 | { 2309 | $function = $Element['handler']; 2310 | $argument = $Element['text']; 2311 | unset($Element['text']); 2312 | $destination = 'rawHtml'; 2313 | } 2314 | else 2315 | { 2316 | $function = $Element['handler']['function']; 2317 | $argument = $Element['handler']['argument']; 2318 | $destination = $Element['handler']['destination']; 2319 | } 2320 | 2321 | $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); 2322 | 2323 | if ($destination === 'handler') 2324 | { 2325 | $Element = $this->handle($Element); 2326 | } 2327 | 2328 | unset($Element['handler']); 2329 | } 2330 | 2331 | return $Element; 2332 | } 2333 | 2334 | protected function handleElementRecursive(array $Element) 2335 | { 2336 | return $this->elementApplyRecursive(array($this, 'handle'), $Element); 2337 | } 2338 | 2339 | protected function handleElementsRecursive(array $Elements) 2340 | { 2341 | return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); 2342 | } 2343 | 2344 | protected function elementApplyRecursive($closure, array $Element) 2345 | { 2346 | $Element = call_user_func($closure, $Element); 2347 | 2348 | if (isset($Element['elements'])) 2349 | { 2350 | $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); 2351 | } 2352 | elseif (isset($Element['element'])) 2353 | { 2354 | $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); 2355 | } 2356 | 2357 | return $Element; 2358 | } 2359 | 2360 | protected function elementApplyRecursiveDepthFirst($closure, array $Element) 2361 | { 2362 | if (isset($Element['elements'])) 2363 | { 2364 | $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); 2365 | } 2366 | elseif (isset($Element['element'])) 2367 | { 2368 | $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); 2369 | } 2370 | 2371 | $Element = call_user_func($closure, $Element); 2372 | 2373 | return $Element; 2374 | } 2375 | 2376 | protected function elementsApplyRecursive($closure, array $Elements) 2377 | { 2378 | foreach ($Elements as &$Element) 2379 | { 2380 | $Element = $this->elementApplyRecursive($closure, $Element); 2381 | } 2382 | 2383 | return $Elements; 2384 | } 2385 | 2386 | protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) 2387 | { 2388 | foreach ($Elements as &$Element) 2389 | { 2390 | $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); 2391 | } 2392 | 2393 | return $Elements; 2394 | } 2395 | 2396 | protected function element(array $Element) 2397 | { 2398 | if ($this->safeMode) 2399 | { 2400 | $Element = $this->sanitiseElement($Element); 2401 | } 2402 | 2403 | # identity map if element has no handler 2404 | $Element = $this->handle($Element); 2405 | 2406 | $hasName = isset($Element['name']); 2407 | 2408 | $markup = ''; 2409 | 2410 | if ($hasName) 2411 | { 2412 | $markup .= '<' . $Element['name']; 2413 | 2414 | if (isset($Element['attributes'])) 2415 | { 2416 | foreach ($Element['attributes'] as $name => $value) 2417 | { 2418 | if ($value === null) 2419 | { 2420 | continue; 2421 | } 2422 | 2423 | $markup .= " $name=\"".self::escape($value).'"'; 2424 | } 2425 | } 2426 | } 2427 | 2428 | $permitRawHtml = false; 2429 | 2430 | if (isset($Element['text'])) 2431 | { 2432 | $text = $Element['text']; 2433 | } 2434 | // very strongly consider an alternative if you're writing an 2435 | // extension 2436 | elseif (isset($Element['rawHtml'])) 2437 | { 2438 | $text = $Element['rawHtml']; 2439 | 2440 | $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; 2441 | $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; 2442 | } 2443 | 2444 | $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); 2445 | 2446 | if ($hasContent) 2447 | { 2448 | $markup .= $hasName ? '>' : ''; 2449 | 2450 | if (isset($Element['elements'])) 2451 | { 2452 | $markup .= $this->elements($Element['elements']); 2453 | } 2454 | elseif (isset($Element['element'])) 2455 | { 2456 | $markup .= $this->element($Element['element']); 2457 | } 2458 | else 2459 | { 2460 | if (!$permitRawHtml) 2461 | { 2462 | $markup .= self::escape($text, true); 2463 | } 2464 | else 2465 | { 2466 | $markup .= $text; 2467 | } 2468 | } 2469 | 2470 | $markup .= $hasName ? '' : ''; 2471 | } 2472 | elseif ($hasName) 2473 | { 2474 | $markup .= ' />'; 2475 | } 2476 | 2477 | return $markup; 2478 | } 2479 | 2480 | protected function elements(array $Elements) 2481 | { 2482 | $markup = ''; 2483 | 2484 | $autoBreak = true; 2485 | 2486 | foreach ($Elements as $Element) 2487 | { 2488 | if (empty($Element)) 2489 | { 2490 | continue; 2491 | } 2492 | 2493 | $autoBreakNext = (isset($Element['autobreak']) 2494 | ? $Element['autobreak'] : isset($Element['name']) 2495 | ); 2496 | // (autobreak === false) covers both sides of an element 2497 | $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; 2498 | 2499 | $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); 2500 | $autoBreak = $autoBreakNext; 2501 | } 2502 | 2503 | $markup .= $autoBreak ? "\n" : ''; 2504 | 2505 | return $markup; 2506 | } 2507 | 2508 | # ~ 2509 | 2510 | protected function li($lines) 2511 | { 2512 | $Elements = $this->linesElements($lines); 2513 | 2514 | if ( ! in_array('', $lines) 2515 | and isset($Elements[0]) and isset($Elements[0]['name']) 2516 | and $Elements[0]['name'] === 'p' 2517 | ) { 2518 | unset($Elements[0]['name']); 2519 | } 2520 | 2521 | return $Elements; 2522 | } 2523 | 2524 | # 2525 | # AST Convenience 2526 | # 2527 | 2528 | /** 2529 | * Replace occurrences $regexp with $Elements in $text. Return an array of 2530 | * elements representing the replacement. 2531 | */ 2532 | protected static function pregReplaceElements($regexp, $Elements, $text) 2533 | { 2534 | $newElements = array(); 2535 | 2536 | while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) 2537 | { 2538 | $offset = $matches[0][1]; 2539 | $before = substr($text, 0, $offset); 2540 | $after = substr($text, $offset + strlen($matches[0][0])); 2541 | 2542 | $newElements[] = array('text' => $before); 2543 | 2544 | foreach ($Elements as $Element) 2545 | { 2546 | $newElements[] = $Element; 2547 | } 2548 | 2549 | $text = $after; 2550 | } 2551 | 2552 | $newElements[] = array('text' => $text); 2553 | 2554 | return $newElements; 2555 | } 2556 | 2557 | # 2558 | # Deprecated Methods 2559 | # 2560 | 2561 | function parse($text) 2562 | { 2563 | $markup = $this->text($text); 2564 | 2565 | return $markup; 2566 | } 2567 | 2568 | protected function sanitiseElement(array $Element) 2569 | { 2570 | static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; 2571 | static $safeUrlNameToAtt = array( 2572 | 'a' => 'href', 2573 | 'img' => 'src', 2574 | ); 2575 | 2576 | if ( ! isset($Element['name'])) 2577 | { 2578 | unset($Element['attributes']); 2579 | return $Element; 2580 | } 2581 | 2582 | if (isset($safeUrlNameToAtt[$Element['name']])) 2583 | { 2584 | $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); 2585 | } 2586 | 2587 | if ( ! empty($Element['attributes'])) 2588 | { 2589 | foreach ($Element['attributes'] as $att => $val) 2590 | { 2591 | # filter out badly parsed attribute 2592 | if ( ! preg_match($goodAttribute, $att)) 2593 | { 2594 | unset($Element['attributes'][$att]); 2595 | } 2596 | # dump onevent attribute 2597 | elseif (self::striAtStart($att, 'on')) 2598 | { 2599 | unset($Element['attributes'][$att]); 2600 | } 2601 | } 2602 | } 2603 | 2604 | return $Element; 2605 | } 2606 | 2607 | protected function filterUnsafeUrlInAttribute(array $Element, $attribute) 2608 | { 2609 | foreach ($this->safeLinksWhitelist as $scheme) 2610 | { 2611 | if (self::striAtStart($Element['attributes'][$attribute], $scheme)) 2612 | { 2613 | return $Element; 2614 | } 2615 | } 2616 | 2617 | $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); 2618 | 2619 | return $Element; 2620 | } 2621 | 2622 | # 2623 | # Static Methods 2624 | # 2625 | 2626 | protected static function escape($text, $allowQuotes = false) 2627 | { 2628 | return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); 2629 | } 2630 | 2631 | protected static function striAtStart($string, $needle) 2632 | { 2633 | $len = strlen($needle); 2634 | 2635 | if ($len > strlen($string)) 2636 | { 2637 | return false; 2638 | } 2639 | else 2640 | { 2641 | return strtolower(substr($string, 0, $len)) === strtolower($needle); 2642 | } 2643 | } 2644 | 2645 | static function instance($name = 'default') 2646 | { 2647 | if (isset(self::$instances[$name])) 2648 | { 2649 | return self::$instances[$name]; 2650 | } 2651 | 2652 | $instance = new static(); 2653 | 2654 | self::$instances[$name] = $instance; 2655 | 2656 | return $instance; 2657 | } 2658 | 2659 | private static $instances = array(); 2660 | 2661 | # 2662 | # Fields 2663 | # 2664 | 2665 | protected $DefinitionData; 2666 | 2667 | # 2668 | # Read-Only 2669 | 2670 | protected $specialCharacters = array( 2671 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' 2672 | ); 2673 | 2674 | protected $StrongRegex = array( 2675 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', 2676 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', 2677 | ); 2678 | 2679 | protected $EmRegex = array( 2680 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 2681 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 2682 | ); 2683 | 2684 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; 2685 | 2686 | protected $voidElements = array( 2687 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 2688 | ); 2689 | 2690 | protected $textLevelElements = array( 2691 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 2692 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 2693 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', 2694 | 'q', 'rt', 'ins', 'font', 'strong', 2695 | 's', 'tt', 'kbd', 'mark', 2696 | 'u', 'xm', 'sub', 'nobr', 2697 | 'sup', 'ruby', 2698 | 'var', 'span', 2699 | 'wbr', 'time', 2700 | ); 2701 | } 2702 | --------------------------------------------------------------------------------