File indexing completed on 2024-12-22 05:37:18
0001 <?php 0002 0003 # 0004 # 0005 # Parsedown 0006 # http://parsedown.org 0007 # 0008 # (c) Emanuil Rusev 0009 # http://erusev.com 0010 # 0011 # For the full license information, view the LICENSE file that was distributed 0012 # with this source code. 0013 # 0014 # 0015 0016 class Parsedown 0017 { 0018 # ~ 0019 0020 const version = '1.8.0-beta-5'; 0021 0022 # ~ 0023 0024 function text($text) 0025 { 0026 $Elements = $this->textElements($text); 0027 0028 # convert to markup 0029 $markup = $this->elements($Elements); 0030 0031 # trim line breaks 0032 $markup = trim($markup, "\n"); 0033 0034 return $markup; 0035 } 0036 0037 protected function textElements($text) 0038 { 0039 # make sure no definitions are set 0040 $this->DefinitionData = array(); 0041 0042 # standardize line breaks 0043 $text = str_replace(array("\r\n", "\r"), "\n", $text); 0044 0045 # remove surrounding line breaks 0046 $text = trim($text, "\n"); 0047 0048 # split text into lines 0049 $lines = explode("\n", $text); 0050 0051 # iterate through lines to identify blocks 0052 return $this->linesElements($lines); 0053 } 0054 0055 # 0056 # Setters 0057 # 0058 0059 function setBreaksEnabled($breaksEnabled) 0060 { 0061 $this->breaksEnabled = $breaksEnabled; 0062 0063 return $this; 0064 } 0065 0066 protected $breaksEnabled; 0067 0068 function setMarkupEscaped($markupEscaped) 0069 { 0070 $this->markupEscaped = $markupEscaped; 0071 0072 return $this; 0073 } 0074 0075 protected $markupEscaped; 0076 0077 function setUrlsLinked($urlsLinked) 0078 { 0079 $this->urlsLinked = $urlsLinked; 0080 0081 return $this; 0082 } 0083 0084 protected $urlsLinked = true; 0085 0086 function setSafeMode($safeMode) 0087 { 0088 $this->safeMode = (bool) $safeMode; 0089 0090 return $this; 0091 } 0092 0093 protected $safeMode; 0094 0095 function setStrictMode($strictMode) 0096 { 0097 $this->strictMode = (bool) $strictMode; 0098 0099 return $this; 0100 } 0101 0102 protected $strictMode; 0103 0104 protected $safeLinksWhitelist = array( 0105 'http://', 0106 'https://', 0107 'ftp://', 0108 'ftps://', 0109 'mailto:', 0110 'tel:', 0111 'data:image/png;base64,', 0112 'data:image/gif;base64,', 0113 'data:image/jpeg;base64,', 0114 'irc:', 0115 'ircs:', 0116 'git:', 0117 'ssh:', 0118 'news:', 0119 'steam:', 0120 ); 0121 0122 # 0123 # Lines 0124 # 0125 0126 protected $BlockTypes = array( 0127 '#' => array('Header'), 0128 '*' => array('Rule', 'List'), 0129 '+' => array('List'), 0130 '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 0131 '0' => array('List'), 0132 '1' => array('List'), 0133 '2' => array('List'), 0134 '3' => array('List'), 0135 '4' => array('List'), 0136 '5' => array('List'), 0137 '6' => array('List'), 0138 '7' => array('List'), 0139 '8' => array('List'), 0140 '9' => array('List'), 0141 ':' => array('Table'), 0142 '<' => array('Comment', 'Markup'), 0143 '=' => array('SetextHeader'), 0144 '>' => array('Quote'), 0145 '[' => array('Reference'), 0146 '_' => array('Rule'), 0147 '`' => array('FencedCode'), 0148 '|' => array('Table'), 0149 '~' => array('FencedCode'), 0150 ); 0151 0152 # ~ 0153 0154 protected $unmarkedBlockTypes = array( 0155 'Code', 0156 ); 0157 0158 # 0159 # Blocks 0160 # 0161 0162 protected function lines(array $lines) 0163 { 0164 return $this->elements($this->linesElements($lines)); 0165 } 0166 0167 protected function linesElements(array $lines) 0168 { 0169 $Elements = array(); 0170 $CurrentBlock = null; 0171 0172 foreach ($lines as $line) 0173 { 0174 if (chop($line) === '') 0175 { 0176 if (isset($CurrentBlock)) 0177 { 0178 $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) 0179 ? $CurrentBlock['interrupted'] + 1 : 1 0180 ); 0181 } 0182 0183 continue; 0184 } 0185 0186 while (($beforeTab = strstr($line, "\t", true)) !== false) 0187 { 0188 $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; 0189 0190 $line = $beforeTab 0191 . str_repeat(' ', $shortage) 0192 . substr($line, strlen($beforeTab) + 1) 0193 ; 0194 } 0195 0196 $indent = strspn($line, ' '); 0197 0198 $text = $indent > 0 ? substr($line, $indent) : $line; 0199 0200 # ~ 0201 0202 $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 0203 0204 # ~ 0205 0206 if (isset($CurrentBlock['continuable'])) 0207 { 0208 $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; 0209 $Block = $this->$methodName($Line, $CurrentBlock); 0210 0211 if (isset($Block)) 0212 { 0213 $CurrentBlock = $Block; 0214 0215 continue; 0216 } 0217 else 0218 { 0219 if ($this->isBlockCompletable($CurrentBlock['type'])) 0220 { 0221 $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 0222 $CurrentBlock = $this->$methodName($CurrentBlock); 0223 } 0224 } 0225 } 0226 0227 # ~ 0228 0229 $marker = $text[0]; 0230 0231 # ~ 0232 0233 $blockTypes = $this->unmarkedBlockTypes; 0234 0235 if (isset($this->BlockTypes[$marker])) 0236 { 0237 foreach ($this->BlockTypes[$marker] as $blockType) 0238 { 0239 $blockTypes []= $blockType; 0240 } 0241 } 0242 0243 # 0244 # ~ 0245 0246 foreach ($blockTypes as $blockType) 0247 { 0248 $Block = $this->{"block$blockType"}($Line, $CurrentBlock); 0249 0250 if (isset($Block)) 0251 { 0252 $Block['type'] = $blockType; 0253 0254 if ( ! isset($Block['identified'])) 0255 { 0256 if (isset($CurrentBlock)) 0257 { 0258 $Elements[] = $this->extractElement($CurrentBlock); 0259 } 0260 0261 $Block['identified'] = true; 0262 } 0263 0264 if ($this->isBlockContinuable($blockType)) 0265 { 0266 $Block['continuable'] = true; 0267 } 0268 0269 $CurrentBlock = $Block; 0270 0271 continue 2; 0272 } 0273 } 0274 0275 # ~ 0276 0277 if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') 0278 { 0279 $Block = $this->paragraphContinue($Line, $CurrentBlock); 0280 } 0281 0282 if (isset($Block)) 0283 { 0284 $CurrentBlock = $Block; 0285 } 0286 else 0287 { 0288 if (isset($CurrentBlock)) 0289 { 0290 $Elements[] = $this->extractElement($CurrentBlock); 0291 } 0292 0293 $CurrentBlock = $this->paragraph($Line); 0294 0295 $CurrentBlock['identified'] = true; 0296 } 0297 } 0298 0299 # ~ 0300 0301 if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 0302 { 0303 $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 0304 $CurrentBlock = $this->$methodName($CurrentBlock); 0305 } 0306 0307 # ~ 0308 0309 if (isset($CurrentBlock)) 0310 { 0311 $Elements[] = $this->extractElement($CurrentBlock); 0312 } 0313 0314 # ~ 0315 0316 return $Elements; 0317 } 0318 0319 protected function extractElement(array $Component) 0320 { 0321 if ( ! isset($Component['element'])) 0322 { 0323 if (isset($Component['markup'])) 0324 { 0325 $Component['element'] = array('rawHtml' => $Component['markup']); 0326 } 0327 elseif (isset($Component['hidden'])) 0328 { 0329 $Component['element'] = array(); 0330 } 0331 } 0332 0333 return $Component['element']; 0334 } 0335 0336 protected function isBlockContinuable($Type) 0337 { 0338 return method_exists($this, 'block' . $Type . 'Continue'); 0339 } 0340 0341 protected function isBlockCompletable($Type) 0342 { 0343 return method_exists($this, 'block' . $Type . 'Complete'); 0344 } 0345 0346 # 0347 # Code 0348 0349 protected function blockCode($Line, $Block = null) 0350 { 0351 if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) 0352 { 0353 return; 0354 } 0355 0356 if ($Line['indent'] >= 4) 0357 { 0358 $text = substr($Line['body'], 4); 0359 0360 $Block = array( 0361 'element' => array( 0362 'name' => 'pre', 0363 'element' => array( 0364 'name' => 'code', 0365 'text' => $text, 0366 ), 0367 ), 0368 ); 0369 0370 return $Block; 0371 } 0372 } 0373 0374 protected function blockCodeContinue($Line, $Block) 0375 { 0376 if ($Line['indent'] >= 4) 0377 { 0378 if (isset($Block['interrupted'])) 0379 { 0380 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 0381 0382 unset($Block['interrupted']); 0383 } 0384 0385 $Block['element']['element']['text'] .= "\n"; 0386 0387 $text = substr($Line['body'], 4); 0388 0389 $Block['element']['element']['text'] .= $text; 0390 0391 return $Block; 0392 } 0393 } 0394 0395 protected function blockCodeComplete($Block) 0396 { 0397 return $Block; 0398 } 0399 0400 # 0401 # Comment 0402 0403 protected function blockComment($Line) 0404 { 0405 if ($this->markupEscaped or $this->safeMode) 0406 { 0407 return; 0408 } 0409 0410 if (strpos($Line['text'], '<!--') === 0) 0411 { 0412 $Block = array( 0413 'element' => array( 0414 'rawHtml' => $Line['body'], 0415 'autobreak' => true, 0416 ), 0417 ); 0418 0419 if (strpos($Line['text'], '-->') !== false) 0420 { 0421 $Block['closed'] = true; 0422 } 0423 0424 return $Block; 0425 } 0426 } 0427 0428 protected function blockCommentContinue($Line, array $Block) 0429 { 0430 if (isset($Block['closed'])) 0431 { 0432 return; 0433 } 0434 0435 $Block['element']['rawHtml'] .= "\n" . $Line['body']; 0436 0437 if (strpos($Line['text'], '-->') !== false) 0438 { 0439 $Block['closed'] = true; 0440 } 0441 0442 return $Block; 0443 } 0444 0445 # 0446 # Fenced Code 0447 0448 protected function blockFencedCode($Line) 0449 { 0450 $marker = $Line['text'][0]; 0451 0452 $openerLength = strspn($Line['text'], $marker); 0453 0454 if ($openerLength < 3) 0455 { 0456 return; 0457 } 0458 0459 $infostring = trim(substr($Line['text'], $openerLength), "\t "); 0460 0461 if (strpos($infostring, '`') !== false) 0462 { 0463 return; 0464 } 0465 0466 $Element = array( 0467 'name' => 'code', 0468 'text' => '', 0469 ); 0470 0471 if ($infostring !== '') 0472 { 0473 $Element['attributes'] = array('class' => "language-$infostring"); 0474 } 0475 0476 $Block = array( 0477 'char' => $marker, 0478 'openerLength' => $openerLength, 0479 'element' => array( 0480 'name' => 'pre', 0481 'element' => $Element, 0482 ), 0483 ); 0484 0485 return $Block; 0486 } 0487 0488 protected function blockFencedCodeContinue($Line, $Block) 0489 { 0490 if (isset($Block['complete'])) 0491 { 0492 return; 0493 } 0494 0495 if (isset($Block['interrupted'])) 0496 { 0497 $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 0498 0499 unset($Block['interrupted']); 0500 } 0501 0502 if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] 0503 and chop(substr($Line['text'], $len), ' ') === '' 0504 ) { 0505 $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); 0506 0507 $Block['complete'] = true; 0508 0509 return $Block; 0510 } 0511 0512 $Block['element']['element']['text'] .= "\n" . $Line['body']; 0513 0514 return $Block; 0515 } 0516 0517 protected function blockFencedCodeComplete($Block) 0518 { 0519 return $Block; 0520 } 0521 0522 # 0523 # Header 0524 0525 protected function blockHeader($Line) 0526 { 0527 $level = strspn($Line['text'], '#'); 0528 0529 if ($level > 6) 0530 { 0531 return; 0532 } 0533 0534 $text = trim($Line['text'], '#'); 0535 0536 if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') 0537 { 0538 return; 0539 } 0540 0541 $text = trim($text, ' '); 0542 0543 $Block = array( 0544 'element' => array( 0545 'name' => 'h' . $level, 0546 'handler' => array( 0547 'function' => 'lineElements', 0548 'argument' => $text, 0549 'destination' => 'elements', 0550 ) 0551 ), 0552 ); 0553 0554 return $Block; 0555 } 0556 0557 # 0558 # List 0559 0560 protected function blockList($Line, array $CurrentBlock = null) 0561 { 0562 list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); 0563 0564 if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) 0565 { 0566 $contentIndent = strlen($matches[2]); 0567 0568 if ($contentIndent >= 5) 0569 { 0570 $contentIndent -= 1; 0571 $matches[1] = substr($matches[1], 0, -$contentIndent); 0572 $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; 0573 } 0574 elseif ($contentIndent === 0) 0575 { 0576 $matches[1] .= ' '; 0577 } 0578 0579 $markerWithoutWhitespace = strstr($matches[1], ' ', true); 0580 0581 $Block = array( 0582 'indent' => $Line['indent'], 0583 'pattern' => $pattern, 0584 'data' => array( 0585 'type' => $name, 0586 'marker' => $matches[1], 0587 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), 0588 ), 0589 'element' => array( 0590 'name' => $name, 0591 'elements' => array(), 0592 ), 0593 ); 0594 $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); 0595 0596 if ($name === 'ol') 0597 { 0598 $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; 0599 0600 if ($listStart !== '1') 0601 { 0602 if ( 0603 isset($CurrentBlock) 0604 and $CurrentBlock['type'] === 'Paragraph' 0605 and ! isset($CurrentBlock['interrupted']) 0606 ) { 0607 return; 0608 } 0609 0610 $Block['element']['attributes'] = array('start' => $listStart); 0611 } 0612 } 0613 0614 $Block['li'] = array( 0615 'name' => 'li', 0616 'handler' => array( 0617 'function' => 'li', 0618 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), 0619 'destination' => 'elements' 0620 ) 0621 ); 0622 0623 $Block['element']['elements'] []= & $Block['li']; 0624 0625 return $Block; 0626 } 0627 } 0628 0629 protected function blockListContinue($Line, array $Block) 0630 { 0631 if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) 0632 { 0633 return null; 0634 } 0635 0636 $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); 0637 0638 if ($Line['indent'] < $requiredIndent 0639 and ( 0640 ( 0641 $Block['data']['type'] === 'ol' 0642 and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 0643 ) or ( 0644 $Block['data']['type'] === 'ul' 0645 and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 0646 ) 0647 ) 0648 ) { 0649 if (isset($Block['interrupted'])) 0650 { 0651 $Block['li']['handler']['argument'] []= ''; 0652 0653 $Block['loose'] = true; 0654 0655 unset($Block['interrupted']); 0656 } 0657 0658 unset($Block['li']); 0659 0660 $text = isset($matches[1]) ? $matches[1] : ''; 0661 0662 $Block['indent'] = $Line['indent']; 0663 0664 $Block['li'] = array( 0665 'name' => 'li', 0666 'handler' => array( 0667 'function' => 'li', 0668 'argument' => array($text), 0669 'destination' => 'elements' 0670 ) 0671 ); 0672 0673 $Block['element']['elements'] []= & $Block['li']; 0674 0675 return $Block; 0676 } 0677 elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) 0678 { 0679 return null; 0680 } 0681 0682 if ($Line['text'][0] === '[' and $this->blockReference($Line)) 0683 { 0684 return $Block; 0685 } 0686 0687 if ($Line['indent'] >= $requiredIndent) 0688 { 0689 if (isset($Block['interrupted'])) 0690 { 0691 $Block['li']['handler']['argument'] []= ''; 0692 0693 $Block['loose'] = true; 0694 0695 unset($Block['interrupted']); 0696 } 0697 0698 $text = substr($Line['body'], $requiredIndent); 0699 0700 $Block['li']['handler']['argument'] []= $text; 0701 0702 return $Block; 0703 } 0704 0705 if ( ! isset($Block['interrupted'])) 0706 { 0707 $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); 0708 0709 $Block['li']['handler']['argument'] []= $text; 0710 0711 return $Block; 0712 } 0713 } 0714 0715 protected function blockListComplete(array $Block) 0716 { 0717 if (isset($Block['loose'])) 0718 { 0719 foreach ($Block['element']['elements'] as &$li) 0720 { 0721 if (end($li['handler']['argument']) !== '') 0722 { 0723 $li['handler']['argument'] []= ''; 0724 } 0725 } 0726 } 0727 0728 return $Block; 0729 } 0730 0731 # 0732 # Quote 0733 0734 protected function blockQuote($Line) 0735 { 0736 if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 0737 { 0738 $Block = array( 0739 'element' => array( 0740 'name' => 'blockquote', 0741 'handler' => array( 0742 'function' => 'linesElements', 0743 'argument' => (array) $matches[1], 0744 'destination' => 'elements', 0745 ) 0746 ), 0747 ); 0748 0749 return $Block; 0750 } 0751 } 0752 0753 protected function blockQuoteContinue($Line, array $Block) 0754 { 0755 if (isset($Block['interrupted'])) 0756 { 0757 return; 0758 } 0759 0760 if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 0761 { 0762 $Block['element']['handler']['argument'] []= $matches[1]; 0763 0764 return $Block; 0765 } 0766 0767 if ( ! isset($Block['interrupted'])) 0768 { 0769 $Block['element']['handler']['argument'] []= $Line['text']; 0770 0771 return $Block; 0772 } 0773 } 0774 0775 # 0776 # Rule 0777 0778 protected function blockRule($Line) 0779 { 0780 $marker = $Line['text'][0]; 0781 0782 if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') 0783 { 0784 $Block = array( 0785 'element' => array( 0786 'name' => 'hr', 0787 ), 0788 ); 0789 0790 return $Block; 0791 } 0792 } 0793 0794 # 0795 # Setext 0796 0797 protected function blockSetextHeader($Line, array $Block = null) 0798 { 0799 if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 0800 { 0801 return; 0802 } 0803 0804 if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') 0805 { 0806 $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 0807 0808 return $Block; 0809 } 0810 } 0811 0812 # 0813 # Markup 0814 0815 protected function blockMarkup($Line) 0816 { 0817 if ($this->markupEscaped or $this->safeMode) 0818 { 0819 return; 0820 } 0821 0822 if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) 0823 { 0824 $element = strtolower($matches[1]); 0825 0826 if (in_array($element, $this->textLevelElements)) 0827 { 0828 return; 0829 } 0830 0831 $Block = array( 0832 'name' => $matches[1], 0833 'element' => array( 0834 'rawHtml' => $Line['text'], 0835 'autobreak' => true, 0836 ), 0837 ); 0838 0839 return $Block; 0840 } 0841 } 0842 0843 protected function blockMarkupContinue($Line, array $Block) 0844 { 0845 if (isset($Block['closed']) or isset($Block['interrupted'])) 0846 { 0847 return; 0848 } 0849 0850 $Block['element']['rawHtml'] .= "\n" . $Line['body']; 0851 0852 return $Block; 0853 } 0854 0855 # 0856 # Reference 0857 0858 protected function blockReference($Line) 0859 { 0860 if (strpos($Line['text'], ']') !== false 0861 and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) 0862 ) { 0863 $id = strtolower($matches[1]); 0864 0865 $Data = array( 0866 'url' => $matches[2], 0867 'title' => isset($matches[3]) ? $matches[3] : null, 0868 ); 0869 0870 $this->DefinitionData['Reference'][$id] = $Data; 0871 0872 $Block = array( 0873 'element' => array(), 0874 ); 0875 0876 return $Block; 0877 } 0878 } 0879 0880 # 0881 # Table 0882 0883 protected function blockTable($Line, array $Block = null) 0884 { 0885 if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 0886 { 0887 return; 0888 } 0889 0890 if ( 0891 strpos($Block['element']['handler']['argument'], '|') === false 0892 and strpos($Line['text'], '|') === false 0893 and strpos($Line['text'], ':') === false 0894 or strpos($Block['element']['handler']['argument'], "\n") !== false 0895 ) { 0896 return; 0897 } 0898 0899 if (chop($Line['text'], ' -:|') !== '') 0900 { 0901 return; 0902 } 0903 0904 $alignments = array(); 0905 0906 $divider = $Line['text']; 0907 0908 $divider = trim($divider); 0909 $divider = trim($divider, '|'); 0910 0911 $dividerCells = explode('|', $divider); 0912 0913 foreach ($dividerCells as $dividerCell) 0914 { 0915 $dividerCell = trim($dividerCell); 0916 0917 if ($dividerCell === '') 0918 { 0919 return; 0920 } 0921 0922 $alignment = null; 0923 0924 if ($dividerCell[0] === ':') 0925 { 0926 $alignment = 'left'; 0927 } 0928 0929 if (substr($dividerCell, - 1) === ':') 0930 { 0931 $alignment = $alignment === 'left' ? 'center' : 'right'; 0932 } 0933 0934 $alignments []= $alignment; 0935 } 0936 0937 # ~ 0938 0939 $HeaderElements = array(); 0940 0941 $header = $Block['element']['handler']['argument']; 0942 0943 $header = trim($header); 0944 $header = trim($header, '|'); 0945 0946 $headerCells = explode('|', $header); 0947 0948 if (count($headerCells) !== count($alignments)) 0949 { 0950 return; 0951 } 0952 0953 foreach ($headerCells as $index => $headerCell) 0954 { 0955 $headerCell = trim($headerCell); 0956 0957 $HeaderElement = array( 0958 'name' => 'th', 0959 'handler' => array( 0960 'function' => 'lineElements', 0961 'argument' => $headerCell, 0962 'destination' => 'elements', 0963 ) 0964 ); 0965 0966 if (isset($alignments[$index])) 0967 { 0968 $alignment = $alignments[$index]; 0969 0970 $HeaderElement['attributes'] = array( 0971 'style' => "text-align: $alignment;", 0972 ); 0973 } 0974 0975 $HeaderElements []= $HeaderElement; 0976 } 0977 0978 # ~ 0979 0980 $Block = array( 0981 'alignments' => $alignments, 0982 'identified' => true, 0983 'element' => array( 0984 'name' => 'table', 0985 'elements' => array(), 0986 ), 0987 ); 0988 0989 $Block['element']['elements'] []= array( 0990 'name' => 'thead', 0991 ); 0992 0993 $Block['element']['elements'] []= array( 0994 'name' => 'tbody', 0995 'elements' => array(), 0996 ); 0997 0998 $Block['element']['elements'][0]['elements'] []= array( 0999 'name' => 'tr', 1000 'elements' => $HeaderElements, 1001 ); 1002 1003 return $Block; 1004 } 1005 1006 protected function blockTableContinue($Line, array $Block) 1007 { 1008 if (isset($Block['interrupted'])) 1009 { 1010 return; 1011 } 1012 1013 if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) 1014 { 1015 $Elements = array(); 1016 1017 $row = $Line['text']; 1018 1019 $row = trim($row); 1020 $row = trim($row, '|'); 1021 1022 preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); 1023 1024 $cells = array_slice($matches[0], 0, count($Block['alignments'])); 1025 1026 foreach ($cells as $index => $cell) 1027 { 1028 $cell = trim($cell); 1029 1030 $Element = array( 1031 'name' => 'td', 1032 'handler' => array( 1033 'function' => 'lineElements', 1034 'argument' => $cell, 1035 'destination' => 'elements', 1036 ) 1037 ); 1038 1039 if (isset($Block['alignments'][$index])) 1040 { 1041 $Element['attributes'] = array( 1042 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', 1043 ); 1044 } 1045 1046 $Elements []= $Element; 1047 } 1048 1049 $Element = array( 1050 'name' => 'tr', 1051 'elements' => $Elements, 1052 ); 1053 1054 $Block['element']['elements'][1]['elements'] []= $Element; 1055 1056 return $Block; 1057 } 1058 } 1059 1060 # 1061 # ~ 1062 # 1063 1064 protected function paragraph($Line) 1065 { 1066 return array( 1067 'type' => 'Paragraph', 1068 'element' => array( 1069 'name' => 'p', 1070 'handler' => array( 1071 'function' => 'lineElements', 1072 'argument' => $Line['text'], 1073 'destination' => 'elements', 1074 ), 1075 ), 1076 ); 1077 } 1078 1079 protected function paragraphContinue($Line, array $Block) 1080 { 1081 if (isset($Block['interrupted'])) 1082 { 1083 return; 1084 } 1085 1086 $Block['element']['handler']['argument'] .= "\n".$Line['text']; 1087 1088 return $Block; 1089 } 1090 1091 # 1092 # Inline Elements 1093 # 1094 1095 protected $InlineTypes = array( 1096 '!' => array('Image'), 1097 '&' => array('SpecialCharacter'), 1098 '*' => array('Emphasis'), 1099 ':' => array('Url'), 1100 '<' => array('UrlTag', 'EmailTag', 'Markup'), 1101 '[' => array('Link'), 1102 '_' => array('Emphasis'), 1103 '`' => array('Code'), 1104 '~' => array('Strikethrough'), 1105 '\\' => array('EscapeSequence'), 1106 ); 1107 1108 # ~ 1109 1110 protected $inlineMarkerList = '!*_&[:<`~\\'; 1111 1112 # 1113 # ~ 1114 # 1115 1116 public function line($text, $nonNestables = array()) 1117 { 1118 return $this->elements($this->lineElements($text, $nonNestables)); 1119 } 1120 1121 protected function lineElements($text, $nonNestables = array()) 1122 { 1123 # standardize line breaks 1124 $text = str_replace(array("\r\n", "\r"), "\n", $text); 1125 1126 $Elements = array(); 1127 1128 $nonNestables = (empty($nonNestables) 1129 ? array() 1130 : array_combine($nonNestables, $nonNestables) 1131 ); 1132 1133 # $excerpt is based on the first occurrence of a marker 1134 1135 while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1136 { 1137 $marker = $excerpt[0]; 1138 1139 $markerPosition = strlen($text) - strlen($excerpt); 1140 1141 $Excerpt = array('text' => $excerpt, 'context' => $text); 1142 1143 foreach ($this->InlineTypes[$marker] as $inlineType) 1144 { 1145 # check to see if the current inline type is nestable in the current context 1146 1147 if (isset($nonNestables[$inlineType])) 1148 { 1149 continue; 1150 } 1151 1152 $Inline = $this->{"inline$inlineType"}($Excerpt); 1153 1154 if ( ! isset($Inline)) 1155 { 1156 continue; 1157 } 1158 1159 # makes sure that the inline belongs to "our" marker 1160 1161 if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1162 { 1163 continue; 1164 } 1165 1166 # sets a default inline position 1167 1168 if ( ! isset($Inline['position'])) 1169 { 1170 $Inline['position'] = $markerPosition; 1171 } 1172 1173 # cause the new element to 'inherit' our non nestables 1174 1175 1176 $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) 1177 ? array_merge($Inline['element']['nonNestables'], $nonNestables) 1178 : $nonNestables 1179 ; 1180 1181 # the text that comes before the inline 1182 $unmarkedText = substr($text, 0, $Inline['position']); 1183 1184 # compile the unmarked text 1185 $InlineText = $this->inlineText($unmarkedText); 1186 $Elements[] = $InlineText['element']; 1187 1188 # compile the inline 1189 $Elements[] = $this->extractElement($Inline); 1190 1191 # remove the examined text 1192 $text = substr($text, $Inline['position'] + $Inline['extent']); 1193 1194 continue 2; 1195 } 1196 1197 # the marker does not belong to an inline 1198 1199 $unmarkedText = substr($text, 0, $markerPosition + 1); 1200 1201 $InlineText = $this->inlineText($unmarkedText); 1202 $Elements[] = $InlineText['element']; 1203 1204 $text = substr($text, $markerPosition + 1); 1205 } 1206 1207 $InlineText = $this->inlineText($text); 1208 $Elements[] = $InlineText['element']; 1209 1210 foreach ($Elements as &$Element) 1211 { 1212 if ( ! isset($Element['autobreak'])) 1213 { 1214 $Element['autobreak'] = false; 1215 } 1216 } 1217 1218 return $Elements; 1219 } 1220 1221 # 1222 # ~ 1223 # 1224 1225 protected function inlineText($text) 1226 { 1227 $Inline = array( 1228 'extent' => strlen($text), 1229 'element' => array(), 1230 ); 1231 1232 $Inline['element']['elements'] = self::pregReplaceElements( 1233 $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', 1234 array( 1235 array('name' => 'br'), 1236 array('text' => "\n"), 1237 ), 1238 $text 1239 ); 1240 1241 return $Inline; 1242 } 1243 1244 protected function inlineCode($Excerpt) 1245 { 1246 $marker = $Excerpt['text'][0]; 1247 1248 if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) 1249 { 1250 $text = $matches[2]; 1251 $text = preg_replace('/[ ]*+\n/', ' ', $text); 1252 1253 return array( 1254 'extent' => strlen($matches[0]), 1255 'element' => array( 1256 'name' => 'code', 1257 'text' => $text, 1258 ), 1259 ); 1260 } 1261 } 1262 1263 protected function inlineEmailTag($Excerpt) 1264 { 1265 $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; 1266 1267 $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' 1268 . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; 1269 1270 if (strpos($Excerpt['text'], '>') !== false 1271 and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) 1272 ){ 1273 $url = $matches[1]; 1274 1275 if ( ! isset($matches[2])) 1276 { 1277 $url = "mailto:$url"; 1278 } 1279 1280 return array( 1281 'extent' => strlen($matches[0]), 1282 'element' => array( 1283 'name' => 'a', 1284 'text' => $matches[1], 1285 'attributes' => array( 1286 'href' => $url, 1287 ), 1288 ), 1289 ); 1290 } 1291 } 1292 1293 protected function inlineEmphasis($Excerpt) 1294 { 1295 if ( ! isset($Excerpt['text'][1])) 1296 { 1297 return; 1298 } 1299 1300 $marker = $Excerpt['text'][0]; 1301 1302 if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1303 { 1304 $emphasis = 'strong'; 1305 } 1306 elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1307 { 1308 $emphasis = 'em'; 1309 } 1310 else 1311 { 1312 return; 1313 } 1314 1315 return array( 1316 'extent' => strlen($matches[0]), 1317 'element' => array( 1318 'name' => $emphasis, 1319 'handler' => array( 1320 'function' => 'lineElements', 1321 'argument' => $matches[1], 1322 'destination' => 'elements', 1323 ) 1324 ), 1325 ); 1326 } 1327 1328 protected function inlineEscapeSequence($Excerpt) 1329 { 1330 if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1331 { 1332 return array( 1333 'element' => array('rawHtml' => $Excerpt['text'][1]), 1334 'extent' => 2, 1335 ); 1336 } 1337 } 1338 1339 protected function inlineImage($Excerpt) 1340 { 1341 if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1342 { 1343 return; 1344 } 1345 1346 $Excerpt['text']= substr($Excerpt['text'], 1); 1347 1348 $Link = $this->inlineLink($Excerpt); 1349 1350 if ($Link === null) 1351 { 1352 return; 1353 } 1354 1355 $Inline = array( 1356 'extent' => $Link['extent'] + 1, 1357 'element' => array( 1358 'name' => 'img', 1359 'attributes' => array( 1360 'src' => $Link['element']['attributes']['href'], 1361 'alt' => $Link['element']['handler']['argument'], 1362 ), 1363 'autobreak' => true, 1364 ), 1365 ); 1366 1367 $Inline['element']['attributes'] += $Link['element']['attributes']; 1368 1369 unset($Inline['element']['attributes']['href']); 1370 1371 return $Inline; 1372 } 1373 1374 protected function inlineLink($Excerpt) 1375 { 1376 $Element = array( 1377 'name' => 'a', 1378 'handler' => array( 1379 'function' => 'lineElements', 1380 'argument' => null, 1381 'destination' => 'elements', 1382 ), 1383 'nonNestables' => array('Url', 'Link'), 1384 'attributes' => array( 1385 'href' => null, 1386 'title' => null, 1387 ), 1388 ); 1389 1390 $extent = 0; 1391 1392 $remainder = $Excerpt['text']; 1393 1394 if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 1395 { 1396 $Element['handler']['argument'] = $matches[1]; 1397 1398 $extent += strlen($matches[0]); 1399 1400 $remainder = substr($remainder, $extent); 1401 } 1402 else 1403 { 1404 return; 1405 } 1406 1407 if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) 1408 { 1409 $Element['attributes']['href'] = $matches[1]; 1410 1411 if (isset($matches[2])) 1412 { 1413 $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1414 } 1415 1416 $extent += strlen($matches[0]); 1417 } 1418 else 1419 { 1420 if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1421 { 1422 $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; 1423 $definition = strtolower($definition); 1424 1425 $extent += strlen($matches[0]); 1426 } 1427 else 1428 { 1429 $definition = strtolower($Element['handler']['argument']); 1430 } 1431 1432 if ( ! isset($this->DefinitionData['Reference'][$definition])) 1433 { 1434 return; 1435 } 1436 1437 $Definition = $this->DefinitionData['Reference'][$definition]; 1438 1439 $Element['attributes']['href'] = $Definition['url']; 1440 $Element['attributes']['title'] = $Definition['title']; 1441 } 1442 1443 return array( 1444 'extent' => $extent, 1445 'element' => $Element, 1446 ); 1447 } 1448 1449 protected function inlineMarkup($Excerpt) 1450 { 1451 if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) 1452 { 1453 return; 1454 } 1455 1456 if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) 1457 { 1458 return array( 1459 'element' => array('rawHtml' => $matches[0]), 1460 'extent' => strlen($matches[0]), 1461 ); 1462 } 1463 1464 if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) 1465 { 1466 return array( 1467 'element' => array('rawHtml' => $matches[0]), 1468 'extent' => strlen($matches[0]), 1469 ); 1470 } 1471 1472 if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) 1473 { 1474 return array( 1475 'element' => array('rawHtml' => $matches[0]), 1476 'extent' => strlen($matches[0]), 1477 ); 1478 } 1479 } 1480 1481 protected function inlineSpecialCharacter($Excerpt) 1482 { 1483 if ($Excerpt['text'][1] !== ' ' and strpos($Excerpt['text'], ';') !== false 1484 and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) 1485 ) { 1486 return array( 1487 'element' => array('rawHtml' => '&' . $matches[1] . ';'), 1488 'extent' => strlen($matches[0]), 1489 ); 1490 } 1491 1492 return; 1493 } 1494 1495 protected function inlineStrikethrough($Excerpt) 1496 { 1497 if ( ! isset($Excerpt['text'][1])) 1498 { 1499 return; 1500 } 1501 1502 if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1503 { 1504 return array( 1505 'extent' => strlen($matches[0]), 1506 'element' => array( 1507 'name' => 'del', 1508 'handler' => array( 1509 'function' => 'lineElements', 1510 'argument' => $matches[1], 1511 'destination' => 'elements', 1512 ) 1513 ), 1514 ); 1515 } 1516 } 1517 1518 protected function inlineUrl($Excerpt) 1519 { 1520 if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1521 { 1522 return; 1523 } 1524 1525 if (strpos($Excerpt['context'], 'http') !== false 1526 and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) 1527 ) { 1528 $url = $matches[0][0]; 1529 1530 $Inline = array( 1531 'extent' => strlen($matches[0][0]), 1532 'position' => $matches[0][1], 1533 'element' => array( 1534 'name' => 'a', 1535 'text' => $url, 1536 'attributes' => array( 1537 'href' => $url, 1538 ), 1539 ), 1540 ); 1541 1542 return $Inline; 1543 } 1544 } 1545 1546 protected function inlineUrlTag($Excerpt) 1547 { 1548 if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) 1549 { 1550 $url = $matches[1]; 1551 1552 return array( 1553 'extent' => strlen($matches[0]), 1554 'element' => array( 1555 'name' => 'a', 1556 'text' => $url, 1557 'attributes' => array( 1558 'href' => $url, 1559 ), 1560 ), 1561 ); 1562 } 1563 } 1564 1565 # ~ 1566 1567 protected function unmarkedText($text) 1568 { 1569 $Inline = $this->inlineText($text); 1570 return $this->element($Inline['element']); 1571 } 1572 1573 # 1574 # Handlers 1575 # 1576 1577 protected function handle(array $Element) 1578 { 1579 if (isset($Element['handler'])) 1580 { 1581 if (!isset($Element['nonNestables'])) 1582 { 1583 $Element['nonNestables'] = array(); 1584 } 1585 1586 if (is_string($Element['handler'])) 1587 { 1588 $function = $Element['handler']; 1589 $argument = $Element['text']; 1590 unset($Element['text']); 1591 $destination = 'rawHtml'; 1592 } 1593 else 1594 { 1595 $function = $Element['handler']['function']; 1596 $argument = $Element['handler']['argument']; 1597 $destination = $Element['handler']['destination']; 1598 } 1599 1600 $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); 1601 1602 if ($destination === 'handler') 1603 { 1604 $Element = $this->handle($Element); 1605 } 1606 1607 unset($Element['handler']); 1608 } 1609 1610 return $Element; 1611 } 1612 1613 protected function handleElementRecursive(array $Element) 1614 { 1615 return $this->elementApplyRecursive(array($this, 'handle'), $Element); 1616 } 1617 1618 protected function handleElementsRecursive(array $Elements) 1619 { 1620 return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); 1621 } 1622 1623 protected function elementApplyRecursive($closure, array $Element) 1624 { 1625 $Element = call_user_func($closure, $Element); 1626 1627 if (isset($Element['elements'])) 1628 { 1629 $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); 1630 } 1631 elseif (isset($Element['element'])) 1632 { 1633 $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); 1634 } 1635 1636 return $Element; 1637 } 1638 1639 protected function elementApplyRecursiveDepthFirst($closure, array $Element) 1640 { 1641 if (isset($Element['elements'])) 1642 { 1643 $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); 1644 } 1645 elseif (isset($Element['element'])) 1646 { 1647 $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); 1648 } 1649 1650 $Element = call_user_func($closure, $Element); 1651 1652 return $Element; 1653 } 1654 1655 protected function elementsApplyRecursive($closure, array $Elements) 1656 { 1657 foreach ($Elements as &$Element) 1658 { 1659 $Element = $this->elementApplyRecursive($closure, $Element); 1660 } 1661 1662 return $Elements; 1663 } 1664 1665 protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) 1666 { 1667 foreach ($Elements as &$Element) 1668 { 1669 $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); 1670 } 1671 1672 return $Elements; 1673 } 1674 1675 protected function element(array $Element) 1676 { 1677 if ($this->safeMode) 1678 { 1679 $Element = $this->sanitiseElement($Element); 1680 } 1681 1682 # identity map if element has no handler 1683 $Element = $this->handle($Element); 1684 1685 $hasName = isset($Element['name']); 1686 1687 $markup = ''; 1688 1689 if ($hasName) 1690 { 1691 $markup .= '<' . $Element['name']; 1692 1693 if (isset($Element['attributes'])) 1694 { 1695 foreach ($Element['attributes'] as $name => $value) 1696 { 1697 if ($value === null) 1698 { 1699 continue; 1700 } 1701 1702 $markup .= " $name=\"".self::escape($value).'"'; 1703 } 1704 } 1705 } 1706 1707 $permitRawHtml = false; 1708 1709 if (isset($Element['text'])) 1710 { 1711 $text = $Element['text']; 1712 } 1713 // very strongly consider an alternative if you're writing an 1714 // extension 1715 elseif (isset($Element['rawHtml'])) 1716 { 1717 $text = $Element['rawHtml']; 1718 1719 $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; 1720 $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; 1721 } 1722 1723 $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); 1724 1725 if ($hasContent) 1726 { 1727 $markup .= $hasName ? '>' : ''; 1728 1729 if (isset($Element['elements'])) 1730 { 1731 $markup .= $this->elements($Element['elements']); 1732 } 1733 elseif (isset($Element['element'])) 1734 { 1735 $markup .= $this->element($Element['element']); 1736 } 1737 else 1738 { 1739 if (!$permitRawHtml) 1740 { 1741 $markup .= self::escape($text, true); 1742 } 1743 else 1744 { 1745 $markup .= $text; 1746 } 1747 } 1748 1749 $markup .= $hasName ? '</' . $Element['name'] . '>' : ''; 1750 } 1751 elseif ($hasName) 1752 { 1753 $markup .= ' />'; 1754 } 1755 1756 return $markup; 1757 } 1758 1759 protected function elements(array $Elements) 1760 { 1761 $markup = ''; 1762 1763 $autoBreak = true; 1764 1765 foreach ($Elements as $Element) 1766 { 1767 if (empty($Element)) 1768 { 1769 continue; 1770 } 1771 1772 $autoBreakNext = (isset($Element['autobreak']) 1773 ? $Element['autobreak'] : isset($Element['name']) 1774 ); 1775 // (autobreak === false) covers both sides of an element 1776 $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; 1777 1778 $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); 1779 $autoBreak = $autoBreakNext; 1780 } 1781 1782 $markup .= $autoBreak ? "\n" : ''; 1783 1784 return $markup; 1785 } 1786 1787 # ~ 1788 1789 protected function li($lines) 1790 { 1791 $Elements = $this->linesElements($lines); 1792 1793 if ( ! in_array('', $lines) 1794 and isset($Elements[0]) and isset($Elements[0]['name']) 1795 and $Elements[0]['name'] === 'p' 1796 ) { 1797 unset($Elements[0]['name']); 1798 } 1799 1800 return $Elements; 1801 } 1802 1803 # 1804 # AST Convenience 1805 # 1806 1807 /** 1808 * Replace occurrences $regexp with $Elements in $text. Return an array of 1809 * elements representing the replacement. 1810 */ 1811 protected static function pregReplaceElements($regexp, $Elements, $text) 1812 { 1813 $newElements = array(); 1814 1815 while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) 1816 { 1817 $offset = $matches[0][1]; 1818 $before = substr($text, 0, $offset); 1819 $after = substr($text, $offset + strlen($matches[0][0])); 1820 1821 $newElements[] = array('text' => $before); 1822 1823 foreach ($Elements as $Element) 1824 { 1825 $newElements[] = $Element; 1826 } 1827 1828 $text = $after; 1829 } 1830 1831 $newElements[] = array('text' => $text); 1832 1833 return $newElements; 1834 } 1835 1836 # 1837 # Deprecated Methods 1838 # 1839 1840 function parse($text) 1841 { 1842 $markup = $this->text($text); 1843 1844 return $markup; 1845 } 1846 1847 protected function sanitiseElement(array $Element) 1848 { 1849 static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; 1850 static $safeUrlNameToAtt = array( 1851 'a' => 'href', 1852 'img' => 'src', 1853 ); 1854 1855 if ( ! isset($Element['name'])) 1856 { 1857 unset($Element['attributes']); 1858 return $Element; 1859 } 1860 1861 if (isset($safeUrlNameToAtt[$Element['name']])) 1862 { 1863 $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); 1864 } 1865 1866 if ( ! empty($Element['attributes'])) 1867 { 1868 foreach ($Element['attributes'] as $att => $val) 1869 { 1870 # filter out badly parsed attribute 1871 if ( ! preg_match($goodAttribute, $att)) 1872 { 1873 unset($Element['attributes'][$att]); 1874 } 1875 # dump onevent attribute 1876 elseif (self::striAtStart($att, 'on')) 1877 { 1878 unset($Element['attributes'][$att]); 1879 } 1880 } 1881 } 1882 1883 return $Element; 1884 } 1885 1886 protected function filterUnsafeUrlInAttribute(array $Element, $attribute) 1887 { 1888 foreach ($this->safeLinksWhitelist as $scheme) 1889 { 1890 if (self::striAtStart($Element['attributes'][$attribute], $scheme)) 1891 { 1892 return $Element; 1893 } 1894 } 1895 1896 $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); 1897 1898 return $Element; 1899 } 1900 1901 # 1902 # Static Methods 1903 # 1904 1905 protected static function escape($text, $allowQuotes = false) 1906 { 1907 return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); 1908 } 1909 1910 protected static function striAtStart($string, $needle) 1911 { 1912 $len = strlen($needle); 1913 1914 if ($len > strlen($string)) 1915 { 1916 return false; 1917 } 1918 else 1919 { 1920 return strtolower(substr($string, 0, $len)) === strtolower($needle); 1921 } 1922 } 1923 1924 static function instance($name = 'default') 1925 { 1926 if (isset(self::$instances[$name])) 1927 { 1928 return self::$instances[$name]; 1929 } 1930 1931 $instance = new static(); 1932 1933 self::$instances[$name] = $instance; 1934 1935 return $instance; 1936 } 1937 1938 private static $instances = array(); 1939 1940 # 1941 # Fields 1942 # 1943 1944 protected $DefinitionData; 1945 1946 # 1947 # Read-Only 1948 1949 protected $specialCharacters = array( 1950 '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' 1951 ); 1952 1953 protected $StrongRegex = array( 1954 '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', 1955 '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', 1956 ); 1957 1958 protected $EmRegex = array( 1959 '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1960 '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1961 ); 1962 1963 protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; 1964 1965 protected $voidElements = array( 1966 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1967 ); 1968 1969 protected $textLevelElements = array( 1970 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1971 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1972 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1973 'q', 'rt', 'ins', 'font', 'strong', 1974 's', 'tt', 'kbd', 'mark', 1975 'u', 'xm', 'sub', 'nobr', 1976 'sup', 'ruby', 1977 'var', 'span', 1978 'wbr', 'time', 1979 ); 1980 }