la_bloger/app/Support/ProseMirror.php
2026-05-20 13:01:33 +02:00

153 lines
5.3 KiB
PHP

<?php
namespace App\Support;
class ProseMirror
{
public static function render(mixed $doc): string
{
if (is_string($doc)) {
$doc = json_decode($doc, true);
}
if (!is_array($doc)) {
return '';
}
if (isset($doc['type']) && $doc['type'] === 'doc') {
return self::renderNodes($doc['content'] ?? []);
}
if (array_is_list($doc)) {
return self::renderNodes($doc);
}
return self::renderNode($doc);
}
private static function renderNodes(array $nodes): string
{
return implode('', array_map([self::class, 'renderNode'], $nodes));
}
private static function renderNode(array $node): string
{
$type = $node['type'] ?? '';
$content = $node['content'] ?? [];
$attrs = $node['attrs'] ?? [];
return match ($type) {
'paragraph' => '<p>' . self::renderNodes($content) . '</p>',
'heading' => self::renderHeading($attrs, $content),
'bulletList' => '<ul>' . self::renderNodes($content) . '</ul>',
'orderedList' => '<ol>' . self::renderNodes($content) . '</ol>',
'listItem' => '<li>' . self::renderNodes($content) . '</li>',
'blockquote' => '<blockquote>' . self::renderNodes($content) . '</blockquote>',
'codeBlock' => '<pre><code>' . e(self::extractText($content)) . '</code></pre>',
'hardBreak' => '<br>',
'horizontalRule' => '<hr>',
'text' => self::renderText($node),
'table' => self::renderTable($content),
'tableRow' => '<tr>' . self::renderNodes($content) . '</tr>',
'tableCell' => self::renderTableCell($attrs, $content, false),
'tableHeader' => self::renderTableCell($attrs, $content, true),
default => self::renderNodes($content),
};
}
private static function renderHeading(array $attrs, array $content): string
{
$level = max(1, min(6, (int) ($attrs['level'] ?? 1)));
return "<h{$level}>" . self::renderNodes($content) . "</h{$level}>";
}
private static function renderText(array $node): string
{
$text = e($node['text'] ?? '');
$marks = $node['marks'] ?? [];
foreach ($marks as $mark) {
$text = self::applyMark($mark, $text);
}
return $text;
}
private static function applyMark(array $mark, string $text): string
{
$type = $mark['type'] ?? '';
$attrs = $mark['attrs'] ?? [];
return match ($type) {
'bold' => "<strong>{$text}</strong>",
'italic' => "<em>{$text}</em>",
'code' => "<code>{$text}</code>",
'strike' => "<s>{$text}</s>",
'underline' => "<u>{$text}</u>",
'link' => self::renderLink($attrs, $text),
default => $text,
};
}
private static function renderLink(array $attrs, string $text): string
{
$href = e($attrs['href'] ?? '');
if (preg_match('/^\s*javascript:/i', rawurldecode($href))) {
$href = '#';
}
$target = isset($attrs['target']) ? ' target="' . e($attrs['target']) . '"' : '';
$rel = $target ? ' rel="noopener noreferrer"' : '';
return "<a href=\"{$href}\"{$target}{$rel}>{$text}</a>";
}
private static function renderTable(array $rows): string
{
if (empty($rows)) {
return '<table></table>';
}
// If the first row consists entirely of tableHeader cells, put it in <thead>
$firstCells = $rows[0]['content'] ?? [];
$headerCount = count(array_filter($firstCells, fn($c) => ($c['type'] ?? '') === 'tableHeader'));
$firstIsHeaderRow = !empty($firstCells) && $headerCount === count($firstCells);
if ($firstIsHeaderRow) {
$thead = '<thead>' . self::renderNode($rows[0]) . '</thead>';
$tbody = count($rows) > 1
? '<tbody>' . self::renderNodes(array_slice($rows, 1)) . '</tbody>'
: '';
return "<table>{$thead}{$tbody}</table>";
}
return '<table><tbody>' . self::renderNodes($rows) . '</tbody></table>';
}
private static function renderTableCell(array $attrs, array $content, bool $header): string
{
$tag = $header ? 'th' : 'td';
$extra = '';
if (!empty($attrs['colspan']) && $attrs['colspan'] > 1) {
$extra .= ' colspan="' . (int) $attrs['colspan'] . '"';
}
if (!empty($attrs['rowspan']) && $attrs['rowspan'] > 1) {
$extra .= ' rowspan="' . (int) $attrs['rowspan'] . '"';
}
// TipTap wraps cell content in a paragraph node; unwrap single paragraphs
if (count($content) === 1 && ($content[0]['type'] ?? '') === 'paragraph') {
$inner = self::renderNodes($content[0]['content'] ?? []);
} else {
$inner = self::renderNodes($content);
}
return "<{$tag}{$extra}>{$inner}</{$tag}>";
}
private static function extractText(array $nodes): string
{
return implode('', array_map(
fn($node) => $node['text'] ?? self::extractText($node['content'] ?? []),
$nodes
));
}
}