144 lines
5.2 KiB
PHP
144 lines
5.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace NeonBlog;
|
|
|
|
final class Markdown
|
|
{
|
|
public static function toHtml(string $markdown): string
|
|
{
|
|
$markdown = str_replace(["\r\n", "\r"], "\n", trim($markdown));
|
|
if ($markdown === '') {
|
|
return '';
|
|
}
|
|
|
|
$lines = explode("\n", $markdown);
|
|
$html = [];
|
|
$count = count($lines);
|
|
$i = 0;
|
|
|
|
while ($i < $count) {
|
|
$line = rtrim($lines[$i]);
|
|
if (trim($line) === '') {
|
|
$i++;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^```([A-Za-z0-9_-]*)\s*$/', $line, $matches)) {
|
|
$language = $matches[1] !== '' ? ' class="language-' . self::e($matches[1]) . '"' : '';
|
|
$code = [];
|
|
$i++;
|
|
while ($i < $count && !preg_match('/^```\s*$/', rtrim($lines[$i]))) {
|
|
$code[] = $lines[$i];
|
|
$i++;
|
|
}
|
|
$html[] = '<pre><code' . $language . '>' . self::e(implode("\n", $code)) . '</code></pre>';
|
|
$i++;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $matches)) {
|
|
$level = strlen($matches[1]);
|
|
$html[] = '<h' . $level . '>' . self::inline($matches[2]) . '</h' . $level . '>';
|
|
$i++;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^(-{3,}|\*{3,}|_{3,})$/', trim($line))) {
|
|
$html[] = '<hr>';
|
|
$i++;
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^\s*>\s?(.*)$/', $line)) {
|
|
$quote = [];
|
|
while ($i < $count && preg_match('/^\s*>\s?(.*)$/', rtrim($lines[$i]), $matches)) {
|
|
$quote[] = $matches[1];
|
|
$i++;
|
|
}
|
|
$html[] = '<blockquote>' . self::toHtml(implode("\n", $quote)) . '</blockquote>';
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^\s*[-*+]\s+(.+)$/', $line)) {
|
|
$items = [];
|
|
while ($i < $count && preg_match('/^\s*[-*+]\s+(.+)$/', rtrim($lines[$i]), $matches)) {
|
|
$items[] = '<li>' . self::inline($matches[1]) . '</li>';
|
|
$i++;
|
|
}
|
|
$html[] = '<ul>' . implode('', $items) . '</ul>';
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^\s*\d+\.\s+(.+)$/', $line)) {
|
|
$items = [];
|
|
while ($i < $count && preg_match('/^\s*\d+\.\s+(.+)$/', rtrim($lines[$i]), $matches)) {
|
|
$items[] = '<li>' . self::inline($matches[1]) . '</li>';
|
|
$i++;
|
|
}
|
|
$html[] = '<ol>' . implode('', $items) . '</ol>';
|
|
continue;
|
|
}
|
|
|
|
$paragraph = [$line];
|
|
$i++;
|
|
while ($i < $count && trim($lines[$i]) !== '' && !self::startsBlock(rtrim($lines[$i]))) {
|
|
$paragraph[] = trim($lines[$i]);
|
|
$i++;
|
|
}
|
|
$html[] = '<p>' . self::inline(implode(' ', $paragraph)) . '</p>';
|
|
}
|
|
|
|
return implode("\n", $html);
|
|
}
|
|
|
|
public static function excerpt(string $markdown, int $words = 28): string
|
|
{
|
|
$text = trim(preg_replace('/\s+/', ' ', strip_tags(self::toHtml($markdown))) ?? '');
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
|
|
$parts = preg_split('/\s+/', $text) ?: [];
|
|
if (count($parts) <= $words) {
|
|
return $text;
|
|
}
|
|
|
|
return implode(' ', array_slice($parts, 0, $words)) . '...';
|
|
}
|
|
|
|
private static function startsBlock(string $line): bool
|
|
{
|
|
return (bool) preg_match('/^(#{1,6}\s+|```|^\s*[-*+]\s+|^\s*\d+\.\s+|^\s*>\s?|(-{3,}|\*{3,}|_{3,})$)/', $line);
|
|
}
|
|
|
|
private static function inline(string $text): string
|
|
{
|
|
$placeholders = [];
|
|
$text = preg_replace_callback('/`([^`]+)`/', static function (array $matches) use (&$placeholders): string {
|
|
$key = "\x1A" . count($placeholders) . "\x1A";
|
|
$placeholders[$key] = '<code>' . self::e($matches[1]) . '</code>';
|
|
return $key;
|
|
}, $text) ?? $text;
|
|
|
|
$text = self::e($text);
|
|
$text = preg_replace_callback('/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/', static function (array $matches): string {
|
|
$title = isset($matches[3]) ? ' title="' . self::e($matches[3]) . '"' : '';
|
|
return '<img src="' . self::e($matches[2]) . '" alt="' . self::e($matches[1]) . '"' . $title . '>';
|
|
}, $text) ?? $text;
|
|
$text = preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/', static function (array $matches): string {
|
|
$title = isset($matches[3]) ? ' title="' . self::e($matches[3]) . '"' : '';
|
|
return '<a href="' . self::e($matches[2]) . '"' . $title . '>' . $matches[1] . '</a>';
|
|
}, $text) ?? $text;
|
|
$text = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $text) ?? $text;
|
|
$text = preg_replace('/\*([^*]+)\*/', '<em>$1</em>', $text) ?? $text;
|
|
|
|
return strtr($text, $placeholders);
|
|
}
|
|
|
|
private static function e(string $value): string
|
|
{
|
|
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
}
|