/
so it renders consistently next to new HTML content. */ class PressReleaseHtmlSanitizer { private const string PURIFIER_PROFILE = 'press_release'; /** * Sanitize HTML before persisting to the database. */ public function clean(?string $html): string { if ($html === null || trim($html) === '') { return ''; } return (string) Purifier::clean($html, self::PURIFIER_PROFILE); } /** * Detect whether the stored text is already HTML (Phase 7+) or * legacy plain text from older imports. */ public function isHtml(?string $text): bool { if ($text === null || $text === '') { return false; } return (bool) preg_match('/<(p|br|h2|h3|strong|em|ul|ol|li|blockquote|a)\b[^>]*>/i', $text); } /** * Produce a display-ready, safe HtmlString. */ public function render(?string $text): HtmlString { if ($text === null || trim($text) === '') { return new HtmlString(''); } if ($this->isHtml($text)) { return new HtmlString($this->clean($text)); } $escaped = e($text); $withBreaks = nl2br($escaped, false); $paragraphs = preg_split('/(?:\s*){2,}/i', $withBreaks) ?: [$withBreaks]; $html = collect($paragraphs) ->map(fn (string $chunk): string => trim($chunk)) ->filter() ->map(fn (string $chunk): string => '

'.$chunk.'

') ->implode(''); return new HtmlString($html); } /** * Plain-text length for character counters (without HTML noise). */ public function plainTextLength(?string $text): int { if ($text === null || $text === '') { return 0; } $stripped = strip_tags($text); $decoded = html_entity_decode($stripped, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return mb_strlen(trim((string) preg_replace('/\s+/u', ' ', $decoded))); } }