| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870 | <?phpnamespace Utils;/** * Parser * * @copyright Copyright (c) 2012 SegmentFault Team. (http://segmentfault.com) * @author Joyqi <joyqi@segmentfault.com> * @license BSD License */class HyperDown{    /**     * _whiteList     *     * @var string     */    private $_commonWhiteList = 'kbd|b|i|strong|em|sup|sub|br|code|del|a|hr|small';    /**     * html tags     *     * @var string     */    private $_blockHtmlTags = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|svg|script|noscript';    /**     * _specialWhiteList     *     * @var mixed     * @access private     */    private $_specialWhiteList = [        'table' => 'table|tbody|thead|tfoot|tr|td|th'    ];    /**     * _footnotes     *     * @var array     */    private $_footnotes;    /**     * @var bool     */    private $_html = false;    /**     * @var bool     */    private $_line = false;    /**     * @var array     */    private $blockParsers = [        ['code', 10],        ['shtml', 20],        ['pre', 30],        ['ahtml', 40],        ['shr', 50],        ['list', 60],        ['math', 70],        ['html', 80],        ['footnote', 90],        ['definition', 100],        ['quote', 110],        ['table', 120],        ['sh', 130],        ['mh', 140],        ['dhr', 150],        ['default', 9999]    ];    /**     * _blocks     *     * @var array     */    private $_blocks;    /**     * _current     *     * @var string     */    private $_current;    /**     * _pos     *     * @var int     */    private $_pos;    /**     * _definitions     *     * @var array     */    private $_definitions;    /**     * @var array     */    private $_hooks = [];    /**     * @var array     */    private $_holders;    /**     * @var string     */    private $_uniqid;    /**     * @var int     */    private $_id;    /**     * @var array     */    private $_parsers = [];    /**     * makeHtml     *     * @param mixed $text     *     * @return string     */    public function makeHtml($text): string    {        $this->_footnotes = [];        $this->_definitions = [];        $this->_holders = [];        $this->_uniqid = md5(uniqid());        $this->_id = 0;        usort($this->blockParsers, function ($a, $b) {            return $a[1] < $b[1] ? - 1 : 1;        });        foreach ($this->blockParsers as $parser) {            [$name] = $parser;            if (isset($parser[2])) {                $this->_parsers[$name] = $parser[2];            } else {                $this->_parsers[$name] = [$this, 'parseBlock' . ucfirst($name)];            }        }        $text = $this->initText($text);        $html = $this->parse($text);        $html = $this->makeFootnotes($html);        $html = $this->optimizeLines($html);        return $this->call('makeHtml', $html);    }    /**     * @param bool $html     */    public function enableHtml(bool $html = true)    {        $this->_html = $html;    }    /**     * @param bool $line     */    public function enableLine(bool $line = true)    {        $this->_line = $line;    }    /**     * @param string $type     * @param callable $callback     */    public function hook(string $type, callable $callback)    {        $this->_hooks[$type][] = $callback;    }    /**     * @param string $str     *     * @return string     */    public function makeHolder(string $str): string    {        $key = "\r" . $this->_uniqid . $this->_id . "\r";        $this->_id ++;        $this->_holders[$key] = $str;        return $key;    }    /**     * @param string $text     *     * @return string     */    private function initText(string $text): string    {        $text = str_replace(["\t", "\r"], ['    ', ''], $text);        return $text;    }    /**     * @param string $html     *     * @return string     */    private function makeFootnotes(string $html): string    {        if (count($this->_footnotes) > 0) {            $html .= '<div class="footnotes"><hr><ol>';            $index = 1;            while ($val = array_shift($this->_footnotes)) {                if (is_string($val)) {                    $val .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">↩</a>";                } else {                    $val[count($val) - 1] .= " <a href=\"#fnref-{$index}\" class=\"footnote-backref\">↩</a>";                    $val = count($val) > 1 ? $this->parse(implode("\n", $val)) : $this->parseInline($val[0]);                }                $html .= "<li id=\"fn-{$index}\">{$val}</li>";                $index ++;            }            $html .= '</ol></div>';        }        return $html;    }    /**     * parse     *     * @param string $text     * @param bool $inline     * @param int $offset     *     * @return string     */    private function parse(string $text, bool $inline = false, int $offset = 0): string    {        $blocks = $this->parseBlock($text, $lines);        $html = '';        // inline mode for single normal block        if ($inline && count($blocks) == 1 && $blocks[0][0] == 'normal') {            $blocks[0][3] = true;        }        foreach ($blocks as $block) {            [$type, $start, $end, $value] = $block;            $extract = array_slice($lines, $start, $end - $start + 1);            $method = 'parse' . ucfirst($type);            $extract = $this->call('before' . ucfirst($method), $extract, $value);            $result = $this->{$method}($extract, $value, $start + $offset, $end + $offset);            $result = $this->call('after' . ucfirst($method), $result, $value);            $html .= $result;        }        return $html;    }    /**     * @param string $text     * @param bool $clearHolders     *     * @return string     */    private function releaseHolder(string $text, bool $clearHolders = true): string    {        $deep = 0;        while (strpos($text, "\r") !== false && $deep < 10) {            $text = str_replace(array_keys($this->_holders), array_values($this->_holders), $text);            $deep ++;        }        if ($clearHolders) {            $this->_holders = [];        }        return $text;    }    /**     * @param int $start     * @param int $end     *     * @return string     */    private function markLine(int $start, int $end = - 1): string    {        if ($this->_line) {            $end = $end < 0 ? $start : $end;            return '<span class="line" data-start="' . $start                . '" data-end="' . $end . '" data-id="' . $this->_uniqid . '"></span>';        }        return '';    }    /**     * @param array $lines     * @param int $start     *     * @return string[]     */    private function markLines(array $lines, int $start): array    {        $i = - 1;        return $this->_line ? array_map(function ($line) use ($start, &$i) {            $i ++;            return $this->markLine($start + $i) . $line;        }, $lines) : $lines;    }    /**     * @param string $html     *     * @return string     */    private function optimizeLines(string $html): string    {        $last = 0;        return $this->_line ?            preg_replace_callback("/class=\"line\" data\-start=\"([0-9]+)\" data\-end=\"([0-9]+)\" (data\-id=\"{$this->_uniqid}\")/",                function ($matches) use (&$last) {                    if ($matches[1] != $last) {                        $replace = 'class="line" data-start="' . $last . '" data-start-original="' . $matches[1] . '" data-end="' . $matches[2] . '" ' . $matches[3];                    } else {                        $replace = $matches[0];                    }                    $last = $matches[2] + 1;                    return $replace;                }, $html) : $html;    }    /**     * @param string $type     * @param ...$args     *     * @return mixed     */    private function call(string $type, ...$args)    {        $value = $args[0];        if (empty($this->_hooks[$type])) {            return $value;        }        foreach ($this->_hooks[$type] as $callback) {            $value = call_user_func_array($callback, $args);            $args[0] = $value;        }        return $value;    }    /**     * parseInline     *     * @param string $text     * @param string $whiteList     * @param bool $clearHolders     * @param bool $enableAutoLink     *     * @return string     */    private function parseInline(        string $text,        string $whiteList = '',        bool $clearHolders = true,        bool $enableAutoLink = true    ): string {        $text = $this->call('beforeParseInline', $text);        // code        $text = preg_replace_callback(            "/(^|[^\\\])(`+)(.+?)\\2/",            function ($matches) {                return $matches[1] . $this->makeHolder(                        '<code>' . htmlspecialchars($matches[3]) . '</code>'                    );            },            $text        );        // mathjax        $text = preg_replace_callback(            "/(^|[^\\\])(\\$+)(.+?)\\2/",            function ($matches) {                return $matches[1] . $this->makeHolder(                        $matches[2] . htmlspecialchars($matches[3]) . $matches[2]                    );            },            $text        );        // escape        $text = preg_replace_callback(            "/\\\(.)/u",            function ($matches) {                $prefix = preg_match("/^[-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]$/", $matches[1]) ? '' : '\\';                $escaped = htmlspecialchars($matches[1]);                $escaped = str_replace('$', '$', $escaped);                return $this->makeHolder($prefix . $escaped);            },            $text        );        // link        $text = preg_replace_callback(            "/<(https?:\/\/.+|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)>/i",            function ($matches) {                $url = $this->cleanUrl($matches[1]);                $link = $this->call('parseLink', $url);                return $this->makeHolder(                    "<a href=\"{$url}\">{$link}</a>"                );            },            $text        );        // encode unsafe tags        $text = preg_replace_callback(            "/<(\/?)([a-z0-9-]+)(\s+[^>]*)?>/i",            function ($matches) use ($whiteList) {                if ($this->_html || false !== stripos(                        '|' . $this->_commonWhiteList . '|' . $whiteList . '|', '|' . $matches[2] . '|'                    )) {                    return $this->makeHolder($matches[0]);                } else {                    return $this->makeHolder(htmlspecialchars($matches[0]));                }            },            $text        );        if ($this->_html) {            $text = preg_replace_callback("/<!\-\-(.*?)\-\->/", function ($matches) {                return $this->makeHolder($matches[0]);            }, $text);        }        $text = str_replace(['<', '>'], ['<', '>'], $text);        // footnote        $text = preg_replace_callback(            "/\[\^((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",            function ($matches) {                $id = array_search($matches[1], $this->_footnotes);                if (false === $id) {                    $id = count($this->_footnotes) + 1;                    $this->_footnotes[$id] = $this->parseInline($matches[1], '', false);                }                return $this->makeHolder(                    "<sup id=\"fnref-{$id}\"><a href=\"#fn-{$id}\" class=\"footnote-ref\">{$id}</a></sup>"                );            },            $text        );        // image        $text = preg_replace_callback(            "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",            function ($matches) {                $escaped = htmlspecialchars($this->escapeBracket($matches[1]));                $url = $this->escapeBracket($matches[2]);                [$url, $title] = $this->cleanUrl($url, true);                $title = empty($title) ? $escaped : " title=\"{$title}\"";                return $this->makeHolder(                    "<img src=\"{$url}!ys\" alt=\"{$title}\" title=\"{$title}\">"                );            },            $text        );        $text = preg_replace_callback(            "/!\[((?:[^\]]|\\\\\]|\\\\\[)*?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",            function ($matches) {                $escaped = htmlspecialchars($this->escapeBracket($matches[1]));                $result = isset($this->_definitions[$matches[2]]) ?                    "<img src=\"{$this->_definitions[$matches[2]]}!ys\" alt=\"{$escaped}\" title=\"{$escaped}\">"                    : $escaped;                return $this->makeHolder($result);            },            $text        );        // link        $text = preg_replace_callback(            "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\(((?:[^\)]|\\\\\)|\\\\\()+?)\)/",            function ($matches) {                $escaped = $this->parseInline(                    $this->escapeBracket($matches[1]), '', false, false                );                $url = $this->escapeBracket($matches[2]);                [$url, $title] = $this->cleanUrl($url, true);                $title = empty($title) ? '' : " title=\"{$title}\"";                return $this->makeHolder("<a href=\"{$url}\"{$title}>{$escaped}</a>");            },            $text        );        $text = preg_replace_callback(            "/\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]\[((?:[^\]]|\\\\\]|\\\\\[)+?)\]/",            function ($matches) {                $escaped = $this->parseInline(                    $this->escapeBracket($matches[1]), '', false                );                $result = isset($this->_definitions[$matches[2]]) ?                    "<a href=\"{$this->_definitions[$matches[2]]}\">{$escaped}</a>"                    : $escaped;                return $this->makeHolder($result);            },            $text        );        // strong and em and some fuck        $text = $this->parseInlineCallback($text);        $text = preg_replace(            "/<([_a-z0-9-\.\+]+@[^@]+\.[a-z]{2,})>/i",            "<a href=\"mailto:\\1\">\\1</a>",            $text        );        // autolink url        if ($enableAutoLink) {            $text = preg_replace_callback(                "/(^|[^\"])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\b([-a-zA-Z0-9@:%_\+.~#?&\/=]*)|(?:mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+)($|[^\"])/",                function ($matches) {                    $url = $this->cleanUrl($matches[2]);                    $link = $this->call('parseLink', $matches[2]);                    return "{$matches[1]}<a href=\"{$url}\">{$link}</a>{$matches[5]}";                },                $text            );        }        $text = $this->call('afterParseInlineBeforeRelease', $text);        $text = $this->releaseHolder($text, $clearHolders);        $text = $this->call('afterParseInline', $text);        return $text;    }    /**     * @param string $text     *     * @return string     */    private function parseInlineCallback(string $text): string    {        $text = preg_replace_callback(            "/(\*{3})(.+?)\\1/",            function ($matches) {                return '<strong><em>' .                    $this->parseInlineCallback($matches[2]) .                    '</em></strong>';            },            $text        );        $text = preg_replace_callback(            "/(\*{2})(.+?)\\1/",            function ($matches) {                return '<strong>' .                    $this->parseInlineCallback($matches[2]) .                    '</strong>';            },            $text        );        $text = preg_replace_callback(            "/(\*)(.+?)\\1/",            function ($matches) {                return '<em>' .                    $this->parseInlineCallback($matches[2]) .                    '</em>';            },            $text        );        $text = preg_replace_callback(            "/(\s+|^)(_{3})(.+?)\\2(\s+|$)/",            function ($matches) {                return $matches[1] . '<strong><em>' .                    $this->parseInlineCallback($matches[3]) .                    '</em></strong>' . $matches[4];            },            $text        );        $text = preg_replace_callback(            "/(\s+|^)(_{2})(.+?)\\2(\s+|$)/",            function ($matches) {                return $matches[1] . '<strong>' .                    $this->parseInlineCallback($matches[3]) .                    '</strong>' . $matches[4];            },            $text        );        $text = preg_replace_callback(            "/(\s+|^)(_)(.+?)\\2(\s+|$)/",            function ($matches) {                return $matches[1] . '<em>' .                    $this->parseInlineCallback($matches[3]) .                    '</em>' . $matches[4];            },            $text        );        $text = preg_replace_callback(            "/(~{2})(.+?)\\1/",            function ($matches) {                return '<del>' .                    $this->parseInlineCallback($matches[2]) .                    '</del>';            },            $text        );        return $text;    }    /**     * parseBlock     *     * @param string $text     * @param array|null $lines     *     * @return array     */    private function parseBlock(string $text, ?array &$lines): array    {        $lines = explode("\n", $text);        $this->_blocks = [];        $this->_current = 'normal';        $this->_pos = - 1;        $state = [            'special' => implode("|", array_keys($this->_specialWhiteList)),            'empty'   => 0,            'html'    => false        ];        // analyze by line        foreach ($lines as $key => $line) {            $block = $this->getBlock();            $args = [$block, $key, $line, &$state, $lines];            if ($this->_current != 'normal') {                $pass = call_user_func_array($this->_parsers[$this->_current], $args);                if (!$pass) {                    continue;                }            }            foreach ($this->_parsers as $name => $parser) {                if ($name != $this->_current) {                    $pass = call_user_func_array($parser, $args);                    if (!$pass) {                        break;                    }                }            }        }        return $this->optimizeBlocks($this->_blocks, $lines);    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockList(?array $block, int $key, string $line, ?array &$state): bool    {        if ($this->isBlock('list') && !preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line)) {            if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line)) {                // ignore code                return true;            } elseif ($state['empty'] <= 1                && preg_match("/^(\s*)\S+/", $line, $matches)                && strlen($matches[1]) >= ($block[3][0] + $state['empty'])) {                $state['empty'] = 0;                $this->setBlock($key);                return false;            } elseif (preg_match("/^(\s*)$/", $line) && $state['empty'] == 0) {                $state['empty'] ++;                $this->setBlock($key);                return false;            }        }        if (preg_match("/^(\s*)((?:[0-9]+\.)|\-|\+|\*)\s+/i", $line, $matches)) {            $space = strlen($matches[1]);            $tab = strlen($matches[0]) - $space;            $state['empty'] = 0;            $type = false !== strpos('+-*', $matches[2]) ? 'ul' : 'ol';            // opened            if ($this->isBlock('list')) {                if ($space < $block[3][0] || ($space == $block[3][0] && $type != $block[3][1])) {                    $this->startBlock('list', $key, [$space, $type, $tab]);                } else {                    $this->setBlock($key);                }            } else {                $this->startBlock('list', $key, [$space, $type, $tab]);            }            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockCode(?array $block, int $key, string $line, ?array &$state): bool    {        if (preg_match("/^(\s*)(~{3,}|`{3,})([^`~]*)$/i", $line, $matches)) {            if ($this->isBlock('code')) {                if ($state['code'] != $matches[2]) {                    $this->setBlock($key);                    return false;                }                $isAfterList = $block[3][2];                if ($isAfterList) {                    $state['empty'] = 0;                    $this->combineBlock()                        ->setBlock($key);                } else {                    $this->setBlock($key)                        ->endBlock();                }            } else {                $isAfterList = false;                if ($this->isBlock('list')) {                    $space = $block[3][0];                    $isAfterList = strlen($matches[1]) >= $space + $state['empty'];                }                $state['code'] = $matches[2];                $this->startBlock('code', $key, [                    $matches[1], $matches[3], $isAfterList                ]);            }            return false;        } elseif ($this->isBlock('code')) {            $this->setBlock($key);            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockShtml(?array $block, int $key, string $line, ?array &$state): bool    {        if ($this->_html) {            if (preg_match("/^(\s*)!!!(\s*)$/", $line, $matches)) {                if ($this->isBlock('shtml')) {                    $this->setBlock($key)->endBlock();                } else {                    $this->startBlock('shtml', $key);                }                return false;            } elseif ($this->isBlock('shtml')) {                $this->setBlock($key);                return false;            }        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockAhtml(?array $block, int $key, string $line, ?array &$state): bool    {        if ($this->_html) {            if (preg_match("/^\s*<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $matches)) {                if ($this->isBlock('ahtml')) {                    $this->setBlock($key);                    return false;                } elseif (empty($matches[2]) || $matches[2] != '/') {                    $this->startBlock('ahtml', $key);                    preg_match_all("/<({$this->_blockHtmlTags})(\s+[^>]*)?>/i", $line, $allMatches);                    $lastMatch = $allMatches[1][count($allMatches[0]) - 1];                    if (strpos($line, "</{$lastMatch}>") !== false) {                        $this->endBlock();                    } else {                        $state['html'] = $lastMatch;                    }                    return false;                }            } elseif (!!$state['html'] && strpos($line, "</{$state['html']}>") !== false) {                $this->setBlock($key)->endBlock();                $state['html'] = false;                return false;            } elseif ($this->isBlock('ahtml')) {                $this->setBlock($key);                return false;            } elseif (preg_match("/^\s*<!\-\-(.*?)\-\->\s*$/", $line, $matches)) {                $this->startBlock('ahtml', $key)->endBlock();                return false;            }        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockMath(?array $block, int $key, string $line): bool    {        if (preg_match("/^(\s*)\\$\\$(\s*)$/", $line, $matches)) {            if ($this->isBlock('math')) {                $this->setBlock($key)->endBlock();            } else {                $this->startBlock('math', $key);            }            return false;        } elseif ($this->isBlock('math')) {            $this->setBlock($key);            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockPre(?array $block, int $key, string $line, ?array &$state): bool    {        if (preg_match("/^ {4}/", $line)) {            if ($this->isBlock('pre')) {                $this->setBlock($key);            } else {                $this->startBlock('pre', $key);            }            return false;        } elseif ($this->isBlock('pre') && preg_match("/^\s*$/", $line)) {            $this->setBlock($key);            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockHtml(?array $block, int $key, string $line, ?array &$state): bool    {        if (preg_match("/^\s*<({$state['special']})(\s+[^>]*)?>/i", $line, $matches)) {            $tag = strtolower($matches[1]);            if (!$this->isBlock('html', $tag) && !$this->isBlock('pre')) {                $this->startBlock('html', $key, $tag);            }            return false;        } elseif (preg_match("/<\/({$state['special']})>\s*$/i", $line, $matches)) {            $tag = strtolower($matches[1]);            if ($this->isBlock('html', $tag)) {                $this->setBlock($key)                    ->endBlock();            }            return false;        } elseif ($this->isBlock('html')) {            $this->setBlock($key);            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockFootnote(?array $block, int $key, string $line): bool    {        if (preg_match("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", $line, $matches)) {            $space = strlen($matches[0]) - 1;            $this->startBlock('footnote', $key, [                $space, $matches[1]            ]);            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockDefinition(?array $block, int $key, string $line): bool    {        if (preg_match("/^\s*\[((?:[^\]]|\\]|\\[)+?)\]:\s*(.+)$/", $line, $matches)) {            $this->_definitions[$matches[1]] = $this->cleanUrl($matches[2]);            $this->startBlock('definition', $key)                ->endBlock();            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockQuote(?array $block, int $key, string $line): bool    {        if (preg_match("/^(\s*)>/", $line, $matches)) {            if ($this->isBlock('list') && strlen($matches[1]) > 0) {                $this->setBlock($key);            } elseif ($this->isBlock('quote')) {                $this->setBlock($key);            } else {                $this->startBlock('quote', $key);            }            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     * @param array|null $lines     *     * @return bool     */    private function parseBlockTable(?array $block, int $key, string $line, ?array &$state, array $lines): bool    {        if (preg_match("/^((?:(?:(?:\||\+)(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:[ :]*\-+[ :]*)(?:\||\+)(?:[ :]*\-+[ :]*))|(?:(?:[ :]*\-+[ :]*)(?:\||\+))|(?:(?:\||\+)(?:[ :]*\-+[ :]*)))+)$/", $line, $matches)) {            if ($this->isBlock('table')) {                $block[3][0][] = $block[3][2];                $block[3][2] ++;                $this->setBlock($key, $block[3]);            } else {                $head = 0;                if (empty($block) ||                    $block[0] != 'normal' ||                    preg_match("/^\s*$/", $lines[$block[2]])) {                    $this->startBlock('table', $key);                } else {                    $head = 1;                    $this->backBlock(1, 'table');                }                if ($matches[1][0] == '|') {                    $matches[1] = substr($matches[1], 1);                    if ($matches[1][strlen($matches[1]) - 1] == '|') {                        $matches[1] = substr($matches[1], 0, - 1);                    }                }                $rows = preg_split("/(\+|\|)/", $matches[1]);                $aligns = [];                foreach ($rows as $row) {                    $align = 'none';                    if (preg_match("/^\s*(:?)\-+(:?)\s*$/", $row, $matches)) {                        if (!empty($matches[1]) && !empty($matches[2])) {                            $align = 'center';                        } elseif (!empty($matches[1])) {                            $align = 'left';                        } elseif (!empty($matches[2])) {                            $align = 'right';                        }                    }                    $aligns[] = $align;                }                $this->setBlock($key, [[$head], $aligns, $head + 1]);            }            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockSh(?array $block, int $key, string $line): bool    {        if (preg_match("/^(#+)(.*)$/", $line, $matches)) {            $num = min(strlen($matches[1]), 6);            $this->startBlock('sh', $key, $num)                ->endBlock();            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     * @param array $lines     *     * @return bool     */    private function parseBlockMh(?array $block, int $key, string $line, ?array &$state, array $lines): bool    {        if (preg_match("/^\s*((=|-){2,})\s*$/", $line, $matches)            && ($block && $block[0] == "normal" && !preg_match("/^\s*$/", $lines[$block[2]]))) {    // check if last line isn't empty            if ($this->isBlock('normal')) {                $this->backBlock(1, 'mh', $matches[1][0] == '=' ? 1 : 2)                    ->setBlock($key)                    ->endBlock();            } else {                $this->startBlock('normal', $key);            }            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockShr(?array $block, int $key, string $line): bool    {        if (preg_match("/^(\* *){3,}\s*$/", $line)) {            $this->startBlock('hr', $key)                ->endBlock();            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     *     * @return bool     */    private function parseBlockDhr(?array $block, int $key, string $line): bool    {        if (preg_match("/^(- *){3,}\s*$/", $line)) {            $this->startBlock('hr', $key)                ->endBlock();            return false;        }        return true;    }    /**     * @param array|null $block     * @param int $key     * @param string $line     * @param array|null $state     *     * @return bool     */    private function parseBlockDefault(?array $block, int $key, string $line, ?array &$state): bool    {        if ($this->isBlock('footnote')) {            preg_match("/^(\s*)/", $line, $matches);            if (strlen($matches[1]) >= $block[3][0]) {                $this->setBlock($key);            } else {                $this->startBlock('normal', $key);            }        } elseif ($this->isBlock('table')) {            if (false !== strpos($line, '|')) {                $block[3][2] ++;                $this->setBlock($key, $block[3]);            } else {                $this->startBlock('normal', $key);            }        } elseif ($this->isBlock('quote')) {            if (!preg_match("/^(\s*)$/", $line)) { // empty line                $this->setBlock($key);            } else {                $this->startBlock('normal', $key);            }        } else {            if (empty($block) || $block[0] != 'normal') {                $this->startBlock('normal', $key);            } else {                $this->setBlock($key);            }        }        return true;    }    /**     * @param array $blocks     * @param array $lines     *     * @return array     */    private function optimizeBlocks(array $blocks, array $lines): array    {        $blocks = $this->call('beforeOptimizeBlocks', $blocks, $lines);        $key = 0;        while (isset($blocks[$key])) {            $moved = false;            $block = &$blocks[$key];            $prevBlock = $blocks[$key - 1] ?? null;            $nextBlock = $blocks[$key + 1] ?? null;            [$type, $from, $to] = $block;            if ('pre' == $type) {                $isEmpty = array_reduce(                    array_slice($lines, $block[1], $block[2] - $block[1] + 1),                    function ($result, $line) {                        return preg_match("/^\s*$/", $line) && $result;                    },                    true                );                if ($isEmpty) {                    $block[0] = $type = 'normal';                }            }            if ('normal' == $type) {                // combine two blocks                $types = ['list', 'quote'];                if ($from == $to && preg_match("/^\s*$/", $lines[$from])                    && !empty($prevBlock) && !empty($nextBlock)) {                    if ($prevBlock[0] == $nextBlock[0] && in_array($prevBlock[0], $types)                        && ($prevBlock[0] != 'list'                            || ($prevBlock[3][0] == $nextBlock[3][0] && $prevBlock[3][1] == $nextBlock[3][1]))) {                        // combine 3 blocks                        $blocks[$key - 1] = [                            $prevBlock[0], $prevBlock[1], $nextBlock[2], $prevBlock[3] ?? null                        ];                        array_splice($blocks, $key, 2);                        // do not move                        $moved = true;                    }                }            }            if (!$moved) {                $key ++;            }        }        return $this->call('afterOptimizeBlocks', $blocks, $lines);    }    /**     * parseCode     *     * @param array $lines     * @param array $parts     * @param int $start     *     * @return string     */    private function parseCode(array $lines, array $parts, int $start): string    {        [$blank, $lang] = $parts;        $lang = trim($lang);        $count = strlen($blank);        if (!preg_match("/^[_a-z0-9-\+\#\:\.]+$/i", $lang)) {            $lang = null;        } else {            $parts = explode(':', $lang);            if (count($parts) > 1) {                [$lang, $rel] = $parts;                $lang = trim($lang);                $rel = trim($rel);            }        }        $isEmpty = true;        $lines = array_map(function ($line) use ($count, &$isEmpty) {            $line = preg_replace("/^[ ]{{$count}}/", '', $line);            if ($isEmpty && !preg_match("/^\s*$/", $line)) {                $isEmpty = false;            }            return htmlspecialchars($line);        }, array_slice($lines, 1, - 1));        $str = implode("\n", $this->markLines($lines, $start + 1));        return $isEmpty ? '' :            '<pre><code' . (!empty($lang) ? " class=\"{$lang}\"" : '')            . (!empty($rel) ? " rel=\"{$rel}\"" : '') . '>'            . $str . '</code></pre>';    }    /**     * parsePre     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parsePre(array $lines, $value, int $start): string    {        foreach ($lines as &$line) {            $line = htmlspecialchars(substr($line, 4));        }        $str = implode("\n", $this->markLines($lines, $start));        return preg_match("/^\s*$/", $str) ? '' : '<pre><code>' . $str . '</code></pre>';    }    /**     * parseAhtml     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseAhtml(array $lines, $value, int $start): string    {        return trim(implode("\n", $this->markLines($lines, $start)));    }    /**     * parseShtml     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseShtml(array $lines, $value, int $start): string    {        return trim(implode("\n", $this->markLines(array_slice($lines, 1, - 1), $start + 1)));    }    /**     * parseMath     *     * @param array $lines     * @param mixed $value     * @param int $start     * @param int $end     *     * @return string     */    private function parseMath(array $lines, $value, int $start, int $end): string    {        return '<p>' . $this->markLine($start, $end) . htmlspecialchars(implode("\n", $lines)) . '</p>';    }    /**     * parseSh     *     * @param array $lines     * @param int $num     * @param int $start     * @param int $end     *     * @return string     */    private function parseSh(array $lines, int $num, int $start, int $end): string    {        $line = $this->markLine($start, $end) . $this->parseInline(trim($lines[0], '# '));        return preg_match("/^\s*$/", $line) ? '' : "<h{$num}>{$line}</h{$num}>";    }    /**     * parseMh     *     * @param array $lines     * @param int $num     * @param int $start     * @param int $end     *     * @return string     */    private function parseMh(array $lines, int $num, int $start, int $end): string    {        return $this->parseSh($lines, $num, $start, $end);    }    /**     * parseQuote     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseQuote(array $lines, $value, int $start): string    {        foreach ($lines as &$line) {            $line = preg_replace("/^\s*> ?/", '', $line);        }        $str = implode("\n", $lines);        return preg_match("/^\s*$/", $str) ? '' : '<blockquote>' . $this->parse($str, true, $start) . '</blockquote>';    }    /**     * parseList     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseList(array $lines, $value, int $start): string    {        $html = '';        [$space, $type, $tab] = $value;        $rows = [];        $suffix = '';        $last = 0;        foreach ($lines as $key => $line) {            if (preg_match("/^(\s{" . $space . "})((?:[0-9]+\.?)|\-|\+|\*)(\s+)(.*)$/i", $line, $matches)) {                // 检测任务列表语法 [ ] 或 [x]                $content = $matches[4];                $isTask = false;                $checked = false;                                if (substr($content, 0, 4) === '[ ] ') {                    $isTask = true;                    $checked = ($taskMatches === 'x');                    $content = substr($content, 4);                }else if (substr($content, 0, 4) === '[x] ' || substr($content, 0, 4) === '[X] '){                    $isTask = true;                    $checked = true;                    $content = substr($content, 4);                }                                                if ($type == 'ol' && $key == 0) {                    $olStart = intval($matches[2]);                    if ($olStart != 1) {                        $suffix = ' start="' . $olStart . '"';                    }                }                $rows[] = [                    'content' => "$content",                    'isTask' => $isTask,                    'checked' => $checked               ];                $last = count($rows) - 1;            } else {                $rows[$last]['content'] .="\n" . preg_replace("/^\s{" . ($tab + $space) . "}/", '', $line);            }        }        foreach ($rows as $row) {            $parsedContent = $this->parse(implode("\n", [$row['content']]), true, $start);            if ($row['isTask']) {                $checkedAttr = $row['checked'] ? ' checked' : '';                $html .= "<li><input type=\"checkbox\" onclick='this.checked=!this.checked' {$checkedAttr}> {$parsedContent}</li>";            } else {                $html .= "<li>{$parsedContent}</li>";            }                    $start += count([$row['content']]);        }        return "<{$type}{$suffix}>{$html}</{$type}>";    }    /**     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseTable(array $lines, $value, int $start): string    {        [$ignores, $aligns] = $value;        $head = count($ignores) > 0 && array_sum($ignores) > 0;        $html = '<table>';        $body = $head ? null : true;        $output = false;        foreach ($lines as $key => $line) {            if (in_array($key, $ignores)) {                if ($head && $output) {                    $head = false;                    $body = true;                }                continue;            }            $line = trim($line);            $output = true;            if ($line[0] == '|') {                $line = substr($line, 1);                if ($line[strlen($line) - 1] == '|') {                    $line = substr($line, 0, - 1);                }            }            $rows = array_map(function ($row) {                if (preg_match("/^\s*$/", $row)) {                    return ' ';                } else {                    return trim($row);                }            }, explode('|', $line));            $columns = [];            $last = - 1;            foreach ($rows as $row) {                if (strlen($row) > 0) {                    $last ++;                    $columns[$last] = [                        isset($columns[$last]) ? $columns[$last][0] + 1 : 1, $row                    ];                } elseif (isset($columns[$last])) {                    $columns[$last][0] ++;                } else {                    $columns[0] = [1, $row];                }            }            if ($head) {                $html .= '<thead>';            } elseif ($body) {                $html .= '<tbody>';            }            $html .= '<tr' . ($this->_line ? ' class="line" data-start="'                    . ($start + $key) . '" data-end="' . ($start + $key)                    . '" data-id="' . $this->_uniqid . '"' : '') . '>';            foreach ($columns as $key => $column) {                [$num, $text] = $column;                $tag = $head ? 'th' : 'td';                $html .= "<{$tag}";                if ($num > 1) {                    $html .= " colspan=\"{$num}\"";                }                if (isset($aligns[$key]) && $aligns[$key] != 'none') {                    $html .= " align=\"{$aligns[$key]}\"";                }                $html .= '>' . $this->parseInline($text) . "</{$tag}>";            }            $html .= '</tr>';            if ($head) {                $html .= '</thead>';            } elseif ($body) {                $body = false;            }        }        if ($body !== null) {            $html .= '</tbody>';        }        $html .= '</table>';        return $html;    }    /**     * parseHr     *     * @param array $lines     * @param mixed $value     * @param int $start     *     * @return string     */    private function parseHr(array $lines, $value, int $start): string    {        return $this->_line ? '<hr class="line" data-start="' . $start . '" data-end="' . $start . '">' : '<hr>';    }    /**     * parseNormal     *     * @param array $lines     * @param mixed $inline     * @param int $start     *     * @return string     */    private function parseNormal(array $lines, $inline, int $start): string    {        foreach ($lines as $key => &$line) {            $line = $this->parseInline($line);            if (!preg_match("/^\s*$/", $line)) {                $line = $this->markLine($start + $key) . $line;            }        }        $str = trim(implode("\n", $lines));        $str = preg_replace_callback("/(\n\s*){2,}/", function () use (&$inline) {            $inline = false;            return "</p><p>";        }, $str);        $str = preg_replace("/\n/", "<br>", $str);        return preg_match("/^\s*$/", $str) ? '' : ($inline ? $str : "<p>{$str}</p>");    }    /**     * parseFootnote     *     * @param array $lines     * @param array $value     *     * @return string     */    private function parseFootnote(array $lines, array $value): string    {        [$space, $note] = $value;        $index = array_search($note, $this->_footnotes);        if (false !== $index) {            $lines[0] = preg_replace("/^\[\^((?:[^\]]|\\]|\\[)+?)\]:/", '', $lines[0]);            $this->_footnotes[$index] = $lines;        }        return '';    }    /**     * parseDefine     *     * @return string     */    private function parseDefinition(): string    {        return '';    }    /**     * parseHtml     *     * @param array $lines     * @param string $type     * @param int $start     *     * @return string     */    private function parseHtml(array $lines, string $type, int $start): string    {        foreach ($lines as &$line) {            $line = $this->parseInline($line,                $this->_specialWhiteList[$type] ?? '');        }        return implode("\n", $this->markLines($lines, $start));    }    /**     * @param $url     * @param bool $parseTitle     *     * @return mixed     */    private function cleanUrl($url, bool $parseTitle = false)    {        $title = null;        $url = trim($url);        if ($parseTitle) {            $pos = strpos($url, ' ');            if ($pos !== false) {                $title = htmlspecialchars(trim(substr($url, $pos + 1), ' "\''));                $url = substr($url, 0, $pos);            }        }        $url = preg_replace("/[\"'<>\s]/", '', $url);        if (preg_match("/^(mailto:)?[_a-z0-9-\.\+]+@[_\w-]+(?:\.[a-z]{2,})+$/i", $url, $matches)) {            if (empty($matches[1])) {                $url = 'mailto:' . $url;            }        }        if (preg_match("/^\w+:/i", $url) && !preg_match("/^(https?|mailto):/i", $url)) {            return '#';        }        return $parseTitle ? [$url, $title] : $url;    }    /**     * @param $str     *     * @return string     */    private function escapeBracket($str): string    {        return str_replace(            ['\[', '\]', '\(', '\)'], ['[', ']', '(', ')'], $str        );    }    /**     * startBlock     *     * @param mixed $type     * @param mixed $start     * @param mixed $value     *     * @return $this     */    private function startBlock($type, $start, $value = null): HyperDown    {        $this->_pos ++;        $this->_current = $type;        $this->_blocks[$this->_pos] = [$type, $start, $start, $value];        return $this;    }    /**     * endBlock     *     * @return $this     */    private function endBlock(): HyperDown    {        $this->_current = 'normal';        return $this;    }    /**     * isBlock     *     * @param mixed $type     * @param mixed $value     *     * @return bool     */    private function isBlock($type, $value = null): bool    {        return $this->_current == $type            && (null === $value || $this->_blocks[$this->_pos][3] == $value);    }    /**     * getBlock     *     * @return array     */    private function getBlock(): ?array    {        return $this->_blocks[$this->_pos] ?? null;    }    /**     * setBlock     *     * @param mixed $to     * @param mixed $value     *     * @return $this     */    private function setBlock($to = null, $value = null): HyperDown    {        if (null !== $to) {            $this->_blocks[$this->_pos][2] = $to;        }        if (null !== $value) {            $this->_blocks[$this->_pos][3] = $value;        }        return $this;    }    /**     * backBlock     *     * @param mixed $step     * @param mixed $type     * @param mixed $value     *     * @return $this     */    private function backBlock($step, $type, $value = null): HyperDown    {        if ($this->_pos < 0) {            return $this->startBlock($type, 0, $value);        }        $last = $this->_blocks[$this->_pos][2];        $this->_blocks[$this->_pos][2] = $last - $step;        if ($this->_blocks[$this->_pos][1] <= $this->_blocks[$this->_pos][2]) {            $this->_pos ++;        }        $this->_current = $type;        $this->_blocks[$this->_pos] = [            $type, $last - $step + 1, $last, $value        ];        return $this;    }    /**     * @return $this     */    private function combineBlock(): HyperDown    {        if ($this->_pos < 1) {            return $this;        }        $prev = $this->_blocks[$this->_pos - 1];        $current = $this->_blocks[$this->_pos];        $prev[2] = $current[2];        $this->_blocks[$this->_pos - 1] = $prev;        $this->_current = $prev[0];        unset($this->_blocks[$this->_pos]);        $this->_pos --;        return $this;    }}
 |