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