diff options
author | ArthurHoaro <arthur@hoa.ro> | 2015-11-17 21:01:11 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2016-01-03 17:36:10 +0100 |
commit | 1be4afacf98e0124258199ec416dc1c4b4948305 (patch) | |
tree | 8c0bdf7997d5a9a048a8fb7503417422caa9f4aa /plugins | |
parent | 79851b489087f8a3027ecd805255cd13ee6fcf63 (diff) | |
download | Shaarli-1be4afacf98e0124258199ec416dc1c4b4948305.tar.gz Shaarli-1be4afacf98e0124258199ec416dc1c4b4948305.tar.zst Shaarli-1be4afacf98e0124258199ec416dc1c4b4948305.zip |
PLUGIN Markdown
Parse link description in Markdown (HTML) before rendering.
* hard remove of Shaarli's HTML before parsing.
* Using Parsedown <https://github.com/erusev/parsedown> PHP lib.
* Includes basic markdown CSS.
* Style: removed 400px height max limit for shaares.
* Unit tests.
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/markdown/Parsedown.php | 1528 | ||||
-rw-r--r-- | plugins/markdown/README.md | 51 | ||||
-rw-r--r-- | plugins/markdown/help.html | 5 | ||||
-rw-r--r-- | plugins/markdown/markdown.css | 137 | ||||
-rw-r--r-- | plugins/markdown/markdown.meta | 1 | ||||
-rw-r--r-- | plugins/markdown/markdown.php | 158 |
6 files changed, 1880 insertions, 0 deletions
diff --git a/plugins/markdown/Parsedown.php b/plugins/markdown/Parsedown.php new file mode 100644 index 00000000..91e05dcc --- /dev/null +++ b/plugins/markdown/Parsedown.php | |||
@@ -0,0 +1,1528 @@ | |||
1 | <?php | ||
2 | |||
3 | # | ||
4 | # | ||
5 | # Parsedown | ||
6 | # http://parsedown.org | ||
7 | # | ||
8 | # (c) Emanuil Rusev | ||
9 | # http://erusev.com | ||
10 | # | ||
11 | # For the full license information, view the LICENSE file that was distributed | ||
12 | # with this source code. | ||
13 | # | ||
14 | # | ||
15 | |||
16 | class Parsedown | ||
17 | { | ||
18 | # ~ | ||
19 | |||
20 | const version = '1.6.0'; | ||
21 | |||
22 | # ~ | ||
23 | |||
24 | function text($text) | ||
25 | { | ||
26 | # make sure no definitions are set | ||
27 | $this->DefinitionData = array(); | ||
28 | |||
29 | # standardize line breaks | ||
30 | $text = str_replace(array("\r\n", "\r"), "\n", $text); | ||
31 | |||
32 | # remove surrounding line breaks | ||
33 | $text = trim($text, "\n"); | ||
34 | |||
35 | # split text into lines | ||
36 | $lines = explode("\n", $text); | ||
37 | |||
38 | # iterate through lines to identify blocks | ||
39 | $markup = $this->lines($lines); | ||
40 | |||
41 | # trim line breaks | ||
42 | $markup = trim($markup, "\n"); | ||
43 | |||
44 | return $markup; | ||
45 | } | ||
46 | |||
47 | # | ||
48 | # Setters | ||
49 | # | ||
50 | |||
51 | function setBreaksEnabled($breaksEnabled) | ||
52 | { | ||
53 | $this->breaksEnabled = $breaksEnabled; | ||
54 | |||
55 | return $this; | ||
56 | } | ||
57 | |||
58 | protected $breaksEnabled; | ||
59 | |||
60 | function setMarkupEscaped($markupEscaped) | ||
61 | { | ||
62 | $this->markupEscaped = $markupEscaped; | ||
63 | |||
64 | return $this; | ||
65 | } | ||
66 | |||
67 | protected $markupEscaped; | ||
68 | |||
69 | function setUrlsLinked($urlsLinked) | ||
70 | { | ||
71 | $this->urlsLinked = $urlsLinked; | ||
72 | |||
73 | return $this; | ||
74 | } | ||
75 | |||
76 | protected $urlsLinked = true; | ||
77 | |||
78 | # | ||
79 | # Lines | ||
80 | # | ||
81 | |||
82 | protected $BlockTypes = array( | ||
83 | '#' => array('Header'), | ||
84 | '*' => array('Rule', 'List'), | ||
85 | '+' => array('List'), | ||
86 | '-' => array('SetextHeader', 'Table', 'Rule', 'List'), | ||
87 | '0' => array('List'), | ||
88 | '1' => array('List'), | ||
89 | '2' => array('List'), | ||
90 | '3' => array('List'), | ||
91 | '4' => array('List'), | ||
92 | '5' => array('List'), | ||
93 | '6' => array('List'), | ||
94 | '7' => array('List'), | ||
95 | '8' => array('List'), | ||
96 | '9' => array('List'), | ||
97 | ':' => array('Table'), | ||
98 | '<' => array('Comment', 'Markup'), | ||
99 | '=' => array('SetextHeader'), | ||
100 | '>' => array('Quote'), | ||
101 | '[' => array('Reference'), | ||
102 | '_' => array('Rule'), | ||
103 | '`' => array('FencedCode'), | ||
104 | '|' => array('Table'), | ||
105 | '~' => array('FencedCode'), | ||
106 | ); | ||
107 | |||
108 | # ~ | ||
109 | |||
110 | protected $unmarkedBlockTypes = array( | ||
111 | 'Code', | ||
112 | ); | ||
113 | |||
114 | # | ||
115 | # Blocks | ||
116 | # | ||
117 | |||
118 | private function lines(array $lines) | ||
119 | { | ||
120 | $CurrentBlock = null; | ||
121 | |||
122 | foreach ($lines as $line) | ||
123 | { | ||
124 | if (chop($line) === '') | ||
125 | { | ||
126 | if (isset($CurrentBlock)) | ||
127 | { | ||
128 | $CurrentBlock['interrupted'] = true; | ||
129 | } | ||
130 | |||
131 | continue; | ||
132 | } | ||
133 | |||
134 | if (strpos($line, "\t") !== false) | ||
135 | { | ||
136 | $parts = explode("\t", $line); | ||
137 | |||
138 | $line = $parts[0]; | ||
139 | |||
140 | unset($parts[0]); | ||
141 | |||
142 | foreach ($parts as $part) | ||
143 | { | ||
144 | $shortage = 4 - mb_strlen($line, 'utf-8') % 4; | ||
145 | |||
146 | $line .= str_repeat(' ', $shortage); | ||
147 | $line .= $part; | ||
148 | } | ||
149 | } | ||
150 | |||
151 | $indent = 0; | ||
152 | |||
153 | while (isset($line[$indent]) and $line[$indent] === ' ') | ||
154 | { | ||
155 | $indent ++; | ||
156 | } | ||
157 | |||
158 | $text = $indent > 0 ? substr($line, $indent) : $line; | ||
159 | |||
160 | # ~ | ||
161 | |||
162 | $Line = array('body' => $line, 'indent' => $indent, 'text' => $text); | ||
163 | |||
164 | # ~ | ||
165 | |||
166 | if (isset($CurrentBlock['continuable'])) | ||
167 | { | ||
168 | $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock); | ||
169 | |||
170 | if (isset($Block)) | ||
171 | { | ||
172 | $CurrentBlock = $Block; | ||
173 | |||
174 | continue; | ||
175 | } | ||
176 | else | ||
177 | { | ||
178 | if (method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) | ||
179 | { | ||
180 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | |||
185 | # ~ | ||
186 | |||
187 | $marker = $text[0]; | ||
188 | |||
189 | # ~ | ||
190 | |||
191 | $blockTypes = $this->unmarkedBlockTypes; | ||
192 | |||
193 | if (isset($this->BlockTypes[$marker])) | ||
194 | { | ||
195 | foreach ($this->BlockTypes[$marker] as $blockType) | ||
196 | { | ||
197 | $blockTypes []= $blockType; | ||
198 | } | ||
199 | } | ||
200 | |||
201 | # | ||
202 | # ~ | ||
203 | |||
204 | foreach ($blockTypes as $blockType) | ||
205 | { | ||
206 | $Block = $this->{'block'.$blockType}($Line, $CurrentBlock); | ||
207 | |||
208 | if (isset($Block)) | ||
209 | { | ||
210 | $Block['type'] = $blockType; | ||
211 | |||
212 | if ( ! isset($Block['identified'])) | ||
213 | { | ||
214 | $Blocks []= $CurrentBlock; | ||
215 | |||
216 | $Block['identified'] = true; | ||
217 | } | ||
218 | |||
219 | if (method_exists($this, 'block'.$blockType.'Continue')) | ||
220 | { | ||
221 | $Block['continuable'] = true; | ||
222 | } | ||
223 | |||
224 | $CurrentBlock = $Block; | ||
225 | |||
226 | continue 2; | ||
227 | } | ||
228 | } | ||
229 | |||
230 | # ~ | ||
231 | |||
232 | if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted'])) | ||
233 | { | ||
234 | $CurrentBlock['element']['text'] .= "\n".$text; | ||
235 | } | ||
236 | else | ||
237 | { | ||
238 | $Blocks []= $CurrentBlock; | ||
239 | |||
240 | $CurrentBlock = $this->paragraph($Line); | ||
241 | |||
242 | $CurrentBlock['identified'] = true; | ||
243 | } | ||
244 | } | ||
245 | |||
246 | # ~ | ||
247 | |||
248 | if (isset($CurrentBlock['continuable']) and method_exists($this, 'block'.$CurrentBlock['type'].'Complete')) | ||
249 | { | ||
250 | $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock); | ||
251 | } | ||
252 | |||
253 | # ~ | ||
254 | |||
255 | $Blocks []= $CurrentBlock; | ||
256 | |||
257 | unset($Blocks[0]); | ||
258 | |||
259 | # ~ | ||
260 | |||
261 | $markup = ''; | ||
262 | |||
263 | foreach ($Blocks as $Block) | ||
264 | { | ||
265 | if (isset($Block['hidden'])) | ||
266 | { | ||
267 | continue; | ||
268 | } | ||
269 | |||
270 | $markup .= "\n"; | ||
271 | $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']); | ||
272 | } | ||
273 | |||
274 | $markup .= "\n"; | ||
275 | |||
276 | # ~ | ||
277 | |||
278 | return $markup; | ||
279 | } | ||
280 | |||
281 | # | ||
282 | # Code | ||
283 | |||
284 | protected function blockCode($Line, $Block = null) | ||
285 | { | ||
286 | if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted'])) | ||
287 | { | ||
288 | return; | ||
289 | } | ||
290 | |||
291 | if ($Line['indent'] >= 4) | ||
292 | { | ||
293 | $text = substr($Line['body'], 4); | ||
294 | |||
295 | $Block = array( | ||
296 | 'element' => array( | ||
297 | 'name' => 'pre', | ||
298 | 'handler' => 'element', | ||
299 | 'text' => array( | ||
300 | 'name' => 'code', | ||
301 | 'text' => $text, | ||
302 | ), | ||
303 | ), | ||
304 | ); | ||
305 | |||
306 | return $Block; | ||
307 | } | ||
308 | } | ||
309 | |||
310 | protected function blockCodeContinue($Line, $Block) | ||
311 | { | ||
312 | if ($Line['indent'] >= 4) | ||
313 | { | ||
314 | if (isset($Block['interrupted'])) | ||
315 | { | ||
316 | $Block['element']['text']['text'] .= "\n"; | ||
317 | |||
318 | unset($Block['interrupted']); | ||
319 | } | ||
320 | |||
321 | $Block['element']['text']['text'] .= "\n"; | ||
322 | |||
323 | $text = substr($Line['body'], 4); | ||
324 | |||
325 | $Block['element']['text']['text'] .= $text; | ||
326 | |||
327 | return $Block; | ||
328 | } | ||
329 | } | ||
330 | |||
331 | protected function blockCodeComplete($Block) | ||
332 | { | ||
333 | $text = $Block['element']['text']['text']; | ||
334 | |||
335 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
336 | |||
337 | $Block['element']['text']['text'] = $text; | ||
338 | |||
339 | return $Block; | ||
340 | } | ||
341 | |||
342 | # | ||
343 | # Comment | ||
344 | |||
345 | protected function blockComment($Line) | ||
346 | { | ||
347 | if ($this->markupEscaped) | ||
348 | { | ||
349 | return; | ||
350 | } | ||
351 | |||
352 | if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') | ||
353 | { | ||
354 | $Block = array( | ||
355 | 'markup' => $Line['body'], | ||
356 | ); | ||
357 | |||
358 | if (preg_match('/-->$/', $Line['text'])) | ||
359 | { | ||
360 | $Block['closed'] = true; | ||
361 | } | ||
362 | |||
363 | return $Block; | ||
364 | } | ||
365 | } | ||
366 | |||
367 | protected function blockCommentContinue($Line, array $Block) | ||
368 | { | ||
369 | if (isset($Block['closed'])) | ||
370 | { | ||
371 | return; | ||
372 | } | ||
373 | |||
374 | $Block['markup'] .= "\n" . $Line['body']; | ||
375 | |||
376 | if (preg_match('/-->$/', $Line['text'])) | ||
377 | { | ||
378 | $Block['closed'] = true; | ||
379 | } | ||
380 | |||
381 | return $Block; | ||
382 | } | ||
383 | |||
384 | # | ||
385 | # Fenced Code | ||
386 | |||
387 | protected function blockFencedCode($Line) | ||
388 | { | ||
389 | if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches)) | ||
390 | { | ||
391 | $Element = array( | ||
392 | 'name' => 'code', | ||
393 | 'text' => '', | ||
394 | ); | ||
395 | |||
396 | if (isset($matches[1])) | ||
397 | { | ||
398 | $class = 'language-'.$matches[1]; | ||
399 | |||
400 | $Element['attributes'] = array( | ||
401 | 'class' => $class, | ||
402 | ); | ||
403 | } | ||
404 | |||
405 | $Block = array( | ||
406 | 'char' => $Line['text'][0], | ||
407 | 'element' => array( | ||
408 | 'name' => 'pre', | ||
409 | 'handler' => 'element', | ||
410 | 'text' => $Element, | ||
411 | ), | ||
412 | ); | ||
413 | |||
414 | return $Block; | ||
415 | } | ||
416 | } | ||
417 | |||
418 | protected function blockFencedCodeContinue($Line, $Block) | ||
419 | { | ||
420 | if (isset($Block['complete'])) | ||
421 | { | ||
422 | return; | ||
423 | } | ||
424 | |||
425 | if (isset($Block['interrupted'])) | ||
426 | { | ||
427 | $Block['element']['text']['text'] .= "\n"; | ||
428 | |||
429 | unset($Block['interrupted']); | ||
430 | } | ||
431 | |||
432 | if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text'])) | ||
433 | { | ||
434 | $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1); | ||
435 | |||
436 | $Block['complete'] = true; | ||
437 | |||
438 | return $Block; | ||
439 | } | ||
440 | |||
441 | $Block['element']['text']['text'] .= "\n".$Line['body'];; | ||
442 | |||
443 | return $Block; | ||
444 | } | ||
445 | |||
446 | protected function blockFencedCodeComplete($Block) | ||
447 | { | ||
448 | $text = $Block['element']['text']['text']; | ||
449 | |||
450 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
451 | |||
452 | $Block['element']['text']['text'] = $text; | ||
453 | |||
454 | return $Block; | ||
455 | } | ||
456 | |||
457 | # | ||
458 | # Header | ||
459 | |||
460 | protected function blockHeader($Line) | ||
461 | { | ||
462 | if (isset($Line['text'][1])) | ||
463 | { | ||
464 | $level = 1; | ||
465 | |||
466 | while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') | ||
467 | { | ||
468 | $level ++; | ||
469 | } | ||
470 | |||
471 | if ($level > 6) | ||
472 | { | ||
473 | return; | ||
474 | } | ||
475 | |||
476 | $text = trim($Line['text'], '# '); | ||
477 | |||
478 | $Block = array( | ||
479 | 'element' => array( | ||
480 | 'name' => 'h' . min(6, $level), | ||
481 | 'text' => $text, | ||
482 | 'handler' => 'line', | ||
483 | ), | ||
484 | ); | ||
485 | |||
486 | return $Block; | ||
487 | } | ||
488 | } | ||
489 | |||
490 | # | ||
491 | # List | ||
492 | |||
493 | protected function blockList($Line) | ||
494 | { | ||
495 | list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]'); | ||
496 | |||
497 | if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches)) | ||
498 | { | ||
499 | $Block = array( | ||
500 | 'indent' => $Line['indent'], | ||
501 | 'pattern' => $pattern, | ||
502 | 'element' => array( | ||
503 | 'name' => $name, | ||
504 | 'handler' => 'elements', | ||
505 | ), | ||
506 | ); | ||
507 | |||
508 | $Block['li'] = array( | ||
509 | 'name' => 'li', | ||
510 | 'handler' => 'li', | ||
511 | 'text' => array( | ||
512 | $matches[2], | ||
513 | ), | ||
514 | ); | ||
515 | |||
516 | $Block['element']['text'] []= & $Block['li']; | ||
517 | |||
518 | return $Block; | ||
519 | } | ||
520 | } | ||
521 | |||
522 | protected function blockListContinue($Line, array $Block) | ||
523 | { | ||
524 | if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches)) | ||
525 | { | ||
526 | if (isset($Block['interrupted'])) | ||
527 | { | ||
528 | $Block['li']['text'] []= ''; | ||
529 | |||
530 | unset($Block['interrupted']); | ||
531 | } | ||
532 | |||
533 | unset($Block['li']); | ||
534 | |||
535 | $text = isset($matches[1]) ? $matches[1] : ''; | ||
536 | |||
537 | $Block['li'] = array( | ||
538 | 'name' => 'li', | ||
539 | 'handler' => 'li', | ||
540 | 'text' => array( | ||
541 | $text, | ||
542 | ), | ||
543 | ); | ||
544 | |||
545 | $Block['element']['text'] []= & $Block['li']; | ||
546 | |||
547 | return $Block; | ||
548 | } | ||
549 | |||
550 | if ($Line['text'][0] === '[' and $this->blockReference($Line)) | ||
551 | { | ||
552 | return $Block; | ||
553 | } | ||
554 | |||
555 | if ( ! isset($Block['interrupted'])) | ||
556 | { | ||
557 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); | ||
558 | |||
559 | $Block['li']['text'] []= $text; | ||
560 | |||
561 | return $Block; | ||
562 | } | ||
563 | |||
564 | if ($Line['indent'] > 0) | ||
565 | { | ||
566 | $Block['li']['text'] []= ''; | ||
567 | |||
568 | $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']); | ||
569 | |||
570 | $Block['li']['text'] []= $text; | ||
571 | |||
572 | unset($Block['interrupted']); | ||
573 | |||
574 | return $Block; | ||
575 | } | ||
576 | } | ||
577 | |||
578 | # | ||
579 | # Quote | ||
580 | |||
581 | protected function blockQuote($Line) | ||
582 | { | ||
583 | if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) | ||
584 | { | ||
585 | $Block = array( | ||
586 | 'element' => array( | ||
587 | 'name' => 'blockquote', | ||
588 | 'handler' => 'lines', | ||
589 | 'text' => (array) $matches[1], | ||
590 | ), | ||
591 | ); | ||
592 | |||
593 | return $Block; | ||
594 | } | ||
595 | } | ||
596 | |||
597 | protected function blockQuoteContinue($Line, array $Block) | ||
598 | { | ||
599 | if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) | ||
600 | { | ||
601 | if (isset($Block['interrupted'])) | ||
602 | { | ||
603 | $Block['element']['text'] []= ''; | ||
604 | |||
605 | unset($Block['interrupted']); | ||
606 | } | ||
607 | |||
608 | $Block['element']['text'] []= $matches[1]; | ||
609 | |||
610 | return $Block; | ||
611 | } | ||
612 | |||
613 | if ( ! isset($Block['interrupted'])) | ||
614 | { | ||
615 | $Block['element']['text'] []= $Line['text']; | ||
616 | |||
617 | return $Block; | ||
618 | } | ||
619 | } | ||
620 | |||
621 | # | ||
622 | # Rule | ||
623 | |||
624 | protected function blockRule($Line) | ||
625 | { | ||
626 | if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text'])) | ||
627 | { | ||
628 | $Block = array( | ||
629 | 'element' => array( | ||
630 | 'name' => 'hr' | ||
631 | ), | ||
632 | ); | ||
633 | |||
634 | return $Block; | ||
635 | } | ||
636 | } | ||
637 | |||
638 | # | ||
639 | # Setext | ||
640 | |||
641 | protected function blockSetextHeader($Line, array $Block = null) | ||
642 | { | ||
643 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) | ||
644 | { | ||
645 | return; | ||
646 | } | ||
647 | |||
648 | if (chop($Line['text'], $Line['text'][0]) === '') | ||
649 | { | ||
650 | $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2'; | ||
651 | |||
652 | return $Block; | ||
653 | } | ||
654 | } | ||
655 | |||
656 | # | ||
657 | # Markup | ||
658 | |||
659 | protected function blockMarkup($Line) | ||
660 | { | ||
661 | if ($this->markupEscaped) | ||
662 | { | ||
663 | return; | ||
664 | } | ||
665 | |||
666 | if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches)) | ||
667 | { | ||
668 | $element = strtolower($matches[1]); | ||
669 | |||
670 | if (in_array($element, $this->textLevelElements)) | ||
671 | { | ||
672 | return; | ||
673 | } | ||
674 | |||
675 | $Block = array( | ||
676 | 'name' => $matches[1], | ||
677 | 'depth' => 0, | ||
678 | 'markup' => $Line['text'], | ||
679 | ); | ||
680 | |||
681 | $length = strlen($matches[0]); | ||
682 | |||
683 | $remainder = substr($Line['text'], $length); | ||
684 | |||
685 | if (trim($remainder) === '') | ||
686 | { | ||
687 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) | ||
688 | { | ||
689 | $Block['closed'] = true; | ||
690 | |||
691 | $Block['void'] = true; | ||
692 | } | ||
693 | } | ||
694 | else | ||
695 | { | ||
696 | if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) | ||
697 | { | ||
698 | return; | ||
699 | } | ||
700 | |||
701 | if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder)) | ||
702 | { | ||
703 | $Block['closed'] = true; | ||
704 | } | ||
705 | } | ||
706 | |||
707 | return $Block; | ||
708 | } | ||
709 | } | ||
710 | |||
711 | protected function blockMarkupContinue($Line, array $Block) | ||
712 | { | ||
713 | if (isset($Block['closed'])) | ||
714 | { | ||
715 | return; | ||
716 | } | ||
717 | |||
718 | if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open | ||
719 | { | ||
720 | $Block['depth'] ++; | ||
721 | } | ||
722 | |||
723 | if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close | ||
724 | { | ||
725 | if ($Block['depth'] > 0) | ||
726 | { | ||
727 | $Block['depth'] --; | ||
728 | } | ||
729 | else | ||
730 | { | ||
731 | $Block['closed'] = true; | ||
732 | } | ||
733 | } | ||
734 | |||
735 | if (isset($Block['interrupted'])) | ||
736 | { | ||
737 | $Block['markup'] .= "\n"; | ||
738 | |||
739 | unset($Block['interrupted']); | ||
740 | } | ||
741 | |||
742 | $Block['markup'] .= "\n".$Line['body']; | ||
743 | |||
744 | return $Block; | ||
745 | } | ||
746 | |||
747 | # | ||
748 | # Reference | ||
749 | |||
750 | protected function blockReference($Line) | ||
751 | { | ||
752 | if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) | ||
753 | { | ||
754 | $id = strtolower($matches[1]); | ||
755 | |||
756 | $Data = array( | ||
757 | 'url' => $matches[2], | ||
758 | 'title' => null, | ||
759 | ); | ||
760 | |||
761 | if (isset($matches[3])) | ||
762 | { | ||
763 | $Data['title'] = $matches[3]; | ||
764 | } | ||
765 | |||
766 | $this->DefinitionData['Reference'][$id] = $Data; | ||
767 | |||
768 | $Block = array( | ||
769 | 'hidden' => true, | ||
770 | ); | ||
771 | |||
772 | return $Block; | ||
773 | } | ||
774 | } | ||
775 | |||
776 | # | ||
777 | # Table | ||
778 | |||
779 | protected function blockTable($Line, array $Block = null) | ||
780 | { | ||
781 | if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) | ||
782 | { | ||
783 | return; | ||
784 | } | ||
785 | |||
786 | if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') | ||
787 | { | ||
788 | $alignments = array(); | ||
789 | |||
790 | $divider = $Line['text']; | ||
791 | |||
792 | $divider = trim($divider); | ||
793 | $divider = trim($divider, '|'); | ||
794 | |||
795 | $dividerCells = explode('|', $divider); | ||
796 | |||
797 | foreach ($dividerCells as $dividerCell) | ||
798 | { | ||
799 | $dividerCell = trim($dividerCell); | ||
800 | |||
801 | if ($dividerCell === '') | ||
802 | { | ||
803 | continue; | ||
804 | } | ||
805 | |||
806 | $alignment = null; | ||
807 | |||
808 | if ($dividerCell[0] === ':') | ||
809 | { | ||
810 | $alignment = 'left'; | ||
811 | } | ||
812 | |||
813 | if (substr($dividerCell, - 1) === ':') | ||
814 | { | ||
815 | $alignment = $alignment === 'left' ? 'center' : 'right'; | ||
816 | } | ||
817 | |||
818 | $alignments []= $alignment; | ||
819 | } | ||
820 | |||
821 | # ~ | ||
822 | |||
823 | $HeaderElements = array(); | ||
824 | |||
825 | $header = $Block['element']['text']; | ||
826 | |||
827 | $header = trim($header); | ||
828 | $header = trim($header, '|'); | ||
829 | |||
830 | $headerCells = explode('|', $header); | ||
831 | |||
832 | foreach ($headerCells as $index => $headerCell) | ||
833 | { | ||
834 | $headerCell = trim($headerCell); | ||
835 | |||
836 | $HeaderElement = array( | ||
837 | 'name' => 'th', | ||
838 | 'text' => $headerCell, | ||
839 | 'handler' => 'line', | ||
840 | ); | ||
841 | |||
842 | if (isset($alignments[$index])) | ||
843 | { | ||
844 | $alignment = $alignments[$index]; | ||
845 | |||
846 | $HeaderElement['attributes'] = array( | ||
847 | 'style' => 'text-align: '.$alignment.';', | ||
848 | ); | ||
849 | } | ||
850 | |||
851 | $HeaderElements []= $HeaderElement; | ||
852 | } | ||
853 | |||
854 | # ~ | ||
855 | |||
856 | $Block = array( | ||
857 | 'alignments' => $alignments, | ||
858 | 'identified' => true, | ||
859 | 'element' => array( | ||
860 | 'name' => 'table', | ||
861 | 'handler' => 'elements', | ||
862 | ), | ||
863 | ); | ||
864 | |||
865 | $Block['element']['text'] []= array( | ||
866 | 'name' => 'thead', | ||
867 | 'handler' => 'elements', | ||
868 | ); | ||
869 | |||
870 | $Block['element']['text'] []= array( | ||
871 | 'name' => 'tbody', | ||
872 | 'handler' => 'elements', | ||
873 | 'text' => array(), | ||
874 | ); | ||
875 | |||
876 | $Block['element']['text'][0]['text'] []= array( | ||
877 | 'name' => 'tr', | ||
878 | 'handler' => 'elements', | ||
879 | 'text' => $HeaderElements, | ||
880 | ); | ||
881 | |||
882 | return $Block; | ||
883 | } | ||
884 | } | ||
885 | |||
886 | protected function blockTableContinue($Line, array $Block) | ||
887 | { | ||
888 | if (isset($Block['interrupted'])) | ||
889 | { | ||
890 | return; | ||
891 | } | ||
892 | |||
893 | if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) | ||
894 | { | ||
895 | $Elements = array(); | ||
896 | |||
897 | $row = $Line['text']; | ||
898 | |||
899 | $row = trim($row); | ||
900 | $row = trim($row, '|'); | ||
901 | |||
902 | preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches); | ||
903 | |||
904 | foreach ($matches[0] as $index => $cell) | ||
905 | { | ||
906 | $cell = trim($cell); | ||
907 | |||
908 | $Element = array( | ||
909 | 'name' => 'td', | ||
910 | 'handler' => 'line', | ||
911 | 'text' => $cell, | ||
912 | ); | ||
913 | |||
914 | if (isset($Block['alignments'][$index])) | ||
915 | { | ||
916 | $Element['attributes'] = array( | ||
917 | 'style' => 'text-align: '.$Block['alignments'][$index].';', | ||
918 | ); | ||
919 | } | ||
920 | |||
921 | $Elements []= $Element; | ||
922 | } | ||
923 | |||
924 | $Element = array( | ||
925 | 'name' => 'tr', | ||
926 | 'handler' => 'elements', | ||
927 | 'text' => $Elements, | ||
928 | ); | ||
929 | |||
930 | $Block['element']['text'][1]['text'] []= $Element; | ||
931 | |||
932 | return $Block; | ||
933 | } | ||
934 | } | ||
935 | |||
936 | # | ||
937 | # ~ | ||
938 | # | ||
939 | |||
940 | protected function paragraph($Line) | ||
941 | { | ||
942 | $Block = array( | ||
943 | 'element' => array( | ||
944 | 'name' => 'p', | ||
945 | 'text' => $Line['text'], | ||
946 | 'handler' => 'line', | ||
947 | ), | ||
948 | ); | ||
949 | |||
950 | return $Block; | ||
951 | } | ||
952 | |||
953 | # | ||
954 | # Inline Elements | ||
955 | # | ||
956 | |||
957 | protected $InlineTypes = array( | ||
958 | '"' => array('SpecialCharacter'), | ||
959 | '!' => array('Image'), | ||
960 | '&' => array('SpecialCharacter'), | ||
961 | '*' => array('Emphasis'), | ||
962 | ':' => array('Url'), | ||
963 | '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'), | ||
964 | '>' => array('SpecialCharacter'), | ||
965 | '[' => array('Link'), | ||
966 | '_' => array('Emphasis'), | ||
967 | '`' => array('Code'), | ||
968 | '~' => array('Strikethrough'), | ||
969 | '\\' => array('EscapeSequence'), | ||
970 | ); | ||
971 | |||
972 | # ~ | ||
973 | |||
974 | protected $inlineMarkerList = '!"*_&[:<>`~\\'; | ||
975 | |||
976 | # | ||
977 | # ~ | ||
978 | # | ||
979 | |||
980 | public function line($text) | ||
981 | { | ||
982 | $markup = ''; | ||
983 | |||
984 | # $excerpt is based on the first occurrence of a marker | ||
985 | |||
986 | while ($excerpt = strpbrk($text, $this->inlineMarkerList)) | ||
987 | { | ||
988 | $marker = $excerpt[0]; | ||
989 | |||
990 | $markerPosition = strpos($text, $marker); | ||
991 | |||
992 | $Excerpt = array('text' => $excerpt, 'context' => $text); | ||
993 | |||
994 | foreach ($this->InlineTypes[$marker] as $inlineType) | ||
995 | { | ||
996 | $Inline = $this->{'inline'.$inlineType}($Excerpt); | ||
997 | |||
998 | if ( ! isset($Inline)) | ||
999 | { | ||
1000 | continue; | ||
1001 | } | ||
1002 | |||
1003 | # makes sure that the inline belongs to "our" marker | ||
1004 | |||
1005 | if (isset($Inline['position']) and $Inline['position'] > $markerPosition) | ||
1006 | { | ||
1007 | continue; | ||
1008 | } | ||
1009 | |||
1010 | # sets a default inline position | ||
1011 | |||
1012 | if ( ! isset($Inline['position'])) | ||
1013 | { | ||
1014 | $Inline['position'] = $markerPosition; | ||
1015 | } | ||
1016 | |||
1017 | # the text that comes before the inline | ||
1018 | $unmarkedText = substr($text, 0, $Inline['position']); | ||
1019 | |||
1020 | # compile the unmarked text | ||
1021 | $markup .= $this->unmarkedText($unmarkedText); | ||
1022 | |||
1023 | # compile the inline | ||
1024 | $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']); | ||
1025 | |||
1026 | # remove the examined text | ||
1027 | $text = substr($text, $Inline['position'] + $Inline['extent']); | ||
1028 | |||
1029 | continue 2; | ||
1030 | } | ||
1031 | |||
1032 | # the marker does not belong to an inline | ||
1033 | |||
1034 | $unmarkedText = substr($text, 0, $markerPosition + 1); | ||
1035 | |||
1036 | $markup .= $this->unmarkedText($unmarkedText); | ||
1037 | |||
1038 | $text = substr($text, $markerPosition + 1); | ||
1039 | } | ||
1040 | |||
1041 | $markup .= $this->unmarkedText($text); | ||
1042 | |||
1043 | return $markup; | ||
1044 | } | ||
1045 | |||
1046 | # | ||
1047 | # ~ | ||
1048 | # | ||
1049 | |||
1050 | protected function inlineCode($Excerpt) | ||
1051 | { | ||
1052 | $marker = $Excerpt['text'][0]; | ||
1053 | |||
1054 | if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) | ||
1055 | { | ||
1056 | $text = $matches[2]; | ||
1057 | $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8'); | ||
1058 | $text = preg_replace("/[ ]*\n/", ' ', $text); | ||
1059 | |||
1060 | return array( | ||
1061 | 'extent' => strlen($matches[0]), | ||
1062 | 'element' => array( | ||
1063 | 'name' => 'code', | ||
1064 | 'text' => $text, | ||
1065 | ), | ||
1066 | ); | ||
1067 | } | ||
1068 | } | ||
1069 | |||
1070 | protected function inlineEmailTag($Excerpt) | ||
1071 | { | ||
1072 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) | ||
1073 | { | ||
1074 | $url = $matches[1]; | ||
1075 | |||
1076 | if ( ! isset($matches[2])) | ||
1077 | { | ||
1078 | $url = 'mailto:' . $url; | ||
1079 | } | ||
1080 | |||
1081 | return array( | ||
1082 | 'extent' => strlen($matches[0]), | ||
1083 | 'element' => array( | ||
1084 | 'name' => 'a', | ||
1085 | 'text' => $matches[1], | ||
1086 | 'attributes' => array( | ||
1087 | 'href' => $url, | ||
1088 | ), | ||
1089 | ), | ||
1090 | ); | ||
1091 | } | ||
1092 | } | ||
1093 | |||
1094 | protected function inlineEmphasis($Excerpt) | ||
1095 | { | ||
1096 | if ( ! isset($Excerpt['text'][1])) | ||
1097 | { | ||
1098 | return; | ||
1099 | } | ||
1100 | |||
1101 | $marker = $Excerpt['text'][0]; | ||
1102 | |||
1103 | if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) | ||
1104 | { | ||
1105 | $emphasis = 'strong'; | ||
1106 | } | ||
1107 | elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) | ||
1108 | { | ||
1109 | $emphasis = 'em'; | ||
1110 | } | ||
1111 | else | ||
1112 | { | ||
1113 | return; | ||
1114 | } | ||
1115 | |||
1116 | return array( | ||
1117 | 'extent' => strlen($matches[0]), | ||
1118 | 'element' => array( | ||
1119 | 'name' => $emphasis, | ||
1120 | 'handler' => 'line', | ||
1121 | 'text' => $matches[1], | ||
1122 | ), | ||
1123 | ); | ||
1124 | } | ||
1125 | |||
1126 | protected function inlineEscapeSequence($Excerpt) | ||
1127 | { | ||
1128 | if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) | ||
1129 | { | ||
1130 | return array( | ||
1131 | 'markup' => $Excerpt['text'][1], | ||
1132 | 'extent' => 2, | ||
1133 | ); | ||
1134 | } | ||
1135 | } | ||
1136 | |||
1137 | protected function inlineImage($Excerpt) | ||
1138 | { | ||
1139 | if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') | ||
1140 | { | ||
1141 | return; | ||
1142 | } | ||
1143 | |||
1144 | $Excerpt['text']= substr($Excerpt['text'], 1); | ||
1145 | |||
1146 | $Link = $this->inlineLink($Excerpt); | ||
1147 | |||
1148 | if ($Link === null) | ||
1149 | { | ||
1150 | return; | ||
1151 | } | ||
1152 | |||
1153 | $Inline = array( | ||
1154 | 'extent' => $Link['extent'] + 1, | ||
1155 | 'element' => array( | ||
1156 | 'name' => 'img', | ||
1157 | 'attributes' => array( | ||
1158 | 'src' => $Link['element']['attributes']['href'], | ||
1159 | 'alt' => $Link['element']['text'], | ||
1160 | ), | ||
1161 | ), | ||
1162 | ); | ||
1163 | |||
1164 | $Inline['element']['attributes'] += $Link['element']['attributes']; | ||
1165 | |||
1166 | unset($Inline['element']['attributes']['href']); | ||
1167 | |||
1168 | return $Inline; | ||
1169 | } | ||
1170 | |||
1171 | protected function inlineLink($Excerpt) | ||
1172 | { | ||
1173 | $Element = array( | ||
1174 | 'name' => 'a', | ||
1175 | 'handler' => 'line', | ||
1176 | 'text' => null, | ||
1177 | 'attributes' => array( | ||
1178 | 'href' => null, | ||
1179 | 'title' => null, | ||
1180 | ), | ||
1181 | ); | ||
1182 | |||
1183 | $extent = 0; | ||
1184 | |||
1185 | $remainder = $Excerpt['text']; | ||
1186 | |||
1187 | if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches)) | ||
1188 | { | ||
1189 | $Element['text'] = $matches[1]; | ||
1190 | |||
1191 | $extent += strlen($matches[0]); | ||
1192 | |||
1193 | $remainder = substr($remainder, $extent); | ||
1194 | } | ||
1195 | else | ||
1196 | { | ||
1197 | return; | ||
1198 | } | ||
1199 | |||
1200 | if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches)) | ||
1201 | { | ||
1202 | $Element['attributes']['href'] = $matches[1]; | ||
1203 | |||
1204 | if (isset($matches[2])) | ||
1205 | { | ||
1206 | $Element['attributes']['title'] = substr($matches[2], 1, - 1); | ||
1207 | } | ||
1208 | |||
1209 | $extent += strlen($matches[0]); | ||
1210 | } | ||
1211 | else | ||
1212 | { | ||
1213 | if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) | ||
1214 | { | ||
1215 | $definition = strlen($matches[1]) ? $matches[1] : $Element['text']; | ||
1216 | $definition = strtolower($definition); | ||
1217 | |||
1218 | $extent += strlen($matches[0]); | ||
1219 | } | ||
1220 | else | ||
1221 | { | ||
1222 | $definition = strtolower($Element['text']); | ||
1223 | } | ||
1224 | |||
1225 | if ( ! isset($this->DefinitionData['Reference'][$definition])) | ||
1226 | { | ||
1227 | return; | ||
1228 | } | ||
1229 | |||
1230 | $Definition = $this->DefinitionData['Reference'][$definition]; | ||
1231 | |||
1232 | $Element['attributes']['href'] = $Definition['url']; | ||
1233 | $Element['attributes']['title'] = $Definition['title']; | ||
1234 | } | ||
1235 | |||
1236 | $Element['attributes']['href'] = str_replace(array('&', '<'), array('&', '<'), $Element['attributes']['href']); | ||
1237 | |||
1238 | return array( | ||
1239 | 'extent' => $extent, | ||
1240 | 'element' => $Element, | ||
1241 | ); | ||
1242 | } | ||
1243 | |||
1244 | protected function inlineMarkup($Excerpt) | ||
1245 | { | ||
1246 | if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false) | ||
1247 | { | ||
1248 | return; | ||
1249 | } | ||
1250 | |||
1251 | if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches)) | ||
1252 | { | ||
1253 | return array( | ||
1254 | 'markup' => $matches[0], | ||
1255 | 'extent' => strlen($matches[0]), | ||
1256 | ); | ||
1257 | } | ||
1258 | |||
1259 | if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) | ||
1260 | { | ||
1261 | return array( | ||
1262 | 'markup' => $matches[0], | ||
1263 | 'extent' => strlen($matches[0]), | ||
1264 | ); | ||
1265 | } | ||
1266 | |||
1267 | if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches)) | ||
1268 | { | ||
1269 | return array( | ||
1270 | 'markup' => $matches[0], | ||
1271 | 'extent' => strlen($matches[0]), | ||
1272 | ); | ||
1273 | } | ||
1274 | } | ||
1275 | |||
1276 | protected function inlineSpecialCharacter($Excerpt) | ||
1277 | { | ||
1278 | if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text'])) | ||
1279 | { | ||
1280 | return array( | ||
1281 | 'markup' => '&', | ||
1282 | 'extent' => 1, | ||
1283 | ); | ||
1284 | } | ||
1285 | |||
1286 | $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot'); | ||
1287 | |||
1288 | if (isset($SpecialCharacter[$Excerpt['text'][0]])) | ||
1289 | { | ||
1290 | return array( | ||
1291 | 'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';', | ||
1292 | 'extent' => 1, | ||
1293 | ); | ||
1294 | } | ||
1295 | } | ||
1296 | |||
1297 | protected function inlineStrikethrough($Excerpt) | ||
1298 | { | ||
1299 | if ( ! isset($Excerpt['text'][1])) | ||
1300 | { | ||
1301 | return; | ||
1302 | } | ||
1303 | |||
1304 | if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) | ||
1305 | { | ||
1306 | return array( | ||
1307 | 'extent' => strlen($matches[0]), | ||
1308 | 'element' => array( | ||
1309 | 'name' => 'del', | ||
1310 | 'text' => $matches[1], | ||
1311 | 'handler' => 'line', | ||
1312 | ), | ||
1313 | ); | ||
1314 | } | ||
1315 | } | ||
1316 | |||
1317 | protected function inlineUrl($Excerpt) | ||
1318 | { | ||
1319 | if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') | ||
1320 | { | ||
1321 | return; | ||
1322 | } | ||
1323 | |||
1324 | if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) | ||
1325 | { | ||
1326 | $Inline = array( | ||
1327 | 'extent' => strlen($matches[0][0]), | ||
1328 | 'position' => $matches[0][1], | ||
1329 | 'element' => array( | ||
1330 | 'name' => 'a', | ||
1331 | 'text' => $matches[0][0], | ||
1332 | 'attributes' => array( | ||
1333 | 'href' => $matches[0][0], | ||
1334 | ), | ||
1335 | ), | ||
1336 | ); | ||
1337 | |||
1338 | return $Inline; | ||
1339 | } | ||
1340 | } | ||
1341 | |||
1342 | protected function inlineUrlTag($Excerpt) | ||
1343 | { | ||
1344 | if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) | ||
1345 | { | ||
1346 | $url = str_replace(array('&', '<'), array('&', '<'), $matches[1]); | ||
1347 | |||
1348 | return array( | ||
1349 | 'extent' => strlen($matches[0]), | ||
1350 | 'element' => array( | ||
1351 | 'name' => 'a', | ||
1352 | 'text' => $url, | ||
1353 | 'attributes' => array( | ||
1354 | 'href' => $url, | ||
1355 | ), | ||
1356 | ), | ||
1357 | ); | ||
1358 | } | ||
1359 | } | ||
1360 | |||
1361 | # ~ | ||
1362 | |||
1363 | protected function unmarkedText($text) | ||
1364 | { | ||
1365 | if ($this->breaksEnabled) | ||
1366 | { | ||
1367 | $text = preg_replace('/[ ]*\n/', "<br />\n", $text); | ||
1368 | } | ||
1369 | else | ||
1370 | { | ||
1371 | $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text); | ||
1372 | $text = str_replace(" \n", "\n", $text); | ||
1373 | } | ||
1374 | |||
1375 | return $text; | ||
1376 | } | ||
1377 | |||
1378 | # | ||
1379 | # Handlers | ||
1380 | # | ||
1381 | |||
1382 | protected function element(array $Element) | ||
1383 | { | ||
1384 | $markup = '<'.$Element['name']; | ||
1385 | |||
1386 | if (isset($Element['attributes'])) | ||
1387 | { | ||
1388 | foreach ($Element['attributes'] as $name => $value) | ||
1389 | { | ||
1390 | if ($value === null) | ||
1391 | { | ||
1392 | continue; | ||
1393 | } | ||
1394 | |||
1395 | $markup .= ' '.$name.'="'.$value.'"'; | ||
1396 | } | ||
1397 | } | ||
1398 | |||
1399 | if (isset($Element['text'])) | ||
1400 | { | ||
1401 | $markup .= '>'; | ||
1402 | |||
1403 | if (isset($Element['handler'])) | ||
1404 | { | ||
1405 | $markup .= $this->{$Element['handler']}($Element['text']); | ||
1406 | } | ||
1407 | else | ||
1408 | { | ||
1409 | $markup .= $Element['text']; | ||
1410 | } | ||
1411 | |||
1412 | $markup .= '</'.$Element['name'].'>'; | ||
1413 | } | ||
1414 | else | ||
1415 | { | ||
1416 | $markup .= ' />'; | ||
1417 | } | ||
1418 | |||
1419 | return $markup; | ||
1420 | } | ||
1421 | |||
1422 | protected function elements(array $Elements) | ||
1423 | { | ||
1424 | $markup = ''; | ||
1425 | |||
1426 | foreach ($Elements as $Element) | ||
1427 | { | ||
1428 | $markup .= "\n" . $this->element($Element); | ||
1429 | } | ||
1430 | |||
1431 | $markup .= "\n"; | ||
1432 | |||
1433 | return $markup; | ||
1434 | } | ||
1435 | |||
1436 | # ~ | ||
1437 | |||
1438 | protected function li($lines) | ||
1439 | { | ||
1440 | $markup = $this->lines($lines); | ||
1441 | |||
1442 | $trimmedMarkup = trim($markup); | ||
1443 | |||
1444 | if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') | ||
1445 | { | ||
1446 | $markup = $trimmedMarkup; | ||
1447 | $markup = substr($markup, 3); | ||
1448 | |||
1449 | $position = strpos($markup, "</p>"); | ||
1450 | |||
1451 | $markup = substr_replace($markup, '', $position, 4); | ||
1452 | } | ||
1453 | |||
1454 | return $markup; | ||
1455 | } | ||
1456 | |||
1457 | # | ||
1458 | # Deprecated Methods | ||
1459 | # | ||
1460 | |||
1461 | function parse($text) | ||
1462 | { | ||
1463 | $markup = $this->text($text); | ||
1464 | |||
1465 | return $markup; | ||
1466 | } | ||
1467 | |||
1468 | # | ||
1469 | # Static Methods | ||
1470 | # | ||
1471 | |||
1472 | static function instance($name = 'default') | ||
1473 | { | ||
1474 | if (isset(self::$instances[$name])) | ||
1475 | { | ||
1476 | return self::$instances[$name]; | ||
1477 | } | ||
1478 | |||
1479 | $instance = new static(); | ||
1480 | |||
1481 | self::$instances[$name] = $instance; | ||
1482 | |||
1483 | return $instance; | ||
1484 | } | ||
1485 | |||
1486 | private static $instances = array(); | ||
1487 | |||
1488 | # | ||
1489 | # Fields | ||
1490 | # | ||
1491 | |||
1492 | protected $DefinitionData; | ||
1493 | |||
1494 | # | ||
1495 | # Read-Only | ||
1496 | |||
1497 | protected $specialCharacters = array( | ||
1498 | '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', | ||
1499 | ); | ||
1500 | |||
1501 | protected $StrongRegex = array( | ||
1502 | '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', | ||
1503 | '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us', | ||
1504 | ); | ||
1505 | |||
1506 | protected $EmRegex = array( | ||
1507 | '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', | ||
1508 | '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us', | ||
1509 | ); | ||
1510 | |||
1511 | protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?'; | ||
1512 | |||
1513 | protected $voidElements = array( | ||
1514 | 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', | ||
1515 | ); | ||
1516 | |||
1517 | protected $textLevelElements = array( | ||
1518 | 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont', | ||
1519 | 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing', | ||
1520 | 'i', 'rp', 'del', 'code', 'strike', 'marquee', | ||
1521 | 'q', 'rt', 'ins', 'font', 'strong', | ||
1522 | 's', 'tt', 'sub', 'mark', | ||
1523 | 'u', 'xm', 'sup', 'nobr', | ||
1524 | 'var', 'ruby', | ||
1525 | 'wbr', 'span', | ||
1526 | 'time', | ||
1527 | ); | ||
1528 | } \ No newline at end of file | ||
diff --git a/plugins/markdown/README.md b/plugins/markdown/README.md new file mode 100644 index 00000000..22d0af35 --- /dev/null +++ b/plugins/markdown/README.md | |||
@@ -0,0 +1,51 @@ | |||
1 | ## Markdown Shaarli plugin | ||
2 | |||
3 | Convert all your shaares description to HTML formatted Markdown. | ||
4 | |||
5 | Read more about Markdown syntax here. | ||
6 | |||
7 | ### Installation | ||
8 | |||
9 | Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. | ||
10 | The directory structure should look like: | ||
11 | |||
12 | ``` | ||
13 | ??? plugins | ||
14 | ??? markdown | ||
15 | ??? help.html | ||
16 | ??? markdown.css | ||
17 | ??? markdown.meta | ||
18 | ??? markdown.php | ||
19 | ??? Parsedown.php | ||
20 | ??? README.md | ||
21 | ``` | ||
22 | |||
23 | To enable the plugin, add `markdown` to your list of enabled plugins in `data/config.php` | ||
24 | (`ENABLED_PLUGINS` array). | ||
25 | |||
26 | This should look like: | ||
27 | |||
28 | ``` | ||
29 | $GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode', 'any_other_plugin', 'markdown') | ||
30 | ``` | ||
31 | |||
32 | ### Known issue | ||
33 | |||
34 | #### Redirector | ||
35 | |||
36 | If you're using a redirector, you *need* to add a space after a link, | ||
37 | otherwise the rest of the line will be `urlencode`. | ||
38 | |||
39 | ``` | ||
40 | [link](http://domain.tld)-->test | ||
41 | ``` | ||
42 | |||
43 | Will consider `http://domain.tld)-->test` as URL. | ||
44 | |||
45 | Instead, add an additional space. | ||
46 | |||
47 | ``` | ||
48 | [link](http://domain.tld) -->test | ||
49 | ``` | ||
50 | |||
51 | > Won't fix because a `)` is a valid part of an URL. | ||
diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html new file mode 100644 index 00000000..a6797838 --- /dev/null +++ b/plugins/markdown/help.html | |||
@@ -0,0 +1,5 @@ | |||
1 | <div class="md_help"> | ||
2 | Description will be rendered with | ||
3 | <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation"> | ||
4 | Markdown syntax</a>. | ||
5 | </div> | ||
diff --git a/plugins/markdown/markdown.css b/plugins/markdown/markdown.css new file mode 100644 index 00000000..6d666dcf --- /dev/null +++ b/plugins/markdown/markdown.css | |||
@@ -0,0 +1,137 @@ | |||
1 | /** | ||
2 | * Credit to Simon Laroche <https://github.com/simonlc/Markdown-CSS> | ||
3 | * whom created the CSS which this file is based on. | ||
4 | * License: Unlicense <http://unlicense.org/> | ||
5 | */ | ||
6 | |||
7 | .markdown p{ | ||
8 | margin:0.75em 0; | ||
9 | } | ||
10 | |||
11 | .markdown img{ | ||
12 | max-width:100%; | ||
13 | } | ||
14 | |||
15 | .markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6{ | ||
16 | font-weight:normal; | ||
17 | font-style:normal; | ||
18 | line-height:1em; | ||
19 | margin:0.75em 0; | ||
20 | } | ||
21 | .markdown h4, .markdown h5, .markdown h6{ font-weight: bold; } | ||
22 | .markdown h1{ font-size:2.5em; } | ||
23 | .markdown h2{ font-size:2em; } | ||
24 | .markdown h3{ font-size:1.5em; } | ||
25 | .markdown h4{ font-size:1.2em; } | ||
26 | .markdown h5{ font-size:1em; } | ||
27 | .markdown h6{ font-size:0.9em; } | ||
28 | |||
29 | .markdown blockquote{ | ||
30 | color:#666666; | ||
31 | padding-left: 3em; | ||
32 | border-left: 0.5em #EEE solid; | ||
33 | margin:0.75em 0; | ||
34 | } | ||
35 | .markdown hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; } | ||
36 | .markdown pre, .markdown code, .markdown kbd, .markdown samp { | ||
37 | font-family: monospace, 'courier new'; | ||
38 | font-size: 0.98em; | ||
39 | } | ||
40 | .markdown pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } | ||
41 | |||
42 | .markdown b, .markdown strong { font-weight: bold; } | ||
43 | |||
44 | .markdown dfn, .markdown em { font-style: italic; } | ||
45 | |||
46 | .markdown ins { background: #ff9; color: #000; text-decoration: none; } | ||
47 | |||
48 | .markdown mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } | ||
49 | |||
50 | .markdown sub, .markdown sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } | ||
51 | .markdown sup { top: -0.5em; } | ||
52 | .markdown sub { bottom: -0.25em; } | ||
53 | |||
54 | .markdown ul, .markdown ol { margin: 1em 0; padding: 0 0 0 2em; } | ||
55 | .markdown li p:last-child { margin:0 } | ||
56 | .markdown dd { margin: 0 0 0 2em; } | ||
57 | |||
58 | .markdown img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } | ||
59 | |||
60 | .markdown table { border-collapse: collapse; border-spacing: 0; } | ||
61 | .markdown td { vertical-align: top; } | ||
62 | |||
63 | @media only screen and (min-width: 480px) { | ||
64 | .markdown {font-size:0.9em;} | ||
65 | } | ||
66 | |||
67 | @media only screen and (min-width: 768px) { | ||
68 | .markdown {font-size:1em;} | ||
69 | } | ||
70 | |||
71 | #linklist .markdown li { | ||
72 | padding: 0; | ||
73 | border: none; | ||
74 | background: none; | ||
75 | } | ||
76 | |||
77 | #linklist .markdown ul li { | ||
78 | list-style: circle; | ||
79 | } | ||
80 | |||
81 | #linklist .markdown ol li { | ||
82 | list-style: decimal; | ||
83 | } | ||
84 | |||
85 | .markdown table { | ||
86 | padding: 0; | ||
87 | } | ||
88 | .markdown table tr { | ||
89 | border-top: 1px solid #cccccc; | ||
90 | background-color: white; | ||
91 | margin: 0; | ||
92 | padding: 0; | ||
93 | } | ||
94 | .markdown table tr:nth-child(2n) { | ||
95 | background-color: #f8f8f8; | ||
96 | } | ||
97 | .markdown table tr th { | ||
98 | font-weight: bold; | ||
99 | border: 1px solid #cccccc; | ||
100 | text-align: left; | ||
101 | margin: 0; | ||
102 | padding: 6px 13px; | ||
103 | } | ||
104 | .markdown table tr td { | ||
105 | border: 1px solid #cccccc; | ||
106 | text-align: left; | ||
107 | margin: 0; | ||
108 | padding: 6px 13px; | ||
109 | } | ||
110 | .markdown table tr th :first-child, .markdown table tr td :first-child { | ||
111 | margin-top: 0; | ||
112 | } | ||
113 | .markdown table tr th :last-child, table tr td :last-child { | ||
114 | margin-bottom: 0; | ||
115 | } | ||
116 | |||
117 | .md_help { | ||
118 | color: white; | ||
119 | } | ||
120 | |||
121 | /* | ||
122 | Remove header links style | ||
123 | */ | ||
124 | #pageheader .md_help a { | ||
125 | color: lightgray; | ||
126 | font-weight: bold; | ||
127 | text-decoration: underline; | ||
128 | |||
129 | background: none; | ||
130 | box-shadow: none; | ||
131 | padding: 0; | ||
132 | margin: 0; | ||
133 | } | ||
134 | |||
135 | #pageheader .md_help a:hover { | ||
136 | color: white; | ||
137 | } | ||
diff --git a/plugins/markdown/markdown.meta b/plugins/markdown/markdown.meta new file mode 100644 index 00000000..af8c0dbe --- /dev/null +++ b/plugins/markdown/markdown.meta | |||
@@ -0,0 +1 @@ | |||
description="Render shaare description with Markdown syntax." | |||
diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php new file mode 100644 index 00000000..3630ef14 --- /dev/null +++ b/plugins/markdown/markdown.php | |||
@@ -0,0 +1,158 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Plugin Markdown. | ||
5 | * | ||
6 | * Shaare's descriptions are parsed with Markdown. | ||
7 | */ | ||
8 | |||
9 | require_once 'Parsedown.php'; | ||
10 | |||
11 | /** | ||
12 | * Parse linklist descriptions. | ||
13 | * | ||
14 | * @param array $data linklist data. | ||
15 | * | ||
16 | * @return mixed linklist data parsed in markdown (and converted to HTML). | ||
17 | */ | ||
18 | function hook_markdown_render_linklist($data) | ||
19 | { | ||
20 | foreach ($data['links'] as &$value) { | ||
21 | $value['description'] = process_markdown($value['description']); | ||
22 | } | ||
23 | |||
24 | return $data; | ||
25 | } | ||
26 | |||
27 | /** | ||
28 | * Parse daily descriptions. | ||
29 | * | ||
30 | * @param array $data daily data. | ||
31 | * | ||
32 | * @return mixed daily data parsed in markdown (and converted to HTML). | ||
33 | */ | ||
34 | function hook_markdown_render_daily($data) | ||
35 | { | ||
36 | // Manipulate columns data | ||
37 | foreach ($data['cols'] as &$value) { | ||
38 | foreach ($value as &$value2) { | ||
39 | $value2['formatedDescription'] = process_markdown($value2['formatedDescription']); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | return $data; | ||
44 | } | ||
45 | |||
46 | /** | ||
47 | * When link list is displayed, include markdown CSS. | ||
48 | * | ||
49 | * @param array $data includes data. | ||
50 | * | ||
51 | * @return mixed - includes data with markdown CSS file added. | ||
52 | */ | ||
53 | function hook_markdown_render_includes($data) | ||
54 | { | ||
55 | if ($data['_PAGE_'] == Router::$PAGE_LINKLIST | ||
56 | || $data['_PAGE_'] == Router::$PAGE_DAILY | ||
57 | || $data['_PAGE_'] == Router::$PAGE_EDITLINK | ||
58 | ) { | ||
59 | |||
60 | $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/markdown/markdown.css'; | ||
61 | } | ||
62 | |||
63 | return $data; | ||
64 | } | ||
65 | |||
66 | /** | ||
67 | * Hook render_editlink. | ||
68 | * Adds an help link to markdown syntax. | ||
69 | * | ||
70 | * @param array $data data passed to plugin | ||
71 | * | ||
72 | * @return array altered $data. | ||
73 | */ | ||
74 | function hook_markdown_render_editlink($data) | ||
75 | { | ||
76 | // Load help HTML into a string | ||
77 | $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html'); | ||
78 | return $data; | ||
79 | } | ||
80 | |||
81 | |||
82 | /** | ||
83 | * Remove HTML links auto generated by Shaarli core system. | ||
84 | * Keeps HREF attributes. | ||
85 | * | ||
86 | * @param string $description input description text. | ||
87 | * | ||
88 | * @return string $description without HTML links. | ||
89 | */ | ||
90 | function reverse_text2clickable($description) | ||
91 | { | ||
92 | return preg_replace('!<a +href="([^ ]*)">[^ ]+</a>!m', '$1', $description); | ||
93 | } | ||
94 | |||
95 | /** | ||
96 | * Remove <br> tag to let markdown handle it. | ||
97 | * | ||
98 | * @param string $description input description text. | ||
99 | * | ||
100 | * @return string $description without <br> tags. | ||
101 | */ | ||
102 | function reverse_nl2br($description) | ||
103 | { | ||
104 | return preg_replace('!<br */?>!im', '', $description); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * Remove HTML spaces ' ' auto generated by Shaarli core system. | ||
109 | * | ||
110 | * @param string $description input description text. | ||
111 | * | ||
112 | * @return string $description without HTML links. | ||
113 | */ | ||
114 | function reverse_space2nbsp($description) | ||
115 | { | ||
116 | return preg_replace('/(^| ) /m', '$1 ', $description); | ||
117 | } | ||
118 | |||
119 | /** | ||
120 | * Remove '>' at start of line auto generated by Shaarli core system | ||
121 | * to allow markdown blockquotes. | ||
122 | * | ||
123 | * @param string $description input description text. | ||
124 | * | ||
125 | * @return string $description without HTML links. | ||
126 | */ | ||
127 | function reset_quote_tags($description) | ||
128 | { | ||
129 | return preg_replace('/^( *)> /m', '$1> ', $description); | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * Render shaare contents through Markdown parser. | ||
134 | * 1. Remove HTML generated by Shaarli core. | ||
135 | * 2. Generate markdown descriptions. | ||
136 | * 3. Wrap description in 'markdown' CSS class. | ||
137 | * | ||
138 | * @param string $description input description text. | ||
139 | * | ||
140 | * @return string HTML processed $description. | ||
141 | */ | ||
142 | function process_markdown($description) | ||
143 | { | ||
144 | $parsedown = new Parsedown(); | ||
145 | |||
146 | $processedDescription = $description; | ||
147 | $processedDescription = reverse_text2clickable($processedDescription); | ||
148 | $processedDescription = reverse_nl2br($processedDescription); | ||
149 | $processedDescription = reverse_space2nbsp($processedDescription); | ||
150 | $processedDescription = reset_quote_tags($processedDescription); | ||
151 | $processedDescription = $parsedown | ||
152 | ->setMarkupEscaped(false) | ||
153 | ->setBreaksEnabled(true) | ||
154 | ->text($processedDescription); | ||
155 | $processedDescription = '<div class="markdown">'. $processedDescription . '</div>'; | ||
156 | |||
157 | return $processedDescription; | ||
158 | } | ||