CMS: Absatz-Tags aus dem Rich-Text-Editor erscheinen nicht mehr als Text
Der Editor packt eingefügten Text in <p>; Felder wie die FAQ-Antwort gibt die Website als Text aus, die Tags waren sichtbar. Beim Speichern entfernt unwrapParagraphs() die Absätze (ein Absatz ohne <p>, mehrere mit <br><br>, Blockinhalte unverändert). cms_inline() gibt die Felder als bereinigtes Inline-HTML aus – auch schon gespeicherte Texte mit <p> erscheinen sauber, Fett und Hervorhebung werden dargestellt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
07816dce19
commit
451e738c8b
20 changed files with 147 additions and 23 deletions
|
|
@ -48,6 +48,30 @@ final class CmsFluxEditorHtmlTransformer
|
||||||
return self::marksToSecondarySpans($html);
|
return self::marksToSecondarySpans($html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Editor packt jeden Text in Absätze – auch eingefügten Klartext (12.10.2026). Für Felder,
|
||||||
|
* die die Website innerhalb einer Zeile oder eines eigenen Absatzes ausgibt, wird daraus
|
||||||
|
* Inline-HTML: ein Absatz ohne `<p>`, mehrere durch `<br><br>` getrennt, reiner Text als
|
||||||
|
* Klartext. Enthält der Text Blockelemente (Überschriften, Listen …), bleibt er unverändert.
|
||||||
|
*/
|
||||||
|
public static function unwrapParagraphs(string $html): string
|
||||||
|
{
|
||||||
|
$html = trim((string) preg_replace('#<p[^>]*>(\s| |<br\s*/?>)*</p>#i', '', $html));
|
||||||
|
|
||||||
|
if ($html === '' || ! str_contains($html, '<p')) {
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('#<(?!/?(?:p|strong|b|em|i|mark|span|br|a)\b)[a-z]#i', $html) === 1) {
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = (string) preg_replace('#</p>\s*<p[^>]*>#i', '<br><br>', $html);
|
||||||
|
$html = trim((string) preg_replace('#^<p[^>]*>|</p>$#i', '', $html));
|
||||||
|
|
||||||
|
return str_contains($html, '<') ? $html : html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array<string, mixed>|array{_value: string}> $items
|
* @param array<int, array<string, mixed>|array{_value: string}> $items
|
||||||
* @return array<int, array<string, mixed>|array{_value: string}>
|
* @return array<int, array<string, mixed>|array{_value: string}>
|
||||||
|
|
@ -92,7 +116,7 @@ final class CmsFluxEditorHtmlTransformer
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($item as $key => $value) {
|
foreach ($item as $key => $value) {
|
||||||
if (in_array($key, self::RICH_TEXT_JSON_FIELDS, true) && is_string($value)) {
|
if (in_array($key, self::RICH_TEXT_JSON_FIELDS, true) && is_string($value)) {
|
||||||
$out[$key] = self::fromEditor($value);
|
$out[$key] = self::unwrapParagraphs(self::fromEditor($value));
|
||||||
} else {
|
} else {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,37 @@ if (! function_exists('references_page_enabled')) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('cms_inline')) {
|
||||||
|
/**
|
||||||
|
* CMS-Text für die Ausgabe innerhalb einer Zeile oder eines Absatzes (12.10.2026): Klartext
|
||||||
|
* wird escaped, Editor-HTML auf Inline-Auszeichnung reduziert (fett, kursiv, Hervorhebung,
|
||||||
|
* Links, Zeilenumbrüche) – Absatz-Tags aus dem Editor erscheinen so nicht als Text.
|
||||||
|
*/
|
||||||
|
function cms_inline(mixed $value): \Illuminate\Support\HtmlString
|
||||||
|
{
|
||||||
|
$text = trim((string) $value);
|
||||||
|
|
||||||
|
if (! str_contains($text, '<')) {
|
||||||
|
return new \Illuminate\Support\HtmlString(e(html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = \App\Services\CmsFluxEditorHtmlTransformer::unwrapParagraphs(
|
||||||
|
\App\Services\CmsFluxEditorHtmlTransformer::fromEditor($text)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! str_contains($html, '<')) {
|
||||||
|
return new \Illuminate\Support\HtmlString(e($html));
|
||||||
|
}
|
||||||
|
$html = (string) preg_replace('#</(p|div|li|h[1-6])>\s*<\1[^>]*>#i', '<br><br>', $html);
|
||||||
|
$html = strip_tags($html, '<strong><b><em><i><span><br><a>');
|
||||||
|
// Nur harmlose Attribute: keine Event-Handler, keine javascript:-Links.
|
||||||
|
$html = (string) preg_replace('#\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)#i', '', $html);
|
||||||
|
$html = (string) preg_replace('#href\s*=\s*(["\'])\s*javascript:[^"\']*\1#i', 'href="#"', $html);
|
||||||
|
|
||||||
|
return new \Illuminate\Support\HtmlString(trim($html));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (! function_exists('whatsapp_link')) {
|
if (! function_exists('whatsapp_link')) {
|
||||||
/**
|
/**
|
||||||
* Klick-zum-Chat-Link, sofern im CMS eine Nummer hinterlegt ist (global.contact.whatsapp).
|
* Klick-zum-Chat-Link, sofern im CMS eine Nummer hinterlegt ist (global.contact.whatsapp).
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ $saveEdit = function () {
|
||||||
|
|
||||||
$valueToSave = $this->editValue;
|
$valueToSave = $this->editValue;
|
||||||
if ($this->editingFieldType === 'html') {
|
if ($this->editingFieldType === 'html') {
|
||||||
$valueToSave = CmsFluxEditorHtmlTransformer::fromEditor($valueToSave);
|
$valueToSave = CmsFluxEditorHtmlTransformer::unwrapParagraphs(CmsFluxEditorHtmlTransformer::fromEditor($valueToSave));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->editingField !== null) {
|
if ($this->editingField !== null) {
|
||||||
|
|
|
||||||
|
|
@ -86,19 +86,19 @@
|
||||||
@if ($item['lead'] !== '')
|
@if ($item['lead'] !== '')
|
||||||
<strong class="font-medium text-foreground">{{ $item['lead'] }}:</strong>
|
<strong class="font-medium text-foreground">{{ $item['lead'] }}:</strong>
|
||||||
@endif
|
@endif
|
||||||
{{ $item['text'] }}
|
{!! cms_inline($item['text'] ?? '') !!}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
@else
|
@else
|
||||||
<p class="text-muted-foreground leading-relaxed mt-4">{{ $block['text'] }}</p>
|
<p class="text-muted-foreground leading-relaxed mt-4">{!! cms_inline($block['text'] ?? '') !!}</p>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
@if (!empty($section['quote']))
|
@if (!empty($section['quote']))
|
||||||
<figure class="mt-8 border-l-4 border-secondary pl-6">
|
<figure class="mt-8 border-l-4 border-secondary pl-6">
|
||||||
<blockquote class="text-large leading-relaxed italic">„{{ $section['quote'] }}"</blockquote>
|
<blockquote class="text-large leading-relaxed italic">„{!! cms_inline($section['quote'] ?? '') !!}"</blockquote>
|
||||||
@if (!empty($section['quote_author']))
|
@if (!empty($section['quote_author']))
|
||||||
<figcaption class="text-sm text-muted-foreground mt-3">{{ $section['quote_author'] }}</figcaption>
|
<figcaption class="text-sm text-muted-foreground mt-3">{{ $section['quote_author'] }}</figcaption>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
@foreach ($brand['services'] as $index => $service)
|
@foreach ($brand['services'] as $index => $service)
|
||||||
<div class="card-elevated rounded-2xl p-6" wire:key="service-{{ $index }}">
|
<div class="card-elevated rounded-2xl p-6" wire:key="service-{{ $index }}">
|
||||||
<h3 class="text-lg font-medium">{{ $service['title'] ?? '' }}</h3>
|
<h3 class="text-lg font-medium">{{ $service['title'] ?? '' }}</h3>
|
||||||
<p class="text-muted-foreground leading-relaxed mt-2">{{ $service['description'] ?? '' }}</p>
|
<p class="text-muted-foreground leading-relaxed mt-2">{!! cms_inline($service['description'] ?? '') !!}</p>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<h3 class="text-xl font-medium">{{ $world['title'] }}</h3>
|
<h3 class="text-xl font-medium">{{ $world['title'] }}</h3>
|
||||||
@endif
|
@endif
|
||||||
<p class="text-muted-foreground leading-relaxed mt-2">
|
<p class="text-muted-foreground leading-relaxed mt-2">
|
||||||
{{ $world['description'] }}
|
{!! cms_inline($world['description'] ?? '') !!}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
x-on:keydown.escape.window="open && hide()">
|
x-on:keydown.escape.window="open && hide()">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
<p class="text-large text-muted-foreground leading-relaxed mt-6">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
|
|
||||||
@if (!empty($content['note']))
|
@if (!empty($content['note']))
|
||||||
<p class="text-sm text-muted-foreground mt-4">{{ $content['note'] }}</p>
|
<p class="text-sm text-muted-foreground mt-4">{{ $content['note'] }}</p>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<div class="px-6 pb-6">
|
<div class="px-6 pb-6">
|
||||||
<div class="border-t border-border pt-4">
|
<div class="border-t border-border pt-4">
|
||||||
<p class="text-muted-foreground leading-relaxed">
|
<p class="text-muted-foreground leading-relaxed">
|
||||||
{{ $faq['answer'] }}
|
{!! cms_inline($faq['answer'] ?? '') !!}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="absolute inset-0 flex items-end justify-center pb-12 px-6">
|
<div class="absolute inset-0 flex items-end justify-center pb-12 px-6">
|
||||||
<div class="text-center slide-up delay-300">
|
<div class="text-center slide-up delay-300">
|
||||||
<p class="text-xl lg:text-2xl font-medium text-white italic max-w-2xl leading-relaxed">
|
<p class="text-xl lg:text-2xl font-medium text-white italic max-w-2xl leading-relaxed">
|
||||||
"{{ $content['quote'] }}"
|
"{!! cms_inline($content['quote'] ?? '') !!}"
|
||||||
</p>
|
</p>
|
||||||
@if (isset($content['author']))
|
@if (isset($content['author']))
|
||||||
<p class="text-sm text-white/70 mt-3">– {{ $content['author'] }}</p>
|
<p class="text-sm text-white/70 mt-3">– {{ $content['author'] }}</p>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="container-narrow">
|
<div class="container-narrow">
|
||||||
<div class="border-t border-border pt-10 text-center">
|
<div class="border-t border-border pt-10 text-center">
|
||||||
<h2 class="text-xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
<h2 class="text-xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
||||||
<p class="text-muted-foreground leading-relaxed mt-3 max-w-2xl mx-auto">{{ $content['text'] ?? '' }}</p>
|
<p class="text-muted-foreground leading-relaxed mt-3 max-w-2xl mx-auto">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
|
|
||||||
@if (!empty($content['link_text']))
|
@if (!empty($content['link_text']))
|
||||||
<a href="{{ $content['link_url'] ?? '/immobilien' }}"
|
<a href="{{ $content['link_url'] ?? '/immobilien' }}"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<section id="marken-cta" class="section-padding bg-secondary text-secondary-foreground">
|
<section id="marken-cta" class="section-padding bg-secondary text-secondary-foreground">
|
||||||
<div class="container-narrow text-center spacing-section">
|
<div class="container-narrow text-center spacing-section">
|
||||||
<h2 class="text-section-title text-white">{!! $content['title'] ?? '' !!}</h2>
|
<h2 class="text-section-title text-white">{!! $content['title'] ?? '' !!}</h2>
|
||||||
<p class="text-large max-w-2xl mx-auto leading-relaxed">{{ $content['text'] ?? '' }}</p>
|
<p class="text-large max-w-2xl mx-auto leading-relaxed">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
|
|
||||||
<div class="pt-4 flex flex-wrap items-center justify-center gap-4">
|
<div class="pt-4 flex flex-wrap items-center justify-center gap-4">
|
||||||
<x-appointment-link :href="$content['button_link'] ?? '/contact'" class="btn-primary-accent px-8 py-4 text-lg">
|
<x-appointment-link :href="$content['button_link'] ?? '/contact'" class="btn-primary-accent px-8 py-4 text-lg">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<section id="wachstum" class="py-16 {{ $bg }}">
|
<section id="wachstum" class="py-16 {{ $bg }}">
|
||||||
<div class="container-narrow text-center">
|
<div class="container-narrow text-center">
|
||||||
<h2 class="text-2xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
<h2 class="text-2xl font-medium">{!! $content['title'] ?? '' !!}</h2>
|
||||||
<p class="text-muted-foreground leading-relaxed mt-4">{{ $content['text'] ?? '' }}</p>
|
<p class="text-muted-foreground leading-relaxed mt-4">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
{!! renderHeroIcon($point['icon'] ?? 'user', 'outline', 'text-secondary-foreground') !!}
|
{!! renderHeroIcon($point['icon'] ?? 'user', 'outline', 'text-secondary-foreground') !!}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium">{{ $point['title'] ?? '' }}</h3>
|
<h3 class="text-lg font-medium">{{ $point['title'] ?? '' }}</h3>
|
||||||
<p class="text-muted-foreground leading-relaxed mt-2">{{ $point['description'] ?? '' }}</p>
|
<p class="text-muted-foreground leading-relaxed mt-2">{!! cms_inline($point['description'] ?? '') !!}</p>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-medium mb-1">{{ $step['title'] }}</h4>
|
<h4 class="font-medium mb-1">{{ $step['title'] }}</h4>
|
||||||
@if (!empty($step['description']))
|
@if (!empty($step['description']))
|
||||||
<p class="text-sm text-muted-foreground leading-relaxed">{{ $step['description'] }}</p>
|
<p class="text-sm text-muted-foreground leading-relaxed">{!! cms_inline($step['description'] ?? '') !!}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
@endif
|
@endif
|
||||||
@if(isset($card['description']))
|
@if(isset($card['description']))
|
||||||
<p class="text-muted-foreground leading-relaxed mt-4 text-center">
|
<p class="text-muted-foreground leading-relaxed mt-4 text-center">
|
||||||
{{ $card['description'] }}
|
{!! cms_inline($card['description'] ?? '') !!}
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ function renderHeroIcon($iconName, $style = 'outline') {
|
||||||
@endif
|
@endif
|
||||||
@if(isset($value['description']))
|
@if(isset($value['description']))
|
||||||
<p class="text-muted-foreground leading-relaxed mt-2 text-center">
|
<p class="text-muted-foreground leading-relaxed mt-2 text-center">
|
||||||
{{ $value['description'] }}
|
{!! cms_inline($value['description'] ?? '') !!}
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="lg:col-span-3 slide-right delay-200">
|
<div class="lg:col-span-3 slide-right delay-200">
|
||||||
<x-web.hero-eyebrow :text="$content['eyebrow'] ?? null" class="mb-6" />
|
<x-web.hero-eyebrow :text="$content['eyebrow'] ?? null" class="mb-6" />
|
||||||
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
||||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
<p class="text-large text-muted-foreground leading-relaxed mt-6">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
<x-web.hero-trust :items="$content['stats'] ?? []" class="mt-8" />
|
<x-web.hero-trust :items="$content['stats'] ?? []" class="mt-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<div class="max-w-3xl slide-right delay-200">
|
<div class="max-w-3xl slide-right delay-200">
|
||||||
<x-web.hero-eyebrow :text="$content['eyebrow'] ?? null" class="mb-6" />
|
<x-web.hero-eyebrow :text="$content['eyebrow'] ?? null" class="mb-6" />
|
||||||
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
<h1 class="text-hero-alt">{!! $content['title'] ?? '' !!}</h1>
|
||||||
<p class="text-large text-muted-foreground leading-relaxed mt-6">{{ $content['text'] ?? '' }}</p>
|
<p class="text-large text-muted-foreground leading-relaxed mt-6">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
<x-web.hero-trust :items="$content['stats'] ?? []" class="mt-8" />
|
<x-web.hero-trust :items="$content['stats'] ?? []" class="mt-8" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="text-center max-w-2xl mx-auto">
|
<div class="text-center max-w-2xl mx-auto">
|
||||||
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
<h2 class="text-section-title">{!! $content['title'] ?? '' !!}</h2>
|
||||||
@if (!empty($content['text']))
|
@if (!empty($content['text']))
|
||||||
<p class="text-large text-muted-foreground leading-relaxed mt-4">{{ $content['text'] }}</p>
|
<p class="text-large text-muted-foreground leading-relaxed mt-4">{!! cms_inline($content['text'] ?? '') !!}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<p class="text-xs uppercase tracking-wider text-clay mt-1">{{ $project['meta'] }}</p>
|
<p class="text-xs uppercase tracking-wider text-clay mt-1">{{ $project['meta'] }}</p>
|
||||||
@endif
|
@endif
|
||||||
@if (!empty($project['description']))
|
@if (!empty($project['description']))
|
||||||
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $project['description'] }}</p>
|
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{!! cms_inline($project['description'] ?? '') !!}</p>
|
||||||
@endif
|
@endif
|
||||||
@if (!empty($project['gallery']))
|
@if (!empty($project['gallery']))
|
||||||
{{-- Weitere Bilder der Referenz als kleine Reihe --}}
|
{{-- Weitere Bilder der Referenz als kleine Reihe --}}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
@endif
|
@endif
|
||||||
</h3>
|
</h3>
|
||||||
@if (!empty($logo['text']))
|
@if (!empty($logo['text']))
|
||||||
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{{ $logo['text'] }}</p>
|
<p class="text-sm text-muted-foreground leading-relaxed mt-2">{!! cms_inline($logo['text'] ?? '') !!}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</{{ $url ? 'a' : 'div' }}>
|
</{{ $url ? 'a' : 'div' }}>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<span class="font-medium text-center leading-snug">
|
<span class="font-medium text-center leading-snug">
|
||||||
{{ $content['text'] ?? '' }}
|
{!! cms_inline($content['text'] ?? '') !!}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if (isset($content['link_text']) && isset($content['link_url']))
|
@if (isset($content['link_text']) && isset($content['link_url']))
|
||||||
|
|
|
||||||
69
tests/Feature/CmsEditorParagraphsTest.php
Normal file
69
tests/Feature/CmsEditorParagraphsTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\CmsFluxEditorHtmlTransformer;
|
||||||
|
use Database\Seeders\CmsContentSeeder;
|
||||||
|
use FluxCms\Core\Models\CmsContent;
|
||||||
|
use Livewire\Volt\Volt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Rich-Text-Editor packt eingefügten Text in <p>; auf der Website erschienen die Tags als
|
||||||
|
* Text, z. B. in der FAQ (12.10.2026).
|
||||||
|
*/
|
||||||
|
it('stores pasted text from the editor without paragraph tags', function (string $editor, string $stored) {
|
||||||
|
expect(CmsFluxEditorHtmlTransformer::unwrapParagraphs($editor))->toBe($stored);
|
||||||
|
})->with([
|
||||||
|
'ein Absatz' => ['<p>Eingefügter Text</p>', 'Eingefügter Text'],
|
||||||
|
'Sonderzeichen' => ['<p>Planung & Montage</p>', 'Planung & Montage'],
|
||||||
|
'fett' => ['<p>Mit <strong>fett</strong></p>', 'Mit <strong>fett</strong>'],
|
||||||
|
'zwei Absätze' => ['<p>Eins</p><p>Zwei</p>', 'Eins<br><br>Zwei'],
|
||||||
|
'leere Absätze' => ['<p>Text</p><p></p><p><br></p>', 'Text'],
|
||||||
|
'Blockinhalt bleibt' => ['<h2>Titel</h2><p>Text</p>', '<h2>Titel</h2><p>Text</p>'],
|
||||||
|
'schon Klartext' => ['Ohne Tags', 'Ohne Tags'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('renders cms text inline: plain text escaped, editor html without paragraphs, nothing unsafe', function (string $value, string $html) {
|
||||||
|
expect((string) cms_inline($value))->toBe($html);
|
||||||
|
})->with([
|
||||||
|
['Planung & Montage', 'Planung & Montage'],
|
||||||
|
['<p>Schon gespeichert</p>', 'Schon gespeichert'],
|
||||||
|
['<p>Mit <mark>Hervorhebung</mark></p>', 'Mit <span class="text-secondary">Hervorhebung</span>'],
|
||||||
|
['<p>a</p><p>b</p>', 'a<br><br>b'],
|
||||||
|
['<a href="javascript:alert(1)" onclick="x()">Link</a>', '<a href="#">Link</a>'],
|
||||||
|
['<p><script></p>', ''],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('saves a pasted faq answer without p tags and shows it cleanly on the page', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
$this->seed(CmsContentSeeder::class);
|
||||||
|
$row = CmsContent::query()->where('group', 'faq')->where('key', 'faq')->first();
|
||||||
|
|
||||||
|
$component = Volt::test('admin.cms.content-index')
|
||||||
|
->call('startFieldEdit', $row->id, 'questions');
|
||||||
|
|
||||||
|
$component->set('jsonItems.0.answer', '<p>Neue Antwort aus der Zwischenablage</p>')
|
||||||
|
->call('saveJsonModal');
|
||||||
|
|
||||||
|
expect($row->refresh()->getTranslation('value', 'de')['questions'][0]['answer'])->toBe('Neue Antwort aus der Zwischenablage');
|
||||||
|
|
||||||
|
$page = $this->get('/faq')->assertSuccessful()->getContent();
|
||||||
|
|
||||||
|
expect(substr($page, strpos($page, 'id="faq"')))->toContain('Neue Antwort aus der Zwischenablage')
|
||||||
|
->not->toContain('<p>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows answers that were already stored with p tags without the tags', function () {
|
||||||
|
$this->seed(CmsContentSeeder::class);
|
||||||
|
$row = CmsContent::query()->where('group', 'faq')->where('key', 'faq')->first();
|
||||||
|
$value = $row->getTranslation('value', 'de');
|
||||||
|
$value['questions'][0]['answer'] = '<p>Bereits gespeichert mit Absatz</p>';
|
||||||
|
$row->setTranslation('value', 'de', $value)->save();
|
||||||
|
app(FluxCms\Core\Services\CmsContentService::class)->clearCache('faq');
|
||||||
|
|
||||||
|
$page = $this->get('/faq')->assertSuccessful()->getContent();
|
||||||
|
$section = substr($page, strpos($page, 'id="faq"'));
|
||||||
|
|
||||||
|
expect($section)->toContain('Bereits gespeichert mit Absatz')
|
||||||
|
->not->toContain('<p>')
|
||||||
|
->not->toContain('<p>Bereits');
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue