*/ public const RICH_TEXT_JSON_FIELDS = [ 'description', 'text', 'content', 'help', 'answer', 'quote', ]; public static function toEditor(string $html): string { if ($html === '' || ! str_contains($html, 'text-secondary')) { return $html; } return self::spansWithClassToMarks($html); } public static function fromEditor(string $html): string { if ($html === '' || ! str_contains($html, '|array{_value: string}> $items * @return array|array{_value: string}> */ public static function toEditorJsonItems(array $items, bool $isStringArray): array { if ($isStringArray) { return $items; } return array_map(function ($item) { if (! is_array($item)) { return $item; } $out = []; foreach ($item as $key => $value) { if (in_array($key, self::RICH_TEXT_JSON_FIELDS, true) && is_string($value)) { $out[$key] = self::toEditor($value); } else { $out[$key] = $value; } } return $out; }, $items); } /** * @param array|array{_value: string}> $items * @return array|array{_value: string}> */ public static function fromEditorJsonItems(array $items, bool $isStringArray): array { if ($isStringArray) { return $items; } return array_map(function ($item) { if (! is_array($item)) { return $item; } $out = []; foreach ($item as $key => $value) { if (in_array($key, self::RICH_TEXT_JSON_FIELDS, true) && is_string($value)) { $out[$key] = self::fromEditor($value); } else { $out[$key] = $value; } } return $out; }, $items); } private static function spansWithClassToMarks(string $html): string { libxml_use_internal_errors(true); $dom = new DOMDocument; $wrapped = '
'.$html.'
'; $dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); libxml_clear_errors(); $root = $dom->getElementById('cms-flux-root'); if (! $root instanceof DOMElement) { return $html; } $xpath = new DOMXPath($dom); $expression = '//span[contains(concat(" ", normalize-space(@class), " "), " text-secondary ")]'; $nodes = []; foreach ($xpath->query($expression) ?? [] as $node) { $nodes[] = $node; } usort($nodes, fn ($a, $b) => self::nodeDepth($b) <=> self::nodeDepth($a)); foreach ($nodes as $span) { if (! $span instanceof DOMElement || $span->tagName !== 'span') { continue; } $mark = $dom->createElement('mark'); while ($span->firstChild) { $mark->appendChild($span->firstChild); } $span->parentNode?->replaceChild($mark, $span); } return self::extractInnerHtml($dom, $root); } private static function marksToSecondarySpans(string $html): string { libxml_use_internal_errors(true); $dom = new DOMDocument; $wrapped = '
'.$html.'
'; $dom->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); libxml_clear_errors(); $root = $dom->getElementById('cms-flux-root'); if (! $root instanceof DOMElement) { return $html; } $xpath = new DOMXPath($dom); $nodes = []; foreach ($xpath->query('//mark') ?? [] as $node) { $nodes[] = $node; } usort($nodes, fn ($a, $b) => self::nodeDepth($b) <=> self::nodeDepth($a)); foreach ($nodes as $mark) { if (! $mark instanceof DOMElement) { continue; } $span = $dom->createElement('span'); $span->setAttribute('class', 'text-secondary'); while ($mark->firstChild) { $span->appendChild($mark->firstChild); } $mark->parentNode?->replaceChild($span, $mark); } return self::extractInnerHtml($dom, $root); } private static function nodeDepth(DOMNode $node): int { $d = 0; while ($node->parentNode) { $d++; $node = $node->parentNode; } return $d; } private static function extractInnerHtml(DOMDocument $dom, DOMElement $root): string { $html = ''; foreach ($root->childNodes as $child) { $html .= $dom->saveHTML($child); } return $html; } }