- Parser additions, pagination

This commit is contained in:
Ty Clifford
2026-07-04 20:29:34 -04:00
parent c4c10c256c
commit bb93bf58fe
6 changed files with 167 additions and 11 deletions
+26 -3
View File
@@ -79,11 +79,12 @@ final class ContentRepository
return $item;
}
/** @return array{items: array<int, array<string, mixed>>, pagination: array<string, int|bool|null>} */
/** @return array{items: array<int, array<string, mixed>>, pagination: array<string, mixed>} */
public function paginate(array $items, int $page, int $perPage): array
{
$perPage = max(1, $perPage);
$total = count($items);
$pages = max(1, (int) ceil($total / max(1, $perPage)));
$pages = max(1, (int) ceil($total / $perPage));
$page = max(1, min($page, $pages));
$offset = ($page - 1) * $perPage;
@@ -98,11 +99,12 @@ final class ContentRepository
'has_next' => $page < $pages,
'previous' => $page > 1 ? $page - 1 : null,
'next' => $page < $pages ? $page + 1 : null,
'numbers' => $this->paginationNumbers($page, $pages, 10),
],
];
}
/** @return array{items: array<int, array<string, mixed>>, pagination: array<string, int|bool|null>} */
/** @return array{items: array<int, array<string, mixed>>, pagination: array<string, mixed>} */
public function search(string $query, int $page, int $perPage): array
{
$query = trim(mb_strtolower($query));
@@ -150,6 +152,27 @@ final class ContentRepository
return $this->paginate($scored, $page, $perPage);
}
/** @return array<int, int> */
private function paginationNumbers(int $page, int $pages, int $window): array
{
if ($pages <= 1) {
return [1];
}
if ($pages <= $window) {
return range(1, $pages);
}
$before = (int) floor(($window - 1) / 2);
$start = max(1, $page - $before);
$end = $start + $window - 1;
if ($end > $pages) {
$end = $pages;
$start = max(1, $end - $window + 1);
}
return range($start, $end);
}
/** @return array<int, array<string, mixed>> */
public function byCategory(string $categorySlug): array
{
+100 -5
View File
@@ -5,9 +5,13 @@ namespace NeonBlog;
final class Markdown
{
public static function toHtml(string $markdown): string
public static function toHtml(string $markdown, bool $executePhp = true): string
{
$markdown = str_replace(["\r\n", "\r"], "\n", trim($markdown));
$markdown = str_replace(["\r\n", "\r"], "\n", $markdown);
if ($executePhp) {
$markdown = self::executePhp($markdown);
}
$markdown = trim($markdown);
if ($markdown === '') {
return '';
}
@@ -24,6 +28,12 @@ final class Markdown
continue;
}
if (self::startsHtmlBlock($line)) {
[$block, $i] = self::consumeHtmlBlock($lines, $i, $count);
$html[] = $block;
continue;
}
if (preg_match('/^```([A-Za-z0-9_-]*)\s*$/', $line, $matches)) {
$language = $matches[1] !== '' ? ' class="language-' . self::e($matches[1]) . '"' : '';
$code = [];
@@ -56,7 +66,7 @@ final class Markdown
$quote[] = $matches[1];
$i++;
}
$html[] = '<blockquote>' . self::toHtml(implode("\n", $quote)) . '</blockquote>';
$html[] = '<blockquote>' . self::toHtml(implode("\n", $quote), false) . '</blockquote>';
continue;
}
@@ -94,7 +104,10 @@ final class Markdown
public static function excerpt(string $markdown, int $words = 28): string
{
$text = trim(preg_replace('/\s+/', ' ', strip_tags(self::toHtml($markdown))) ?? '');
$markdown = preg_replace('/<\?(?:php|=)?[\s\S]*?\?>/i', ' ', $markdown) ?? $markdown;
$html = self::toHtml($markdown, false);
$html = preg_replace('#<(script|style)\b[^>]*>.*?</\1>#is', ' ', $html) ?? $html;
$text = trim(preg_replace('/\s+/', ' ', strip_tags($html)) ?? '');
if ($text === '') {
return '';
}
@@ -109,7 +122,8 @@ final class Markdown
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);
return self::startsHtmlBlock($line)
|| (bool) preg_match('/^(#{1,6}\s+|```|^\s*[-*+]\s+|^\s*\d+\.\s+|^\s*>\s?|(-{3,}|\*{3,}|_{3,})$)/', $line);
}
private static function inline(string $text): string
@@ -121,6 +135,12 @@ final class Markdown
return $key;
}, $text) ?? $text;
$text = preg_replace_callback('/<\/?[A-Za-z][A-Za-z0-9:-]*(?:\s+[^<>]*)?>|<!--.*?-->|<![A-Z][^>]*>/s', static function (array $matches) use (&$placeholders): string {
$key = "\x1A" . count($placeholders) . "\x1A";
$placeholders[$key] = $matches[0];
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]) . '"' : '';
@@ -136,6 +156,81 @@ final class Markdown
return strtr($text, $placeholders);
}
private static function executePhp(string $source): string
{
if (!str_contains($source, '<?')) {
return $source;
}
$placeholders = [];
$source = preg_replace_callback('/```[\s\S]*?```/', static function (array $matches) use (&$placeholders): string {
$key = "\x1BPHPBLOCK" . count($placeholders) . "\x1B";
$placeholders[$key] = $matches[0];
return $key;
}, $source) ?? $source;
$source = preg_replace_callback('/`[^`\n]*<\?[^`\n]*`/', static function (array $matches) use (&$placeholders): string {
$key = "\x1BPHPBLOCK" . count($placeholders) . "\x1B";
$placeholders[$key] = $matches[0];
return $key;
}, $source) ?? $source;
ob_start();
try {
eval('?>' . $source);
return strtr((string) ob_get_clean(), $placeholders);
} catch (\Throwable $error) {
ob_end_clean();
return strtr($source, $placeholders) . "\n\n<!-- PHP content error: " . self::e($error->getMessage()) . " -->";
}
}
private static function startsHtmlBlock(string $line): bool
{
$line = trim($line);
return (bool) preg_match('/^(<!--|<![A-Z]+|<\/?[A-Za-z][A-Za-z0-9:-]*(\s|>|\/>))/i', $line);
}
/** @param array<int, string> $lines @return array{0: string, 1: int} */
private static function consumeHtmlBlock(array $lines, int $i, int $count): array
{
$block = [rtrim($lines[$i])];
$trimmed = trim($lines[$i]);
$i++;
if (str_starts_with($trimmed, '<!--')) {
while ($i < $count && !str_contains(end($block), '-->')) {
$block[] = rtrim($lines[$i]);
$i++;
}
return [implode("\n", $block), $i];
}
if (!preg_match('/^<([A-Za-z][A-Za-z0-9:-]*)\b/i', $trimmed, $matches)) {
return [implode("\n", $block), $i];
}
$tag = strtolower($matches[1]);
$voidTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
if (in_array($tag, $voidTags, true) || str_ends_with($trimmed, '/>') || stripos($trimmed, '</' . $tag . '>') !== false) {
return [implode("\n", $block), $i];
}
while ($i < $count) {
$block[] = rtrim($lines[$i]);
if (stripos((string) end($block), '</' . $tag . '>') !== false) {
$i++;
break;
}
if (trim($lines[$i]) === '' && !in_array($tag, ['article', 'aside', 'div', 'footer', 'form', 'header', 'iframe', 'main', 'nav', 'pre', 'script', 'section', 'style', 'table', 'textarea'], true)) {
$i++;
break;
}
$i++;
}
return [implode("\n", $block), $i];
}
private static function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');