<?php
 namespace App\Services\Ticket; use Illuminate\Support\Str; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\MarkdownConverter; class TicketContentFormatter { public function htmlToMarkdown(string $html): string { $html = $this->sanitizeHtml($html); $markdown = $this->convertToMarkdown($html); $markdown = preg_replace('/\n{3,}/', "\n\n", $markdown); $markdown = preg_replace('/[ \t]+$/m', '', (string) $markdown); return trim((string) $markdown); } public function markdownToSafeHtml(string $markdown): string { $config = [ 'html_input' => 'escape', 'allow_unsafe_links' => false, 'max_nesting_level' => 25, 'renderer' => [ 'soft_break' => "<br>\n", ], ]; $environment = new Environment($config); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new AutolinkExtension()); $environment->addExtension(new TableExtension()); $converter = new MarkdownConverter($environment); return (string) $converter->convert($markdown); } public function plainToSafeHtml(string $text): string { return nl2br(e($text)); } public function render(string $message, ?string $format): string { return $format === 'markdown' ? $this->markdownToSafeHtml($message) : $this->plainToSafeHtml($message); } public function sanitizeHtml(string $html): string { if (trim($html) === '') { return ''; } $html = preg_replace( '#<(script|style|iframe|object|embed|form|link|meta|base)\b[^>]*>.*?</\1>#is', '', $html ); $html = preg_replace( '#<(script|style|iframe|object|embed|form|link|meta|base)\b[^>]*/?>#is', '', (string) $html ); $html = preg_replace('#\son[a-z]+\s*=\s*"[^"]*"#is', '', (string) $html); $html = preg_replace("#\son[a-z]+\s*=\s*'[^']*'#is", '', (string) $html); $html = preg_replace('#\son[a-z]+\s*=\s*[^\s>]+#is', '', (string) $html); $html = preg_replace('#(href|src)\s*=\s*(["\']?)\s*(javascript|vbscript):[^"\'>\s]*\2#is', '$1=$2#$2', (string) $html); return (string) $html; } private function convertToMarkdown(string $html): string { if (class_exists(\League\HTMLToMarkdown\HtmlConverter::class)) { $converter = new \League\HTMLToMarkdown\HtmlConverter([ 'strip_tags' => true, 'remove_nodes' => 'script style head title meta link', 'hard_break' => true, 'use_autolinks' => false, ]); return $converter->convert($html); } return $this->fallbackHtmlToMarkdown($html); } private function fallbackHtmlToMarkdown(string $html): string { $html = str_replace(["\r\n", "\r"], "\n", $html); $html = preg_replace('#<br\s*/?>#i', "\n", $html); $html = preg_replace('#</p>#i', "\n\n", $html); $html = preg_replace('#<p\b[^>]*>#i', '', $html); $html = preg_replace('#<hr\s*/?>#i', "\n\n---\n\n", $html); for ($i = 1; $i <= 6; $i++) { $prefix = str_repeat('#', $i) . ' '; $html = preg_replace_callback( "#<h{$i}\b[^>]*>(.*?)</h{$i}>#is", fn ($m) => "\n\n" . $prefix . trim(strip_tags($m[1])) . "\n\n", $html ); } $html = preg_replace('#<(strong|b)\b[^>]*>(.*?)</\1>#is', '**$2**', $html); $html = preg_replace('#<(em|i)\b[^>]*>(.*?)</\1>#is', '*$2*', $html); $html = preg_replace_callback( '#<a\b[^>]*href\s*=\s*["\']([^"\']*)["\'][^>]*>(.*?)</a>#is', fn ($m) => '[' . trim(strip_tags($m[2])) . '](' . $m[1] . ')', (string) $html ); $html = preg_replace_callback( '#<img\b[^>]*>#is', function ($m) { $src = ''; $alt = ''; if (preg_match('#src\s*=\s*["\']([^"\']*)["\']#i', $m[0], $s)) { $src = $s[1]; } if (preg_match('#alt\s*=\s*["\']([^"\']*)["\']#i', $m[0], $a)) { $alt = $a[1]; } return $src !== '' ? '![' . $alt . '](' . $src . ')' : ''; }, (string) $html ); $html = preg_replace_callback( '#<blockquote\b[^>]*>(.*?)</blockquote>#is', function ($m) { $inner = trim(strip_tags($m[1])); $lines = array_map(fn ($l) => '> ' . $l, explode("\n", $inner)); return "\n\n" . implode("\n", $lines) . "\n\n"; }, (string) $html ); $html = preg_replace_callback( '#<ul\b[^>]*>(.*?)</ul>#is', function ($m) { $items = []; if (preg_match_all('#<li\b[^>]*>(.*?)</li>#is', $m[1], $li)) { foreach ($li[1] as $item) { $items[] = '- ' . trim(strip_tags($item)); } } return "\n" . implode("\n", $items) . "\n"; }, (string) $html ); $html = preg_replace_callback( '#<ol\b[^>]*>(.*?)</ol>#is', function ($m) { $items = []; if (preg_match_all('#<li\b[^>]*>(.*?)</li>#is', $m[1], $li)) { $n = 1; foreach ($li[1] as $item) { $items[] = ($n++) . '. ' . trim(strip_tags($item)); } } return "\n" . implode("\n", $items) . "\n"; }, (string) $html ); $text = strip_tags($html); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return $text; } public function markdownImage(string $altText, string $url): string { $altText = trim(preg_replace('/[\[\]\r\n]+/', ' ', $altText)) ?: 'image'; return '![' . $altText . '](' . $url . ')'; } } 