├── CaptchaPlus └── Plugin.php ├── LICENSE ├── Parsedown ├── Parsedown.php └── Plugin.php └── README.md /CaptchaPlus/Plugin.php: -------------------------------------------------------------------------------- 1 | comment = __CLASS__ . '::filter'; 43 | } 44 | 45 | /** 46 | * 禁用插件方法,如果禁用失败,直接抛出异常 47 | */ 48 | public static function deactivate() 49 | { 50 | } 51 | 52 | /** 53 | * 获取插件配置面板 54 | * 55 | * @param Form $form 56 | */ 57 | public static function config(Form $form) 58 | { 59 | $captcha_choose = new Radio('captcha_choose', array("hcaptcha" => "hCaptcha", "turnstile" => "Turnstile"), "hcaptcha", _t('验证工具'), _t('选择使用 hCpatcha 或者 Cloudflare Turnstile 验证')); 60 | $form->addInput($captcha_choose); 61 | 62 | $site_key = new Text('site_key', NULL, '', _t('Site Key'), _t('需要注册 hCaptcha 或者 Cloudflare 账号以获取 site keysecret key')); 63 | $form->addInput($site_key); 64 | 65 | $secret_key = new Text('secret_key', NULL, '', _t('Secret Key'), _t('')); 66 | $form->addInput($secret_key); 67 | 68 | $widget_theme = new Radio('widget_theme', array("light" => "浅色", "dark" => "深色"), "light", _t('主题'), _t('设置验证工具主题颜色,默认为浅色')); 69 | $form->addInput($widget_theme); 70 | 71 | $widget_size = new Radio('widget_size', array("normal" => "常规", "compact" => "紧凑"), "normal", _t('样式'), _t('设置验证工具布局样式,默认为常规')); 72 | $form->addInput($widget_size); 73 | 74 | $opt_noru = new Radio( 75 | 'opt_noru', 76 | array("none" => "无动作", "waiting" => "标记为待审核", "spam" => "标记为垃圾", "abandon" => "评论失败"), 77 | "abandon", 78 | _t('俄文评论操作'), 79 | _t('如果评论中包含俄文,则强行按该操作执行') 80 | ); 81 | $form->addInput($opt_noru); 82 | 83 | $opt_nocn = new Radio( 84 | 'opt_nocn', 85 | array("none" => "无动作", "waiting" => "标记为待审核", "spam" => "标记为垃圾", "abandon" => "评论失败"), 86 | "waiting", 87 | _t('非中文评论操作'), 88 | _t('如果评论中不包含中文,则强行按该操作执行') 89 | ); 90 | $form->addInput($opt_nocn); 91 | 92 | $opt_ban = new Radio( 93 | 'opt_ban', 94 | array("none" => "无动作", "waiting" => "标记为待审核", "spam" => "标记为垃圾", "abandon" => "评论失败"), 95 | "abandon", 96 | _t('禁止词汇操作'), 97 | _t('如果评论中包含禁止词汇列表中的词汇,将执行该操作') 98 | ); 99 | $form->addInput($opt_ban); 100 | 101 | $words_ban = new Textarea( 102 | 'words_ban', 103 | NULL, 104 | "fuck\n傻逼\ncnm", 105 | _t('禁止词汇'), 106 | _t('多条词汇请用换行符隔开') 107 | ); 108 | $form->addInput($words_ban); 109 | 110 | $opt_chk = new Radio( 111 | 'opt_chk', 112 | array("none" => "无动作", "waiting" => "标记为待审核", "spam" => "标记为垃圾", "abandon" => "评论失败"), 113 | "waiting", 114 | _t('敏感词汇操作'), 115 | _t('如果评论中包含敏感词汇列表中的词汇,将执行该操作') 116 | ); 117 | $form->addInput($opt_chk); 118 | 119 | $words_chk = new Textarea( 120 | 'words_chk', 121 | NULL, 122 | "http://\nhttps://", 123 | _t('敏感词汇'), 124 | _t('多条词汇请用换行符隔开
注意:如果词汇同时出现于禁止词汇,则执行禁止词汇操作') 125 | ); 126 | $form->addInput($words_chk); 127 | } 128 | 129 | /** 130 | * 个人用户的配置面板 131 | * 132 | * @param Form $form 133 | */ 134 | public static function personalConfig(Form $form) 135 | { 136 | } 137 | 138 | /** 139 | * 显示 hCaptcha / Turnstile 140 | */ 141 | public static function output() 142 | { 143 | $filter_set = Options::alloc()->plugin('CaptchaPlus'); 144 | $captcha_choose = $filter_set->captcha_choose; 145 | $site_key = $filter_set->site_key; 146 | $secret_key = $filter_set->secret_key; 147 | $widget_theme = $filter_set->widget_theme; 148 | $widget_size = $filter_set->widget_size; 149 | $script = ""; 150 | if ($site_key != "" && $secret_key != "") { 151 | if ($captcha_choose == "hcaptcha") { 152 | $script = '
'; 153 | } else { 154 | $script = '
'; 155 | } 156 | echo $script; 157 | } else { 158 | // throw new Exception(_t('Error, No hCaptcha Site/Secret Keys.')); 159 | } 160 | } 161 | 162 | /** 163 | * 插件实现方法 164 | * 165 | * @access public 166 | */ 167 | public static function filter($comment) 168 | { 169 | $filter_set = Options::alloc()->plugin('CaptchaPlus'); 170 | $user = Widget::widget('Widget_User'); 171 | $captcha_choose = $filter_set->captcha_choose; 172 | $secret_key = $filter_set->secret_key; 173 | $post_token = ""; 174 | if ($captcha_choose == "hcaptcha") { 175 | $post_token = $_POST['h-captcha-response']; 176 | $url_path = "https://hcaptcha.com/siteverify"; 177 | } else { 178 | $post_token = $_POST['cf-turnstile-response']; 179 | $url_path = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; 180 | } 181 | if ($user->hasLogin() && $user->pass('administrator', true)) { 182 | return $comment; 183 | } elseif (isset($post_token)) { 184 | $postdata = array('secret' => $secret_key, 'response' => $post_token); 185 | $options = array( 186 | 'http' => array( 187 | 'method' => 'POST', 188 | 'content' => http_build_query($postdata) 189 | ) 190 | ); 191 | $context = stream_context_create($options); 192 | $response = file_get_contents($url_path, false, $context); 193 | $response_data = json_decode($response); 194 | if ($response_data->success == true) { 195 | $opt = "none"; 196 | $error = ""; 197 | // 俄文评论处理 198 | if ($opt == "none" && $filter_set->opt_noru != "none") { 199 | if (preg_match("/([\x{0400}-\x{04FF}]|[\x{0500}-\x{052F}]|[\x{2DE0}-\x{2DFF}]|[\x{A640}-\x{A69F}]|[\x{1C80}-\x{1C8F}])/u", $comment['text']) > 0) { 200 | $error = "Error."; 201 | $opt = $filter_set->opt_noru; 202 | } 203 | } 204 | // 非中文评论处理 205 | if ($opt == "none" && $filter_set->opt_nocn != "none") { 206 | if (preg_match("/[\x{4e00}-\x{9fa5}]/u", $comment['text']) == 0) { 207 | $error = "At least one Chinese character is required."; 208 | $opt = $filter_set->opt_nocn; 209 | } 210 | } 211 | // 禁止词汇处理 212 | if ($opt == "none" && $filter_set->opt_ban != "none") { 213 | if (CaptchaPlus_Plugin::check_in($filter_set->words_ban, $comment['text'])) { 214 | $error = "More friendly, plz :)"; 215 | $opt = $filter_set->opt_ban; 216 | } 217 | } 218 | // 敏感词汇处理 219 | if ($opt == "none" && $filter_set->opt_chk != "none") { 220 | if (CaptchaPlus_Plugin::check_in($filter_set->words_chk, $comment['text'])) { 221 | $error = "Error."; 222 | $opt = $filter_set->opt_chk; 223 | } 224 | } 225 | // 执行操作 226 | if ($opt == "abandon") { 227 | Cookie::set('__typecho_remember_text', $comment['text']); 228 | throw new Exception($error); 229 | } elseif ($opt == "spam") { 230 | $comment['status'] = 'spam'; 231 | } elseif ($opt == "waiting") { 232 | $comment['status'] = 'waiting'; 233 | } 234 | Cookie::delete('__typecho_remember_text'); 235 | return $comment; 236 | } else { 237 | throw new Exception(_t('Captcha verification failed. Please try again.')); 238 | } 239 | } else { 240 | throw new Exception(_t('Could not connect to the service. Please check your internet connection and reload to get a captcha challenge.')); 241 | } 242 | } 243 | 244 | /** 245 | * 检查 $str 中是否含有 $words_str 中的词汇 246 | * 247 | */ 248 | private static function check_in($words_str, $str) 249 | { 250 | $words = explode("\n", $words_str); 251 | if (empty($words)) { 252 | return false; 253 | } 254 | foreach ($words as $word) { 255 | if (false !== strpos($str, trim($word))) { 256 | return true; 257 | } 258 | } 259 | return false; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ATP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Parsedown/Parsedown.php: -------------------------------------------------------------------------------- 1 | textElements($text); 27 | 28 | # convert to markup 29 | $markup = $this->elements($Elements); 30 | 31 | # trim line breaks 32 | $markup = trim($markup, "\n"); 33 | 34 | return $markup; 35 | } 36 | 37 | protected function textElements($text) 38 | { 39 | # make sure no definitions are set 40 | $this->DefinitionData = array(); 41 | 42 | # standardize line breaks 43 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 44 | 45 | # remove surrounding line breaks 46 | $text = trim($text, "\n"); 47 | 48 | # split text into lines 49 | $lines = explode("\n", $text); 50 | 51 | # iterate through lines to identify blocks 52 | return $this->linesElements($lines); 53 | } 54 | 55 | # 56 | # Setters 57 | # 58 | 59 | function setBreaksEnabled($breaksEnabled) 60 | { 61 | $this->breaksEnabled = $breaksEnabled; 62 | 63 | return $this; 64 | } 65 | 66 | protected $breaksEnabled; 67 | 68 | function setMarkupEscaped($markupEscaped) 69 | { 70 | $this->markupEscaped = $markupEscaped; 71 | 72 | return $this; 73 | } 74 | 75 | protected $markupEscaped; 76 | 77 | function setUrlsLinked($urlsLinked) 78 | { 79 | $this->urlsLinked = $urlsLinked; 80 | 81 | return $this; 82 | } 83 | 84 | protected $urlsLinked = true; 85 | 86 | function setSafeMode($safeMode) 87 | { 88 | $this->safeMode = (bool) $safeMode; 89 | 90 | return $this; 91 | } 92 | 93 | protected $safeMode; 94 | 95 | function setStrictMode($strictMode) 96 | { 97 | $this->strictMode = (bool) $strictMode; 98 | 99 | return $this; 100 | } 101 | 102 | protected $strictMode; 103 | 104 | protected $safeLinksWhitelist = array( 105 | 'http://', 106 | 'https://', 107 | 'ftp://', 108 | 'ftps://', 109 | 'mailto:', 110 | 'tel:', 111 | 'data:image/png;base64,', 112 | 'data:image/gif;base64,', 113 | 'data:image/jpeg;base64,', 114 | 'irc:', 115 | 'ircs:', 116 | 'git:', 117 | 'ssh:', 118 | 'news:', 119 | 'steam:', 120 | ); 121 | 122 | # 123 | # Lines 124 | # 125 | 126 | protected $BlockTypes = array( 127 | '#' => array('Header'), 128 | '*' => array('Rule', 'List'), 129 | '+' => array('List'), 130 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), 131 | '0' => array('List'), 132 | '1' => array('List'), 133 | '2' => array('List'), 134 | '3' => array('List'), 135 | '4' => array('List'), 136 | '5' => array('List'), 137 | '6' => array('List'), 138 | '7' => array('List'), 139 | '8' => array('List'), 140 | '9' => array('List'), 141 | ':' => array('Table'), 142 | '<' => array('Comment', 'Markup'), 143 | '=' => array('SetextHeader'), 144 | '>' => array('Quote'), 145 | '[' => array('Reference'), 146 | '_' => array('Rule'), 147 | '`' => array('FencedCode'), 148 | '|' => array('Table'), 149 | '~' => array('FencedCode'), 150 | ); 151 | 152 | # ~ 153 | 154 | protected $unmarkedBlockTypes = array( 155 | 'Code', 156 | ); 157 | 158 | # 159 | # Blocks 160 | # 161 | 162 | protected function lines(array $lines) 163 | { 164 | return $this->elements($this->linesElements($lines)); 165 | } 166 | 167 | protected function linesElements(array $lines) 168 | { 169 | $Elements = array(); 170 | $CurrentBlock = null; 171 | 172 | foreach ($lines as $line) 173 | { 174 | if (chop($line) === '') 175 | { 176 | if (isset($CurrentBlock)) 177 | { 178 | $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted']) 179 | ? $CurrentBlock['interrupted'] + 1 : 1 180 | ); 181 | } 182 | 183 | continue; 184 | } 185 | 186 | while (($beforeTab = strstr($line, "\t", true)) !== false) 187 | { 188 | $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4; 189 | 190 | $line = $beforeTab 191 | . str_repeat(' ', $shortage) 192 | . substr($line, strlen($beforeTab) + 1) 193 | ; 194 | } 195 | 196 | $indent = strspn($line, ' '); 197 | 198 | $text = $indent > 0 ? substr($line, $indent) : $line; 199 | 200 | # ~ 201 | 202 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); 203 | 204 | # ~ 205 | 206 | if (isset($CurrentBlock['continuable'])) 207 | { 208 | $methodName = 'block' . $CurrentBlock['type'] . 'Continue'; 209 | $Block = $this->$methodName($Line, $CurrentBlock); 210 | 211 | if (isset($Block)) 212 | { 213 | $CurrentBlock = $Block; 214 | 215 | continue; 216 | } 217 | else 218 | { 219 | if ($this->isBlockCompletable($CurrentBlock['type'])) 220 | { 221 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 222 | $CurrentBlock = $this->$methodName($CurrentBlock); 223 | } 224 | } 225 | } 226 | 227 | # ~ 228 | 229 | $marker = $text[0]; 230 | 231 | # ~ 232 | 233 | $blockTypes = $this->unmarkedBlockTypes; 234 | 235 | if (isset($this->BlockTypes[$marker])) 236 | { 237 | foreach ($this->BlockTypes[$marker] as $blockType) 238 | { 239 | $blockTypes []= $blockType; 240 | } 241 | } 242 | 243 | # 244 | # ~ 245 | 246 | foreach ($blockTypes as $blockType) 247 | { 248 | $Block = $this->{"block$blockType"}($Line, $CurrentBlock); 249 | 250 | if (isset($Block)) 251 | { 252 | $Block['type'] = $blockType; 253 | 254 | if ( ! isset($Block['identified'])) 255 | { 256 | if (isset($CurrentBlock)) 257 | { 258 | $Elements[] = $this->extractElement($CurrentBlock); 259 | } 260 | 261 | $Block['identified'] = true; 262 | } 263 | 264 | if ($this->isBlockContinuable($blockType)) 265 | { 266 | $Block['continuable'] = true; 267 | } 268 | 269 | $CurrentBlock = $Block; 270 | 271 | continue 2; 272 | } 273 | } 274 | 275 | # ~ 276 | 277 | if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') 278 | { 279 | $Block = $this->paragraphContinue($Line, $CurrentBlock); 280 | } 281 | 282 | if (isset($Block)) 283 | { 284 | $CurrentBlock = $Block; 285 | } 286 | else 287 | { 288 | if (isset($CurrentBlock)) 289 | { 290 | $Elements[] = $this->extractElement($CurrentBlock); 291 | } 292 | 293 | $CurrentBlock = $this->paragraph($Line); 294 | 295 | $CurrentBlock['identified'] = true; 296 | } 297 | } 298 | 299 | # ~ 300 | 301 | if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) 302 | { 303 | $methodName = 'block' . $CurrentBlock['type'] . 'Complete'; 304 | $CurrentBlock = $this->$methodName($CurrentBlock); 305 | } 306 | 307 | # ~ 308 | 309 | if (isset($CurrentBlock)) 310 | { 311 | $Elements[] = $this->extractElement($CurrentBlock); 312 | } 313 | 314 | # ~ 315 | 316 | return $Elements; 317 | } 318 | 319 | protected function extractElement(array $Component) 320 | { 321 | if ( ! isset($Component['element'])) 322 | { 323 | if (isset($Component['markup'])) 324 | { 325 | $Component['element'] = array('rawHtml' => $Component['markup']); 326 | } 327 | elseif (isset($Component['hidden'])) 328 | { 329 | $Component['element'] = array(); 330 | } 331 | } 332 | 333 | return $Component['element']; 334 | } 335 | 336 | protected function isBlockContinuable($Type) 337 | { 338 | return method_exists($this, 'block' . $Type . 'Continue'); 339 | } 340 | 341 | protected function isBlockCompletable($Type) 342 | { 343 | return method_exists($this, 'block' . $Type . 'Complete'); 344 | } 345 | 346 | # 347 | # Code 348 | 349 | protected function blockCode($Line, $Block = null) 350 | { 351 | if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted'])) 352 | { 353 | return; 354 | } 355 | 356 | if ($Line['indent'] >= 4) 357 | { 358 | $text = substr($Line['body'], 4); 359 | 360 | $Block = array( 361 | 'element' => array( 362 | 'name' => 'pre', 363 | 'element' => array( 364 | 'name' => 'code', 365 | 'text' => $text, 366 | ), 367 | ), 368 | ); 369 | 370 | return $Block; 371 | } 372 | } 373 | 374 | protected function blockCodeContinue($Line, $Block) 375 | { 376 | if ($Line['indent'] >= 4) 377 | { 378 | if (isset($Block['interrupted'])) 379 | { 380 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 381 | 382 | unset($Block['interrupted']); 383 | } 384 | 385 | $Block['element']['element']['text'] .= "\n"; 386 | 387 | $text = substr($Line['body'], 4); 388 | 389 | $Block['element']['element']['text'] .= $text; 390 | 391 | return $Block; 392 | } 393 | } 394 | 395 | protected function blockCodeComplete($Block) 396 | { 397 | return $Block; 398 | } 399 | 400 | # 401 | # Comment 402 | 403 | protected function blockComment($Line) 404 | { 405 | if ($this->markupEscaped or $this->safeMode) 406 | { 407 | return; 408 | } 409 | 410 | if (strpos($Line['text'], '') !== false) 420 | { 421 | $Block['closed'] = true; 422 | } 423 | 424 | return $Block; 425 | } 426 | } 427 | 428 | protected function blockCommentContinue($Line, array $Block) 429 | { 430 | if (isset($Block['closed'])) 431 | { 432 | return; 433 | } 434 | 435 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 436 | 437 | if (strpos($Line['text'], '-->') !== false) 438 | { 439 | $Block['closed'] = true; 440 | } 441 | 442 | return $Block; 443 | } 444 | 445 | # 446 | # Fenced Code 447 | 448 | protected function blockFencedCode($Line) 449 | { 450 | $marker = $Line['text'][0]; 451 | 452 | $openerLength = strspn($Line['text'], $marker); 453 | 454 | if ($openerLength < 3) 455 | { 456 | return; 457 | } 458 | 459 | $infostring = trim(substr($Line['text'], $openerLength), "\t "); 460 | 461 | if (strpos($infostring, '`') !== false) 462 | { 463 | return; 464 | } 465 | 466 | $Element = array( 467 | 'name' => 'code', 468 | 'text' => '', 469 | ); 470 | 471 | if ($infostring !== '') 472 | { 473 | /** 474 | * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes 475 | * Every HTML element may have a class attribute specified. 476 | * The attribute, if specified, must have a value that is a set 477 | * of space-separated tokens representing the various classes 478 | * that the element belongs to. 479 | * [...] 480 | * The space characters, for the purposes of this specification, 481 | * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab), 482 | * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and 483 | * U+000D CARRIAGE RETURN (CR). 484 | */ 485 | $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r")); 486 | 487 | $Element['attributes'] = array('class' => "language-$language"); 488 | } 489 | 490 | $Block = array( 491 | 'char' => $marker, 492 | 'openerLength' => $openerLength, 493 | 'element' => array( 494 | 'name' => 'pre', 495 | 'element' => $Element, 496 | ), 497 | ); 498 | 499 | return $Block; 500 | } 501 | 502 | protected function blockFencedCodeContinue($Line, $Block) 503 | { 504 | if (isset($Block['complete'])) 505 | { 506 | return; 507 | } 508 | 509 | if (isset($Block['interrupted'])) 510 | { 511 | $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']); 512 | 513 | unset($Block['interrupted']); 514 | } 515 | 516 | if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] 517 | and chop(substr($Line['text'], $len), ' ') === '' 518 | ) { 519 | $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1); 520 | 521 | $Block['complete'] = true; 522 | 523 | return $Block; 524 | } 525 | 526 | $Block['element']['element']['text'] .= "\n" . $Line['body']; 527 | 528 | return $Block; 529 | } 530 | 531 | protected function blockFencedCodeComplete($Block) 532 | { 533 | return $Block; 534 | } 535 | 536 | # 537 | # Header 538 | 539 | protected function blockHeader($Line) 540 | { 541 | $level = strspn($Line['text'], '#'); 542 | 543 | if ($level > 6) 544 | { 545 | return; 546 | } 547 | 548 | $text = trim($Line['text'], '#'); 549 | 550 | if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') 551 | { 552 | return; 553 | } 554 | 555 | $text = trim($text, ' '); 556 | 557 | $Block = array( 558 | 'element' => array( 559 | 'name' => 'h' . $level, 560 | 'handler' => array( 561 | 'function' => 'lineElements', 562 | 'argument' => $text, 563 | 'destination' => 'elements', 564 | ) 565 | ), 566 | ); 567 | 568 | return $Block; 569 | } 570 | 571 | # 572 | # List 573 | 574 | protected function blockList($Line, array $CurrentBlock = null) 575 | { 576 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]'); 577 | 578 | if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) 579 | { 580 | $contentIndent = strlen($matches[2]); 581 | 582 | if ($contentIndent >= 5) 583 | { 584 | $contentIndent -= 1; 585 | $matches[1] = substr($matches[1], 0, -$contentIndent); 586 | $matches[3] = str_repeat(' ', $contentIndent) . $matches[3]; 587 | } 588 | elseif ($contentIndent === 0) 589 | { 590 | $matches[1] .= ' '; 591 | } 592 | 593 | $markerWithoutWhitespace = strstr($matches[1], ' ', true); 594 | 595 | $Block = array( 596 | 'indent' => $Line['indent'], 597 | 'pattern' => $pattern, 598 | 'data' => array( 599 | 'type' => $name, 600 | 'marker' => $matches[1], 601 | 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)), 602 | ), 603 | 'element' => array( 604 | 'name' => $name, 605 | 'elements' => array(), 606 | ), 607 | ); 608 | $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/'); 609 | 610 | if ($name === 'ol') 611 | { 612 | $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0'; 613 | 614 | if ($listStart !== '1') 615 | { 616 | if ( 617 | isset($CurrentBlock) 618 | and $CurrentBlock['type'] === 'Paragraph' 619 | and ! isset($CurrentBlock['interrupted']) 620 | ) { 621 | return; 622 | } 623 | 624 | $Block['element']['attributes'] = array('start' => $listStart); 625 | } 626 | } 627 | 628 | $Block['li'] = array( 629 | 'name' => 'li', 630 | 'handler' => array( 631 | 'function' => 'li', 632 | 'argument' => !empty($matches[3]) ? array($matches[3]) : array(), 633 | 'destination' => 'elements' 634 | ) 635 | ); 636 | 637 | $Block['element']['elements'] []= & $Block['li']; 638 | 639 | return $Block; 640 | } 641 | } 642 | 643 | protected function blockListContinue($Line, array $Block) 644 | { 645 | if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) 646 | { 647 | return null; 648 | } 649 | 650 | $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker'])); 651 | 652 | if ($Line['indent'] < $requiredIndent 653 | and ( 654 | ( 655 | $Block['data']['type'] === 'ol' 656 | and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 657 | ) or ( 658 | $Block['data']['type'] === 'ul' 659 | and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches) 660 | ) 661 | ) 662 | ) { 663 | if (isset($Block['interrupted'])) 664 | { 665 | $Block['li']['handler']['argument'] []= ''; 666 | 667 | $Block['loose'] = true; 668 | 669 | unset($Block['interrupted']); 670 | } 671 | 672 | unset($Block['li']); 673 | 674 | $text = isset($matches[1]) ? $matches[1] : ''; 675 | 676 | $Block['indent'] = $Line['indent']; 677 | 678 | $Block['li'] = array( 679 | 'name' => 'li', 680 | 'handler' => array( 681 | 'function' => 'li', 682 | 'argument' => array($text), 683 | 'destination' => 'elements' 684 | ) 685 | ); 686 | 687 | $Block['element']['elements'] []= & $Block['li']; 688 | 689 | return $Block; 690 | } 691 | elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) 692 | { 693 | return null; 694 | } 695 | 696 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) 697 | { 698 | return $Block; 699 | } 700 | 701 | if ($Line['indent'] >= $requiredIndent) 702 | { 703 | if (isset($Block['interrupted'])) 704 | { 705 | $Block['li']['handler']['argument'] []= ''; 706 | 707 | $Block['loose'] = true; 708 | 709 | unset($Block['interrupted']); 710 | } 711 | 712 | $text = substr($Line['body'], $requiredIndent); 713 | 714 | $Block['li']['handler']['argument'] []= $text; 715 | 716 | return $Block; 717 | } 718 | 719 | if ( ! isset($Block['interrupted'])) 720 | { 721 | $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']); 722 | 723 | $Block['li']['handler']['argument'] []= $text; 724 | 725 | return $Block; 726 | } 727 | } 728 | 729 | protected function blockListComplete(array $Block) 730 | { 731 | if (isset($Block['loose'])) 732 | { 733 | foreach ($Block['element']['elements'] as &$li) 734 | { 735 | if (end($li['handler']['argument']) !== '') 736 | { 737 | $li['handler']['argument'] []= ''; 738 | } 739 | } 740 | } 741 | 742 | return $Block; 743 | } 744 | 745 | # 746 | # Quote 747 | 748 | protected function blockQuote($Line) 749 | { 750 | if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 751 | { 752 | $Block = array( 753 | 'element' => array( 754 | 'name' => 'blockquote', 755 | 'handler' => array( 756 | 'function' => 'linesElements', 757 | 'argument' => (array) $matches[1], 758 | 'destination' => 'elements', 759 | ) 760 | ), 761 | ); 762 | 763 | return $Block; 764 | } 765 | } 766 | 767 | protected function blockQuoteContinue($Line, array $Block) 768 | { 769 | if (isset($Block['interrupted'])) 770 | { 771 | return; 772 | } 773 | 774 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) 775 | { 776 | $Block['element']['handler']['argument'] []= $matches[1]; 777 | 778 | return $Block; 779 | } 780 | 781 | if ( ! isset($Block['interrupted'])) 782 | { 783 | $Block['element']['handler']['argument'] []= $Line['text']; 784 | 785 | return $Block; 786 | } 787 | } 788 | 789 | # 790 | # Rule 791 | 792 | protected function blockRule($Line) 793 | { 794 | $marker = $Line['text'][0]; 795 | 796 | if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') 797 | { 798 | $Block = array( 799 | 'element' => array( 800 | 'name' => 'hr', 801 | ), 802 | ); 803 | 804 | return $Block; 805 | } 806 | } 807 | 808 | # 809 | # Setext 810 | 811 | protected function blockSetextHeader($Line, array $Block = null) 812 | { 813 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 814 | { 815 | return; 816 | } 817 | 818 | if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') 819 | { 820 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; 821 | 822 | return $Block; 823 | } 824 | } 825 | 826 | # 827 | # Markup 828 | 829 | protected function blockMarkup($Line) 830 | { 831 | if ($this->markupEscaped or $this->safeMode) 832 | { 833 | return; 834 | } 835 | 836 | if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches)) 837 | { 838 | $element = strtolower($matches[1]); 839 | 840 | if (in_array($element, $this->textLevelElements)) 841 | { 842 | return; 843 | } 844 | 845 | $Block = array( 846 | 'name' => $matches[1], 847 | 'element' => array( 848 | 'rawHtml' => $Line['text'], 849 | 'autobreak' => true, 850 | ), 851 | ); 852 | 853 | return $Block; 854 | } 855 | } 856 | 857 | protected function blockMarkupContinue($Line, array $Block) 858 | { 859 | if (isset($Block['closed']) or isset($Block['interrupted'])) 860 | { 861 | return; 862 | } 863 | 864 | $Block['element']['rawHtml'] .= "\n" . $Line['body']; 865 | 866 | return $Block; 867 | } 868 | 869 | # 870 | # Reference 871 | 872 | protected function blockReference($Line) 873 | { 874 | if (strpos($Line['text'], ']') !== false 875 | and preg_match('/^\[(.+?)\]:[ ]*+?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches) 876 | ) { 877 | $id = strtolower($matches[1]); 878 | 879 | $Data = array( 880 | 'url' => $matches[2], 881 | 'title' => isset($matches[3]) ? $matches[3] : null, 882 | ); 883 | 884 | $this->DefinitionData['Reference'][$id] = $Data; 885 | 886 | $Block = array( 887 | 'element' => array(), 888 | ); 889 | 890 | return $Block; 891 | } 892 | } 893 | 894 | # 895 | # Table 896 | 897 | protected function blockTable($Line, array $Block = null) 898 | { 899 | if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) 900 | { 901 | return; 902 | } 903 | 904 | if ( 905 | strpos($Block['element']['handler']['argument'], '|') === false 906 | and strpos($Line['text'], '|') === false 907 | and strpos($Line['text'], ':') === false 908 | or strpos($Block['element']['handler']['argument'], "\n") !== false 909 | ) { 910 | return; 911 | } 912 | 913 | if (chop($Line['text'], ' -:|') !== '') 914 | { 915 | return; 916 | } 917 | 918 | $alignments = array(); 919 | 920 | $divider = $Line['text']; 921 | 922 | $divider = trim($divider); 923 | $divider = trim($divider, '|'); 924 | 925 | $dividerCells = explode('|', $divider); 926 | 927 | foreach ($dividerCells as $dividerCell) 928 | { 929 | $dividerCell = trim($dividerCell); 930 | 931 | if ($dividerCell === '') 932 | { 933 | return; 934 | } 935 | 936 | $alignment = null; 937 | 938 | if ($dividerCell[0] === ':') 939 | { 940 | $alignment = 'left'; 941 | } 942 | 943 | if (substr($dividerCell, - 1) === ':') 944 | { 945 | $alignment = $alignment === 'left' ? 'center' : 'right'; 946 | } 947 | 948 | $alignments []= $alignment; 949 | } 950 | 951 | # ~ 952 | 953 | $HeaderElements = array(); 954 | 955 | $header = $Block['element']['handler']['argument']; 956 | 957 | $header = trim($header); 958 | $header = trim($header, '|'); 959 | 960 | $headerCells = explode('|', $header); 961 | 962 | if (count($headerCells) !== count($alignments)) 963 | { 964 | return; 965 | } 966 | 967 | foreach ($headerCells as $index => $headerCell) 968 | { 969 | $headerCell = trim($headerCell); 970 | 971 | $HeaderElement = array( 972 | 'name' => 'th', 973 | 'handler' => array( 974 | 'function' => 'lineElements', 975 | 'argument' => $headerCell, 976 | 'destination' => 'elements', 977 | ) 978 | ); 979 | 980 | if (isset($alignments[$index])) 981 | { 982 | $alignment = $alignments[$index]; 983 | 984 | $HeaderElement['attributes'] = array( 985 | 'style' => "text-align: $alignment;", 986 | ); 987 | } 988 | 989 | $HeaderElements []= $HeaderElement; 990 | } 991 | 992 | # ~ 993 | 994 | $Block = array( 995 | 'alignments' => $alignments, 996 | 'identified' => true, 997 | 'element' => array( 998 | 'name' => 'table', 999 | 'elements' => array(), 1000 | ), 1001 | ); 1002 | 1003 | $Block['element']['elements'] []= array( 1004 | 'name' => 'thead', 1005 | ); 1006 | 1007 | $Block['element']['elements'] []= array( 1008 | 'name' => 'tbody', 1009 | 'elements' => array(), 1010 | ); 1011 | 1012 | $Block['element']['elements'][0]['elements'] []= array( 1013 | 'name' => 'tr', 1014 | 'elements' => $HeaderElements, 1015 | ); 1016 | 1017 | return $Block; 1018 | } 1019 | 1020 | protected function blockTableContinue($Line, array $Block) 1021 | { 1022 | if (isset($Block['interrupted'])) 1023 | { 1024 | return; 1025 | } 1026 | 1027 | if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) 1028 | { 1029 | $Elements = array(); 1030 | 1031 | $row = $Line['text']; 1032 | 1033 | $row = trim($row); 1034 | $row = trim($row, '|'); 1035 | 1036 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches); 1037 | 1038 | $cells = array_slice($matches[0], 0, count($Block['alignments'])); 1039 | 1040 | foreach ($cells as $index => $cell) 1041 | { 1042 | $cell = trim($cell); 1043 | 1044 | $Element = array( 1045 | 'name' => 'td', 1046 | 'handler' => array( 1047 | 'function' => 'lineElements', 1048 | 'argument' => $cell, 1049 | 'destination' => 'elements', 1050 | ) 1051 | ); 1052 | 1053 | if (isset($Block['alignments'][$index])) 1054 | { 1055 | $Element['attributes'] = array( 1056 | 'style' => 'text-align: ' . $Block['alignments'][$index] . ';', 1057 | ); 1058 | } 1059 | 1060 | $Elements []= $Element; 1061 | } 1062 | 1063 | $Element = array( 1064 | 'name' => 'tr', 1065 | 'elements' => $Elements, 1066 | ); 1067 | 1068 | $Block['element']['elements'][1]['elements'] []= $Element; 1069 | 1070 | return $Block; 1071 | } 1072 | } 1073 | 1074 | # 1075 | # ~ 1076 | # 1077 | 1078 | protected function paragraph($Line) 1079 | { 1080 | return array( 1081 | 'type' => 'Paragraph', 1082 | 'element' => array( 1083 | 'name' => 'p', 1084 | 'handler' => array( 1085 | 'function' => 'lineElements', 1086 | 'argument' => $Line['text'], 1087 | 'destination' => 'elements', 1088 | ), 1089 | ), 1090 | ); 1091 | } 1092 | 1093 | protected function paragraphContinue($Line, array $Block) 1094 | { 1095 | if (isset($Block['interrupted'])) 1096 | { 1097 | return; 1098 | } 1099 | 1100 | $Block['element']['handler']['argument'] .= "\n".$Line['text']; 1101 | 1102 | return $Block; 1103 | } 1104 | 1105 | # 1106 | # Inline Elements 1107 | # 1108 | 1109 | protected $InlineTypes = array( 1110 | '!' => array('Image'), 1111 | '&' => array('SpecialCharacter'), 1112 | '*' => array('Emphasis'), 1113 | ':' => array('Url'), 1114 | '<' => array('UrlTag', 'EmailTag', 'Markup'), 1115 | '[' => array('Link'), 1116 | '_' => array('Emphasis'), 1117 | '`' => array('Code'), 1118 | '~' => array('Strikethrough'), 1119 | '\\' => array('EscapeSequence'), 1120 | ); 1121 | 1122 | # ~ 1123 | 1124 | protected $inlineMarkerList = '!*_&[:<`~\\'; 1125 | 1126 | # 1127 | # ~ 1128 | # 1129 | 1130 | public function line($text, $nonNestables = array()) 1131 | { 1132 | return $this->elements($this->lineElements($text, $nonNestables)); 1133 | } 1134 | 1135 | protected function lineElements($text, $nonNestables = array()) 1136 | { 1137 | # standardize line breaks 1138 | $text = str_replace(array("\r\n", "\r"), "\n", $text); 1139 | 1140 | $Elements = array(); 1141 | 1142 | $nonNestables = (empty($nonNestables) 1143 | ? array() 1144 | : array_combine($nonNestables, $nonNestables) 1145 | ); 1146 | 1147 | # $excerpt is based on the first occurrence of a marker 1148 | 1149 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) 1150 | { 1151 | $marker = $excerpt[0]; 1152 | 1153 | $markerPosition = strlen($text) - strlen($excerpt); 1154 | 1155 | $Excerpt = array('text' => $excerpt, 'context' => $text); 1156 | 1157 | foreach ($this->InlineTypes[$marker] as $inlineType) 1158 | { 1159 | # check to see if the current inline type is nestable in the current context 1160 | 1161 | if (isset($nonNestables[$inlineType])) 1162 | { 1163 | continue; 1164 | } 1165 | 1166 | $Inline = $this->{"inline$inlineType"}($Excerpt); 1167 | 1168 | if ( ! isset($Inline)) 1169 | { 1170 | continue; 1171 | } 1172 | 1173 | # makes sure that the inline belongs to "our" marker 1174 | 1175 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) 1176 | { 1177 | continue; 1178 | } 1179 | 1180 | # sets a default inline position 1181 | 1182 | if ( ! isset($Inline['position'])) 1183 | { 1184 | $Inline['position'] = $markerPosition; 1185 | } 1186 | 1187 | # cause the new element to 'inherit' our non nestables 1188 | 1189 | 1190 | $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables']) 1191 | ? array_merge($Inline['element']['nonNestables'], $nonNestables) 1192 | : $nonNestables 1193 | ; 1194 | 1195 | # the text that comes before the inline 1196 | $unmarkedText = substr($text, 0, $Inline['position']); 1197 | 1198 | # compile the unmarked text 1199 | $InlineText = $this->inlineText($unmarkedText); 1200 | $Elements[] = $InlineText['element']; 1201 | 1202 | # compile the inline 1203 | $Elements[] = $this->extractElement($Inline); 1204 | 1205 | # remove the examined text 1206 | $text = substr($text, $Inline['position'] + $Inline['extent']); 1207 | 1208 | continue 2; 1209 | } 1210 | 1211 | # the marker does not belong to an inline 1212 | 1213 | $unmarkedText = substr($text, 0, $markerPosition + 1); 1214 | 1215 | $InlineText = $this->inlineText($unmarkedText); 1216 | $Elements[] = $InlineText['element']; 1217 | 1218 | $text = substr($text, $markerPosition + 1); 1219 | } 1220 | 1221 | $InlineText = $this->inlineText($text); 1222 | $Elements[] = $InlineText['element']; 1223 | 1224 | foreach ($Elements as &$Element) 1225 | { 1226 | if ( ! isset($Element['autobreak'])) 1227 | { 1228 | $Element['autobreak'] = false; 1229 | } 1230 | } 1231 | 1232 | return $Elements; 1233 | } 1234 | 1235 | # 1236 | # ~ 1237 | # 1238 | 1239 | protected function inlineText($text) 1240 | { 1241 | $Inline = array( 1242 | 'extent' => strlen($text), 1243 | 'element' => array(), 1244 | ); 1245 | 1246 | $Inline['element']['elements'] = self::pregReplaceElements( 1247 | $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/', 1248 | array( 1249 | array('name' => 'br'), 1250 | array('text' => "\n"), 1251 | ), 1252 | $text 1253 | ); 1254 | 1255 | return $Inline; 1256 | } 1257 | 1258 | protected function inlineCode($Excerpt) 1259 | { 1260 | $marker = $Excerpt['text'][0]; 1261 | 1262 | if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]), 1269 | 'element' => array( 1270 | 'name' => 'code', 1271 | 'text' => $text, 1272 | ), 1273 | ); 1274 | } 1275 | } 1276 | 1277 | protected function inlineEmailTag($Excerpt) 1278 | { 1279 | $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'; 1280 | 1281 | $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@' 1282 | . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*'; 1283 | 1284 | if (strpos($Excerpt['text'], '>') !== false 1285 | and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches) 1286 | ){ 1287 | $url = $matches[1]; 1288 | 1289 | if ( ! isset($matches[2])) 1290 | { 1291 | $url = "mailto:$url"; 1292 | } 1293 | 1294 | return array( 1295 | 'extent' => strlen($matches[0]), 1296 | 'element' => array( 1297 | 'name' => 'a', 1298 | 'text' => $matches[1], 1299 | 'attributes' => array( 1300 | 'href' => $url, 1301 | ), 1302 | ), 1303 | ); 1304 | } 1305 | } 1306 | 1307 | protected function inlineEmphasis($Excerpt) 1308 | { 1309 | if ( ! isset($Excerpt['text'][1])) 1310 | { 1311 | return; 1312 | } 1313 | 1314 | $marker = $Excerpt['text'][0]; 1315 | 1316 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) 1317 | { 1318 | $emphasis = 'strong'; 1319 | } 1320 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) 1321 | { 1322 | $emphasis = 'em'; 1323 | } 1324 | else 1325 | { 1326 | return; 1327 | } 1328 | 1329 | return array( 1330 | 'extent' => strlen($matches[0]), 1331 | 'element' => array( 1332 | 'name' => $emphasis, 1333 | 'handler' => array( 1334 | 'function' => 'lineElements', 1335 | 'argument' => $matches[1], 1336 | 'destination' => 'elements', 1337 | ) 1338 | ), 1339 | ); 1340 | } 1341 | 1342 | protected function inlineEscapeSequence($Excerpt) 1343 | { 1344 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) 1345 | { 1346 | return array( 1347 | 'element' => array('rawHtml' => $Excerpt['text'][1]), 1348 | 'extent' => 2, 1349 | ); 1350 | } 1351 | } 1352 | 1353 | protected function inlineImage($Excerpt) 1354 | { 1355 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') 1356 | { 1357 | return; 1358 | } 1359 | 1360 | $Excerpt['text']= substr($Excerpt['text'], 1); 1361 | 1362 | $Link = $this->inlineLink($Excerpt); 1363 | 1364 | if ($Link === null) 1365 | { 1366 | return; 1367 | } 1368 | 1369 | $Inline = array( 1370 | 'extent' => $Link['extent'] + 1, 1371 | 'element' => array( 1372 | 'name' => 'img', 1373 | 'attributes' => array( 1374 | 'src' => $Link['element']['attributes']['href'], 1375 | 'alt' => $Link['element']['handler']['argument'], 1376 | ), 1377 | 'autobreak' => true, 1378 | ), 1379 | ); 1380 | 1381 | $Inline['element']['attributes'] += $Link['element']['attributes']; 1382 | 1383 | unset($Inline['element']['attributes']['href']); 1384 | 1385 | return $Inline; 1386 | } 1387 | 1388 | protected function inlineLink($Excerpt) 1389 | { 1390 | $Element = array( 1391 | 'name' => 'a', 1392 | 'handler' => array( 1393 | 'function' => 'lineElements', 1394 | 'argument' => null, 1395 | 'destination' => 'elements', 1396 | ), 1397 | 'nonNestables' => array('Url', 'Link'), 1398 | 'attributes' => array( 1399 | 'href' => null, 1400 | 'title' => null, 1401 | ), 1402 | ); 1403 | 1404 | $extent = 0; 1405 | 1406 | $remainder = $Excerpt['text']; 1407 | 1408 | if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) 1409 | { 1410 | $Element['handler']['argument'] = $matches[1]; 1411 | 1412 | $extent += strlen($matches[0]); 1413 | 1414 | $remainder = substr($remainder, $extent); 1415 | } 1416 | else 1417 | { 1418 | return; 1419 | } 1420 | 1421 | if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) 1422 | { 1423 | $Element['attributes']['href'] = $matches[1]; 1424 | 1425 | if (isset($matches[2])) 1426 | { 1427 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); 1428 | } 1429 | 1430 | $extent += strlen($matches[0]); 1431 | } 1432 | else 1433 | { 1434 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) 1435 | { 1436 | $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument']; 1437 | $definition = strtolower($definition); 1438 | 1439 | $extent += strlen($matches[0]); 1440 | } 1441 | else 1442 | { 1443 | $definition = strtolower($Element['handler']['argument']); 1444 | } 1445 | 1446 | if ( ! isset($this->DefinitionData['Reference'][$definition])) 1447 | { 1448 | return; 1449 | } 1450 | 1451 | $Definition = $this->DefinitionData['Reference'][$definition]; 1452 | 1453 | $Element['attributes']['href'] = $Definition['url']; 1454 | $Element['attributes']['title'] = $Definition['title']; 1455 | } 1456 | 1457 | return array( 1458 | 'extent' => $extent, 1459 | 'element' => $Element, 1460 | ); 1461 | } 1462 | 1463 | protected function inlineMarkup($Excerpt) 1464 | { 1465 | if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) 1466 | { 1467 | return; 1468 | } 1469 | 1470 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) 1471 | { 1472 | return array( 1473 | 'element' => array('rawHtml' => $matches[0]), 1474 | 'extent' => strlen($matches[0]), 1475 | ); 1476 | } 1477 | 1478 | if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches)) 1479 | { 1480 | return array( 1481 | 'element' => array('rawHtml' => $matches[0]), 1482 | 'extent' => strlen($matches[0]), 1483 | ); 1484 | } 1485 | 1486 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) 1487 | { 1488 | return array( 1489 | 'element' => array('rawHtml' => $matches[0]), 1490 | 'extent' => strlen($matches[0]), 1491 | ); 1492 | } 1493 | } 1494 | 1495 | protected function inlineSpecialCharacter($Excerpt) 1496 | { 1497 | if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false 1498 | and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches) 1499 | ) { 1500 | return array( 1501 | 'element' => array('rawHtml' => '&' . $matches[1] . ';'), 1502 | 'extent' => strlen($matches[0]), 1503 | ); 1504 | } 1505 | 1506 | return; 1507 | } 1508 | 1509 | protected function inlineStrikethrough($Excerpt) 1510 | { 1511 | if ( ! isset($Excerpt['text'][1])) 1512 | { 1513 | return; 1514 | } 1515 | 1516 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) 1517 | { 1518 | return array( 1519 | 'extent' => strlen($matches[0]), 1520 | 'element' => array( 1521 | 'name' => 'del', 1522 | 'handler' => array( 1523 | 'function' => 'lineElements', 1524 | 'argument' => $matches[1], 1525 | 'destination' => 'elements', 1526 | ) 1527 | ), 1528 | ); 1529 | } 1530 | } 1531 | 1532 | protected function inlineUrl($Excerpt) 1533 | { 1534 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') 1535 | { 1536 | return; 1537 | } 1538 | 1539 | if (strpos($Excerpt['context'], 'http') !== false 1540 | and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE) 1541 | ) { 1542 | $url = $matches[0][0]; 1543 | 1544 | $Inline = array( 1545 | 'extent' => strlen($matches[0][0]), 1546 | 'position' => $matches[0][1], 1547 | 'element' => array( 1548 | 'name' => 'a', 1549 | 'text' => $url, 1550 | 'attributes' => array( 1551 | 'href' => $url, 1552 | ), 1553 | ), 1554 | ); 1555 | 1556 | return $Inline; 1557 | } 1558 | } 1559 | 1560 | protected function inlineUrlTag($Excerpt) 1561 | { 1562 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) 1563 | { 1564 | $url = $matches[1]; 1565 | 1566 | return array( 1567 | 'extent' => strlen($matches[0]), 1568 | 'element' => array( 1569 | 'name' => 'a', 1570 | 'text' => $url, 1571 | 'attributes' => array( 1572 | 'href' => $url, 1573 | ), 1574 | ), 1575 | ); 1576 | } 1577 | } 1578 | 1579 | # ~ 1580 | 1581 | protected function unmarkedText($text) 1582 | { 1583 | $Inline = $this->inlineText($text); 1584 | return $this->element($Inline['element']); 1585 | } 1586 | 1587 | # 1588 | # Handlers 1589 | # 1590 | 1591 | protected function handle(array $Element) 1592 | { 1593 | if (isset($Element['handler'])) 1594 | { 1595 | if (!isset($Element['nonNestables'])) 1596 | { 1597 | $Element['nonNestables'] = array(); 1598 | } 1599 | 1600 | if (is_string($Element['handler'])) 1601 | { 1602 | $function = $Element['handler']; 1603 | $argument = $Element['text']; 1604 | unset($Element['text']); 1605 | $destination = 'rawHtml'; 1606 | } 1607 | else 1608 | { 1609 | $function = $Element['handler']['function']; 1610 | $argument = $Element['handler']['argument']; 1611 | $destination = $Element['handler']['destination']; 1612 | } 1613 | 1614 | $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']); 1615 | 1616 | if ($destination === 'handler') 1617 | { 1618 | $Element = $this->handle($Element); 1619 | } 1620 | 1621 | unset($Element['handler']); 1622 | } 1623 | 1624 | return $Element; 1625 | } 1626 | 1627 | protected function handleElementRecursive(array $Element) 1628 | { 1629 | return $this->elementApplyRecursive(array($this, 'handle'), $Element); 1630 | } 1631 | 1632 | protected function handleElementsRecursive(array $Elements) 1633 | { 1634 | return $this->elementsApplyRecursive(array($this, 'handle'), $Elements); 1635 | } 1636 | 1637 | protected function elementApplyRecursive($closure, array $Element) 1638 | { 1639 | $Element = call_user_func($closure, $Element); 1640 | 1641 | if (isset($Element['elements'])) 1642 | { 1643 | $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']); 1644 | } 1645 | elseif (isset($Element['element'])) 1646 | { 1647 | $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']); 1648 | } 1649 | 1650 | return $Element; 1651 | } 1652 | 1653 | protected function elementApplyRecursiveDepthFirst($closure, array $Element) 1654 | { 1655 | if (isset($Element['elements'])) 1656 | { 1657 | $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']); 1658 | } 1659 | elseif (isset($Element['element'])) 1660 | { 1661 | $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']); 1662 | } 1663 | 1664 | $Element = call_user_func($closure, $Element); 1665 | 1666 | return $Element; 1667 | } 1668 | 1669 | protected function elementsApplyRecursive($closure, array $Elements) 1670 | { 1671 | foreach ($Elements as &$Element) 1672 | { 1673 | $Element = $this->elementApplyRecursive($closure, $Element); 1674 | } 1675 | 1676 | return $Elements; 1677 | } 1678 | 1679 | protected function elementsApplyRecursiveDepthFirst($closure, array $Elements) 1680 | { 1681 | foreach ($Elements as &$Element) 1682 | { 1683 | $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element); 1684 | } 1685 | 1686 | return $Elements; 1687 | } 1688 | 1689 | protected function element(array $Element) 1690 | { 1691 | if ($this->safeMode) 1692 | { 1693 | $Element = $this->sanitiseElement($Element); 1694 | } 1695 | 1696 | # identity map if element has no handler 1697 | $Element = $this->handle($Element); 1698 | 1699 | $hasName = isset($Element['name']); 1700 | 1701 | $markup = ''; 1702 | 1703 | if ($hasName) 1704 | { 1705 | $markup .= '<' . $Element['name']; 1706 | 1707 | if (isset($Element['attributes'])) 1708 | { 1709 | foreach ($Element['attributes'] as $name => $value) 1710 | { 1711 | if ($value === null) 1712 | { 1713 | continue; 1714 | } 1715 | 1716 | $markup .= " $name=\"".self::escape($value).'"'; 1717 | } 1718 | } 1719 | } 1720 | 1721 | $permitRawHtml = false; 1722 | 1723 | if (isset($Element['text'])) 1724 | { 1725 | $text = $Element['text']; 1726 | } 1727 | // very strongly consider an alternative if you're writing an 1728 | // extension 1729 | elseif (isset($Element['rawHtml'])) 1730 | { 1731 | $text = $Element['rawHtml']; 1732 | 1733 | $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode']; 1734 | $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode; 1735 | } 1736 | 1737 | $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']); 1738 | 1739 | if ($hasContent) 1740 | { 1741 | $markup .= $hasName ? '>' : ''; 1742 | 1743 | if (isset($Element['elements'])) 1744 | { 1745 | $markup .= $this->elements($Element['elements']); 1746 | } 1747 | elseif (isset($Element['element'])) 1748 | { 1749 | $markup .= $this->element($Element['element']); 1750 | } 1751 | else 1752 | { 1753 | if (!$permitRawHtml) 1754 | { 1755 | $markup .= self::escape($text, true); 1756 | } 1757 | else 1758 | { 1759 | $markup .= $text; 1760 | } 1761 | } 1762 | 1763 | $markup .= $hasName ? '' : ''; 1764 | } 1765 | elseif ($hasName) 1766 | { 1767 | $markup .= ' />'; 1768 | } 1769 | 1770 | return $markup; 1771 | } 1772 | 1773 | protected function elements(array $Elements) 1774 | { 1775 | $markup = ''; 1776 | 1777 | $autoBreak = true; 1778 | 1779 | foreach ($Elements as $Element) 1780 | { 1781 | if (empty($Element)) 1782 | { 1783 | continue; 1784 | } 1785 | 1786 | $autoBreakNext = (isset($Element['autobreak']) 1787 | ? $Element['autobreak'] : isset($Element['name']) 1788 | ); 1789 | // (autobreak === false) covers both sides of an element 1790 | $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext; 1791 | 1792 | $markup .= ($autoBreak ? "\n" : '') . $this->element($Element); 1793 | $autoBreak = $autoBreakNext; 1794 | } 1795 | 1796 | $markup .= $autoBreak ? "\n" : ''; 1797 | 1798 | return $markup; 1799 | } 1800 | 1801 | # ~ 1802 | 1803 | protected function li($lines) 1804 | { 1805 | $Elements = $this->linesElements($lines); 1806 | 1807 | if ( ! in_array('', $lines) 1808 | and isset($Elements[0]) and isset($Elements[0]['name']) 1809 | and $Elements[0]['name'] === 'p' 1810 | ) { 1811 | unset($Elements[0]['name']); 1812 | } 1813 | 1814 | return $Elements; 1815 | } 1816 | 1817 | # 1818 | # AST Convenience 1819 | # 1820 | 1821 | /** 1822 | * Replace occurrences $regexp with $Elements in $text. Return an array of 1823 | * elements representing the replacement. 1824 | */ 1825 | protected static function pregReplaceElements($regexp, $Elements, $text) 1826 | { 1827 | $newElements = array(); 1828 | 1829 | while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) 1830 | { 1831 | $offset = $matches[0][1]; 1832 | $before = substr($text, 0, $offset); 1833 | $after = substr($text, $offset + strlen($matches[0][0])); 1834 | 1835 | $newElements[] = array('text' => $before); 1836 | 1837 | foreach ($Elements as $Element) 1838 | { 1839 | $newElements[] = $Element; 1840 | } 1841 | 1842 | $text = $after; 1843 | } 1844 | 1845 | $newElements[] = array('text' => $text); 1846 | 1847 | return $newElements; 1848 | } 1849 | 1850 | # 1851 | # Deprecated Methods 1852 | # 1853 | 1854 | function parse($text) 1855 | { 1856 | $markup = $this->text($text); 1857 | 1858 | return $markup; 1859 | } 1860 | 1861 | protected function sanitiseElement(array $Element) 1862 | { 1863 | static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/'; 1864 | static $safeUrlNameToAtt = array( 1865 | 'a' => 'href', 1866 | 'img' => 'src', 1867 | ); 1868 | 1869 | if ( ! isset($Element['name'])) 1870 | { 1871 | unset($Element['attributes']); 1872 | return $Element; 1873 | } 1874 | 1875 | if (isset($safeUrlNameToAtt[$Element['name']])) 1876 | { 1877 | $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]); 1878 | } 1879 | 1880 | if ( ! empty($Element['attributes'])) 1881 | { 1882 | foreach ($Element['attributes'] as $att => $val) 1883 | { 1884 | # filter out badly parsed attribute 1885 | if ( ! preg_match($goodAttribute, $att)) 1886 | { 1887 | unset($Element['attributes'][$att]); 1888 | } 1889 | # dump onevent attribute 1890 | elseif (self::striAtStart($att, 'on')) 1891 | { 1892 | unset($Element['attributes'][$att]); 1893 | } 1894 | } 1895 | } 1896 | 1897 | return $Element; 1898 | } 1899 | 1900 | protected function filterUnsafeUrlInAttribute(array $Element, $attribute) 1901 | { 1902 | foreach ($this->safeLinksWhitelist as $scheme) 1903 | { 1904 | if (self::striAtStart($Element['attributes'][$attribute], $scheme)) 1905 | { 1906 | return $Element; 1907 | } 1908 | } 1909 | 1910 | $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]); 1911 | 1912 | return $Element; 1913 | } 1914 | 1915 | # 1916 | # Static Methods 1917 | # 1918 | 1919 | protected static function escape($text, $allowQuotes = false) 1920 | { 1921 | return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8'); 1922 | } 1923 | 1924 | protected static function striAtStart($string, $needle) 1925 | { 1926 | $len = strlen($needle); 1927 | 1928 | if ($len > strlen($string)) 1929 | { 1930 | return false; 1931 | } 1932 | else 1933 | { 1934 | return strtolower(substr($string, 0, $len)) === strtolower($needle); 1935 | } 1936 | } 1937 | 1938 | static function instance($name = 'default') 1939 | { 1940 | if (isset(self::$instances[$name])) 1941 | { 1942 | return self::$instances[$name]; 1943 | } 1944 | 1945 | $instance = new static(); 1946 | 1947 | self::$instances[$name] = $instance; 1948 | 1949 | return $instance; 1950 | } 1951 | 1952 | private static $instances = array(); 1953 | 1954 | # 1955 | # Fields 1956 | # 1957 | 1958 | protected $DefinitionData; 1959 | 1960 | # 1961 | # Read-Only 1962 | 1963 | protected $specialCharacters = array( 1964 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~' 1965 | ); 1966 | 1967 | protected $StrongRegex = array( 1968 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s', 1969 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us', 1970 | ); 1971 | 1972 | protected $EmRegex = array( 1973 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', 1974 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', 1975 | ); 1976 | 1977 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+'; 1978 | 1979 | protected $voidElements = array( 1980 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 1981 | ); 1982 | 1983 | protected $textLevelElements = array( 1984 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', 1985 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', 1986 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', 1987 | 'q', 'rt', 'ins', 'font', 'strong', 1988 | 's', 'tt', 'kbd', 'mark', 1989 | 'u', 'xm', 'sub', 'nobr', 1990 | 'sup', 'ruby', 1991 | 'var', 'span', 1992 | 'wbr', 'time', 1993 | ); 1994 | } -------------------------------------------------------------------------------- /Parsedown/Plugin.php: -------------------------------------------------------------------------------- 1 | markdown = __CLASS__ . '::parse'; 24 | \Typecho\Plugin::factory('Widget_Abstract_Comments')->markdown = __CLASS__ . '::parse'; 25 | } 26 | 27 | /** 28 | * 禁用插件方法,如果禁用失败,直接抛出异常 29 | */ 30 | public static function deactivate() 31 | {} 32 | 33 | /** 34 | * 获取插件配置面板 35 | * 36 | * @param Form $form 配置面板 37 | */ 38 | public static function config(Form $form) 39 | {} 40 | 41 | /** 42 | * 个人用户的配置面板 43 | * 44 | * @param Form $form 45 | */ 46 | public static function personalConfig(Form $form) 47 | {} 48 | 49 | public static function parse($text) 50 | { 51 | return Parsedown::instance()->setBreaksEnabled(true)->text($text); 52 | } 53 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Typecho plugins 2 | 3 | # 目录 4 | 5 | * [CaptchaPlus](#captchaplus) 6 | * [Parsedown](#parsedown) 7 | 8 | # 使用 9 | 10 | ## CaptchaPlus 11 | 12 | _**Update 2023-01-22**_ 13 | 14 | 添加 [Cloudflare Turnstile](https://www.cloudflare-cn.com/products/turnstile/) 验证工具支持。 15 | 16 | --- 17 | 18 | **Typecho 版本:>= 1.2.0** 19 | 20 | 1. 注册 [hCaptcha](https://www.hcaptcha.com/signup-interstitial) 或者 [Cloudflare](https://dash.cloudflare.com/sign-up) 账号,在 Sites 菜单栏里点击 `New Site` 添加一个网站获取 `Site Key`,点击你的头像 - Settings 获取 `Secret Key`; 21 | 22 | 2. 下载插件,文件夹命名为 `CaptchaPlus` 上传到 Typecho 网站目录 `/usr/plugins/` 路径下; 23 | 24 | 3. 进入网站后台-控制台-插件,找到 CaptchaPlus 点击启用并设置。 25 | 26 | 4. 打开 `/usr/themes/` 你的主题目录下 `comments.php` 文件,在提交按钮前面/后面插入下面代码: 27 | ```php 28 | 29 | ``` 30 | 31 | 5. 如果提交评论失败,可能是开启了评论反垃圾保护导致,在网站后台-设置-评论里关闭,或者在主题目录下的 `functions.php` 文件中找到 `function themeInit()` 函数,里面添加: 32 | ```php 33 | $options = Helper::options(); 34 | $options -> commentsAntiSpam = false; 35 | ``` 36 | 37 | 在[我的博客](https://atpx.com/typecho-captchaplus-plugin/)中查看更详细的介绍。 38 | 39 | ### Thanks 40 | - [reCAPTCHA](https://github.com/shuxiao9058/reCAPTCHA) 41 | - [CommentFilter](https://www.imhan.com) 42 | 43 | ## Parsedown 44 | 45 | **Typecho 版本:>= 1.2.0** 46 | 47 | 将 Typecho 默认的 Markdown 解析器 [Hyperdown](https://github.com/segmentfault/HyperDown) 替换为 [Parsedown](https://github.com/erusev/parsedown)。 48 | 49 | 1. 下载插件,文件夹命名为 `Parsedown` 上传到 Typecho 网站目录 `/usr/plugins/` 路径下; 50 | 51 | 2. 进入网站后台-控制台-插件,找到 Parsedown 点击启用即可,默认会替换文章和评论内容的解析。 52 | 53 | --------------------------------------------------------------------------------