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[] = ''; } // Header (title + subtitle) if ($this->title !== null || $this->subtitle !== null) { $headerElements = []; if ($this->title !== null) { $headerElements[] = '

' . htmlspecialchars($this->title) . '

'; } if ($this->subtitle !== null) { $headerElements[] = '

' . htmlspecialchars($this->subtitle) . '

'; } $elements[] = '
' . implode('', $headerElements) . '
'; } // 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[] = '
' . $this->bodyContent . '
'; } else { // Content is plain text - escape it $elements[] = '
' . htmlspecialchars($this->bodyContent) . '
'; } // Footer if ($this->footer !== null) { $elements[] = ''; } 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 = [' $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); } }