Файл: vendor/laravel/prompts/src/Themes/Default/Concerns/InteractsWithStrings.php
Строк: 237
<?php
namespace LaravelPromptsThemesDefaultConcerns;
trait InteractsWithStrings
{
/**
* Get the length of the longest line.
*
* @param array<string> $lines
*/
protected function longest(array $lines, int $padding = 0): int
{
return max(
$this->minWidth,
count($lines) > 0 ? max(array_map(fn ($line) => mb_strwidth($this->stripEscapeSequences($line)) + $padding, $lines)) : null
);
}
/**
* Pad text ignoring ANSI escape sequences.
*/
protected function pad(string $text, int $length, string $char = ' '): string
{
$rightPadding = str_repeat($char, max(0, $length - mb_strwidth($this->stripEscapeSequences($text))));
return "{$text}{$rightPadding}";
}
/**
* Strip ANSI escape sequences from the given text.
*/
protected function stripEscapeSequences(string $text): string
{
// Strip ANSI escape sequences.
$text = preg_replace("/e[^m]*m/", '', $text);
// Strip Symfony named style tags.
$text = preg_replace("/<(info|comment|question|error)>(.*?)</\1>/", '$2', $text);
// Strip Symfony inline style tags.
return preg_replace("/<(?:(?:[fb]g|options)=[a-z,;]+)+>(.*?)</>/i", '$1', $text);
}
/**
* Multi-byte version of wordwrap.
*
* @param non-empty-string $break
*/
protected function mbWordwrap(
string $string,
int $width = 75,
string $break = "n",
bool $cut_long_words = false
): string {
$lines = explode($break, $string);
$result = [];
foreach ($lines as $originalLine) {
if (mb_strwidth($originalLine) <= $width) {
$result[] = $originalLine;
continue;
}
$words = explode(' ', $originalLine);
$line = null;
$lineWidth = 0;
if ($cut_long_words) {
foreach ($words as $index => $word) {
$characters = mb_str_split($word);
$strings = [];
$str = '';
foreach ($characters as $character) {
$tmp = $str.$character;
if (mb_strwidth($tmp) > $width) {
$strings[] = $str;
$str = $character;
} else {
$str = $tmp;
}
}
if ($str !== '') {
$strings[] = $str;
}
$words[$index] = implode(' ', $strings);
}
$words = explode(' ', implode(' ', $words));
}
foreach ($words as $word) {
$tmp = ($line === null) ? $word : $line.' '.$word;
// Look for zero-width joiner characters (combined emojis)
preg_match('/p{Cf}/u', $word, $joinerMatches);
$wordWidth = count($joinerMatches) > 0 ? 2 : mb_strwidth($word);
$lineWidth += $wordWidth;
if ($line !== null) {
// Space between words
$lineWidth += 1;
}
if ($lineWidth <= $width) {
$line = $tmp;
} else {
$result[] = $line;
$line = $word;
$lineWidth = $wordWidth;
}
}
if ($line !== '') {
$result[] = $line;
}
$line = null;
}
return implode($break, $result);
}
/**
* Word wrap text while preserving ANSI escape sequences.
*
* @return array<int, string>
*/
protected function ansiWordwrap(string $text, int $width): array
{
// Parse segments and build character array with codes
$segments = $this->parseAnsiText($text);
$plainText = $this->stripEscapeSequences($text);
$chars = [];
foreach ($segments as $segment) {
$segmentChars = mb_str_split($segment['text']);
foreach ($segmentChars as $char) {
$chars[] = ['char' => $char, 'codes' => $segment['codes']];
}
}
// Word wrap the plain text
$wrappedLines = $this->mbWordwrap($plainText, $width, "n", false);
$plainLines = explode("n", $wrappedLines);
// Rebuild each wrapped line with ANSI codes
$result = [];
$charIndex = 0;
foreach ($plainLines as $plainLine) {
$line = '';
$lastCodes = '';
$lineChars = mb_str_split($plainLine);
foreach ($lineChars as $lineChar) {
// Find matching character in original (handling spaces removed by wordwrap)
while ($charIndex < count($chars) && $chars[$charIndex]['char'] !== $lineChar) {
// Skip spaces that wordwrap removed
if ($chars[$charIndex]['char'] === ' ') {
$charIndex++;
} else {
break;
}
}
if ($charIndex < count($chars)) {
$codes = $chars[$charIndex]['codes'];
if ($codes !== $lastCodes) {
if ($lastCodes !== '') {
$line .= "e[0m";
}
if ($codes !== '') {
$line .= $codes;
}
$lastCodes = $codes;
}
$line .= $lineChar;
$charIndex++;
} else {
$line .= $lineChar;
}
}
// Close any open ANSI codes
if ($lastCodes !== '' && ! str_ends_with($line, "e[0m")) {
$line .= "e[0m";
}
$result[] = $line;
}
return $result;
}
/**
* Parse text into segments with their associated ANSI codes.
*
* @return array<int, array{text: string, codes: string}>
*/
protected function parseAnsiText(string $text): array
{
$segments = [];
$currentCodes = '';
$currentText = '';
$i = 0;
$textLength = strlen($text);
while ($i < $textLength) {
if ($text[$i] === "e" && ($i + 1 < $textLength) && $text[$i + 1] === '[') {
// Save current segment if it has text
if ($currentText !== '') {
$segments[] = ['text' => $currentText, 'codes' => $currentCodes];
$currentText = '';
}
// Extract ANSI escape sequence
$escapeSequence = '';
while ($i < $textLength) {
$escapeSequence .= $text[$i];
$i++;
if (preg_match('/^\e\[[0-9;]*m$/', $escapeSequence)) {
// Update current codes
if ($escapeSequence === "e[0m") {
$currentCodes = '';
} else {
$currentCodes = $escapeSequence;
}
break;
}
}
continue;
}
$currentText .= $text[$i];
$i++;
}
// Add final segment
if ($currentText !== '') {
$segments[] = ['text' => $currentText, 'codes' => $currentCodes];
}
return $segments;
}
}