<?php
declare(strict_types=1);
namespace Termwind\Html;
use Iterator;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableCellStyle;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Termwind\Components\Element;
use Termwind\HtmlRenderer;
use Termwind\Termwind;
use Termwind\ValueObjects\Node;
use Termwind\ValueObjects\Styles;
/**
* @internal
*/
final class TableRenderer
{
/**
* Symfony table object uses for table generation.
*/
private Table $table;
/**
* This object is used for accumulating output data from Symfony table object and return it as a string.
*/
private BufferedOutput $output;
public function __construct()
{
$this->output = new BufferedOutput(
// Content should output as is, without changes
OutputInterface::VERBOSITY_NORMAL | OutputInterface::OUTPUT_RAW,
true
);
$this->table = new Table($this->output);
}
/**
* Converts table output to the content element.
*/
public function toElement(Node $node): Element
{
$this->parseTable($node);
$this->table->render();
$content = preg_replace('/\n$/', '', $this->output->fetch()) ?? '';
return Termwind::div($content, '', [
'isFirstChild' => $node->isFirstChild(),
]);
}
/**
* Looks for thead, tfoot, tbody, tr elements in a given DOM and appends rows from them to the Symfony table object.
*/
private function parseTable(Node $node): void
{
$style = $node->getAttribute('style');
if ($style !== '') {
$this->table->setStyle($style);
}
foreach ($node->getChildNodes() as $child) {
match ($child->getName()) {
'thead' => $this->parseHeader($child),
'tfoot' => $this->parseFoot($child),
'tbody' => $this->parseBody($child),
default => $this->parseRows($child)
};
}
}
/**
* Looks for table header title and tr elements in a given thead DOM node and adds them to the Symfony table object.
*/
private function parseHeader(Node $node): void
{
$title = $node->getAttribute('title');
if ($title !== '') {
$this->table->getStyle()->setHeaderTitleFormat(
$this->parseTitleStyle($node)
);
$this->table->setHeaderTitle($title);
}
foreach ($node->getChildNodes() as $child) {
if ($child->isName('tr')) {
foreach ($this->parseRow($child) as $row) {
if (! is_array($row)) {
continue;
}
$this->table->setHeaders($row);
}
}
}
}
/**
* Looks for table footer and tr elements in a given tfoot DOM node and adds them to the Symfony table object.
*/
private function parseFoot(Node $node): void
{
$title = $node->getAttribute('title');
if ($title !== '') {
$this->table->getStyle()->setFooterTitleFormat(
$this->parseTitleStyle($node)
);
$this->table->setFooterTitle($title);
}
foreach ($node->getChildNodes() as $child) {
if ($child->isName('tr')) {
$rows = iterator_to_array($this->parseRow($child));
if (count($rows) > 0) {
$this->table->addRow(new TableSeparator());
$this->table->addRows($rows);
}
}
}
}
/**
* Looks for tr elements in a given DOM node and adds them to the Symfony table object.
*/
private function parseBody(Node $node): void
{
foreach ($node->getChildNodes() as $child) {
if ($child->isName('tr')) {
$this->parseRows($child);
}
}
}
/**
* Parses table tr elements.
*/
private function parseRows(Node $node): void
{
foreach ($this->parseRow($node) as $row) {
$this->table->addRow($row);
}
}
/**
* Looks for th, td elements in a given DOM node and converts them to a table cells.
*
* @return Iterator<array<int, TableCell>|TableSeparator>
*/
private function parseRow(Node $node): Iterator
{
$row = [];
foreach ($node->getChildNodes() as $child) {
if ($child->isName('th') || $child->isName('td')) {
$align = $child->getAttribute('align');
$class = $child->getClassAttribute();
if ($child->isName('th')) {
$class .= ' strong';
}
$text = (string) (new HtmlRenderer)->parse(
trim(preg_replace('/<br\s?+\/?>/', "\n", $child->getHtml()) ?? '')
);
if ((bool) preg_match(Styles::STYLING_REGEX, $text)) {
$class .= ' font-normal';
}
$row[] = new TableCell(
// I need only spaces after applying margin, padding and width except tags.
// There is no place for tags, they broke cell formatting.
(string) Termwind::span($text, $class),
[
// Gets rowspan and colspan from tr and td tag attributes
'colspan' => max((int) $child->getAttribute('colspan'), 1),
'rowspan' => max((int) $child->getAttribute('rowspan'), 1),
// There are background and foreground and options
'style' => $this->parseCellStyle(
$class,
$align === '' ? TableCellStyle::DEFAULT_ALIGN : $align
),
]
);
}
}
if ($row !== []) {
yield $row;
}
$border = (int) $node->getAttribute('border');
for ($i = $border; $i--; $i > 0) {
yield new TableSeparator();
}
}
/**
* Parses tr, td tag class attribute and passes bg, fg and options to a table cell style.
*/
private function parseCellStyle(string $styles, string $align = TableCellStyle::DEFAULT_ALIGN): TableCellStyle
{
// I use this empty span for getting styles for bg, fg and options
// It will be a good idea to get properties without element object and then pass them to an element object
$element = Termwind::span('%s', $styles);
$styles = [];
$colors = $element->getProperties()['colors'] ?? [];
foreach ($colors as $option => $content) {
if (in_array($option, ['fg', 'bg'], true)) {
$content = is_array($content) ? array_pop($content) : $content;
$styles[] = "$option=$content";
}
}
// If there are no styles we don't need extra tags
if ($styles === []) {
$cellFormat = '%s';
} else {
$cellFormat = '<'.implode(';', $styles).'>%s</>';
}
return new TableCellStyle([
'align' => $align,
'cellFormat' => $cellFormat,
]);
}
/**
* Get styled representation of title.
*/
private function parseTitleStyle(Node $node): string
{
return (string) Termwind::span(' %s ', $node->getClassAttribute());
}
}
In the digital age, privacy concerns have become increasingly paramount, prompting the European Union to enact the General Data Protection Regulation (GDPR) in 2018. Among its many provisions, GDPR sets strict guidelines for the collection and processing of personal data, including the use of cookies on websites. Privacy Policy