', "\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('/^\[(.+?)\]:[ ]*+(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $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 ? '' . $Element['name'] . '>' : '';
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 |
--------------------------------------------------------------------------------