\n
" . self::escape(implode("\n", $code)) . "
\n"; continue; } if (preg_match('/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/', $line)) { $blocks[] = "\n
\n"; $i++; continue; } if ($i + 1 < $count && self::isTableRow($line) && self::isTableRule($lines[$i + 1])) { $rows = [self::cells($line)]; for ($i += 2; $i < $count && self::isTableRow($lines[$i]); $i++) { $rows[] = self::cells($lines[$i]); } $blocks[] = self::table($rows); continue; } if (preg_match('/^(#{1,6})\s+(.+?)\s*#*\s*$/', $line, $m)) { $level = max(2, strlen($m[1])); $attrs = $level === 2 ? '' : ' {"level":' . $level . '}'; $blocks[] = "\n" . self::inline($m[2]) . "\n"; $i++; continue; } if (preg_match('/^\s*([-*+]|\d+[.)])\s+/', $line, $m)) { $ordered = !in_array($m[1], ['-', '*', '+'], true); $items = []; while ($i < $count && preg_match('/^\s*([-*+]|\d+[.)])\s+(.*)$/', $lines[$i], $item) && in_array($item[1], ['-', '*', '+'], true) !== $ordered) { $items[] = "\n
  • " . self::inline($item[2]) . "
  • \n"; $i++; } $tag = $ordered ? 'ol' : 'ul'; $attrs = $ordered ? ' {"ordered":true}' : ''; $blocks[] = "\n<{$tag} class=\"wp-block-list\">" . implode("\n", $items) . "\n"; continue; } if (str_starts_with(ltrim($line), '>')) { $quote = []; while ($i < $count && str_starts_with(ltrim($lines[$i]), '>')) { $quote[] = (string) preg_replace('/^\s*>\s?/', '', $lines[$i]); $i++; } $blocks[] = "\n
    " . self::paragraph(implode("\n", $quote)) . "
    \n"; continue; } $text = []; while ($i < $count && trim($lines[$i]) !== '' && !preg_match('/^(#{1,6}\s|```|\s*([-*+]|\d+[.)])\s|\s*>|\s{0,3}([-*_])(\s*\3){2,}\s*$)/', $lines[$i]) && !($i + 1 < $count && self::isTableRow($lines[$i]) && self::isTableRule($lines[$i + 1]))) { $text[] = trim($lines[$i]); $i++; } $blocks[] = self::paragraph(implode("\n", $text)); } return implode("\n\n", $blocks); } private static function isTableRow(string $line): bool { return str_contains($line, '|') && trim($line) !== '' && count(self::cells($line)) >= 2; } private static function isTableRule(string $line): bool { return (bool) preg_match('/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/', $line); } /** * @return list */ private static function cells(string $line): array { return array_map('trim', explode('|', trim(trim($line), '|'))); } /** * A core table: the first row is the header. Rows are padded or cut to the header's width. * * @param list> $rows */ private static function table(array $rows): string { $width = count($rows[0]); $row = static fn (array $cells, string $tag): string => '' . implode('', array_map(static fn (int $c): string => "<{$tag}>" . self::inline($cells[$c] ?? '') . "", range(0, $width - 1))) . ''; $body = array_map(static fn (array $cells): string => $row($cells, 'td'), array_slice($rows, 1)); return "\n
    " . $row($rows[0], 'th') . '' . ($body !== [] ? '' . implode('', $body) . '' : '') . "
    \n"; } private static function paragraph(string $text): string { return "\n

    " . str_replace("\n", '
    ', self::inline(trim($text))) . "

    \n"; } /** * Inline marks on escaped text. */ private static function inline(string $text): string { $code = []; $text = (string) preg_replace_callback('/`([^`]+)`/', static function (array $m) use (&$code): string { $code[] = '' . self::escape($m[1]) . ''; return "\u{E000}" . (count($code) - 1) . "\u{E001}"; }, $text); $text = self::escape($text); $text = (string) preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/', static function (array $m): string { $url = html_entity_decode($m[2], ENT_QUOTES | ENT_HTML5, 'UTF-8'); if (!preg_match('~^(https?://|/|#)~i', $url)) { return $m[1]; } return '' . $m[1] . ''; }, $text); $text = (string) preg_replace('/\*\*(.+?)\*\*/', '$1', $text); $text = (string) preg_replace('/(?$1', $text); $text = (string) preg_replace('/(?$1', $text); return (string) preg_replace_callback("/\u{E000}(\d+)\u{E001}/u", static fn (array $m): string => $code[(int) $m[1]] ?? '', $text); } private static function escape(string $text): string { return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } }