Markdown.php 5.25 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\console;

use yii\helpers\Console;

/**
 * A Markdown parser that enhances markdown for reading in console environments.
 *
 * Based on [cebe/markdown](https://github.com/cebe/markdown).
 *
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
class Markdown extends \cebe\markdown\Parser
{
    /**
     * @var array these are "escapeable" characters. When using one of these prefixed with a
     * backslash, the character will be outputted without the backslash and is not interpreted
     * as markdown.
     */
    protected $escapeCharacters = [
        '\\', // backslash
        '`', // backtick
        '*', // asterisk
        '_', // underscore
    ];

34

35 36 37 38 39 40 41 42 43 44 45 46 47
    /**
     * @inheritDoc
     */
    protected function identifyLine($lines, $current)
    {
        if (isset($lines[$current]) && (strncmp($lines[$current], '```', 3) === 0 || strncmp($lines[$current], '~~~', 3) === 0)) {
            return 'fencedCode';
        }
        return parent::identifyLine($lines, $current);
    }

    /**
     * Consume lines for a fenced code block
48 49 50 51
     *
     * @param array $lines
     * @param integer $current
     * @return array
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
     */
    protected function consumeFencedCode($lines, $current)
    {
        // consume until ```
        $block = [
            'type' => 'code',
            'content' => [],
        ];
        $line = rtrim($lines[$current]);
        $fence = substr($line, 0, $pos = strrpos($line, $line[0]) + 1);
        $language = substr($line, $pos);
        if (!empty($language)) {
            $block['language'] = $language;
        }
        for ($i = $current + 1, $count = count($lines); $i < $count; $i++) {
            if (rtrim($line = $lines[$i]) !== $fence) {
                $block['content'][] = $line;
            } else {
                break;
            }
        }
        return [$block, $i];
    }

    /**
     * Renders a code block
78 79 80
     *
     * @param array $block
     * @return string
81 82 83 84 85 86
     */
    protected function renderCode($block)
    {
        return Console::ansiFormat(implode("\n", $block['content']), [Console::BG_GREY]) . "\n";
    }

87 88 89
    /**
     * @inheritdoc
     */
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
    protected function renderParagraph($block)
    {
        return rtrim($this->parseInline(implode("\n", $block['content']))) . "\n";
    }

    /**
     * @inheritDoc
     */
    protected function inlineMarkers()
    {
        return [
            '*'     => 'parseEmphStrong',
            '_'     => 'parseEmphStrong',
            '\\'    => 'parseEscape',
            '`'     => 'parseCode',
            '~~'    => 'parseStrike',
        ];
    }

    /**
     * Parses an inline code span `` ` ``.
111 112
     * @param string $text
     * @return array
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
     */
    protected function parseCode($text)
    {
        // skip fenced code
        if (strncmp($text, '```', 3) === 0) {
            return [$text[0], 1];
        }
        if (preg_match('/^(`+) (.+?) \1/', $text, $matches)) { // code with enclosed backtick
            return [
                Console::ansiFormat($matches[2], [Console::UNDERLINE]),
                strlen($matches[0])
            ];
        } elseif (preg_match('/^`(.+?)`/', $text, $matches)) {
            return [
                Console::ansiFormat($matches[1], [Console::UNDERLINE]),
                strlen($matches[0])
            ];
        }
        return [$text[0], 1];
    }

    /**
     * Parses empathized and strong elements.
136 137
     * @param string $text
     * @return array
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
     */
    protected function parseEmphStrong($text)
    {
        $marker = $text[0];

        if (!isset($text[1])) {
            return [$text[0], 1];
        }

        if ($marker == $text[1]) { // strong
            if ($marker == '*' && preg_match('/^[*]{2}((?:[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s', $text, $matches) ||
                $marker == '_' && preg_match('/^__((?:[^_]|_[^_]*_)+?)__(?!_)/us', $text, $matches)) {

                return [Console::ansiFormat($this->parseInline($matches[1]), Console::BOLD), strlen($matches[0])];
            }
        } else { // emph
            if ($marker == '*' && preg_match('/^[*]((?:[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s', $text, $matches) ||
                $marker == '_' && preg_match('/^_((?:[^_]|__[^_]*__)+?)_(?!_)\b/us', $text, $matches)) {
                return [Console::ansiFormat($this->parseInline($matches[1]), Console::ITALIC), strlen($matches[0])];
            }
        }
        return [$text[0], 1];
    }

    /**
     * Parses the strikethrough feature.
164 165
     * @param string $markdown
     * @return array
166 167 168 169 170 171 172 173 174 175 176 177 178 179
     */
    protected function parseStrike($markdown)
    {
        if (preg_match('/^~~(.+?)~~/', $markdown, $matches)) {
            return [
                Console::ansiFormat($this->parseInline($matches[1]), [Console::CROSSED_OUT]),
                strlen($matches[0])
            ];
        }
        return [$markdown[0] . $markdown[1], 2];
    }

    /**
     * Parses escaped special characters.
180 181
     * @param string $text
     * @return array
182 183 184 185 186 187 188 189 190
     */
    protected function parseEscape($text)
    {
        if (isset($text[1]) && in_array($text[1], $this->escapeCharacters)) {
            return [$text[1], 2];
        }
        return [$text[0], 1];
    }
}