<?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['&raquo;'] = substr($substring, 1, $position - 1);

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

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

                                $substring = false;
                            } else {
                                $reference['&raquo;'] = 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 &raquo; 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['&raquo;'] = $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['&raquo;'] = $this->reference_map[$reference]['&raquo;'];

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

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

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

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

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

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

                            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',
    );

}
