<?php
declare(strict_types=1);
namespace Termwind\ValueObjects;
use Closure;
use Termwind\Actions\StyleToMethod;
use Termwind\Components\Element;
use Termwind\Components\Hr;
use Termwind\Components\Li;
use Termwind\Components\Ol;
use Termwind\Components\Ul;
use Termwind\Enums\Color;
use Termwind\Exceptions\ColorNotFound;
use Termwind\Exceptions\InvalidStyle;
use Termwind\Repositories\Styles as StyleRepository;
use function Termwind\terminal;
/**
* @internal
*/
final class Styles
{
/**
* Finds all the styling on a string.
*/
public const STYLING_REGEX = "/\<[\w=#\/\;,:.&,%?-]+\>|\\e\[\d+m/";
/** @var array<int, string> */
private array $styles = [];
private ?Element $element = null;
/**
* Creates a Style formatter instance.
*
* @param array<string, mixed> $properties
* @param array<string, Closure(string, array<string, string|int>, array<string, int[]>): string> $textModifiers
* @param array<string, Closure(string, array<string, string|int>): string> $styleModifiers
* @param string[] $defaultStyles
*/
final public function __construct(
private array $properties = [
'colors' => [],
'options' => [],
'isFirstChild' => false,
],
private array $textModifiers = [],
private array $styleModifiers = [],
private array $defaultStyles = []
) {
}
/**
* @param Element $element
* @return $this
*/
public function setElement(Element $element): self
{
$this->element = $element;
return $this;
}
/**
* Gets default styles.
*
* @return string[]
*/
public function defaultStyles(): array
{
return $this->defaultStyles;
}
/**
* Gets the element's style properties.
*
* @return array<string, mixed>
*/
final public function getProperties(): array
{
return $this->properties;
}
/**
* Sets the element's style properties.
*
* @param array<string, mixed> $properties
*/
public function setProperties(array $properties): self
{
$this->properties = $properties;
return $this;
}
/**
* Sets the styles to the element.
*/
final public function setStyle(string $style): self
{
$this->styles = array_unique(array_merge($this->styles, [$style]));
return $this;
}
/**
* Checks if the element has the style.
*/
final public function hasStyle(string $style): bool
{
return in_array($style, $this->styles, true);
}
/**
* Adds a style to the element.
*/
final public function addStyle(string $style): self
{
return StyleToMethod::multiple($this, $style);
}
/**
* Inherit styles from given Styles object.
*/
final public function inheritFromStyles(self $styles): self
{
foreach (['ml', 'mr', 'pl', 'pr', 'width', 'minWidth', 'maxWidth', 'spaceY', 'spaceX'] as $style) {
$this->properties['parentStyles'][$style] = array_merge(
$this->properties['parentStyles'][$style] ?? [],
$styles->properties['parentStyles'][$style] ?? []
);
$this->properties['parentStyles'][$style][] = $styles->properties['styles'][$style] ?? 0;
}
$this->properties['parentStyles']['justifyContent'] = $styles->properties['styles']['justifyContent'] ?? false;
foreach (['bg', 'fg'] as $colorType) {
$value = (array) ($this->properties['colors'][$colorType] ?? []);
$parentValue = (array) ($styles->properties['colors'][$colorType] ?? []);
if ($value === [] && $parentValue !== []) {
$this->properties['colors'][$colorType] = $styles->properties['colors'][$colorType];
}
}
if (! is_null($this->properties['options']['bold'] ?? null) ||
! is_null($styles->properties['options']['bold'] ?? null)) {
$this->properties['options']['bold'] = $this->properties['options']['bold']
?? $styles->properties['options']['bold']
?? false;
}
return $this;
}
/**
* Adds a background color to the element.
*/
final public function bg(string $color, int $variant = 0): self
{
return $this->with(['colors' => [
'bg' => $this->getColorVariant($color, $variant),
]]);
}
/**
* Adds a bold style to the element.
*/
final public function fontBold(): self
{
return $this->with(['options' => [
'bold' => true,
]]);
}
/**
* Removes the bold style on the element.
*/
final public function fontNormal(): self
{
return $this->with(['options' => [
'bold' => false,
]]);
}
/**
* Adds a bold style to the element.
*/
final public function strong(): self
{
$this->styleModifiers[__METHOD__] = static fn ($text): string => sprintf("\e[1m%s\e[0m", $text);
return $this;
}
/**
* Adds an italic style to the element.
*/
final public function italic(): self
{
$this->styleModifiers[__METHOD__] = static fn ($text): string => sprintf("\e[3m%s\e[0m", $text);
return $this;
}
/**
* Adds an underline style.
*/
final public function underline(): self
{
$this->styleModifiers[__METHOD__] = static fn ($text): string => sprintf("\e[4m%s\e[0m", $text);
return $this;
}
/**
* Adds the given margin left to the element.
*/
final public function ml(int $margin): self
{
return $this->with(['styles' => [
'ml' => $margin,
]]);
}
/**
* Adds the given margin right to the element.
*/
final public function mr(int $margin): self
{
return $this->with(['styles' => [
'mr' => $margin,
]]);
}
/**
* Adds the given margin bottom to the element.
*/
final public function mb(int $margin): self
{
return $this->with(['styles' => [
'mb' => $margin,
]]);
}
/**
* Adds the given margin top to the element.
*/
final public function mt(int $margin): self
{
return $this->with(['styles' => [
'mt' => $margin,
]]);
}
/**
* Adds the given horizontal margin to the element.
*/
final public function mx(int $margin): self
{
return $this->with(['styles' => [
'ml' => $margin,
'mr' => $margin,
]]);
}
/**
* Adds the given vertical margin to the element.
*/
final public function my(int $margin): self
{
return $this->with(['styles' => [
'mt' => $margin,
'mb' => $margin,
]]);
}
/**
* Adds the given margin to the element.
*/
final public function m(int $margin): self
{
return $this->my($margin)->mx($margin);
}
/**
* Adds the given padding left to the element.
*/
final public function pl(int $padding): static
{
return $this->with(['styles' => [
'pl' => $padding,
]]);
}
/**
* Adds the given padding right.
*/
final public function pr(int $padding): static
{
return $this->with(['styles' => [
'pr' => $padding,
]]);
}
/**
* Adds the given horizontal padding.
*/
final public function px(int $padding): self
{
return $this->pl($padding)->pr($padding);
}
/**
* Adds the given padding top.
*/
final public function pt(int $padding): static
{
return $this->with(['styles' => [
'pt' => $padding,
]]);
}
/**
* Adds the given padding bottom.
*/
final public function pb(int $padding): static
{
return $this->with(['styles' => [
'pb' => $padding,
]]);
}
/**
* Adds the given vertical padding.
*/
final public function py(int $padding): self
{
return $this->pt($padding)->pb($padding);
}
/**
* Adds the given padding.
*/
final public function p(int $padding): self
{
return $this->pt($padding)->pr($padding)->pb($padding)->pl($padding);
}
/**
* Adds the given vertical margin to the childs, ignoring the first child.
*/
final public function spaceY(int $space): self
{
return $this->with(['styles' => [
'spaceY' => $space,
]]);
}
/**
* Adds the given horizontal margin to the childs, ignoring the first child.
*/
final public function spaceX(int $space): self
{
return $this->with(['styles' => [
'spaceX' => $space,
]]);
}
/**
* Adds a border on top of each element.
*/
final public function borderT(int $width = 1): self
{
if (! $this->element instanceof Hr) {
throw new InvalidStyle('`border-t` can only be used on an "hr" element.');
}
$this->styleModifiers[__METHOD__] = function ($text, $styles): string {
$length = $this->getLength($text);
if ($length < 1) {
$margins = (int) ($styles['ml'] ?? 0) + ($styles['mr'] ?? 0);
return str_repeat('─', self::getParentWidth($this->properties['parentStyles'] ?? []) - $margins);
}
return str_repeat('─', $length);
};
return $this;
}
/**
* Adds a text alignment or color to the element.
*/
final public function text(string $value, int $variant = 0): self
{
if (in_array($value, ['left', 'right', 'center'], true)) {
return $this->with(['styles' => [
'text-align' => $value,
]]);
}
return $this->with(['colors' => [
'fg' => $this->getColorVariant($value, $variant),
]]);
}
/**
* Truncates the text of the element.
*/
final public function truncate(int $limit = 0, string $end = '…'): self
{
$this->textModifiers[__METHOD__] = function ($text, $styles) use ($limit, $end): string {
$width = $styles['width'] ?? 0;
if (is_string($width)) {
$width = self::calcWidthFromFraction(
$width,
$styles,
$this->properties['parentStyles'] ?? []
);
}
[, $paddingRight, , $paddingLeft] = $this->getPaddings();
$width -= $paddingRight + $paddingLeft;
$limit = $limit > 0 ? $limit : $width;
if ($limit === 0) {
return $text;
}
$limit -= mb_strwidth($end, 'UTF-8');
if ($this->getLength($text) <= $limit) {
return $text;
}
return rtrim(self::trimText($text, $limit).$end);
};
return $this;
}
/**
* Forces the width of the element.
*/
final public function w(int|string $width): static
{
return $this->with(['styles' => [
'width' => $width,
]]);
}
/**
* Forces the element width to the full width of the terminal.
*/
final public function wFull(): static
{
return $this->w('1/1');
}
/**
* Removes the width set on the element.
*/
final public function wAuto(): static
{
return $this->with(['styles' => [
'width' => null,
]]);
}
/**
* Defines a minimum width of an element.
*/
final public function minW(int|string $width): static
{
return $this->with(['styles' => [
'minWidth' => $width,
]]);
}
/**
* Defines a maximum width of an element.
*/
final public function maxW(int|string $width): static
{
return $this->with(['styles' => [
'maxWidth' => $width,
]]);
}
/**
* Makes the element's content uppercase.
*/
final public function uppercase(): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => mb_strtoupper($text, 'UTF-8');
return $this;
}
/**
* Makes the element's content lowercase.
*/
final public function lowercase(): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => mb_strtolower($text, 'UTF-8');
return $this;
}
/**
* Makes the element's content capitalize.
*/
final public function capitalize(): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => mb_convert_case($text, MB_CASE_TITLE, 'UTF-8');
return $this;
}
/**
* Makes the element's content in snakecase.
*/
final public function snakecase(): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => mb_strtolower(
(string) preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $text),
'UTF-8'
);
return $this;
}
/**
* Makes the element's content with a line through.
*/
final public function lineThrough(): self
{
$this->styleModifiers[__METHOD__] = static fn ($text): string => sprintf("\e[9m%s\e[0m", $text);
return $this;
}
/**
* Makes the element's content invisible.
*/
final public function invisible(): self
{
$this->styleModifiers[__METHOD__] = static fn ($text): string => sprintf("\e[8m%s\e[0m", $text);
return $this;
}
/**
* Do not display element's content.
*/
final public function hidden(): self
{
return $this->with(['styles' => [
'display' => 'hidden',
]]);
}
/**
* Makes a line break before the element's content.
*/
final public function block(): self
{
return $this->with(['styles' => [
'display' => 'block',
]]);
}
/**
* Makes an element eligible to work with flex-1 element's style.
*/
final public function flex(): self
{
return $this->with(['styles' => [
'display' => 'flex',
]]);
}
/**
* Makes an element grow and shrink as needed, ignoring the initial size.
*/
final public function flex1(): self
{
return $this->with(['styles' => [
'flex-1' => true,
]]);
}
/**
* Justifies childs along the element with an equal amount of space between.
*/
final public function justifyBetween(): self
{
return $this->with(['styles' => [
'justifyContent' => 'between',
]]);
}
/**
* Justifies childs along the element with an equal amount of space between
* each item and half around.
*/
final public function justifyAround(): self
{
return $this->with(['styles' => [
'justifyContent' => 'around',
]]);
}
/**
* Justifies childs along the element with an equal amount of space around each item.
*/
final public function justifyEvenly(): self
{
return $this->with(['styles' => [
'justifyContent' => 'evenly',
]]);
}
/**
* Justifies childs along the center of the container’s main axis.
*/
final public function justifyCenter(): self
{
return $this->with(['styles' => [
'justifyContent' => 'center',
]]);
}
/**
* Repeats the string given until it fills all the content.
*/
final public function contentRepeat(string $string): self
{
$string = preg_replace("/\[?'?([^'|\]]+)'?\]?/", '$1', $string) ?? '';
$this->textModifiers[__METHOD__] = static fn (): string => str_repeat($string, (int) floor(terminal()->width() / mb_strlen($string, 'UTF-8')));
return $this->with(['styles' => [
'contentRepeat' => true,
]]);
}
/**
* Prepends text to the content.
*/
final public function prepend(string $string): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => $string.$text;
return $this;
}
/**
* Appends text to the content.
*/
final public function append(string $string): self
{
$this->textModifiers[__METHOD__] = static fn ($text): string => $text.$string;
return $this;
}
/**
* Prepends the list style type to the content.
*/
final public function list(string $type, int $index = 0): self
{
if (! $this->element instanceof Ul && ! $this->element instanceof Ol && ! $this->element instanceof Li) {
throw new InvalidStyle(sprintf(
'Style list-none cannot be used with %s',
$this->element !== null ? $this->element::class : 'unknown element'
));
}
if (! $this->element instanceof Li) {
return $this;
}
return match ($type) {
'square' => $this->prepend('▪ '),
'disc' => $this->prepend('• '),
'decimal' => $this->prepend(sprintf('%d. ', $index)),
default => $this,
};
}
/**
* Adds the given properties to the element.
*
* @param array<string, mixed> $properties
*/
public function with(array $properties): self
{
$this->properties = array_replace_recursive($this->properties, $properties);
return $this;
}
/**
* Sets the href property to the element.
*/
final public function href(string $href): self
{
$href = str_replace('%', '%%', $href);
return $this->with(['href' => array_filter([$href])]);
}
/**
* Formats a given string.
*/
final public function format(string $content): string
{
foreach ($this->textModifiers as $modifier) {
$content = $modifier(
$content,
$this->properties['styles'] ?? [],
$this->properties['parentStyles'] ?? []
);
}
$content = $this->applyWidth($content);
foreach ($this->styleModifiers as $modifier) {
$content = $modifier($content, $this->properties['styles'] ?? []);
}
return $this->applyStyling($content);
}
/**
* Get the format string including required styles.
*/
private function getFormatString(): string
{
$styles = [];
/** @var array<int, string> $href */
$href = $this->properties['href'] ?? [];
if ($href !== []) {
$styles[] = sprintf('href=%s', array_pop($href));
}
$colors = $this->properties['colors'] ?? [];
foreach ($colors as $option => $content) {
if (in_array($option, ['fg', 'bg'], true)) {
$content = is_array($content) ? array_pop($content) : $content;
$styles[] = "$option=$content";
}
}
$options = $this->properties['options'] ?? [];
if ($options !== []) {
$options = array_keys(array_filter(
$options, fn ($option) => $option === true
));
$styles[] = count($options) > 0
? 'options='.implode(',', $options)
: 'options=,';
}
// If there are no styles we don't need extra tags
if ($styles === []) {
return '%s%s%s%s%s';
}
return '%s<'.implode(';', $styles).'>%s%s%s</>%s';
}
/**
* Get the margins applied to the element.
*
* @return array{0: int, 1: int, 2: int, 3: int}
*/
private function getMargins(): array
{
$isFirstChild = (bool) $this->properties['isFirstChild'];
$spaceY = $this->properties['parentStyles']['spaceY'] ?? [];
$spaceY = ! $isFirstChild ? end($spaceY) : 0;
$spaceX = $this->properties['parentStyles']['spaceX'] ?? [];
$spaceX = ! $isFirstChild ? end($spaceX) : 0;
return [
$spaceY > 0 ? $spaceY : $this->properties['styles']['mt'] ?? 0,
$this->properties['styles']['mr'] ?? 0,
$this->properties['styles']['mb'] ?? 0,
$spaceX > 0 ? $spaceX : $this->properties['styles']['ml'] ?? 0,
];
}
/**
* Get the paddings applied to the element.
*
* @return array{0: int, 1: int, 2: int, 3: int}
*/
private function getPaddings(): array
{
return [
$this->properties['styles']['pt'] ?? 0,
$this->properties['styles']['pr'] ?? 0,
$this->properties['styles']['pb'] ?? 0,
$this->properties['styles']['pl'] ?? 0,
];
}
/**
* It applies the correct width for the content.
*/
private function applyWidth(string $content): string
{
$styles = $this->properties['styles'] ?? [];
$minWidth = $styles['minWidth'] ?? -1;
$width = max($styles['width'] ?? -1, $minWidth);
$maxWidth = $styles['maxWidth'] ?? 0;
if ($width < 0) {
return $content;
}
if ($width === 0) {
return '';
}
if (is_string($width)) {
$width = self::calcWidthFromFraction(
$width,
$styles,
$this->properties['parentStyles'] ?? []
);
}
if ($maxWidth > 0) {
$width = min($styles['maxWidth'], $width);
}
$width -= ($styles['pl'] ?? 0) + ($styles['pr'] ?? 0);
$length = $this->getLength($content);
preg_match_all("/\n+/", $content, $matches);
$width *= count($matches[0] ?? []) + 1;
$width += mb_strlen($matches[0][0] ?? '', 'UTF-8');
if ($length <= $width) {
$space = $width - $length;
return match ($styles['text-align'] ?? '') {
'right' => str_repeat(' ', $space).$content,
'center' => str_repeat(' ', (int) floor($space / 2)).$content.str_repeat(' ', (int) ceil($space / 2)),
default => $content.str_repeat(' ', $space),
};
}
return self::trimText($content, $width);
}
/**
* It applies the styling for the content.
*/
private function applyStyling(string $content): string
{
$display = $this->properties['styles']['display'] ?? 'inline';
if ($display === 'hidden') {
return '';
}
$isFirstChild = (bool) $this->properties['isFirstChild'];
[$marginTop, $marginRight, $marginBottom, $marginLeft] = $this->getMargins();
[$paddingTop, $paddingRight, $paddingBottom, $paddingLeft] = $this->getPaddings();
$content = (string) preg_replace('/\r[ \t]?/', "\n",
(string) preg_replace(
'/\n/',
str_repeat(' ', $marginRight + $paddingRight)
."\n".
str_repeat(' ', $marginLeft + $paddingLeft),
$content)
);
$formatted = sprintf(
$this->getFormatString(),
str_repeat(' ', $marginLeft),
str_repeat(' ', $paddingLeft),
$content,
str_repeat(' ', $paddingRight),
str_repeat(' ', $marginRight),
);
$empty = str_replace(
$content,
str_repeat(' ', $this->getLength($content)),
$formatted
);
$items = [];
if (in_array($display, ['block', 'flex'], true) && ! $isFirstChild) {
$items[] = "\n";
}
if ($marginTop > 0) {
$items[] = str_repeat("\n", $marginTop);
}
if ($paddingTop > 0) {
$items[] = $empty."\n";
}
$items[] = $formatted;
if ($paddingBottom > 0) {
$items[] = "\n".$empty;
}
if ($marginBottom > 0) {
$items[] = str_repeat("\n", $marginBottom);
}
return implode('', $items);
}
/**
* Get the length of the text provided without the styling tags.
*/
public function getLength(string $text = null): int
{
return mb_strlen(preg_replace(
self::STYLING_REGEX,
'',
$text ?? $this->element?->toString() ?? ''
) ?? '', 'UTF-8');
}
/**
* Get the length of the element without margins.
*/
public function getInnerWidth(): int
{
$innerLength = $this->getLength();
[, $marginRight, , $marginLeft] = $this->getMargins();
return $innerLength - $marginLeft - $marginRight;
}
/**
* Get the constant variant color from Color class.
*/
private function getColorVariant(string $color, int $variant): string
{
if ($variant > 0) {
$color .= '-'.$variant;
}
if (StyleRepository::has($color)) {
return StyleRepository::get($color)->getColor();
}
$colorConstant = mb_strtoupper(str_replace('-', '_', $color), 'UTF-8');
if (! defined(Color::class."::$colorConstant")) {
throw new ColorNotFound($colorConstant);
}
return constant(Color::class."::$colorConstant");
}
/**
* Calculates the width based on the fraction provided.
*
* @param array<string, int> $styles
* @param array<string, array<int, int|string>> $parentStyles
*/
private static function calcWidthFromFraction(string $fraction, array $styles, array $parentStyles): int
{
$width = self::getParentWidth($parentStyles);
preg_match('/(\d+)\/(\d+)/', $fraction, $matches);
if (count($matches) !== 3 || $matches[2] === '0') {
throw new InvalidStyle(sprintf('Style [%s] is invalid.', "w-$fraction"));
}
/** @@phpstan-ignore-next-line */
$width = (int) floor($width * $matches[1] / $matches[2]);
$width -= ($styles['ml'] ?? 0) + ($styles['mr'] ?? 0);
return $width;
}
/**
* Gets the width of the parent element.
*
* @param array<string, array<int|string>> $styles
*/
public static function getParentWidth(array $styles): int
{
$width = terminal()->width();
foreach ($styles['width'] ?? [] as $index => $parentWidth) {
$minWidth = (int) $styles['minWidth'][$index];
$maxWidth = (int) $styles['maxWidth'][$index];
$margins = (int) $styles['ml'][$index] + (int) $styles['mr'][$index];
$parentWidth = max($parentWidth, $minWidth);
if ($parentWidth < 1) {
$parentWidth = $width;
} elseif (is_int($parentWidth)) {
$parentWidth += $margins;
}
preg_match('/(\d+)\/(\d+)/', (string) $parentWidth, $matches);
$width = count($matches) !== 3
? (int) $parentWidth
: (int) floor($width * $matches[1] / $matches[2]); //@phpstan-ignore-line
if ($maxWidth > 0) {
$width = min($maxWidth, $width);
}
$width -= $margins;
$width -= (int) $styles['pl'][$index] + (int) $styles['pr'][$index];
}
return $width;
}
/**
* It trims the text properly ignoring all escape codes and
* `<bg;fg;options>` tags.
*/
private static function trimText(string $text, int $width): string
{
preg_match_all(self::STYLING_REGEX, $text, $matches, PREG_OFFSET_CAPTURE);
$text = rtrim(mb_strimwidth(preg_replace(self::STYLING_REGEX, '', $text) ?? '', 0, $width, '', 'UTF-8'));
foreach ($matches[0] ?? [] as [$part, $index]) {
$text = substr($text, 0, $index).$part.substr($text, $index, null);
}
return $text;
}
}
About Section
NFC Pay was founded with a vision to transform the way people handle transactions. Our journey is defined by a commitment to innovation, security, and convenience. We strive to deliver seamless, user-friendly payment solutions that make everyday transactions effortless and secure. Our mission is to empower you to pay with ease and confidence, anytime, anywhere.
FAQ Section
Here are answers to some common questions about NFC Pay. We aim to provide clear and concise information to help you understand how our platform works and how it can benefit you. If you have any further inquiries, please don’t hesitate to contact our support team.
Download the app and sign up using your email or phone number, then complete the verification process.
Yes, we use advanced encryption and security protocols to protect your payment details.
Absolutely, you can link multiple debit or credit cards to your wallet.
Go to the transfer section, select the recipient, enter the amount, and authorize the transfer.
Use the “Forgot PIN” feature in the app to reset it following the provided instructions.
Sign up for a merchant account through the app and follow the setup instructions to start accepting payments.
Yes, you can view and track your payment status in the account dashboard