first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

21
vendor/nunomaduro/termwind/LICENSE.md vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Nuno Maduro <enunomaduro@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

33
vendor/nunomaduro/termwind/Makefile vendored Normal file
View File

@ -0,0 +1,33 @@
# Well documented Makefiles
DEFAULT_GOAL := help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ [Docker]
start: ## Spin up the container
docker-compose up -d
stop: ## Shut down the containers
docker-compose down
build: ## Build all docker images
docker-compose build
##@ [Application]
composer: ## Run composer commands. Specify the command e.g. via "make composer ARGS="install|update|require <dependency>"
docker-compose run --rm app composer $(ARGS)
lint: ## Run the Linter
docker-compose run --rm app ./vendor/bin/pint -v
test-lint: ## Run the Linter Test
docker-compose run --rm app ./vendor/bin/pint --test -v
test-types: ## Run the PHPStan analysis
docker-compose run --rm app ./vendor/bin/phpstan analyse --ansi
test-unit: ## Run the Pest Test Suite
docker-compose run --rm app ./vendor/bin/pest --colors=always
test: ## Run the tests. Apply arguments via make test ARGS="--init"
make test-lint && make test-types && make test-unit

View File

@ -0,0 +1,69 @@
{
"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.0",
"ext-mbstring": "*",
"symfony/console": "^5.3.0|^6.0.0"
},
"require-dev": {
"ergebnis/phpstan-rules": "^1.0.",
"illuminate/console": "^8.0|^9.0",
"illuminate/support": "^8.0|^9.0",
"laravel/pint": "^1.0.0",
"pestphp/pest": "^1.21.0",
"pestphp/pest-plugin-mock": "^1.0",
"phpstan/phpstan": "^1.4.6",
"phpstan/phpstan-strict-rules": "^1.1.0",
"symfony/var-dumper": "^5.2.7|^6.0.0",
"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"
]
}
}
}

View File

@ -0,0 +1,13 @@
version: '3'
services:
app:
image: termwind-docker
container_name: termwind-docker
stdin_open: true
tty: true
build:
context: .
dockerfile: docker/Dockerfile
volumes:
- .:/usr/src/app

View File

@ -0,0 +1,11 @@
FROM php:8.2-cli-alpine
# INSTALL AND UPDATE COMPOSER
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN composer self-update
WORKDIR /usr/src/app
COPY . .
# INSTALL YOUR DEPENDENCIES
RUN composer install --prefer-dist

View File

@ -0,0 +1,14 @@
<?php
require_once __DIR__.'/vendor/autoload.php';
use function Termwind\render;
render(<<<'HTML'
<div class="mx-2 my-1">
<div class="flex space-x-1">
<span class="flex-1 truncate">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Sunt illo et nisi omnis porro at, mollitia harum quas esse, aperiam dolorem ab recusandae fugiat nesciunt doloribus rem eaque nostrum itaque.</span>
<span class="text-green">DONE</span>
</div>
</div>
HTML);

View File

@ -0,0 +1,154 @@
<?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.
*
* @return Styles
*/
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);
}
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 '';
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Anchor extends Element
{
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class BreakLine extends Element
{
/**
* Get the string representation of the element.
*/
public function toString(): string
{
$display = $this->styles->getProperties()['styles']['display'] ?? 'inline';
if ($display === 'hidden') {
return '';
}
if ($display === 'block') {
return parent::toString();
}
return parent::toString()."\r";
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Dd extends Element
{
protected static array $defaultStyles = ['block', 'ml-4'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Div extends Element
{
protected static array $defaultStyles = ['block'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Dl extends Element
{
protected static array $defaultStyles = ['block'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Dt extends Element
{
protected static array $defaultStyles = ['block', 'font-bold'];
}

View File

@ -0,0 +1,121 @@
<?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)) {
$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();
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Hr extends Element
{
protected static array $defaultStyles = ['block', 'border-t'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Li extends Element
{
protected static array $defaultStyles = ['block'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Ol extends Element
{
protected static array $defaultStyles = ['block', 'list-decimal'];
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Paragraph extends Element
{
protected static array $defaultStyles = ['block', 'my-1'];
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
/**
* @internal
*/
final class Raw extends Element
{
/**
* Get the string representation of the element.
*/
public function toString(): string
{
return is_array($this->content) ? implode('', $this->content) : $this->content;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Span extends Element
{
// ..
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Termwind\Components;
final class Ul extends Element
{
protected static array $defaultStyles = ['block', 'list-disc'];
}

View File

@ -0,0 +1,482 @@
<?php
declare(strict_types=1);
namespace Termwind\Enums;
final class Color
{
public const BLACK = 'black';
public const WHITE = 'white';
public const BRIGHTWHITE = 'bright-white';
public const SLATE_50 = '#f8fafc';
public const SLATE_100 = '#f1f5f9';
public const SLATE_200 = '#e2e8f0';
public const SLATE_300 = '#cbd5e1';
public const SLATE_400 = '#94a3b8';
public const SLATE_500 = '#64748b';
public const SLATE_600 = '#475569';
public const SLATE_700 = '#334155';
public const SLATE_800 = '#1e293b';
public const SLATE_900 = '#0f172a';
public const GRAY = 'gray';
public const GRAY_50 = '#f9fafb';
public const GRAY_100 = '#f3f4f6';
public const GRAY_200 = '#e5e7eb';
public const GRAY_300 = '#d1d5db';
public const GRAY_400 = '#9ca3af';
public const GRAY_500 = '#6b7280';
public const GRAY_600 = '#4b5563';
public const GRAY_700 = '#374151';
public const GRAY_800 = '#1f2937';
public const GRAY_900 = '#111827';
public const ZINC_50 = '#fafafa';
public const ZINC_100 = '#f4f4f5';
public const ZINC_200 = '#e4e4e7';
public const ZINC_300 = '#d4d4d8';
public const ZINC_400 = '#a1a1aa';
public const ZINC_500 = '#71717a';
public const ZINC_600 = '#52525b';
public const ZINC_700 = '#3f3f46';
public const ZINC_800 = '#27272a';
public const ZINC_900 = '#18181b';
public const NEUTRAL_50 = '#fafafa';
public const NEUTRAL_100 = '#f5f5f5';
public const NEUTRAL_200 = '#e5e5e5';
public const NEUTRAL_300 = '#d4d4d4';
public const NEUTRAL_400 = '#a3a3a3';
public const NEUTRAL_500 = '#737373';
public const NEUTRAL_600 = '#525252';
public const NEUTRAL_700 = '#404040';
public const NEUTRAL_800 = '#262626';
public const NEUTRAL_900 = '#171717';
public const STONE_50 = '#fafaf9';
public const STONE_100 = '#f5f5f4';
public const STONE_200 = '#e7e5e4';
public const STONE_300 = '#d6d3d1';
public const STONE_400 = '#a8a29e';
public const STONE_500 = '#78716c';
public const STONE_600 = '#57534e';
public const STONE_700 = '#44403c';
public const STONE_800 = '#292524';
public const STONE_900 = '#1c1917';
public const RED = 'red';
public const BRIGHTRED = 'bright-red';
public const RED_50 = '#fef2f2';
public const RED_100 = '#fee2e2';
public const RED_200 = '#fecaca';
public const RED_300 = '#fca5a5';
public const RED_400 = '#f87171';
public const RED_500 = '#ef4444';
public const RED_600 = '#dc2626';
public const RED_700 = '#b91c1c';
public const RED_800 = '#991b1b';
public const RED_900 = '#7f1d1d';
public const ORANGE = '#f97316';
public const ORANGE_50 = '#fff7ed';
public const ORANGE_100 = '#ffedd5';
public const ORANGE_200 = '#fed7aa';
public const ORANGE_300 = '#fdba74';
public const ORANGE_400 = '#fb923c';
public const ORANGE_500 = '#f97316';
public const ORANGE_600 = '#ea580c';
public const ORANGE_700 = '#c2410c';
public const ORANGE_800 = '#9a3412';
public const ORANGE_900 = '#7c2d12';
public const AMBER_50 = '#fffbeb';
public const AMBER_100 = '#fef3c7';
public const AMBER_200 = '#fde68a';
public const AMBER_300 = '#fcd34d';
public const AMBER_400 = '#fbbf24';
public const AMBER_500 = '#f59e0b';
public const AMBER_600 = '#d97706';
public const AMBER_700 = '#b45309';
public const AMBER_800 = '#92400e';
public const AMBER_900 = '#78350f';
public const YELLOW = 'yellow';
public const BRIGHTYELLOW = 'bright-yellow';
public const YELLOW_50 = '#fefce8';
public const YELLOW_100 = '#fef9c3';
public const YELLOW_200 = '#fef08a';
public const YELLOW_300 = '#fde047';
public const YELLOW_400 = '#facc15';
public const YELLOW_500 = '#eab308';
public const YELLOW_600 = '#ca8a04';
public const YELLOW_700 = '#a16207';
public const YELLOW_800 = '#854d0e';
public const YELLOW_900 = '#713f12';
public const LIME_50 = '#f7fee7';
public const LIME_100 = '#ecfccb';
public const LIME_200 = '#d9f99d';
public const LIME_300 = '#bef264';
public const LIME_400 = '#a3e635';
public const LIME_500 = '#84cc16';
public const LIME_600 = '#65a30d';
public const LIME_700 = '#4d7c0f';
public const LIME_800 = '#3f6212';
public const LIME_900 = '#365314';
public const GREEN = 'green';
public const BRIGHTGREEN = 'bright-green';
public const GREEN_50 = '#f0fdf4';
public const GREEN_100 = '#dcfce7';
public const GREEN_200 = '#bbf7d0';
public const GREEN_300 = '#86efac';
public const GREEN_400 = '#4ade80';
public const GREEN_500 = '#22c55e';
public const GREEN_600 = '#16a34a';
public const GREEN_700 = '#15803d';
public const GREEN_800 = '#166534';
public const GREEN_900 = '#14532d';
public const EMERALD_50 = '#ecfdf5';
public const EMERALD_100 = '#d1fae5';
public const EMERALD_200 = '#a7f3d0';
public const EMERALD_300 = '#6ee7b7';
public const EMERALD_400 = '#34d399';
public const EMERALD_500 = '#10b981';
public const EMERALD_600 = '#059669';
public const EMERALD_700 = '#047857';
public const EMERALD_800 = '#065f46';
public const EMERALD_900 = '#064e3b';
public const TEAL_50 = '#f0fdfa';
public const TEAL_100 = '#ccfbf1';
public const TEAL_200 = '#99f6e4';
public const TEAL_300 = '#5eead4';
public const TEAL_400 = '#2dd4bf';
public const TEAL_500 = '#14b8a6';
public const TEAL_600 = '#0d9488';
public const TEAL_700 = '#0f766e';
public const TEAL_800 = '#115e59';
public const TEAL_900 = '#134e4a';
public const CYAN = 'cyan';
public const BRIGHTCYAN = 'bright-cyan';
public const CYAN_50 = '#ecfeff';
public const CYAN_100 = '#cffafe';
public const CYAN_200 = '#a5f3fc';
public const CYAN_300 = '#67e8f9';
public const CYAN_400 = '#22d3ee';
public const CYAN_500 = '#06b6d4';
public const CYAN_600 = '#0891b2';
public const CYAN_700 = '#0e7490';
public const CYAN_800 = '#155e75';
public const CYAN_900 = '#164e63';
public const SKY_50 = '#f0f9ff';
public const SKY_100 = '#e0f2fe';
public const SKY_200 = '#bae6fd';
public const SKY_300 = '#7dd3fc';
public const SKY_400 = '#38bdf8';
public const SKY_500 = '#0ea5e9';
public const SKY_600 = '#0284c7';
public const SKY_700 = '#0369a1';
public const SKY_800 = '#075985';
public const SKY_900 = '#0c4a6e';
public const BLUE = 'blue';
public const BRIGHTBLUE = 'bright-blue';
public const BLUE_50 = '#eff6ff';
public const BLUE_100 = '#dbeafe';
public const BLUE_200 = '#bfdbfe';
public const BLUE_300 = '#93c5fd';
public const BLUE_400 = '#60a5fa';
public const BLUE_500 = '#3b82f6';
public const BLUE_600 = '#2563eb';
public const BLUE_700 = '#1d4ed8';
public const BLUE_800 = '#1e40af';
public const BLUE_900 = '#1e3a8a';
public const INDIGO_50 = '#eef2ff';
public const INDIGO_100 = '#e0e7ff';
public const INDIGO_200 = '#c7d2fe';
public const INDIGO_300 = '#a5b4fc';
public const INDIGO_400 = '#818cf8';
public const INDIGO_500 = '#6366f1';
public const INDIGO_600 = '#4f46e5';
public const INDIGO_700 = '#4338ca';
public const INDIGO_800 = '#3730a3';
public const INDIGO_900 = '#312e81';
public const VIOLET_50 = '#f5f3ff';
public const VIOLET_100 = '#ede9fe';
public const VIOLET_200 = '#ddd6fe';
public const VIOLET_300 = '#c4b5fd';
public const VIOLET_400 = '#a78bfa';
public const VIOLET_500 = '#8b5cf6';
public const VIOLET_600 = '#7c3aed';
public const VIOLET_700 = '#6d28d9';
public const VIOLET_800 = '#5b21b6';
public const VIOLET_900 = '#4c1d95';
public const PURPLE_50 = '#faf5ff';
public const PURPLE_100 = '#f3e8ff';
public const PURPLE_200 = '#e9d5ff';
public const PURPLE_300 = '#d8b4fe';
public const PURPLE_400 = '#c084fc';
public const PURPLE_500 = '#a855f7';
public const PURPLE_600 = '#9333ea';
public const PURPLE_700 = '#7e22ce';
public const PURPLE_800 = '#6b21a8';
public const PURPLE_900 = '#581c87';
public const FUCHSIA_50 = '#fdf4ff';
public const FUCHSIA_100 = '#fae8ff';
public const FUCHSIA_200 = '#f5d0fe';
public const FUCHSIA_300 = '#f0abfc';
public const FUCHSIA_400 = '#e879f9';
public const FUCHSIA_500 = '#d946ef';
public const FUCHSIA_600 = '#c026d3';
public const FUCHSIA_700 = '#a21caf';
public const FUCHSIA_800 = '#86198f';
public const FUCHSIA_900 = '#701a75';
public const PINK_50 = '#fdf2f8';
public const PINK_100 = '#fce7f3';
public const PINK_200 = '#fbcfe8';
public const PINK_300 = '#f9a8d4';
public const PINK_400 = '#f472b6';
public const PINK_500 = '#ec4899';
public const PINK_600 = '#db2777';
public const PINK_700 = '#be185d';
public const PINK_800 = '#9d174d';
public const PINK_900 = '#831843';
public const ROSE_50 = '#fff1f2';
public const ROSE_100 = '#ffe4e6';
public const ROSE_200 = '#fecdd3';
public const ROSE_300 = '#fda4af';
public const ROSE_400 = '#fb7185';
public const ROSE_500 = '#f43f5e';
public const ROSE_600 = '#e11d48';
public const ROSE_700 = '#be123c';
public const ROSE_800 = '#9f1239';
public const ROSE_900 = '#881337';
public const MAGENTA = 'magenta';
public const BRIGHTMAGENTA = 'bright-magenta';
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Termwind\Exceptions;
use InvalidArgumentException;
/**
* @internal
*/
final class ColorNotFound extends InvalidArgumentException
{
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Termwind\Exceptions;
use InvalidArgumentException;
/**
* @internal
*/
final class InvalidChild extends InvalidArgumentException
{
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Termwind\Exceptions;
use InvalidArgumentException;
/**
* @internal
*/
final class InvalidColor extends InvalidArgumentException
{
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Termwind\Exceptions;
use InvalidArgumentException;
/**
* @internal
*/
final class InvalidStyle extends InvalidArgumentException
{
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Termwind\Exceptions;
use InvalidArgumentException;
/**
* @internal
*/
final class StyleNotFound extends InvalidArgumentException
{
/**
* Creates a new style not found instance.
*/
private function __construct(string $message)
{
parent::__construct($message, 0, $this->getPrevious());
}
/**
* Creates a new style not found instance from the given style.
*/
public static function fromStyle(string $style): self
{
return new self(sprintf('Style [%s] not found.', $style));
}
}

View File

@ -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|null $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);
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Termwind\Helpers;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
/**
* @internal
*/
final class QuestionHelper extends SymfonyQuestionHelper
{
/**
* {@inheritdoc}
*/
protected function writePrompt(OutputInterface $output, Question $question): void
{
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
$output->write($text);
}
}

View File

@ -0,0 +1,282 @@
<?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]);
}
}

View File

@ -0,0 +1,218 @@
<?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];
}
}

View File

@ -0,0 +1,46 @@
<?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)
);
}
}

View File

@ -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());
}
}

View File

@ -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),
};
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Termwind\Laravel;
use Illuminate\Console\OutputStyle;
use Illuminate\Support\ServiceProvider;
use Termwind\Termwind;
final class TermwindServiceProvider extends ServiceProvider
{
/**
* Sets the correct renderer to be used.
*/
public function register(): void
{
$this->app->resolving(OutputStyle::class, function ($style): void {
Termwind::renderUsing($style->getOutput());
});
}
}

View File

@ -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,
);
}
}

View File

@ -0,0 +1,59 @@
<?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
* @return Style
*/
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];
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,205 @@
<?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 static($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 static($node);
if ($node->isEmpty()) {
$node = $node->node;
continue;
}
if (! $node->isComment()) {
return $node;
}
$node = $node->node;
}
return is_null($node) ? null : new static($node);
}
/**
* Gets the next sibling from the node.
*/
public function getNextSibling(): static|null
{
$node = $this->node;
while ($node = $node->nextSibling) {
$node = new static($node);
if ($node->isEmpty()) {
$node = $node->node;
continue;
}
if (! $node->isComment()) {
return $node;
}
$node = $node->node;
}
return is_null($node) ? null : new static($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;
}
}

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff