Secure
Markdown.php
← Back to Folder Raw Code
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
<?php
class Markdown {
    private static $currentFilePath = '';
    private static $webPath = '';
    public static function parse($text, $baseUrl = '', $webPath = '') {
        self::$currentFilePath = $baseUrl;
        self::$webPath = $webPath;
        $text = str_replace(["\r\n", "\r"], "\n", $text);
        $codeBlocks = [];
        $inlineCodes = [];
        $htmlTags = [];
        $text = preg_replace_callback('/<(img|a|p|div|span|picture|source|br|hr)[^>]*>/i', function($matches) use (&$htmlTags) {
            $id = count($htmlTags);
            $placeholder = "XHTMLTAGREPLACEX" . $id . "XHTMLTAGREPLACEX";
            $htmlTags[$placeholder] = $matches[0];
            return $placeholder;
        }, $text);
        $text = preg_replace_callback('/<\/(img|a|p|div|span|picture|source|br|hr)>/i', function($matches) use (&$htmlTags) {
            $id = count($htmlTags);
            $placeholder = "XHTMLTAGREPLACEX" . $id . "XHTMLTAGREPLACEX";
            $htmlTags[$placeholder] = $matches[0];
            return $placeholder;
        }, $text);
        $text = preg_replace_callback('/<!--.*?-->/s', function($matches) use (&$htmlTags) {
            $id = count($htmlTags);
            $placeholder = "XHTMLTAGREPLACEX" . $id . "XHTMLTAGREPLACEX";
            $htmlTags[$placeholder] = $matches[0];
            return $placeholder;
        }, $text);
        $text = preg_replace_callback('/&[a-zA-Z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/', function($matches) use (&$htmlTags) {
            $id = count($htmlTags);
            $placeholder = "XHTMLTAGREPLACEX" . $id . "XHTMLTAGREPLACEX";
            $htmlTags[$placeholder] = $matches[0];
            return $placeholder;
        }, $text);
        $text = preg_replace_callback('/```([a-zA-Z0-9\-_]*)\n?(.*?)\n?```/s', function($matches) use (&$codeBlocks) {
            $id = count($codeBlocks);
            $placeholder = "XCODEBLOCKREPLACEX" . $id . "XCODEBLOCKREPLACEX";
            $codeBlocks[$placeholder] = trim($matches[2]);
            return "\n" . $placeholder . "\n";
        }, $text);
        $text = preg_replace_callback('/`([^`\n]+?)`/', function($matches) use (&$inlineCodes) {
            $id = count($inlineCodes);
            $placeholder = "XINLINECODEREPLACEX" . $id . "XINLINECODEREPLACEX";
            $inlineCodes[$placeholder] = $matches[1];
            return $placeholder;
        }, $text);
        $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
        $text = preg_replace_callback('/^###### (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h6 id="' . $id . '">' . $m[1] . '</h6>';
        }, $text);
        $text = preg_replace_callback('/^##### (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h5 id="' . $id . '">' . $m[1] . '</h5>';
        }, $text);
        $text = preg_replace_callback('/^#### (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h4 id="' . $id . '">' . $m[1] . '</h4>';
        }, $text);
        $text = preg_replace_callback('/^### (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h3 id="' . $id . '">' . $m[1] . '</h3>';
        }, $text);
        $text = preg_replace_callback('/^## (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h2 id="' . $id . '">' . $m[1] . '</h2>';
        }, $text);
        $text = preg_replace_callback('/^# (.+?)$/m', function($m) {
            $id = self::generateHeaderId($m[1]);
            return '<h1 id="' . $id . '">' . $m[1] . '</h1>';
        }, $text);
        $text = preg_replace_callback('/!\[([^\]]*?)\]\(([^)]+?)\)/', function($matches) {
            $alt = $matches[1];
            $src = $matches[2];
            if (!preg_match('/^(https?:\/\/|\/\/|data:)/', $src)) {
                $baseUrl = self::$currentFilePath;
                $src = rtrim($baseUrl, '/') . '/' . ltrim($src, '/');
            }
            return '<img src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '" alt="' . htmlspecialchars($alt, ENT_QUOTES, 'UTF-8') . '" class="markdown-img">';
        }, $text);
        $text = preg_replace_callback('/\[([^\]]+?)\]\(([^)]+?)\)/', function($matches) {
            $linkText = $matches[1];
            $linkUrl = $matches[2];
            $processedUrl = self::processMarkdownLink($linkUrl);
            return '<a href="' . $processedUrl . '" class="markdown-link">' . $linkText . '</a>';
        }, $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)\*\*\*([^*\n]+?)\*\*\*(?!XINLINECODEREPLACEX)/', '<strong><em>$1</em></strong>', $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)\*\*([^*\n]+?)\*\*(?!XINLINECODEREPLACEX)/', '<strong>$1</strong>', $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)(?<!\*)\*([^*\n]+?)\*(?!\*)(?!XINLINECODEREPLACEX)/', '<em>$1</em>', $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)___([^_\n]+?)___(?!XINLINECODEREPLACEX)/', '<strong><em>$1</em></strong>', $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)(?<!_)__([^_\n]+?)__(?!_)(?!XINLINECODEREPLACEX)/', '<strong>$1</strong>', $text);
        $text = preg_replace('/(?<!XINLINECODEREPLACEX)(?<!_)_([^_\n]+?)_(?!_)(?!XINLINECODEREPLACEX)/', '<em>$1</em>', $text);
        $text = preg_replace('/~~([^~\n]+?)~~/', '<del>$1</del>', $text);
        $text = preg_replace('/^\s*---\s*$/m', '<hr class="markdown-hr">', $text);
        $text = preg_replace('/^\s*\*\*\*\s*$/m', '<hr class="markdown-hr">', $text);
        $text = preg_replace('/^&gt; (.+?)$/m', '<blockquote class="markdown-blockquote">$1</blockquote>', $text);
        $text = preg_replace('/^(\s*)[\*\-\+] (.+?)$/m', '$1<li class="markdown-li">$2</li>', $text);
        $text = preg_replace('/^(\s*)\d+\. (.+?)$/m', '$1<li class="markdown-li markdown-li-ordered">$2</li>', $text);
        $text = self::wrapLists($text);
        $text = preg_replace_callback('/(?:^\|.+\|\s*$\n?)+/m', function($matches) {
            return self::parseTable($matches[0]);
        }, $text);
        $text = self::wrapParagraphs($text);
        foreach ($codeBlocks as $placeholder => $content) {
            $escapedContent = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
            $codeHtml = '<pre class="code-block"><code>' . $escapedContent . '</code></pre>';
            $text = str_replace($placeholder, $codeHtml, $text);
        }
        foreach ($inlineCodes as $placeholder => $content) {
            $escapedContent = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
            $codeHtml = '<code class="inline-code">' . $escapedContent . '</code>';
            $text = str_replace($placeholder, $codeHtml, $text);
        }
        foreach ($htmlTags as $placeholder => $content) {
            if (preg_match('/<img\s/i', $content)) {
                $content = preg_replace_callback('/src\s*=\s*["\']([^"\']+)["\']/i', function($matches) {
                    $src = $matches[1];
                    if (!preg_match('/^(https?:\/\/|\/\/|data:)/', $src)) {
                        $baseUrl = self::$currentFilePath;
                        $src = rtrim($baseUrl, '/') . '/' . ltrim($src, '/');
                    }
                    return 'src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '"';
                }, $content);
                $content = preg_replace_callback('/<img([^>]*?)>/i', function($m) {
                    $attrs = $m[1];
                    $style = '';
                    if (preg_match('/width\s*=\s*["\']?(\d+)["\']?/i', $attrs, $widthMatch)) {
                        $style .= 'width: ' . $widthMatch[1] . 'px; ';
                    }
                    if (preg_match('/height\s*=\s*["\']?(\d+)["\']?/i', $attrs, $heightMatch)) {
                        $style .= 'height: ' . $heightMatch[1] . 'px; ';
                    }
                    if ($style) {
                        $style = 'max-width: 100%; ' . $style;
                        if (preg_match('/style\s*=\s*["\']([^"\']*)["\']/', $attrs)) {
                            $attrs = preg_replace('/style\s*=\s*["\']([^"\']*)["\']/', 'style="$1 ' . $style . '"', $attrs);
                        } else {
                            $attrs .= ' style="' . trim($style) . '"';
                        }
                    }
                    return '<img' . $attrs . '>';
                }, $content);
                if (preg_match('/class\s*=\s*["\']([^"\']*)["\']/', $content, $classMatch)) {
                    if (strpos($classMatch[1], 'markdown-img') === false) {
                        $newClass = trim($classMatch[1] . ' markdown-img');
                        $content = preg_replace('/class\s*=\s*["\']([^"\']*)["\']/', 'class="' . $newClass . '"', $content);
                    }
                } else {
                    $content = preg_replace('/<img\s/', '<img class="markdown-img" ', $content);
                }
            }
            $text = str_replace($placeholder, $content, $text);
        }
        return $text;
    }
    private static function processMarkdownLink($linkUrl) {
        if (preg_match('/^(https?:\/\/|\/\/|mailto:|#)/', $linkUrl)) {
            return htmlspecialchars($linkUrl, ENT_QUOTES, 'UTF-8');
        }
        $urlParts = parse_url($linkUrl);
        $path = $urlParts['path'] ?? $linkUrl;
        $existingQuery = $urlParts['query'] ?? '';
        $fragment = $urlParts['fragment'] ?? '';
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        $isMarkdown = in_array($extension, ['md', 'markdown']);
        $baseUrl = self::$currentFilePath;
        $newUrl = rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
        if (substr($newUrl, -1) !== '/') {
            $newUrl .= '/';
        }
        if ($existingQuery) {
            $newUrl .= '?' . $existingQuery;
        } elseif ($isMarkdown) {
            $newUrl .= '?view=default';
        }
        if ($fragment) {
            $newUrl .= '#' . $fragment;
        }
        return htmlspecialchars($newUrl, ENT_QUOTES, 'UTF-8');
    }
    private static function resolveRelativePath($currentDir, $relativePath) {
        $parts = $currentDir ? explode('/', $currentDir) : [];
        $relParts = explode('/', $relativePath);
        foreach ($relParts as $part) {
            if ($part === '' || $part === '.') {
                continue;
            } elseif ($part === '..') {
                if (count($parts) > 0) {
                    array_pop($parts);
                }
            } else {
                $parts[] = $part;
            }
        }
        return implode('/', $parts);
    }
    private static function generateHeaderId($headerText) {
        $headerText = strip_tags($headerText);
        $id = strtolower($headerText);
        $id = preg_replace('/[^a-z0-9]+/', '-', $id);
        $id = trim($id, '-');
        return $id;
    }
    private static function wrapLists($text) {
        $lines = explode("\n", $text);
        $result = [];
        $inList = false;
        $listType = '';
        $lastWasListItem = false;
        foreach ($lines as $line) {
            if (preg_match('/^(\s*)<li class="markdown-li( markdown-li-ordered)?"/', $line, $matches)) {
                $isOrdered = !empty($matches[2]);
                $newListType = $isOrdered ? 'ol' : 'ul';
                if (!$inList) {
                    $result[] = "<$newListType class=\"markdown-list\">";
                    $listType = $newListType;
                    $inList = true;
                } elseif ($listType !== $newListType) {
                    if (!($listType === 'ol' && $newListType === 'ol')) {
                        $result[] = "</$listType>";
                        $result[] = "<$newListType class=\"markdown-list\">";
                        $listType = $newListType;
                    }
                }
                $result[] = $line;
                $lastWasListItem = true;
            } else {
                if ($inList && trim($line) === '' && $lastWasListItem) {
                    $lastWasListItem = false;
                    continue;
                }
                if ($inList && trim($line) !== '') {
                    $result[] = "</$listType>";
                    $inList = false;
                }
                if (trim($line) !== '') {
                    $result[] = $line;
                    $lastWasListItem = false;
                }
            }
        }
        if ($inList) {
            $result[] = "</$listType>";
        }
        return implode("\n", $result);
    }
    private static function parseTable($table) {
        $table = trim($table);
        $rows = explode("\n", $table);
        $html = '<table class="markdown-table">';
        $isHeader = true;
        foreach ($rows as $row) {
            if (empty(trim($row))) continue;
            if (preg_match('/^\|[\s\-\|:]+\|$/', $row)) {
                $isHeader = false;
                continue;
            }
            $cells = explode('|', trim($row, '|'));
            $cells = array_map('trim', $cells);
            $tag = $isHeader ? 'th' : 'td';
            $class = $isHeader ? 'markdown-th' : 'markdown-td';
            $html .= '<tr class="markdown-tr">';
            foreach ($cells as $cell) {
                $html .= "<$tag class=\"$class\">$cell</$tag>";
            }
            $html .= '</tr>';
            if ($isHeader) $isHeader = false;
        }
        $html .= '</table>';
        return $html;
    }
    private static function wrapParagraphs($text) {
        $paragraphs = preg_split('/\n\s*\n/', $text);
        $result = [];
        foreach ($paragraphs as $paragraph) {
            $paragraph = trim($paragraph);
            if (empty($paragraph)) continue;
            if (preg_match('/^XCODEBLOCKREPLACEX\d+XCODEBLOCKREPLACEX$/', $paragraph)) {
                $result[] = $paragraph;
            }
            elseif (preg_match('/^<(h[1-6]|ul|ol|blockquote|pre|hr|table|div)/i', $paragraph)) {
                $result[] = $paragraph;
            }
            else {
                if (preg_match('/<(p|div|a|picture|img)[^>]*>/', $paragraph)) {
                    $result[] = $paragraph;
                } else {
                    $paragraph = preg_replace('/\n(?!<)/', '<br>', $paragraph);
                    $result[] = '<p class="markdown-p">' . $paragraph . '</p>';
                }
            }
        }
        return implode("\n\n", $result);
    }
}
?>