mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-11 18:51:31 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "nunomaduro/termwind",
|
||||
"description": "Its like Tailwind CSS, but for the console.",
|
||||
"keywords": ["php", "cli", "package", "console", "css", "style"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-mbstring": "*",
|
||||
"symfony/console": "^6.4.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"illuminate/console": "^10.48.24",
|
||||
"illuminate/support": "^10.48.24",
|
||||
"laravel/pint": "^1.18.2",
|
||||
"pestphp/pest": "^2.36.0",
|
||||
"pestphp/pest-plugin-mock": "2.0.0",
|
||||
"phpstan/phpstan": "^1.12.11",
|
||||
"phpstan/phpstan-strict-rules": "^1.6.1",
|
||||
"symfony/var-dumper": "^6.4.15",
|
||||
"thecodingmachine/phpstan-strict-rules": "^1.0.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Termwind\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/Functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "pint -v",
|
||||
"test:lint": "pint --test -v",
|
||||
"test:types": "phpstan analyse --ansi",
|
||||
"test:unit": "pest --colors=always",
|
||||
"test": [
|
||||
"@test:lint",
|
||||
"@test:types",
|
||||
"@test:unit"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Termwind\\Laravel\\TermwindServiceProvider"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use function Termwind\render;
|
||||
|
||||
render(<<<'HTML'
|
||||
<div class="ml-2">
|
||||
<pre>
|
||||
─ ─ ─ ─
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ──, │ │ ──, │ │ │ │ ─
|
||||
│/ \ / │ │ │ │/ / │ │/ \─│/ │/
|
||||
│ │─/\─/│─/ \─/│─/│──/\─/│─/\─/ │──/│──/
|
||||
</pre>
|
||||
<div class="px-1 bg-green-300 text-black">by ⚙️ Configured</div>
|
||||
<div class="px-1 mt-1 bg-blue-300 text-black">{{ $version }}</div>
|
||||
<em class="ml-1">
|
||||
Create portable PHP CLI applications w/ PHP Micro
|
||||
</em>
|
||||
</div>
|
||||
HTML);
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Actions;
|
||||
|
||||
use Termwind\Exceptions\StyleNotFound;
|
||||
use Termwind\Repositories\Styles as StyleRepository;
|
||||
use Termwind\Terminal;
|
||||
use Termwind\ValueObjects\Styles;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StyleToMethod
|
||||
{
|
||||
/**
|
||||
* Finds if there is any media query on the style class.
|
||||
*/
|
||||
private const MEDIA_QUERIES_REGEX = "/^(sm|md|lg|xl|2xl)\:(.*)/";
|
||||
|
||||
/**
|
||||
* Defines the Media Query Breakpoints.
|
||||
*/
|
||||
public const MEDIA_QUERY_BREAKPOINTS = [
|
||||
'sm' => 64,
|
||||
'md' => 76,
|
||||
'lg' => 102,
|
||||
'xl' => 128,
|
||||
'2xl' => 153,
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a new action instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private Styles $styles,
|
||||
private string $style,
|
||||
) {
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies multiple styles to the given styles.
|
||||
*/
|
||||
public static function multiple(Styles $styles, string $stylesString): Styles
|
||||
{
|
||||
$stylesString = self::sortStyles(array_merge(
|
||||
$styles->defaultStyles(),
|
||||
array_filter((array) preg_split('/(?![^\[]*\])\s/', $stylesString))
|
||||
));
|
||||
|
||||
foreach ($stylesString as $style) {
|
||||
$styles = (new self($styles, $style))->__invoke();
|
||||
}
|
||||
|
||||
return $styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given style to a method name.
|
||||
*/
|
||||
public function __invoke(string|int ...$arguments): Styles
|
||||
{
|
||||
if (StyleRepository::has($this->style)) {
|
||||
return StyleRepository::get($this->style)($this->styles, ...$arguments);
|
||||
}
|
||||
|
||||
$method = $this->applyMediaQuery($this->style);
|
||||
|
||||
if ($method === '') {
|
||||
return $this->styles;
|
||||
}
|
||||
|
||||
$method = array_filter(
|
||||
(array) preg_split('/(?![^\[]*\])-/', $method),
|
||||
fn ($item) => $item !== false
|
||||
);
|
||||
|
||||
$method = array_slice($method, 0, count($method) - count($arguments));
|
||||
|
||||
$methodName = implode(' ', $method);
|
||||
$methodName = ucwords($methodName);
|
||||
$methodName = lcfirst($methodName);
|
||||
$methodName = str_replace(' ', '', $methodName);
|
||||
|
||||
if ($methodName === '') {
|
||||
throw StyleNotFound::fromStyle($this->style);
|
||||
}
|
||||
|
||||
if (! method_exists($this->styles, $methodName)) {
|
||||
$argument = array_pop($method);
|
||||
|
||||
$arguments[] = is_numeric($argument) ? (int) $argument : (string) $argument;
|
||||
|
||||
return $this->__invoke(...$arguments);
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->styles
|
||||
->setStyle($this->style)
|
||||
->$methodName(...array_reverse($arguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts all the styles based on the correct render order.
|
||||
*
|
||||
* @param string[] $styles
|
||||
* @return string[]
|
||||
*/
|
||||
private static function sortStyles(array $styles): array
|
||||
{
|
||||
$keys = array_keys(self::MEDIA_QUERY_BREAKPOINTS);
|
||||
|
||||
usort($styles, function ($a, $b) use ($keys) {
|
||||
$existsA = (bool) preg_match(self::MEDIA_QUERIES_REGEX, $a, $matchesA);
|
||||
$existsB = (bool) preg_match(self::MEDIA_QUERIES_REGEX, $b, $matchesB);
|
||||
|
||||
if ($existsA && ! $existsB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($existsA && array_search($matchesA[1], $keys, true) > array_search($matchesB[1], $keys, true)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
|
||||
return $styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the media query if exists.
|
||||
*/
|
||||
private function applyMediaQuery(string $method): string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match(self::MEDIA_QUERIES_REGEX, $method, $matches);
|
||||
|
||||
if (count($matches) < 1) {
|
||||
return $method;
|
||||
}
|
||||
|
||||
[, $size, $method] = $matches;
|
||||
|
||||
if ((new Terminal)->width() >= self::MEDIA_QUERY_BREAKPOINTS[$size]) {
|
||||
return $method;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Components;
|
||||
|
||||
final class Anchor extends Element {}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Components;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Termwind\Actions\StyleToMethod;
|
||||
use Termwind\Html\InheritStyles;
|
||||
use Termwind\ValueObjects\Styles;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @method Element inheritFromStyles(Styles $styles)
|
||||
* @method Element fontBold()
|
||||
* @method Element strong()
|
||||
* @method Element italic()
|
||||
* @method Element underline()
|
||||
* @method Element lineThrough()
|
||||
* @method int getLength()
|
||||
* @method int getInnerWidth()
|
||||
* @method array getProperties()
|
||||
* @method Element href(string $href)
|
||||
* @method bool hasStyle(string $style)
|
||||
* @method Element addStyle(string $style)
|
||||
*/
|
||||
abstract class Element
|
||||
{
|
||||
/** @var string[] */
|
||||
protected static array $defaultStyles = [];
|
||||
|
||||
protected Styles $styles;
|
||||
|
||||
/**
|
||||
* Creates an element instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
*/
|
||||
final public function __construct(
|
||||
protected OutputInterface $output,
|
||||
protected array|string $content,
|
||||
Styles|null $styles = null
|
||||
) {
|
||||
$this->styles = $styles ?? new Styles(defaultStyles: static::$defaultStyles);
|
||||
$this->styles->setElement($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an element instance with the given styles.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
final public static function fromStyles(OutputInterface $output, array|string $content, string $styles = '', array $properties = []): static
|
||||
{
|
||||
$element = new static($output, $content);
|
||||
if ($properties !== []) {
|
||||
$element->styles->setProperties($properties);
|
||||
}
|
||||
|
||||
$elementStyles = StyleToMethod::multiple($element->styles, $styles);
|
||||
|
||||
return new static($output, $content, $elementStyles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of the element.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
if (is_array($this->content)) {
|
||||
$inheritance = new InheritStyles;
|
||||
$this->content = implode('', $inheritance($this->content, $this->styles));
|
||||
}
|
||||
|
||||
return $this->styles->format($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
if (method_exists($this->styles, $name)) {
|
||||
// @phpstan-ignore-next-line
|
||||
$result = $this->styles->{$name}(...$arguments);
|
||||
|
||||
if (str_starts_with($name, 'get') || str_starts_with($name, 'has')) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content of the element.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
*/
|
||||
final public function setContent(array|string $content): static
|
||||
{
|
||||
return new static($this->output, $content, $this->styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the string representation of the element on the output.
|
||||
*/
|
||||
final public function render(int $options): void
|
||||
{
|
||||
$this->output->writeln($this->toString(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of the element.
|
||||
*/
|
||||
final public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ColorNotFound extends InvalidArgumentException {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidChild extends InvalidArgumentException {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidColor extends InvalidArgumentException {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Exceptions;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InvalidStyle extends InvalidArgumentException {}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind;
|
||||
|
||||
use Closure;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Termwind\Repositories\Styles as StyleRepository;
|
||||
use Termwind\ValueObjects\Style;
|
||||
use Termwind\ValueObjects\Styles;
|
||||
|
||||
if (! function_exists('Termwind\renderUsing')) {
|
||||
/**
|
||||
* Sets the renderer implementation.
|
||||
*/
|
||||
function renderUsing(?OutputInterface $renderer): void
|
||||
{
|
||||
Termwind::renderUsing($renderer);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('Termwind\style')) {
|
||||
/**
|
||||
* Creates a new style.
|
||||
*
|
||||
* @param (Closure(Styles $renderable, string|int ...$arguments): Styles)|null $callback
|
||||
*/
|
||||
function style(string $name, ?Closure $callback = null): Style
|
||||
{
|
||||
return StyleRepository::create($name, $callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('Termwind\render')) {
|
||||
/**
|
||||
* Render HTML to a string.
|
||||
*/
|
||||
function render(string $html, int $options = OutputInterface::OUTPUT_NORMAL): void
|
||||
{
|
||||
(new HtmlRenderer)->render($html, $options);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('Termwind\terminal')) {
|
||||
/**
|
||||
* Returns a Terminal instance.
|
||||
*/
|
||||
function terminal(): Terminal
|
||||
{
|
||||
return new Terminal;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('Termwind\ask')) {
|
||||
/**
|
||||
* Renders a prompt to the user.
|
||||
*
|
||||
* @param iterable<array-key, string>|null $autocomplete
|
||||
*/
|
||||
function ask(string $question, ?iterable $autocomplete = null): mixed
|
||||
{
|
||||
return (new Question)->ask($question, $autocomplete);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?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
|
||||
* @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
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use Termwind\Html\CodeRenderer;
|
||||
use Termwind\Html\PreRenderer;
|
||||
use Termwind\Html\TableRenderer;
|
||||
use Termwind\ValueObjects\Node;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class HtmlRenderer
|
||||
{
|
||||
/**
|
||||
* Renders the given html.
|
||||
*/
|
||||
public function render(string $html, int $options): void
|
||||
{
|
||||
$this->parse($html)->render($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given html.
|
||||
*/
|
||||
public function parse(string $html): Components\Element
|
||||
{
|
||||
$dom = new DOMDocument;
|
||||
|
||||
if (strip_tags($html) === $html) {
|
||||
return Termwind::span($html);
|
||||
}
|
||||
|
||||
$html = '<?xml encoding="UTF-8">'.trim($html);
|
||||
$dom->loadHTML($html, LIBXML_NOERROR | LIBXML_COMPACT | LIBXML_HTML_NODEFDTD | LIBXML_NOBLANKS | LIBXML_NOXMLDECL);
|
||||
|
||||
/** @var DOMNode $body */
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
$el = $this->convert(new Node($body));
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
return is_string($el)
|
||||
? Termwind::span($el)
|
||||
: $el;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a tree of DOM nodes to a tree of termwind elements.
|
||||
*/
|
||||
private function convert(Node $node): Components\Element|string
|
||||
{
|
||||
$children = [];
|
||||
|
||||
if ($node->isName('table')) {
|
||||
return (new TableRenderer)->toElement($node);
|
||||
} elseif ($node->isName('code')) {
|
||||
return (new CodeRenderer)->toElement($node);
|
||||
} elseif ($node->isName('pre')) {
|
||||
return (new PreRenderer)->toElement($node);
|
||||
}
|
||||
|
||||
foreach ($node->getChildNodes() as $child) {
|
||||
$children[] = $this->convert($child);
|
||||
}
|
||||
|
||||
$children = array_filter($children, fn ($child) => $child !== '');
|
||||
|
||||
return $this->toElement($node, $children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a given DOM node to it's termwind element equivalent.
|
||||
*
|
||||
* @param array<int, Components\Element|string> $children
|
||||
*/
|
||||
private function toElement(Node $node, array $children): Components\Element|string
|
||||
{
|
||||
if ($node->isText() || $node->isComment()) {
|
||||
return (string) $node;
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $properties */
|
||||
$properties = [
|
||||
'isFirstChild' => $node->isFirstChild(),
|
||||
];
|
||||
|
||||
$styles = $node->getClassAttribute();
|
||||
|
||||
return match ($node->getName()) {
|
||||
'body' => $children[0], // Pick only the first element from the body node
|
||||
'div' => Termwind::div($children, $styles, $properties),
|
||||
'p' => Termwind::paragraph($children, $styles, $properties),
|
||||
'ul' => Termwind::ul($children, $styles, $properties),
|
||||
'ol' => Termwind::ol($children, $styles, $properties),
|
||||
'li' => Termwind::li($children, $styles, $properties),
|
||||
'dl' => Termwind::dl($children, $styles, $properties),
|
||||
'dt' => Termwind::dt($children, $styles, $properties),
|
||||
'dd' => Termwind::dd($children, $styles, $properties),
|
||||
'span' => Termwind::span($children, $styles, $properties),
|
||||
'br' => Termwind::breakLine($styles, $properties),
|
||||
'strong' => Termwind::span($children, $styles, $properties)->strong(),
|
||||
'b' => Termwind::span($children, $styles, $properties)->fontBold(),
|
||||
'em', 'i' => Termwind::span($children, $styles, $properties)->italic(),
|
||||
'u' => Termwind::span($children, $styles, $properties)->underline(),
|
||||
's' => Termwind::span($children, $styles, $properties)->lineThrough(),
|
||||
'a' => Termwind::anchor($children, $styles, $properties)->href($node->getAttribute('href')),
|
||||
'hr' => Termwind::hr($styles, $properties),
|
||||
default => Termwind::div($children, $styles, $properties),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind;
|
||||
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Input\StreamableInputInterface;
|
||||
use Symfony\Component\Console\Question\Question as SymfonyQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Termwind\Helpers\QuestionHelper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Question
|
||||
{
|
||||
/**
|
||||
* The streamable input to receive the input from the user.
|
||||
*/
|
||||
private static StreamableInputInterface|null $streamableInput;
|
||||
|
||||
/**
|
||||
* An instance of Symfony's question helper.
|
||||
*/
|
||||
private SymfonyQuestionHelper $helper;
|
||||
|
||||
public function __construct(?SymfonyQuestionHelper $helper = null)
|
||||
{
|
||||
$this->helper = $helper ?? new QuestionHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the streamable input implementation.
|
||||
*/
|
||||
public static function setStreamableInput(StreamableInputInterface|null $streamableInput): void
|
||||
{
|
||||
self::$streamableInput = $streamableInput ?? new ArgvInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the streamable input implementation.
|
||||
*/
|
||||
public static function getStreamableInput(): StreamableInputInterface
|
||||
{
|
||||
return self::$streamableInput ??= new ArgvInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a prompt to the user.
|
||||
*
|
||||
* @param iterable<array-key, string>|null $autocomplete
|
||||
*/
|
||||
public function ask(string $question, ?iterable $autocomplete = null): mixed
|
||||
{
|
||||
$html = (new HtmlRenderer)->parse($question)->toString();
|
||||
|
||||
$question = new SymfonyQuestion($html);
|
||||
|
||||
if ($autocomplete !== null) {
|
||||
$question->setAutocompleterValues($autocomplete);
|
||||
}
|
||||
|
||||
$output = Termwind::getRenderer();
|
||||
|
||||
if ($output instanceof SymfonyStyle) {
|
||||
$property = (new ReflectionClass(SymfonyStyle::class))
|
||||
->getProperty('questionHelper');
|
||||
|
||||
$property->setAccessible(true);
|
||||
|
||||
$currentHelper = $property->isInitialized($output)
|
||||
? $property->getValue($output)
|
||||
: new SymfonyQuestionHelper;
|
||||
|
||||
$property->setValue($output, new QuestionHelper);
|
||||
|
||||
try {
|
||||
return $output->askQuestion($question);
|
||||
} finally {
|
||||
$property->setValue($output, $currentHelper);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->helper->ask(
|
||||
self::getStreamableInput(),
|
||||
Termwind::getRenderer(),
|
||||
$question,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\Repositories;
|
||||
|
||||
use Closure;
|
||||
use Termwind\ValueObjects\Style;
|
||||
use Termwind\ValueObjects\Styles as StylesValueObject;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Styles
|
||||
{
|
||||
/**
|
||||
* @var array<string, Style>
|
||||
*/
|
||||
private static array $storage = [];
|
||||
|
||||
/**
|
||||
* Creates a new style from the given arguments.
|
||||
*
|
||||
* @param (Closure(StylesValueObject $element, string|int ...$arguments): StylesValueObject)|null $callback
|
||||
*/
|
||||
public static function create(string $name, ?Closure $callback = null): Style
|
||||
{
|
||||
self::$storage[$name] = $style = new Style(
|
||||
$callback ?? static fn (StylesValueObject $styles) => $styles
|
||||
);
|
||||
|
||||
return $style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all existing styles.
|
||||
*/
|
||||
public static function flush(): void
|
||||
{
|
||||
self::$storage = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a style with the given name exists.
|
||||
*/
|
||||
public static function has(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, self::$storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the style with the given name.
|
||||
*/
|
||||
public static function get(string $name): Style
|
||||
{
|
||||
return self::$storage[$name];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind;
|
||||
|
||||
use Symfony\Component\Console\Terminal as ConsoleTerminal;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Terminal
|
||||
{
|
||||
/**
|
||||
* An instance of Symfony's console terminal.
|
||||
*/
|
||||
private ConsoleTerminal $terminal;
|
||||
|
||||
/**
|
||||
* Creates a new terminal instance.
|
||||
*/
|
||||
public function __construct(?ConsoleTerminal $terminal = null)
|
||||
{
|
||||
$this->terminal = $terminal ?? new ConsoleTerminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the terminal width.
|
||||
*/
|
||||
public function width(): int
|
||||
{
|
||||
return $this->terminal->getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the terminal height.
|
||||
*/
|
||||
public function height(): int
|
||||
{
|
||||
return $this->terminal->getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the terminal screen.
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
Termwind::getRenderer()->write("\ec");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind;
|
||||
|
||||
use Closure;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Termwind\Components\Element;
|
||||
use Termwind\Exceptions\InvalidChild;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Termwind
|
||||
{
|
||||
/**
|
||||
* The implementation of the output.
|
||||
*/
|
||||
private static OutputInterface|null $renderer;
|
||||
|
||||
/**
|
||||
* Sets the renderer implementation.
|
||||
*/
|
||||
public static function renderUsing(OutputInterface|null $renderer): void
|
||||
{
|
||||
self::$renderer = $renderer ?? new ConsoleOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a div element instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function div(array|string $content = '', string $styles = '', array $properties = []): Components\Div
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Div::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a paragraph element instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function paragraph(array|string $content = '', string $styles = '', array $properties = []): Components\Paragraph
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Paragraph::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a span element instance with the given style.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function span(array|string $content = '', string $styles = '', array $properties = []): Components\Span
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Span::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an element instance with raw content.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
*/
|
||||
public static function raw(array|string $content = ''): Components\Raw
|
||||
{
|
||||
return Components\Raw::fromStyles(
|
||||
self::getRenderer(), $content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an anchor element instance with the given style.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function anchor(array|string $content = '', string $styles = '', array $properties = []): Components\Anchor
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Anchor::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unordered list instance.
|
||||
*
|
||||
* @param array<int, string|Element> $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function ul(array $content = [], string $styles = '', array $properties = []): Components\Ul
|
||||
{
|
||||
$ul = Components\Ul::fromStyles(
|
||||
self::getRenderer(), '', $styles, $properties
|
||||
);
|
||||
|
||||
$content = self::prepareElements(
|
||||
$content,
|
||||
$styles,
|
||||
static function ($li) use ($ul): string|Element {
|
||||
if (is_string($li)) {
|
||||
return $li;
|
||||
}
|
||||
|
||||
if (! $li instanceof Components\Li) {
|
||||
throw new InvalidChild('Unordered lists only accept `li` as child');
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$li->hasStyle('list-none') => $li,
|
||||
$ul->hasStyle('list-none') => $li->addStyle('list-none'),
|
||||
$ul->hasStyle('list-square') => $li->addStyle('list-square'),
|
||||
$ul->hasStyle('list-disc') => $li->addStyle('list-disc'),
|
||||
default => $li->addStyle('list-none'),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return $ul->setContent($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ordered list instance.
|
||||
*
|
||||
* @param array<int, string|Element> $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function ol(array $content = [], string $styles = '', array $properties = []): Components\Ol
|
||||
{
|
||||
$ol = Components\Ol::fromStyles(
|
||||
self::getRenderer(), '', $styles, $properties
|
||||
);
|
||||
|
||||
$index = 0;
|
||||
|
||||
$content = self::prepareElements(
|
||||
$content,
|
||||
$styles,
|
||||
static function ($li) use ($ol, &$index): string|Element {
|
||||
if (is_string($li)) {
|
||||
return $li;
|
||||
}
|
||||
|
||||
if (! $li instanceof Components\Li) {
|
||||
throw new InvalidChild('Ordered lists only accept `li` as child');
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
$li->hasStyle('list-none') => $li->addStyle('list-none'),
|
||||
$ol->hasStyle('list-none') => $li->addStyle('list-none'),
|
||||
$ol->hasStyle('list-decimal') => $li->addStyle('list-decimal-'.(++$index)),
|
||||
default => $li->addStyle('list-none'),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return $ol->setContent($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list item instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function li(array|string $content = '', string $styles = '', array $properties = []): Components\Li
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Li::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a description list instance.
|
||||
*
|
||||
* @param array<int, string|Element> $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function dl(array $content = [], string $styles = '', array $properties = []): Components\Dl
|
||||
{
|
||||
$content = self::prepareElements(
|
||||
$content,
|
||||
$styles,
|
||||
static function ($element): string|Element {
|
||||
if (is_string($element)) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
if (! $element instanceof Components\Dt && ! $element instanceof Components\Dd) {
|
||||
throw new InvalidChild('Description lists only accept `dt` and `dd` as children');
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
);
|
||||
|
||||
return Components\Dl::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a description term instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function dt(array|string $content = '', string $styles = '', array $properties = []): Components\Dt
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Dt::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a description details instance.
|
||||
*
|
||||
* @param array<int, Element|string>|string $content
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function dd(array|string $content = '', string $styles = '', array $properties = []): Components\Dd
|
||||
{
|
||||
$content = self::prepareElements($content, $styles);
|
||||
|
||||
return Components\Dd::fromStyles(
|
||||
self::getRenderer(), $content, $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontal rule instance.
|
||||
*
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function hr(string $styles = '', array $properties = []): Components\Hr
|
||||
{
|
||||
return Components\Hr::fromStyles(
|
||||
self::getRenderer(), '', $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an break line element instance.
|
||||
*
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
public static function breakLine(string $styles = '', array $properties = []): Components\BreakLine
|
||||
{
|
||||
return Components\BreakLine::fromStyles(
|
||||
self::getRenderer(), '', $styles, $properties
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current renderer instance.
|
||||
*/
|
||||
public static function getRenderer(): OutputInterface
|
||||
{
|
||||
return self::$renderer ??= new ConsoleOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert child elements to a string.
|
||||
*
|
||||
* @param array<int, string|Element>|string $elements
|
||||
* @return array<int, string|Element>
|
||||
*/
|
||||
private static function prepareElements($elements, string $styles = '', Closure|null $callback = null): array
|
||||
{
|
||||
if ($callback === null) {
|
||||
$callback = static fn ($element): string|Element => $element;
|
||||
}
|
||||
|
||||
$elements = is_array($elements) ? $elements : [$elements];
|
||||
|
||||
return array_map($callback, $elements);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\ValueObjects;
|
||||
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Node
|
||||
{
|
||||
/**
|
||||
* A value object with helper methods for working with DOM node.
|
||||
*/
|
||||
public function __construct(private \DOMNode $node) {}
|
||||
|
||||
/**
|
||||
* Gets the value of the node.
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->node->nodeValue ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets child nodes of the node.
|
||||
*
|
||||
* @return Generator<Node>
|
||||
*/
|
||||
public function getChildNodes(): Generator
|
||||
{
|
||||
foreach ($this->node->childNodes as $node) {
|
||||
yield new self($node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a text.
|
||||
*/
|
||||
public function isText(): bool
|
||||
{
|
||||
return $this->node instanceof \DOMText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is a comment.
|
||||
*/
|
||||
public function isComment(): bool
|
||||
{
|
||||
return $this->node instanceof \DOMComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current node name with a given name.
|
||||
*/
|
||||
public function isName(string $name): bool
|
||||
{
|
||||
return $this->getName() === $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current node type name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->node->nodeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value of [class] attribute.
|
||||
*/
|
||||
public function getClassAttribute(): string
|
||||
{
|
||||
return $this->getAttribute('class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value of attribute with a given name.
|
||||
*/
|
||||
public function getAttribute(string $name): string
|
||||
{
|
||||
if ($this->node instanceof \DOMElement) {
|
||||
return $this->node->getAttribute($name);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is empty.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->isText() && preg_replace('/\s+/', '', $this->getValue()) === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the previous sibling from the node.
|
||||
*/
|
||||
public function getPreviousSibling(): static|null
|
||||
{
|
||||
$node = $this->node;
|
||||
|
||||
while ($node = $node->previousSibling) {
|
||||
$node = new self($node);
|
||||
|
||||
if ($node->isEmpty()) {
|
||||
$node = $node->node;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $node->isComment()) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
$node = $node->node;
|
||||
}
|
||||
|
||||
return is_null($node) ? null : new self($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next sibling from the node.
|
||||
*/
|
||||
public function getNextSibling(): static|null
|
||||
{
|
||||
$node = $this->node;
|
||||
|
||||
while ($node = $node->nextSibling) {
|
||||
$node = new self($node);
|
||||
|
||||
if ($node->isEmpty()) {
|
||||
$node = $node->node;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $node->isComment()) {
|
||||
return $node;
|
||||
}
|
||||
|
||||
$node = $node->node;
|
||||
}
|
||||
|
||||
return is_null($node) ? null : new self($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node is the first child.
|
||||
*/
|
||||
public function isFirstChild(): bool
|
||||
{
|
||||
return is_null($this->getPreviousSibling());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inner HTML representation of the node including child nodes.
|
||||
*/
|
||||
public function getHtml(): string
|
||||
{
|
||||
$html = '';
|
||||
foreach ($this->node->childNodes as $child) {
|
||||
if ($child->ownerDocument instanceof \DOMDocument) {
|
||||
$html .= $child->ownerDocument->saveXML($child);
|
||||
}
|
||||
}
|
||||
|
||||
return html_entity_decode($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the node to a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->isComment()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->getValue() === ' ') {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
if ($this->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = preg_replace('/\s+/', ' ', $this->getValue()) ?? '';
|
||||
|
||||
if (is_null($this->getPreviousSibling())) {
|
||||
$text = ltrim($text);
|
||||
}
|
||||
|
||||
if (is_null($this->getNextSibling())) {
|
||||
$text = rtrim($text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Termwind\ValueObjects;
|
||||
|
||||
use Closure;
|
||||
use Termwind\Actions\StyleToMethod;
|
||||
use Termwind\Exceptions\InvalidColor;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Style
|
||||
{
|
||||
/**
|
||||
* Creates a new value object instance.
|
||||
*
|
||||
* @param Closure(Styles $styles, string|int ...$argument): Styles $callback
|
||||
*/
|
||||
public function __construct(private Closure $callback, private string $color = '')
|
||||
{
|
||||
// ..
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given set of styles to the styles.
|
||||
*/
|
||||
public function apply(string $styles): void
|
||||
{
|
||||
$callback = clone $this->callback;
|
||||
|
||||
$this->callback = static function (
|
||||
Styles $formatter,
|
||||
string|int ...$arguments
|
||||
) use ($callback, $styles): Styles {
|
||||
$formatter = $callback($formatter, ...$arguments);
|
||||
|
||||
return StyleToMethod::multiple($formatter, $styles);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color to the style.
|
||||
*/
|
||||
public function color(string $color): void
|
||||
{
|
||||
if (preg_match('/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/', $color) < 1) {
|
||||
throw new InvalidColor(sprintf('The color %s is invalid.', $color));
|
||||
}
|
||||
|
||||
$this->color = $color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color.
|
||||
*/
|
||||
public function getColor(): string
|
||||
{
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles the given formatter with this style.
|
||||
*/
|
||||
public function __invoke(Styles $styles, string|int ...$arguments): Styles
|
||||
{
|
||||
return ($this->callback)($styles, ...$arguments);
|
||||
}
|
||||
}
|
||||
+1060
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user