<?php

#
#
# Parsedown
# http://parsedown.org
#
# (c) Emanuil Rusev
# http://erusev.com
#
# For the full license information, view the LICENSE file that was distributed
# with this source code.
#
#

class Parsedown
{
    # Multiton

    static function instance($name = 'default')
    {
        if (isset(self::$instances[$name]))
        {
            return self::$instances[$name];
        }

        $instance = new Parsedown();

        self::$instances[$name] = $instance;

        return $instance;
    }

    private static $instances = array();

    #
    # Setters
    #

    # Enables GFM line breaks.

    function set_breaks_enabled($breaks_enabled)
    {
        $this->breaks_enabled = $breaks_enabled;

        return $this;
    }

    private $breaks_enabled = false;

    #
    # Synopsis
    #

    # Markdown is intended to be easy-to-read by humans - those of us who read
    # line by line, left to right, top to bottom. In order to take advantage of
    # this, Parsedown tries to read in a similar way. It breaks texts into
    # lines, it iterates through them and it looks at how they start and relate
    # to each other.

    #
    # Methods
    #

    function parse($text)
    {
        # standardize line breaks
        $text = str_replace("\r\n", "\n", $text);
        $text = str_replace("\r", "\n", $text);

        # replace tabs with spaces
        $text = str_replace("\t", '    ', $text);

        # remove surrounding line breaks
        $text = trim($text, "\n");

        # split text into lines
        $lines = explode("\n", $text);

        # convert lines into html
        $text = $this->parse_block_elements($lines);

        # remove trailing line breaks
        $text = chop($text, "\n");

        return $text;
    }

    #
    # Private

    private function parse_block_elements(array $lines, $context = '')
    {
        $blocks = array();

        $block = array(
            'type' => '',
        );

        foreach ($lines as $line)
        {
            # context

            switch ($block['type'])
            {
                case 'fenced':

                    if ( ! isset($block['closed']))
                    {
                        if (preg_match('/^[ ]*'.$block['fence'][0].'{3,}[ ]*$/', $line))
                        {
                            $block['closed'] = true;
                        }
                        else
                        {
                            if ($block['text'] !== '')
                            {
                                $block['text'] .= "\n";
                            }

                            $block['text'] .= $line;
                        }

                        continue 2;
                    }

                    break;

                case 'markup':

                    if ( ! isset($block['closed']))
                    {
                        if (strpos($line, $block['start']) !== false) # opening tag
                        {
                            $block['depth']++;
                        }

                        if (strpos($line, $block['end']) !== false) # closing tag
                        {
                            if ($block['depth'] > 0)
                            {
                                $block['depth']--;
                            }
                            else
                            {
                                $block['closed'] = true;
                            }
                        }

                        $block['text'] .= "\n".$line;

                        continue 2;
                    }

                    break;
            }

            # ~

            $indentation = 0;

            while(isset($line[$indentation]) and $line[$indentation] === ' ')
            {
                $indentation++;
            }

            $outdented_line = $indentation > 0 ? ltrim($line) : $line;

            # blank

            if ($outdented_line === '')
            {
                $block['interrupted'] = true;

                continue;
            }

            # context

            switch ($block['type'])
            {
                case 'quote':

                    if ( ! isset($block['interrupted']))
                    {
                        $line = preg_replace('/^[ ]*>[ ]?/', '', $line);

                        $block['lines'] []= $line;

                        continue 2;
                    }

                    break;

                case 'li':

                    if ($block['indentation'] === $indentation and preg_match('/^'.$block['marker'].'[ ]+(.*)/', $outdented_line, $matches))
                    {
                        unset($block['last']);

                        $blocks []= $block;

                        $block['last'] = true;
                        $block['lines'] = array($matches[1]);

                        unset($block['first']);
                        unset($block['interrupted']);

                        continue 2;
                    }

                    if ( ! isset($block['interrupted']))
                    {
                        $line = preg_replace('/^[ ]{0,'.$block['baseline'].'}/', '', $line);

                        $block['lines'] []= $line;

                        continue 2;
                    }
                    elseif ($line[0] === ' ')
                    {
                        $block['lines'] []= '';

                        $line = preg_replace('/^[ ]{0,'.$block['baseline'].'}/', '', $line);

                        $block['lines'] []= $line;

                        unset($block['interrupted']);

                        continue 2;
                    }

                    break;
            }

            # indentation sensitive types

            switch ($line[0])
            {
                case ' ':

                    # code

                    if ($indentation >= 4)
                    {
                        $code_line = substr($line, 4);

                        if ($block['type'] === 'code')
                        {
                            if (isset($block['interrupted']))
                            {
                                $block['text'] .= "\n";

                                unset($block['interrupted']);
                            }

                            $block['text'] .= "\n".$code_line;
                        }
                        else
                        {
                            $blocks []= $block;

                            $block = array(
                                'type' => 'code',
                                'text' => $code_line,
                            );
                        }

                        continue 2;
                    }

                    break;

                case '#':

                    # atx heading (#)

                    if (isset($line[1]))
                    {
                        $blocks []= $block;

                        $level = 1;

                        while (isset($line[$level]) and $line[$level] === '#')
                        {
                            $level++;
                        }

                        $block = array(
                            'type' => 'heading',
                            'text' => trim($line, '# '),
                            'level' => $level,
                        );

                        continue 2;
                    }

                    break;

                case '-':
                case '=':

                    # setext heading (===)

                    if ($block['type'] === 'paragraph' and isset($block['interrupted']) === false)
                    {
                        $chopped_line = chop($line);

                        $i = 1;

                        while (isset($chopped_line[$i]))
                        {
                            if ($chopped_line[$i] !== $line[0])
                            {
                                break 2;
                            }

                            $i++;
                        }

                        $block['type'] = 'heading';

                        $block['level'] = $line[0] === '-' ? 2 : 1;

                        continue 2;
                    }

                    break;
            }

            # indentation insensitive types

            switch ($outdented_line[0])
            {
                case '<':

                    $position = strpos($outdented_line, '>');

                    if ($position > 1)
                    {
                        $substring = substr($outdented_line, 1, $position - 1);

                        $substring = chop($substring);

                        if (substr($substring, -1) === '/')
                        {
                            $is_self_closing = true;

                            $substring = substr($substring, 0, -1);
                        }

                        $position = strpos($substring, ' ');

                        if ($position)
                        {
                            $name = substr($substring, 0, $position);
                        }
                        else
                        {
                            $name = $substring;
                        }

                        if ( ! ctype_alpha($name))
                        {
                            break;
                        }

                        if (in_array($name, self::$text_level_elements))
                        {
                            break;
                        }

                        $blocks []= $block;

                        if (isset($is_self_closing))
                        {
                            $block = array(
                                'type' => 'self-closing tag',
                                'text' => $outdented_line,
                            );

                            unset($is_self_closing);

                            continue 2;
                        }

                        $block = array(
                            'type' => 'markup',
                            'text' => $outdented_line,
                            'start' => '<'.$name.'>',
                            'end' => '</'.$name.'>',
                            'depth' => 0,
                        );

                        if (strpos($outdented_line, $block['end']))
                        {
                            $block['closed'] = true;
                        }

                        continue 2;
                    }

                    break;

                case '>':

                    # quote

                    if (preg_match('/^>[ ]?(.*)/', $outdented_line, $matches))
                    {
                        $blocks []= $block;

                        $block = array(
                            'type' => 'quote',
                            'lines' => array(
                                $matches[1],
                            ),
                        );

                        continue 2;
                    }

                    break;

                case '[':

                    # reference

                    $position = strpos($outdented_line, ']:');

                    if ($position)
                    {
                        $reference = array();

                        $label = substr($outdented_line, 1, $position - 1);
                        $label = strtolower($label);

                        $substring = substr($outdented_line, $position + 2);
                        $substring = trim($substring);

                        if ($substring === '')
                        {
                            break;
                        }

                        if ($substring[0] === '<')
                        {
                            $position = strpos($substring, '>');

                            if ($position === false)
                            {
                                break;
                            }

                            $reference['»'] = substr($substring, 1, $position - 1);

                            $substring = substr($substring, $position + 1);
                        }
                        else
                        {
                            $position = strpos($substring, ' ');

                            if ($position === false)
                            {
                                $reference['»'] = $substring;

                                $substring = false;
                            }
                            else
                            {
                                $reference['»'] = substr($substring, 0, $position);

                                $substring = substr($substring, $position + 1);
                            }
                        }

                        if ($substring !== false)
                        {
                            if ($substring[0] !== '"' and $substring[0] !== "'" and $substring[0] !== '(')
                            {
                                break;
                            }

                            $last_char = substr($substring, -1);

                            if ($last_char !== '"' and $last_char !== "'" and $last_char !== ')')
                            {
                                break;
                            }

                            $reference['#'] = substr($substring, 1, -1);
                        }

                        $this->reference_map[$label] = $reference;

                        continue 2;
                    }

                    break;

                case '`':
                case '~':

                    # fenced code block

                    if (preg_match('/^([`]{3,}|[~]{3,})[ ]*(\S+)?[ ]*$/', $outdented_line, $matches))
                    {
                        $blocks []= $block;

                        $block = array(
                            'type' => 'fenced',
                            'text' => '',
                            'fence' => $matches[1],
                        );

                        if (isset($matches[2]))
                        {
                            $block['language'] = $matches[2];
                        }

                        continue 2;
                    }

                    break;

                case '*':
                case '+':
                case '-':
                case '_':

                    # hr

                    if (preg_match('/^([-*_])([ ]{0,2}\1){2,}[ ]*$/', $outdented_line))
                    {
                        $blocks []= $block;

                        $block = array(
                            'type' => 'rule',
                        );

                        continue 2;
                    }

                    # li

                    if (preg_match('/^([*+-][ ]+)(.*)/', $outdented_line, $matches))
                    {
                        $blocks []= $block;

                        $baseline = $indentation + strlen($matches[1]);

                        $block = array(
                            'type' => 'li',
                            'indentation' => $indentation,
                            'baseline' => $baseline,
                            'marker' => '[*+-]',
                            'first' => true,
                            'last' => true,
                            'lines' => array(),
                        );

                        $block['lines'] []= preg_replace('/^[ ]{0,4}/', '', $matches[2]);

                        continue 2;
                    }
            }

            # li

            if ($outdented_line[0] <= '9' and preg_match('/^(\d+[.][ ]+)(.*)/', $outdented_line, $matches))
            {
                $blocks []= $block;

                $baseline = $indentation + strlen($matches[1]);

                $block = array(
                    'type' => 'li',
                    'indentation' => $indentation,
                    'baseline' => $baseline,
                    'marker' => '\d+[.]',
                    'first' => true,
                    'last' => true,
                    'ordered' => true,
                    'lines' => array(),
                );

                $block['lines'] []= preg_replace('/^[ ]{0,4}/', '', $matches[2]);

                continue;
            }

            # paragraph

            if ($block['type'] === 'paragraph')
            {
                if (isset($block['interrupted']))
                {
                    $blocks []= $block;

                    $block['text'] = $line;

                    unset($block['interrupted']);
                }
                else
                {
                    if ($this->breaks_enabled)
                    {
                        $block['text'] .= '  ';
                    }

                    $block['text'] .= "\n".$line;
                }
            }
            else
            {
                $blocks []= $block;

                $block = array(
                    'type' => 'paragraph',
                    'text' => $line,
                );
            }
        }

        $blocks []= $block;

        unset($blocks[0]);

        # $blocks » HTML

        $markup = '';

        foreach ($blocks as $block)
        {
            switch ($block['type'])
            {
                case 'paragraph':

                    $text = $this->parse_span_elements($block['text']);

                    if ($context === 'li' and $markup === '')
                    {
                        if (isset($block['interrupted']))
                        {
                            $markup .= "\n".'<p>'.$text.'</p>'."\n";
                        }
                        else
                        {
                            $markup .= $text;

                            if (isset($blocks[2]))
                            {
                                $markup .= "\n";
                            }
                        }
                    }
                    else
                    {
                        $markup .= '<p>'.$text.'</p>'."\n";
                    }

                    break;

                case 'quote':

                    $text = $this->parse_block_elements($block['lines']);

                    $markup .= '<blockquote>'."\n".$text.'</blockquote>'."\n";

                    break;

                case 'code':

                    $text = htmlspecialchars($block['text'], ENT_NOQUOTES, 'UTF-8');

                    $markup .= '<pre><code>'.$text.'</code></pre>'."\n";

                    break;

                case 'fenced':

                    $text = htmlspecialchars($block['text'], ENT_NOQUOTES, 'UTF-8');

                    $markup .= '<pre><code';

                    if (isset($block['language']))
                    {
                        $markup .= ' class="language-'.$block['language'].'"';
                    }

                    $markup .= '>'.$text.'</code></pre>'."\n";

                    break;

                case 'heading':

                    $text = $this->parse_span_elements($block['text']);

                    $markup .= '<h'.$block['level'].'>'.$text.'</h'.$block['level'].'>'."\n";

                    break;

                case 'rule':

                    $markup .= '<hr />'."\n";

                    break;

                case 'li':

                    if (isset($block['first']))
                    {
                        $type = isset($block['ordered']) ? 'ol' : 'ul';

                        $markup .= '<'.$type.'>'."\n";
                    }

                    if (isset($block['interrupted']) and ! isset($block['last']))
                    {
                        $block['lines'] []= '';
                    }

                    $text = $this->parse_block_elements($block['lines'], 'li');

                    $markup .= '<li>'.$text.'</li>'."\n";

                    if (isset($block['last']))
                    {
                        $type = isset($block['ordered']) ? 'ol' : 'ul';

                        $markup .= '</'.$type.'>'."\n";
                    }

                    break;

                case 'markup':

                    $markup .= $block['text']."\n";

                    break;

                default:

                    $markup .= $block['text']."\n";
            }
        }

        return $markup;
    }

    private function parse_span_elements($text, $markers = array("  \n", '![', '&', '*', '<', '[', '\\', '_', '`', 'http', '~~'))
    {
        if (isset($text[1]) === false or $markers === array())
        {
            return $text;
        }

        # ~

        $markup = '';

        while ($markers)
        {
            $closest_marker = null;
            $closest_marker_index = 0;
            $closest_marker_position = null;

            foreach ($markers as $index => $marker)
            {
                $marker_position = strpos($text, $marker);

                if ($marker_position === false)
                {
                    unset($markers[$index]);

                    continue;
                }

                if ($closest_marker === null or $marker_position < $closest_marker_position)
                {
                    $closest_marker = $marker;
                    $closest_marker_index = $index;
                    $closest_marker_position = $marker_position;
                }
            }

            # ~

            if ($closest_marker === null or isset($text[$closest_marker_position + 1]) === false)
            {
                $markup .= $text;

                break;
            }
            else
            {
                $markup .= substr($text, 0, $closest_marker_position);
            }

            $text = substr($text, $closest_marker_position);

            # ~

            unset($markers[$closest_marker_index]);

            # ~

            switch ($closest_marker)
            {
                case "  \n":

                    $markup .= '<br />'."\n";

                    $offset = 3;

                    break;

                case '![':
                case '[':

                    if (strpos($text, ']') and preg_match('/\[((?:[^][]|(?R))*)\]/', $text, $matches))
                    {
                        $element = array(
                            '!' => $text[0] === '!',
                            'a' => $matches[1],
                        );

                        $offset = strlen($matches[0]);

                        if ($element['!'])
                        {
                            $offset++;
                        }

                        $remaining_text = substr($text, $offset);

                        if ($remaining_text[0] === '(' and preg_match('/\([ ]*(.*?)(?:[ ]+[\'"](.+?)[\'"])?[ ]*\)/', $remaining_text, $matches))
                        {
                            $element['»'] = $matches[1];

                            if (isset($matches[2]))
                            {
                                $element['#'] = $matches[2];
                            }

                            $offset += strlen($matches[0]);
                        }
                        elseif ($this->reference_map)
                        {
                            $reference = $element['a'];

                            if (preg_match('/^\s*\[(.*?)\]/', $remaining_text, $matches))
                            {
                                $reference = $matches[1] ? $matches[1] : $element['a'];

                                $offset += strlen($matches[0]);
                            }

                            $reference = strtolower($reference);

                            if (isset($this->reference_map[$reference]))
                            {
                                $element['»'] = $this->reference_map[$reference]['»'];

                                if (isset($this->reference_map[$reference]['#']))
                                {
                                    $element['#'] = $this->reference_map[$reference]['#'];
                                }
                            }
                            else
                            {
                                unset($element);
                            }
                        }
                        else
                        {
                            unset($element);
                        }
                    }

                    if (isset($element))
                    {
                        $element['»'] = str_replace('&', '&amp;', $element['»']);
                        $element['»'] = str_replace('<', '&lt;', $element['»']);

                        if ($element['!'])
                        {
                            $markup .= '<img alt="'.$element['a'].'" src="'.$element['»'].'"';

                            if (isset($element['#']))
                            {
                                $markup .= ' title="'.$element['#'].'"';
                            }

                            $markup .= ' />';
                        }
                        else
                        {
                            $element['a'] = $this->parse_span_elements($element['a'], $markers);

                            $markup .= '<a href="'.$element['»'].'"';

                            if (isset($element['#']))
                            {
                                $markup .= ' title="'.$element['#'].'"';
                            }

                            $markup .= '>'.$element['a'].'</a>';
                        }

                        unset($element);
                    }
                    else
                    {
                        $markup .= $closest_marker;

                        $offset = $closest_marker === '![' ? 2 : 1;
                    }

                    break;

                case '&':

                    if (preg_match('/^&#?\w+;/', $text, $matches))
                    {
                        $markup .= $matches[0];

                        $offset = strlen($matches[0]);
                    }
                    else
                    {
                        $markup .= '&amp;';

                        $offset = 1;
                    }

                    break;

                case '*':
                case '_':

                    if ($text[1] === $closest_marker and preg_match(self::$strong_regex[$closest_marker], $text, $matches))
                    {
                        $markers[] = $closest_marker;
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);

                        $markup .= '<strong>'.$matches[1].'</strong>';
                    }
                    elseif (preg_match(self::$em_regex[$closest_marker], $text, $matches))
                    {
                        $markers[] = $closest_marker;
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);

                        $markup .= '<em>'.$matches[1].'</em>';
                    }

                    if (isset($matches) and $matches)
                    {
                        $offset = strlen($matches[0]);
                    }
                    else
                    {
                        $markup .= $closest_marker;

                        $offset = 1;
                    }

                    break;

                case '<':

                    if (strpos($text, '>') !== false)
                    {
                        if ($text[1] === 'h' and preg_match('/^<(https?:[\/]{2}[^\s]+?)>/i', $text, $matches))
                        {
                            $element_url = $matches[1];
                            $element_url = str_replace('&', '&amp;', $element_url);
                            $element_url = str_replace('<', '&lt;', $element_url);

                            $markup .= '<a href="'.$element_url.'">'.$element_url.'</a>';

                            $offset = strlen($matches[0]);
                        }
                        elseif (strpos($text, '@') > 1 and preg_match('/<(\S+?@\S+?)>/', $text, $matches))
                        {
                            $markup .= '<a href="mailto:'.$matches[1].'">'.$matches[1].'</a>';

                            $offset = strlen($matches[0]);
                        }
                        elseif (preg_match('/^<\/?\w.*?>/', $text, $matches))
                        {
                            $markup .= $matches[0];

                            $offset = strlen($matches[0]);
                        }
                        else
                        {
                            $markup .= '&lt;';

                            $offset = 1;
                        }
                    }
                    else
                    {
                        $markup .= '&lt;';

                        $offset = 1;
                    }

                    break;

                case '\\':

                    if (in_array($text[1], self::$special_characters))
                    {
                        $markup .= $text[1];

                        $offset = 2;
                    }
                    else
                    {
                        $markup .= '\\';

                        $offset = 1;
                    }

                    break;

                case '`':

                    if (preg_match('/^(`+)[ ]*(.+?)[ ]*(?<!`)\1(?!`)/', $text, $matches))
                    {
                        $element_text = $matches[2];
                        $element_text = htmlspecialchars($element_text, ENT_NOQUOTES, 'UTF-8');

                        $markup .= '<code>'.$element_text.'</code>';

                        $offset = strlen($matches[0]);
                    }
                    else
                    {
                        $markup .= '`';

                        $offset = 1;
                    }

                    break;

                case 'http':

                    if (preg_match('/^https?:[\/]{2}[^\s]+\b\/*/ui', $text, $matches))
                    {
                        $element_url = $matches[0];
                        $element_url = str_replace('&', '&amp;', $element_url);
                        $element_url = str_replace('<', '&lt;', $element_url);

                        $markup .= '<a href="'.$element_url.'">'.$element_url.'</a>';

                        $offset = strlen($matches[0]);
                    }
                    else
                    {
                        $markup .= 'http';

                        $offset = 4;
                    }

                    break;

                case '~~':

                    if (preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $text, $matches))
                    {
                        $matches[1] = $this->parse_span_elements($matches[1], $markers);

                        $markup .= '<del>'.$matches[1].'</del>';

                        $offset = strlen($matches[0]);
                    }
                    else
                    {
                        $markup .= '~~';

                        $offset = 2;
                    }

                    break;
            }

            if (isset($offset))
            {
                $text = substr($text, $offset);
            }

            $markers[$closest_marker_index] = $closest_marker;
        }

        return $markup;
    }

    #
    # Fields
    #

    private $reference_map = array();

    #
    # Read-only

    private static $strong_regex = array(
        '*' => '/^[*]{2}((?:[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
        '_' => '/^__((?:[^_]|_[^_]*_)+?)__(?!_)/us',
    );

    private static $em_regex = array(
        '*' => '/^[*]((?:[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
        '_' => '/^_((?:[^_]|__[^_]*__)+?)_(?!_)\b/us',
    );

    private static $special_characters = array(
        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!',
    );

    private static $text_level_elements = array(
        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
        'i', 'rp', 'sub', 'code',          'strike', 'marquee',
        'q', 'rt', 'sup', 'font',          'strong',
        's', 'tt', 'var', 'mark',
        'u', 'xm', 'wbr', 'nobr',
                          'ruby',
                          'span',
                          'time',
    );
}