Skip to content

Commit

Permalink
Merge branch 'main' into pr/2
Browse files Browse the repository at this point in the history
  • Loading branch information
inxilpro committed Apr 18, 2024
2 parents 09b73c0 + 780cfa4 commit f26dc56
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 39 deletions.
27 changes: 20 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ format. This project adheres to [Semantic Versioning](https://semver.org/spec/v2

## [Unreleased]

## [0.0.1]
## [0.0.1] - 2024-04-17

### Added

- Added support for `length` (number of chars) and `width` (multibyte width of chars)
- Added support for `prepend` and `append`
- Added support for `startsWith` and `endsWith`
- Added support for `padLeft`, `padRight`, and `padBoth`
- Added support for `wordwrap`
- Added support for `explode`
- Added support for `is` (for comparing two ansi strings)

# Keep a Changelog Syntax

- `Added` for new features.
- `Changed` for changes in existing functionality.
- `Deprecated` for soon-to-be removed features.
- `Removed` for now removed features.
- `Fixed` for any bug fixes.
- `Security` in case of vulnerabilities.
- `Added` for new features.
- `Changed` for changes in existing functionality.
- `Deprecated` for soon-to-be removed features.
- `Removed` for now removed features.
- `Fixed` for any bug fixes.
- `Security` in case of vulnerabilities.

[Unreleased]: https://github.com/glhd/ansipants/compare/0.0.1...HEAD

[0.0.1]: https://github.com/glhd/ansipants/compare/0.0.1...0.0.1

[0.0.1]: https://github.com/glhd/ansipants/compare/0.0.1...0.0.1
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,29 @@
</a>
</div>

# AnsiPants 👖💫
# ANSI Pants 👖💫

## Installation

```shell
composer require glhd/ansipants
```

## Usage

You can instantiate a new ANSI string using the `ansi()` helper, with `new AnsiString()`,
or with `AnsiString::make()`. All string manipulation functions can be chained, just like
the Laravel `Stringable` class. Where appropriate, you can pass an additional `ignore_style: true`
argument into a function to make that function ignore the ANSI styles that are applied
(like color or font style).

An example:

```php
ansi("\e[1mHello💥 \e[3mwo\e[0mrld")
->append(" 🥸🥸🥸")
->padLeft(100)
->wordWrap();
->wordwrap();
```

## Resources
Expand Down
17 changes: 17 additions & 0 deletions src/AnsiChar.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,30 @@

class AnsiChar implements Stringable
{
protected ?int $width = null;

public function __construct(
public string $value,
/** @var \Glhd\AnsiPants\Flag[] */
public array $flags = [],
) {
}

public function width(): int
{
return $this->width ??= mb_strwidth($this->value);
}

public function is(AnsiChar $other, bool $ignore_style = false): bool
{
return $this->value === $other->value && ($ignore_style || $this->flags === $other->flags);
}

public function isNot(AnsiChar $other, bool $ignore_style = false): bool
{
return $this->value !== $other->value || (! $ignore_style && $this->flags !== $other->flags);
}

public function withFlags(Flag ...$flags): static
{
return new static($this->value, $flags);
Expand Down
200 changes: 178 additions & 22 deletions src/AnsiString.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ public static function make(AnsiString|Collection|string $input): static
public function __construct(AnsiString|AnsiChar|Collection|string $input)
{
if ($input instanceof AnsiChar) {
$input = new Collection([$input]);
}

if ($input instanceof Collection) {
$this->chars = new Collection([clone $input]);
} elseif ($input instanceof Collection) {
$input->ensure(AnsiChar::class);
$this->chars = clone $input;
} elseif ($input instanceof AnsiString) {
Expand All @@ -37,17 +35,47 @@ public function __construct(AnsiString|AnsiChar|Collection|string $input)

public function withFlags(Flag ...$flags): static
{
return new static($this->chars->map(fn(AnsiChar $char) => $char->withFlags(...$flags)));
return new static($this->chars->map(fn(AnsiChar $char) => (clone $char)->withFlags(...$flags)));
}

public function prepend(AnsiString|AnsiChar|string $string): static
{
return new AnsiString(AnsiString::make($string)->chars->merge($this->chars));
$prepended = collect();

if ($string instanceof AnsiChar) {
$prepended->push(clone $string);
} else {
$string = new AnsiString($string);
foreach ($string->chars as $char) {
$prepended->push(clone $char);
}
}

foreach ($this->chars as $char) {
$prepended->push(clone $char);
}

return new AnsiString($prepended);
}

public function append(AnsiString|AnsiChar|string $string): static
{
return new AnsiString($this->chars->merge(AnsiString::make($string)->chars));
$appended = collect();

foreach ($this->chars as $char) {
$appended->push(clone $char);
}

if ($string instanceof AnsiChar) {
$appended->push(clone $string);
} else {
$string = new AnsiString($string);
foreach ($string->chars as $char) {
$appended->push(clone $char);
}
}

return new AnsiString($appended);
}

public function padLeft(int $length, AnsiString|string $pad = ' '): static
Expand Down Expand Up @@ -112,11 +140,138 @@ public function wordwrap(int $width = 75, AnsiString|string $break = "\e[0m\n",
return $wrapped;
}

/** @return Collection<int,\Glhd\AnsiPants\AnsiString> */
public function explode(AnsiString|string $break, bool $ignore_style = true): Collection
{
$break = new AnsiString($break);
$results = new Collection();
$buffer = new AnsiString('');
$tail_buffer = new AnsiString('');

foreach ($this->chars as $char) {
$tail_buffer = $tail_buffer->append($char);

// If our partial buffer is the break value, we'll push the current
// buffer to the results, and reset for the next chunk.
if ($tail_buffer->is($break, $ignore_style)) {
$results->push($buffer);
$buffer = new AnsiString('');
$tail_buffer = new AnsiString('');
continue;
}

// If our partial buffer looks like it's the beginning of the break value
// then we'll just continue buffering
if ($break->startsWith($tail_buffer)) {
continue;
}

// Otherwise (if the partial buffer isn't part of the break), just push it
// to the current buffer and reset our partial buffer
$buffer = $buffer->append($tail_buffer);
$tail_buffer = new AnsiString('');
}

// If we have anything remaining on the partial buffer when we get to the
// end of the string, we just need to append it to our current buffer
if ($tail_buffer->length() > 0) {
$buffer = $buffer->append($tail_buffer);
}

// And if there's anything remaining on the buffer once we're done, add
// that to the final results
if ($buffer->length() > 0) {
$results->push($buffer);
}

return $results;
}

public function is(AnsiString|string $other, bool $ignore_style = false): bool
{
$other = new AnsiString($other);
$length = $this->length();

if ($other->length() !== $length || $other->width() !== $this->width()) {
return false;
}

for ($i = 0; $i < $length; $i++) {
if ($this->chars[$i]->isNot($other->chars[$i], $ignore_style)) {
return false;
}
}

return true;
}

public function startsWith(AnsiString|string $needle, bool $ignore_style = false): bool
{
$needle = new AnsiString($needle);

$index = 0;
$last_index = $needle->length() - 1;

while ($index <= $last_index) {
if (
! isset($this->chars[$index])
|| $this->chars[$index]->isNot($needle->chars[$index], $ignore_style)
) {
return false;
}

$index++;
}

return true;
}

public function endsWith(AnsiString|string $needle, bool $ignore_style = false): bool
{
$needle = new AnsiString($needle);

$this_index = $this->length() - 1;
$needle_index = $needle->length() - 1;

while ($needle_index >= 0) {
if (
! isset($this->chars[$this_index])
|| $this->chars[$this_index]->isNot($needle->chars[$needle_index], $ignore_style)
) {
return false;
}

$this_index--;
$needle_index--;
}

return true;
}

public function length(): int
{
return count($this->chars);
}

public function width(): int
{
return $this->chars->sum(fn(AnsiChar $char) => $char->width());
}

public function dump(): static
{
dump($this);

return $this;
}

public function dd(): static
{
dd($this);

return $this;
}

public function __toString(): string
{
$result = '';
Expand All @@ -126,22 +281,23 @@ public function __toString(): string
[$remove, $add] = $this->diffFlags($active_flags, $char->flags);

// If it's simpler, just reset and then add them all
if (count($remove) >= count($char->flags)) {
[$remove, $add] = [[Flag::Reset], $char->flags];
if (count($remove)) {
$result .= Flag::Reset->getEscapeSequence();
// [$remove, $add] = [[], $char->flags];
}

foreach ($remove as $remove_flag) {
// First, check to see if any of the flags we're adding overrides the flag
// we're removing (in which case, we don't need to explicitly remove it).
foreach ($add as $add_flag) {
if ($add_flag->overrides($remove_flag)) {
break;
}
}

// Otherwise, add the inverse sequence
$result .= $remove_flag->getInverseEscapeSequence();
}
// foreach ($remove as $remove_flag) {
// // First, check to see if any of the flags we're adding overrides the flag
// // we're removing (in which case, we don't need to explicitly remove it).
// foreach ($add as $add_flag) {
// if ($add_flag->overrides($remove_flag)) {
// break;
// }
// }
//
// // Otherwise, add the inverse sequence
// $result .= $remove_flag->getInverseEscapeSequence();
// }

foreach ($add as $add_flag) {
$result .= $add_flag->getEscapeSequence();
Expand Down Expand Up @@ -207,7 +363,7 @@ protected function parse(string $input): Collection
if ($token instanceof EscapeSequence) {
$active_flags = $active_flags
->reject(fn(Flag $flag) => $token->flag->overrides($flag))
->push($token->flag);
->when($token->flag->hasStyle(), fn(Collection $active_flags) => $active_flags->push($token->flag));
}

if ($token instanceof Text) {
Expand Down
17 changes: 17 additions & 0 deletions src/Flag.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ public function inverse(): ?Flag
return null;
}

/**
* Determine whether the flag conveys style, or is just a reset of some sort
*
* @return bool
*/
public function hasStyle(): bool
{
return ! in_array($this, [
self::Reset,
self::Normal,
self::NotItalic,
self::NotUnderline,
self::DefaultForeground,
self::DefaultBackground,
]);
}

protected function pairs(): array
{
return [
Expand Down
Loading

0 comments on commit f26dc56

Please sign in to comment.