Some checks failed
Deploy Application / deploy (push) Has been cancelled
168 lines
5.9 KiB
PHP
168 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\View\Components;
|
|
|
|
use App\Framework\View\Attributes\ComponentName;
|
|
use App\Framework\View\Components\Helpers\ComponentAttributeHelper;
|
|
use App\Framework\View\Components\Helpers\ComponentClassBuilder;
|
|
use App\Framework\View\Contracts\StaticComponent;
|
|
use App\Framework\View\Dom\ElementNode;
|
|
use App\Framework\View\Dom\Node;
|
|
use App\Framework\View\Dom\TextNode;
|
|
|
|
#[ComponentName('card')]
|
|
final readonly class Card implements StaticComponent
|
|
{
|
|
private string $bodyContent;
|
|
private ?string $title;
|
|
private ?string $subtitle;
|
|
private ?string $footer;
|
|
private ?string $imageSrc;
|
|
private ?string $imageAlt;
|
|
private string $variant;
|
|
private array $attributes;
|
|
|
|
public function __construct(
|
|
string $content = '',
|
|
array $attributes = []
|
|
) {
|
|
// Content is the card body
|
|
$this->bodyContent = $content;
|
|
$this->attributes = $attributes;
|
|
|
|
// Extract attributes using Helper
|
|
$this->variant = ComponentAttributeHelper::extractVariant($attributes, 'default');
|
|
$this->title = ComponentAttributeHelper::extractString($attributes, 'title');
|
|
$this->subtitle = ComponentAttributeHelper::extractString($attributes, 'subtitle');
|
|
$this->footer = ComponentAttributeHelper::extractString($attributes, 'footer');
|
|
$this->imageSrc = ComponentAttributeHelper::extractString($attributes, 'image-src');
|
|
$this->imageAlt = ComponentAttributeHelper::extractString($attributes, 'image-alt');
|
|
}
|
|
|
|
public function getRootNode(): Node
|
|
{
|
|
$div = new ElementNode('div');
|
|
|
|
// Build CSS classes using Helper
|
|
$additionalClasses = [];
|
|
if (isset($this->attributes['class']) && is_string($this->attributes['class'])) {
|
|
$additionalClasses[] = $this->attributes['class'];
|
|
}
|
|
|
|
$classes = ComponentClassBuilder::build('card', $this->variant, null, $additionalClasses);
|
|
$div->setAttribute('class', $classes);
|
|
|
|
// Apply additional attributes
|
|
$filteredAttributes = ComponentAttributeHelper::filterSpecialAttributes(
|
|
$this->attributes,
|
|
['title', 'subtitle', 'footer', 'image-src', 'image-alt']
|
|
);
|
|
foreach ($filteredAttributes as $key => $value) {
|
|
$div->setAttribute($key, (string)$value);
|
|
}
|
|
|
|
// Build complex nested content as HTML string
|
|
$contentHtml = $this->buildContent();
|
|
$div->appendChild(new TextNode($contentHtml));
|
|
|
|
return $div;
|
|
}
|
|
|
|
private function buildContent(): string
|
|
{
|
|
$elements = [];
|
|
|
|
// Image
|
|
if ($this->imageSrc !== null) {
|
|
$altAttr = $this->imageAlt !== null
|
|
? ' alt="' . htmlspecialchars($this->imageAlt) . '"'
|
|
: '';
|
|
$elements[] = '<img src="' . htmlspecialchars($this->imageSrc) . '"' . $altAttr . ' class="card__image" />';
|
|
}
|
|
|
|
// Header (title + subtitle)
|
|
if ($this->title !== null || $this->subtitle !== null) {
|
|
$headerElements = [];
|
|
|
|
if ($this->title !== null) {
|
|
$headerElements[] = '<h3 class="card__title">' . htmlspecialchars($this->title) . '</h3>';
|
|
}
|
|
|
|
if ($this->subtitle !== null) {
|
|
$headerElements[] = '<p class="card__subtitle">' . htmlspecialchars($this->subtitle) . '</p>';
|
|
}
|
|
|
|
$elements[] = '<div class="card__header">' . implode('', $headerElements) . '</div>';
|
|
}
|
|
|
|
// Body
|
|
// Content may already be HTML (from processed placeholders), so check before escaping
|
|
if ($this->isHtmlContent($this->bodyContent)) {
|
|
// Content is already HTML - output directly
|
|
$elements[] = '<div class="card__body">' . $this->bodyContent . '</div>';
|
|
} else {
|
|
// Content is plain text - escape it
|
|
$elements[] = '<div class="card__body">' . htmlspecialchars($this->bodyContent) . '</div>';
|
|
}
|
|
|
|
// Footer
|
|
if ($this->footer !== null) {
|
|
$elements[] = '<div class="card__footer">' . htmlspecialchars($this->footer) . '</div>';
|
|
}
|
|
|
|
return implode('', $elements);
|
|
}
|
|
|
|
/**
|
|
* Check if content contains HTML tags (is already HTML)
|
|
* Uses a simple and fast check to avoid performance issues
|
|
*/
|
|
private function isHtmlContent(string $content): bool
|
|
{
|
|
// Trim whitespace to avoid false positives
|
|
$trimmed = trim($content);
|
|
|
|
// Empty content is not HTML
|
|
if (empty($trimmed)) {
|
|
return false;
|
|
}
|
|
|
|
// Simple check: if content starts with < and contains >, it's likely HTML
|
|
// This is much faster than regex and avoids performance issues
|
|
if (str_starts_with($trimmed, '<') && str_contains($trimmed, '>')) {
|
|
// Additional check: ensure it's not just a single character or escaped text
|
|
// Look for common HTML tags (table, div, span, etc.)
|
|
$commonTags = ['<table', '<div', '<span', '<p', '<h', '<ul', '<ol', '<li', '<a', '<button'];
|
|
foreach ($commonTags as $tag) {
|
|
if (stripos($trimmed, $tag) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Factory methods for programmatic usage
|
|
public static function create(string $bodyContent): self
|
|
{
|
|
return new self($bodyContent);
|
|
}
|
|
|
|
public static function withTitle(string $title, string $bodyContent): self
|
|
{
|
|
return new self($bodyContent, ['title' => $title]);
|
|
}
|
|
|
|
public static function withImage(string $imageSrc, string $bodyContent, ?string $imageAlt = null): self
|
|
{
|
|
$attributes = ['image-src' => $imageSrc];
|
|
if ($imageAlt !== null) {
|
|
$attributes['image-alt'] = $imageAlt;
|
|
}
|
|
return new self($bodyContent, $attributes);
|
|
}
|
|
}
|