1<?php
2
3#
4#
5# Parsedown Extra
6# https://github.com/erusev/parsedown-extra
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
16class ParsedownExtra extends Parsedown
17{
18    # ~
19
20    const version = '0.7.0';
21
22    # ~
23
24    function __construct()
25    {
26        if (parent::version < '1.5.0')
27        {
28            throw new Exception('ParsedownExtra requires a later version of Parsedown');
29        }
30
31        $this->BlockTypes[':'] []= 'DefinitionList';
32        $this->BlockTypes['*'] []= 'Abbreviation';
33
34        # identify footnote definitions before reference definitions
35        array_unshift($this->BlockTypes['['], 'Footnote');
36
37        # identify footnote markers before before links
38        array_unshift($this->InlineTypes['['], 'FootnoteMarker');
39    }
40
41    #
42    # ~
43
44    function text($text)
45    {
46        $markup = parent::text($text);
47
48        # merge consecutive dl elements
49
50        $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
51
52        # add footnotes
53
54        if (isset($this->DefinitionData['Footnote']))
55        {
56            $Element = $this->buildFootnoteElement();
57
58            $markup .= "\n" . $this->element($Element);
59        }
60
61        return $markup;
62    }
63
64    #
65    # Blocks
66    #
67
68    #
69    # Abbreviation
70
71    protected function blockAbbreviation($Line)
72    {
73        if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
74        {
75            $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
76
77            $Block = array(
78                'hidden' => true,
79            );
80
81            return $Block;
82        }
83    }
84
85    #
86    # Footnote
87
88    protected function blockFootnote($Line)
89    {
90        if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
91        {
92            $Block = array(
93                'label' => $matches[1],
94                'text' => $matches[2],
95                'hidden' => true,
96            );
97
98            return $Block;
99        }
100    }
101
102    protected function blockFootnoteContinue($Line, $Block)
103    {
104        if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
105        {
106            return;
107        }
108
109        if (isset($Block['interrupted']))
110        {
111            if ($Line['indent'] >= 4)
112            {
113                $Block['text'] .= "\n\n" . $Line['text'];
114
115                return $Block;
116            }
117        }
118        else
119        {
120            $Block['text'] .= "\n" . $Line['text'];
121
122            return $Block;
123        }
124    }
125
126    protected function blockFootnoteComplete($Block)
127    {
128        $this->DefinitionData['Footnote'][$Block['label']] = array(
129            'text' => $Block['text'],
130            'count' => null,
131            'number' => null,
132        );
133
134        return $Block;
135    }
136
137    #
138    # Definition List
139
140    protected function blockDefinitionList($Line, $Block)
141    {
142        if ( ! isset($Block) or isset($Block['type']))
143        {
144            return;
145        }
146
147        $Element = array(
148            'name' => 'dl',
149            'handler' => 'elements',
150            'text' => array(),
151        );
152
153        $terms = explode("\n", $Block['element']['text']);
154
155        foreach ($terms as $term)
156        {
157            $Element['text'] []= array(
158                'name' => 'dt',
159                'handler' => 'line',
160                'text' => $term,
161            );
162        }
163
164        $Block['element'] = $Element;
165
166        $Block = $this->addDdElement($Line, $Block);
167
168        return $Block;
169    }
170
171    protected function blockDefinitionListContinue($Line, array $Block)
172    {
173        if ($Line['text'][0] === ':')
174        {
175            $Block = $this->addDdElement($Line, $Block);
176
177            return $Block;
178        }
179        else
180        {
181            if (isset($Block['interrupted']) and $Line['indent'] === 0)
182            {
183                return;
184            }
185
186            if (isset($Block['interrupted']))
187            {
188                $Block['dd']['handler'] = 'text';
189                $Block['dd']['text'] .= "\n\n";
190
191                unset($Block['interrupted']);
192            }
193
194            $text = substr($Line['body'], min($Line['indent'], 4));
195
196            $Block['dd']['text'] .= "\n" . $text;
197
198            return $Block;
199        }
200    }
201
202    #
203    # Header
204
205    protected function blockHeader($Line)
206    {
207        $Block = parent::blockHeader($Line);
208
209        if (preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
210        {
211            $attributeString = $matches[1][0];
212
213            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
214
215            $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
216        }
217
218        return $Block;
219    }
220
221    #
222    # Markup
223
224    protected function blockMarkupComplete($Block)
225    {
226        if ( ! isset($Block['void']))
227        {
228            $Block['markup'] = $this->processTag($Block['markup']);
229        }
230
231        return $Block;
232    }
233
234    #
235    # Setext
236
237    protected function blockSetextHeader($Line, array $Block = null)
238    {
239        $Block = parent::blockSetextHeader($Line, $Block);
240
241        if (preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['text'], $matches, PREG_OFFSET_CAPTURE))
242        {
243            $attributeString = $matches[1][0];
244
245            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);
246
247            $Block['element']['text'] = substr($Block['element']['text'], 0, $matches[0][1]);
248        }
249
250        return $Block;
251    }
252
253    #
254    # Inline Elements
255    #
256
257    #
258    # Footnote Marker
259
260    protected function inlineFootnoteMarker($Excerpt)
261    {
262        if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
263        {
264            $name = $matches[1];
265
266            if ( ! isset($this->DefinitionData['Footnote'][$name]))
267            {
268                return;
269            }
270
271            $this->DefinitionData['Footnote'][$name]['count'] ++;
272
273            if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
274            {
275                $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
276            }
277
278            $Element = array(
279                'name' => 'sup',
280                'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
281                'handler' => 'element',
282                'text' => array(
283                    'name' => 'a',
284                    'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
285                    'text' => $this->DefinitionData['Footnote'][$name]['number'],
286                ),
287            );
288
289            return array(
290                'extent' => strlen($matches[0]),
291                'element' => $Element,
292            );
293        }
294    }
295
296    private $footnoteCount = 0;
297
298    #
299    # Link
300
301    protected function inlineLink($Excerpt)
302    {
303        $Link = parent::inlineLink($Excerpt);
304
305        $remainder = substr($Excerpt['text'], $Link['extent']);
306
307        if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
308        {
309            $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
310
311            $Link['extent'] += strlen($matches[0]);
312        }
313
314        return $Link;
315    }
316
317    #
318    # ~
319    #
320
321    protected function unmarkedText($text)
322    {
323        $text = parent::unmarkedText($text);
324
325        if (isset($this->DefinitionData['Abbreviation']))
326        {
327            foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
328            {
329                $pattern = '/\b'.preg_quote($abbreviation, '/').'\b/';
330
331                $text = preg_replace($pattern, '<abbr title="'.$meaning.'">'.$abbreviation.'</abbr>', $text);
332            }
333        }
334
335        return $text;
336    }
337
338    #
339    # Util Methods
340    #
341
342    protected function addDdElement(array $Line, array $Block)
343    {
344        $text = substr($Line['text'], 1);
345        $text = trim($text);
346
347        unset($Block['dd']);
348
349        $Block['dd'] = array(
350            'name' => 'dd',
351            'handler' => 'line',
352            'text' => $text,
353        );
354
355        if (isset($Block['interrupted']))
356        {
357            $Block['dd']['handler'] = 'text';
358
359            unset($Block['interrupted']);
360        }
361
362        $Block['element']['text'] []= & $Block['dd'];
363
364        return $Block;
365    }
366
367    protected function buildFootnoteElement()
368    {
369        $Element = array(
370            'name' => 'div',
371            'attributes' => array('class' => 'footnotes'),
372            'handler' => 'elements',
373            'text' => array(
374                array(
375                    'name' => 'hr',
376                ),
377                array(
378                    'name' => 'ol',
379                    'handler' => 'elements',
380                    'text' => array(),
381                ),
382            ),
383        );
384
385        uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
386
387        foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
388        {
389            if ( ! isset($DefinitionData['number']))
390            {
391                continue;
392            }
393
394            $text = $DefinitionData['text'];
395
396            $text = parent::text($text);
397
398            $numbers = range(1, $DefinitionData['count']);
399
400            $backLinksMarkup = '';
401
402            foreach ($numbers as $number)
403            {
404                $backLinksMarkup .= ' <a href="#fnref'.$number.':'.$definitionId.'" rev="footnote" class="footnote-backref">&#8617;</a>';
405            }
406
407            $backLinksMarkup = substr($backLinksMarkup, 1);
408
409            if (substr($text, - 4) === '</p>')
410            {
411                $backLinksMarkup = '&#160;'.$backLinksMarkup;
412
413                $text = substr_replace($text, $backLinksMarkup.'</p>', - 4);
414            }
415            else
416            {
417                $text .= "\n".'<p>'.$backLinksMarkup.'</p>';
418            }
419
420            $Element['text'][1]['text'] []= array(
421                'name' => 'li',
422                'attributes' => array('id' => 'fn:'.$definitionId),
423                'text' => "\n".$text."\n",
424            );
425        }
426
427        return $Element;
428    }
429
430    # ~
431
432    protected function parseAttributeData($attributeString)
433    {
434        $Data = array();
435
436        $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
437
438        foreach ($attributes as $attribute)
439        {
440            if ($attribute[0] === '#')
441            {
442                $Data['id'] = substr($attribute, 1);
443            }
444            else # "."
445            {
446                $classes []= substr($attribute, 1);
447            }
448        }
449
450        if (isset($classes))
451        {
452            $Data['class'] = implode(' ', $classes);
453        }
454
455        return $Data;
456    }
457
458    # ~
459
460    protected function processTag($elementMarkup) # recursive
461    {
462        # http://stackoverflow.com/q/1148928/200145
463        libxml_use_internal_errors(true);
464
465        $DOMDocument = new DOMDocument;
466
467        # http://stackoverflow.com/q/11309194/200145
468        $elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
469
470        # http://stackoverflow.com/q/4879946/200145
471        $DOMDocument->loadHTML($elementMarkup);
472        $DOMDocument->removeChild($DOMDocument->doctype);
473        $DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
474
475        $elementText = '';
476
477        if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
478        {
479            foreach ($DOMDocument->documentElement->childNodes as $Node)
480            {
481                $elementText .= $DOMDocument->saveHTML($Node);
482            }
483
484            $DOMDocument->documentElement->removeAttribute('markdown');
485
486            $elementText = "\n".$this->text($elementText)."\n";
487        }
488        else
489        {
490            foreach ($DOMDocument->documentElement->childNodes as $Node)
491            {
492                $nodeMarkup = $DOMDocument->saveHTML($Node);
493
494                if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
495                {
496                    $elementText .= $this->processTag($nodeMarkup);
497                }
498                else
499                {
500                    $elementText .= $nodeMarkup;
501                }
502            }
503        }
504
505        # because we don't want for markup to get encoded
506        $DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
507
508        $markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
509        $markup = str_replace('placeholder\x1A', $elementText, $markup);
510
511        return $markup;
512    }
513
514    # ~
515
516    protected function sortFootnotes($A, $B) # callback
517    {
518        return $A['number'] - $B['number'];
519    }
520
521    #
522    # Fields
523    #
524
525    protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
526}
527