/home/mip/www/img/credit/datatables/Html.tar
TableRenderer.php000064400000017036151521005060007775 0ustar00<?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());
    }
}
CodeRenderer.php000064400000020100151521005060007602 0ustar00<?php

declare(strict_types=1);

namespace Termwind\Html;

use Termwind\Components\Element;
use Termwind\Termwind;
use Termwind\ValueObjects\Node;

/**
 * @internal
 */
final class CodeRenderer
{
    public const TOKEN_DEFAULT = 'token_default';

    public const TOKEN_COMMENT = 'token_comment';

    public const TOKEN_STRING = 'token_string';

    public const TOKEN_HTML = 'token_html';

    public const TOKEN_KEYWORD = 'token_keyword';

    public const ACTUAL_LINE_MARK = 'actual_line_mark';

    public const LINE_NUMBER = 'line_number';

    private const ARROW_SYMBOL_UTF8 = '➜';

    private const DELIMITER_UTF8 = '▕ '; // '▶';

    private const LINE_NUMBER_DIVIDER = 'line_divider';

    private const MARKED_LINE_NUMBER = 'marked_line';

    private const WIDTH = 3;

    /**
     * Holds the theme.
     *
     * @var array<string, string>
     */
    private const THEME = [
        self::TOKEN_STRING => 'text-gray',
        self::TOKEN_COMMENT => 'text-gray italic',
        self::TOKEN_KEYWORD => 'text-magenta strong',
        self::TOKEN_DEFAULT => 'strong',
        self::TOKEN_HTML => 'text-blue strong',

        self::ACTUAL_LINE_MARK => 'text-red strong',
        self::LINE_NUMBER => 'text-gray',
        self::MARKED_LINE_NUMBER => 'italic strong',
        self::LINE_NUMBER_DIVIDER => 'text-gray',
    ];

    private string $delimiter = self::DELIMITER_UTF8;

    private string $arrow = self::ARROW_SYMBOL_UTF8;

    private const NO_MARK = '    ';

    /**
     * Highlights HTML content from a given node and converts to the content element.
     */
    public function toElement(Node $node): Element
    {
        $line = max((int) $node->getAttribute('line'), 0);
        $startLine = max((int) $node->getAttribute('start-line'), 1);

        $html = $node->getHtml();
        $lines = explode("\n", $html);
        $extraSpaces = $this->findExtraSpaces($lines);

        if ($extraSpaces !== '') {
            $lines = array_map(static function (string $line) use ($extraSpaces): string {
                return str_starts_with($line, $extraSpaces) ? substr($line, strlen($extraSpaces)) : $line;
            }, $lines);
            $html = implode("\n", $lines);
        }

        $tokenLines = $this->getHighlightedLines(trim($html, "\n"), $startLine);
        $lines = $this->colorLines($tokenLines);
        $lines = $this->lineNumbers($lines, $line);

        return Termwind::div(trim($lines, "\n"));
    }

    /**
     * Finds extra spaces which should be removed from HTML.
     *
     * @param  array<int, string>  $lines
     */
    private function findExtraSpaces(array $lines): string
    {
        foreach ($lines as $line) {
            if ($line === '') {
                continue;
            }

            if (preg_replace('/\s+/', '', $line) === '') {
                return $line;
            }
        }

        return '';
    }

    /**
     * Returns content split into lines with numbers.
     *
     * @return array<int, array<int, array{0: string, 1: non-empty-string}>>
     */
    private function getHighlightedLines(string $source, int $startLine): array
    {
        $source = str_replace(["\r\n", "\r"], "\n", $source);
        $tokens = $this->tokenize($source);

        return $this->splitToLines($tokens, $startLine - 1);
    }

    /**
     * Splits content into tokens.
     *
     * @return array<int, array{0: string, 1: string}>
     */
    private function tokenize(string $source): array
    {
        $tokens = token_get_all($source);

        $output = [];
        $currentType = null;
        $newType = self::TOKEN_KEYWORD;
        $buffer = '';

        foreach ($tokens as $token) {
            if (is_array($token)) {
                if ($token[0] !== T_WHITESPACE) {
                    $newType = match ($token[0]) {
                        T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG, T_STRING, T_VARIABLE,
                        T_DIR, T_FILE, T_METHOD_C, T_DNUMBER, T_LNUMBER, T_NS_C,
                        T_LINE, T_CLASS_C, T_FUNC_C, T_TRAIT_C => self::TOKEN_DEFAULT,
                        T_COMMENT, T_DOC_COMMENT => self::TOKEN_COMMENT,
                        T_ENCAPSED_AND_WHITESPACE, T_CONSTANT_ENCAPSED_STRING => self::TOKEN_STRING,
                        T_INLINE_HTML => self::TOKEN_HTML,
                        default => self::TOKEN_KEYWORD
                    };
                }
            } else {
                $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD;
            }

            if ($currentType === null) {
                $currentType = $newType;
            }

            if ($currentType !== $newType) {
                $output[] = [$currentType, $buffer];
                $buffer = '';
                $currentType = $newType;
            }

            $buffer .= is_array($token) ? $token[1] : $token;
        }

        $output[] = [$newType, $buffer];

        return $output;
    }

    /**
     * Splits tokens into lines.
     *
     * @param  array<int, array{0: string, 1: string}>  $tokens
     * @param  int  $startLine
     * @return array<int, array<int, array{0: string, 1: non-empty-string}>>
     */
    private function splitToLines(array $tokens, int $startLine): array
    {
        $lines = [];

        $line = [];
        foreach ($tokens as $token) {
            foreach (explode("\n", $token[1]) as $count => $tokenLine) {
                if ($count > 0) {
                    $lines[$startLine++] = $line;
                    $line = [];
                }

                if ($tokenLine === '') {
                    continue;
                }

                $line[] = [$token[0], $tokenLine];
            }
        }

        $lines[$startLine++] = $line;

        return $lines;
    }

    /**
     * Applies colors to tokens according to a color schema.
     *
     * @param  array<int, array<int, array{0: string, 1: non-empty-string}>>  $tokenLines
     * @return array<int, string>
     */
    private function colorLines(array $tokenLines): array
    {
        $lines = [];

        foreach ($tokenLines as $lineCount => $tokenLine) {
            $line = '';
            foreach ($tokenLine as $token) {
                [$tokenType, $tokenValue] = $token;
                $line .= $this->styleToken($tokenType, $tokenValue);
            }

            $lines[$lineCount] = $line;
        }

        return $lines;
    }

    /**
     * Prepends line numbers into lines.
     *
     * @param  array<int, string>  $lines
     * @param  int  $markLine
     * @return string
     */
    private function lineNumbers(array $lines, int $markLine): string
    {
        $lastLine = (int) array_key_last($lines);
        $lineLength = strlen((string) ($lastLine + 1));
        $lineLength = $lineLength < self::WIDTH ? self::WIDTH : $lineLength;

        $snippet = '';
        $mark = '  '.$this->arrow.' ';
        foreach ($lines as $i => $line) {
            $coloredLineNumber = $this->coloredLineNumber(self::LINE_NUMBER, $i, $lineLength);

            if (0 !== $markLine) {
                $snippet .= ($markLine === $i + 1
                    ? $this->styleToken(self::ACTUAL_LINE_MARK, $mark)
                    : self::NO_MARK
                );

                $coloredLineNumber = ($markLine === $i + 1 ?
                    $this->coloredLineNumber(self::MARKED_LINE_NUMBER, $i, $lineLength) :
                    $coloredLineNumber
                );
            }

            $snippet .= $coloredLineNumber;
            $snippet .= $this->styleToken(self::LINE_NUMBER_DIVIDER, $this->delimiter);
            $snippet .= $line.PHP_EOL;
        }

        return $snippet;
    }

    /**
     * Formats line number and applies color according to a color schema.
     */
    private function coloredLineNumber(string $token, int $lineNumber, int $length): string
    {
        return $this->styleToken(
            $token, str_pad((string) ($lineNumber + 1), $length, ' ', STR_PAD_LEFT)
        );
    }

    /**
     * Formats string and applies color according to a color schema.
     */
    private function styleToken(string $token, string $string): string
    {
        return (string) Termwind::span($string, self::THEME[$token]);
    }
}
PreRenderer.php000064400000001775151521005060007477 0ustar00<?php

declare(strict_types=1);

namespace Termwind\Html;

use Termwind\Components\Element;
use Termwind\Termwind;
use Termwind\ValueObjects\Node;

/**
 * @internal
 */
final class PreRenderer
{
    /**
     * Gets HTML content from a given node and converts to the content element.
     */
    public function toElement(Node $node): Element
    {
        $lines = explode("\n", $node->getHtml());
        if (reset($lines) === '') {
            array_shift($lines);
        }

        if (end($lines) === '') {
            array_pop($lines);
        }

        $maxStrLen = array_reduce(
            $lines,
            static fn (int $max, string $line) => ($max < strlen($line)) ? strlen($line) : $max,
            0
        );

        $styles = $node->getClassAttribute();
        $html = array_map(
            static fn (string $line) => (string) Termwind::div(str_pad($line, $maxStrLen + 3), $styles),
            $lines
        );

        return Termwind::raw(
            implode('', $html)
        );
    }
}
InheritStyles.php000064400000014270151521005060010062 0ustar00<?php

declare(strict_types=1);

namespace Termwind\Html;

use Termwind\Components\Element;
use Termwind\Termwind;
use Termwind\ValueObjects\Styles;

/**
 * @internal
 */
final class InheritStyles
{
    /**
     * Applies styles from parent element to child elements.
     *
     * @param  array<int, Element|string>  $elements
     * @return array<int, Element|string>
     */
    public function __invoke(array $elements, Styles $styles): array
    {
        $elements = array_values($elements);

        foreach ($elements as &$element) {
            if (is_string($element)) {
                $element = Termwind::raw($element);
            }

            $element->inheritFromStyles($styles);
        }

        /** @var Element[] $elements */
        if (($styles->getProperties()['styles']['display'] ?? 'inline') === 'flex') {
            $elements = $this->applyFlex($elements);
        }

        return match ($styles->getProperties()['styles']['justifyContent'] ?? false) {
            'between' => $this->applyJustifyBetween($elements),
            'evenly' => $this->applyJustifyEvenly($elements),
            'around' => $this->applyJustifyAround($elements),
            'center' => $this->applyJustifyCenter($elements),
            default => $elements,
        };
    }

    /**
     * Applies flex-1 to child elements with the class.
     *
     * @param  array<int, Element>  $elements
     * @return array<int, Element>
     */
    private function applyFlex(array $elements): array
    {
        [$totalWidth, $parentWidth] = $this->getWidthFromElements($elements);

        $width = max(0, array_reduce($elements, function ($carry, $element) {
            return $carry += $element->hasStyle('flex-1') ? $element->getInnerWidth() : 0;
        }, $parentWidth - $totalWidth));

        $flexed = array_values(array_filter(
            $elements, fn ($element) => $element->hasStyle('flex-1')
        ));

        foreach ($flexed as $index => &$element) {
            if ($width === 0 && ! ($element->getProperties()['styles']['contentRepeat'] ?? false)) {
                continue;
            }

            $float = $width / count($flexed);
            $elementWidth = floor($float);

            if ($index === count($flexed) - 1) {
                $elementWidth += ($float - floor($float)) * count($flexed);
            }

            $element->addStyle("w-{$elementWidth}");
        }

        return $elements;
    }

    /**
     * Applies the space between the elements.
     *
     * @param  array<int, Element>  $elements
     * @return array<int, Element|string>
     */
    private function applyJustifyBetween(array $elements): array
    {
        if (count($elements) <= 1) {
            return $elements;
        }

        [$totalWidth, $parentWidth] = $this->getWidthFromElements($elements);
        $space = ($parentWidth - $totalWidth) / (count($elements) - 1);

        if ($space < 1) {
            return $elements;
        }

        $arr = [];

        foreach ($elements as $index => &$element) {
            if ($index !== 0) {
                // Since there is no float pixel, on the last one it should round up...
                $length = $index === count($elements) - 1 ? ceil($space) : floor($space);
                $arr[] = str_repeat(' ', (int) $length);
            }

            $arr[] = $element;
        }

        return $arr;
    }

    /**
     * Applies the space between and around the elements.
     *
     * @param  array<int, Element>  $elements
     * @return array<int, Element|string>
     */
    private function applyJustifyEvenly(array $elements): array
    {
        [$totalWidth, $parentWidth] = $this->getWidthFromElements($elements);
        $space = ($parentWidth - $totalWidth) / (count($elements) + 1);

        if ($space < 1) {
            return $elements;
        }

        $arr = [];
        foreach ($elements as &$element) {
            $arr[] = str_repeat(' ', (int) floor($space));
            $arr[] = $element;
        }

        $decimals = ceil(($space - floor($space)) * (count($elements) + 1));
        $arr[] = str_repeat(' ', (int) (floor($space) + $decimals));

        return $arr;
    }

    /**
     * Applies the space around the elements.
     *
     * @param  array<int, Element>  $elements
     * @return array<int, Element|string>
     */
    private function applyJustifyAround(array $elements): array
    {
        if (count($elements) === 0) {
            return $elements;
        }

        [$totalWidth, $parentWidth] = $this->getWidthFromElements($elements);
        $space = ($parentWidth - $totalWidth) / count($elements);

        if ($space < 1) {
            return $elements;
        }

        $contentSize = $totalWidth;
        $arr = [];

        foreach ($elements as $index => &$element) {
            if ($index !== 0) {
                $arr[] = str_repeat(' ', (int) ceil($space));
                $contentSize += ceil($space);
            }

            $arr[] = $element;
        }

        return [
            str_repeat(' ', (int) floor(($parentWidth - $contentSize) / 2)),
            ...$arr,
            str_repeat(' ', (int) ceil(($parentWidth - $contentSize) / 2)),
        ];
    }

    /**
     * Applies the space on before first element and after last element.
     *
     * @param  array<int, Element>  $elements
     * @return array<int, Element|string>
     */
    private function applyJustifyCenter(array $elements): array
    {
        [$totalWidth, $parentWidth] = $this->getWidthFromElements($elements);
        $space = $parentWidth - $totalWidth;

        if ($space < 1) {
            return $elements;
        }

        return [
            str_repeat(' ', (int) floor($space / 2)),
            ...$elements,
            str_repeat(' ', (int) ceil($space / 2)),
        ];
    }

    /**
     * Gets the total width for the elements and their parent width.
     *
     * @param  array<int, Element>  $elements
     * @return int[]
     */
    private function getWidthFromElements(array $elements)
    {
        $totalWidth = (int) array_reduce($elements, fn ($carry, $element) => $carry += $element->getLength(), 0);
        $parentWidth = Styles::getParentWidth($elements[0]->getProperties()['parentStyles'] ?? []);

        return [$totalWidth, $parentWidth];
    }
}