allow vendord
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
All checks were successful
Build, Push and Deploy / build-and-push (push) Successful in 3m3s
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 16s
Build, Push and Deploy / deploy-staging (push) Successful in 32s
Build, Push and Deploy / deploy-production (push) Has been skipped
Build, Push and Deploy / cleanup (push) Successful in 1s
This commit is contained in:
319
vendor/psy/psysh/src/Formatter/CodeFormatter.php
vendored
Normal file
319
vendor/psy/psysh/src/Formatter/CodeFormatter.php
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Psy\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* A pretty-printer for code.
|
||||
*/
|
||||
class CodeFormatter implements ReflectorFormatter
|
||||
{
|
||||
const LINE_MARKER = ' <urgent>></urgent> ';
|
||||
const NO_LINE_MARKER = ' ';
|
||||
|
||||
const HIGHLIGHT_DEFAULT = 'default';
|
||||
const HIGHLIGHT_KEYWORD = 'keyword';
|
||||
|
||||
const HIGHLIGHT_PUBLIC = 'public';
|
||||
const HIGHLIGHT_PROTECTED = 'protected';
|
||||
const HIGHLIGHT_PRIVATE = 'private';
|
||||
|
||||
const HIGHLIGHT_CONST = 'const';
|
||||
const HIGHLIGHT_NUMBER = 'number';
|
||||
const HIGHLIGHT_STRING = 'string';
|
||||
const HIGHLIGHT_COMMENT = 'code_comment';
|
||||
const HIGHLIGHT_INLINE_HTML = 'inline_html';
|
||||
|
||||
private const TOKEN_MAP = [
|
||||
// Not highlighted
|
||||
\T_OPEN_TAG => self::HIGHLIGHT_DEFAULT,
|
||||
\T_OPEN_TAG_WITH_ECHO => self::HIGHLIGHT_DEFAULT,
|
||||
\T_CLOSE_TAG => self::HIGHLIGHT_DEFAULT,
|
||||
\T_STRING => self::HIGHLIGHT_DEFAULT,
|
||||
\T_VARIABLE => self::HIGHLIGHT_DEFAULT,
|
||||
\T_NS_SEPARATOR => self::HIGHLIGHT_DEFAULT,
|
||||
|
||||
// Visibility
|
||||
\T_PUBLIC => self::HIGHLIGHT_PUBLIC,
|
||||
\T_PROTECTED => self::HIGHLIGHT_PROTECTED,
|
||||
\T_PRIVATE => self::HIGHLIGHT_PRIVATE,
|
||||
|
||||
// Constants
|
||||
\T_DIR => self::HIGHLIGHT_CONST,
|
||||
\T_FILE => self::HIGHLIGHT_CONST,
|
||||
\T_METHOD_C => self::HIGHLIGHT_CONST,
|
||||
\T_NS_C => self::HIGHLIGHT_CONST,
|
||||
\T_LINE => self::HIGHLIGHT_CONST,
|
||||
\T_CLASS_C => self::HIGHLIGHT_CONST,
|
||||
\T_FUNC_C => self::HIGHLIGHT_CONST,
|
||||
\T_TRAIT_C => self::HIGHLIGHT_CONST,
|
||||
|
||||
// Types
|
||||
\T_DNUMBER => self::HIGHLIGHT_NUMBER,
|
||||
\T_LNUMBER => self::HIGHLIGHT_NUMBER,
|
||||
\T_ENCAPSED_AND_WHITESPACE => self::HIGHLIGHT_STRING,
|
||||
\T_CONSTANT_ENCAPSED_STRING => self::HIGHLIGHT_STRING,
|
||||
|
||||
// Comments
|
||||
\T_COMMENT => self::HIGHLIGHT_COMMENT,
|
||||
\T_DOC_COMMENT => self::HIGHLIGHT_COMMENT,
|
||||
|
||||
// @todo something better here?
|
||||
\T_INLINE_HTML => self::HIGHLIGHT_INLINE_HTML,
|
||||
];
|
||||
|
||||
/**
|
||||
* Format the code represented by $reflector for shell output.
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*
|
||||
* @return string formatted code
|
||||
*/
|
||||
public static function format(\Reflector $reflector): string
|
||||
{
|
||||
if (self::isReflectable($reflector)) {
|
||||
// @phan-suppress-next-line PhanUndeclaredMethod - getFileName/getEndLine exist on ReflectionClass/ReflectionFunctionAbstract
|
||||
if ($code = @\file_get_contents($reflector->getFileName())) {
|
||||
// @phan-suppress-next-line PhanUndeclaredMethod - getEndLine exists on ReflectionClass/ReflectionFunctionAbstract
|
||||
return self::formatCode($code, self::getStartLine($reflector), $reflector->getEndLine());
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Source code unavailable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format code for shell output.
|
||||
*
|
||||
* Optionally, restrict by $startLine and $endLine line numbers, or pass $markLine to add a line marker.
|
||||
*
|
||||
* @param string $code
|
||||
* @param int $startLine
|
||||
* @param int|null $endLine
|
||||
* @param int|null $markLine
|
||||
*
|
||||
* @return string formatted code
|
||||
*/
|
||||
public static function formatCode(string $code, int $startLine = 1, ?int $endLine = null, ?int $markLine = null): string
|
||||
{
|
||||
$spans = self::tokenizeSpans($code);
|
||||
$lines = self::splitLines($spans, $startLine, $endLine);
|
||||
$lines = self::formatLines($lines);
|
||||
$lines = self::numberLines($lines, $markLine);
|
||||
|
||||
return \implode('', \iterator_to_array($lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start line for a given Reflector.
|
||||
*
|
||||
* Tries to incorporate doc comments if possible.
|
||||
*
|
||||
* This is typehinted as \Reflector but we've narrowed the input via self::isReflectable already.
|
||||
*
|
||||
* @param \ReflectionClass|\ReflectionFunctionAbstract $reflector
|
||||
*/
|
||||
private static function getStartLine(\Reflector $reflector): int
|
||||
{
|
||||
$startLine = $reflector->getStartLine();
|
||||
|
||||
if ($docComment = $reflector->getDocComment()) {
|
||||
$startLine -= \preg_match_all('/(\r\n?|\n)/', $docComment) + 1;
|
||||
}
|
||||
|
||||
return \max($startLine, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split code into highlight spans.
|
||||
*
|
||||
* Tokenize via \token_get_all, then map these tokens to internal highlight types, combining
|
||||
* adjacent spans of the same highlight type.
|
||||
*
|
||||
* @todo consider switching \token_get_all() out for PHP-Parser-based formatting at some point.
|
||||
*
|
||||
* @param string $code
|
||||
*
|
||||
* @return \Generator [$spanType, $spanText] highlight spans
|
||||
*/
|
||||
private static function tokenizeSpans(string $code): \Generator
|
||||
{
|
||||
$spanType = null;
|
||||
$buffer = '';
|
||||
|
||||
foreach (\token_get_all($code) as $token) {
|
||||
$nextType = self::nextHighlightType($token, $spanType);
|
||||
$spanType = $spanType ?: $nextType;
|
||||
|
||||
if ($spanType !== $nextType) {
|
||||
yield [$spanType, $buffer];
|
||||
$spanType = $nextType;
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= \is_array($token) ? $token[1] : $token;
|
||||
}
|
||||
|
||||
if ($spanType !== null && $buffer !== '') {
|
||||
yield [$spanType, $buffer];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a token and the current highlight span type, compute the next type.
|
||||
*
|
||||
* @param array|string $token \token_get_all token
|
||||
* @param string|null $currentType
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private static function nextHighlightType($token, $currentType)
|
||||
{
|
||||
if ($token === '"') {
|
||||
return self::HIGHLIGHT_STRING;
|
||||
}
|
||||
|
||||
if (\is_array($token)) {
|
||||
if ($token[0] === \T_WHITESPACE) {
|
||||
return $currentType;
|
||||
}
|
||||
|
||||
if (\array_key_exists($token[0], self::TOKEN_MAP)) {
|
||||
return self::TOKEN_MAP[$token[0]];
|
||||
}
|
||||
}
|
||||
|
||||
return self::HIGHLIGHT_KEYWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group highlight spans into an array of lines.
|
||||
*
|
||||
* Optionally, restrict by start and end line numbers.
|
||||
*
|
||||
* @param \Generator $spans as [$spanType, $spanText] pairs
|
||||
* @param int $startLine
|
||||
* @param int|null $endLine
|
||||
*
|
||||
* @return \Generator lines, each an array of [$spanType, $spanText] pairs
|
||||
*/
|
||||
private static function splitLines(\Generator $spans, int $startLine = 1, ?int $endLine = null): \Generator
|
||||
{
|
||||
$lineNum = 1;
|
||||
$buffer = [];
|
||||
|
||||
foreach ($spans as list($spanType, $spanText)) {
|
||||
foreach (\preg_split('/(\r\n?|\n)/', $spanText) as $index => $spanLine) {
|
||||
if ($index > 0) {
|
||||
if ($lineNum >= $startLine) {
|
||||
yield $lineNum => $buffer;
|
||||
}
|
||||
|
||||
$lineNum++;
|
||||
$buffer = [];
|
||||
|
||||
if ($endLine !== null && $lineNum > $endLine) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($spanLine !== '') {
|
||||
$buffer[] = [$spanType, $spanLine];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($buffer)) {
|
||||
yield $lineNum => $buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format lines of highlight spans for shell output.
|
||||
*
|
||||
* @param \Generator $spanLines lines, each an array of [$spanType, $spanText] pairs
|
||||
*
|
||||
* @return \Generator Formatted lines
|
||||
*/
|
||||
private static function formatLines(\Generator $spanLines): \Generator
|
||||
{
|
||||
foreach ($spanLines as $lineNum => $spanLine) {
|
||||
$line = '';
|
||||
|
||||
foreach ($spanLine as list($spanType, $spanText)) {
|
||||
if ($spanType === self::HIGHLIGHT_DEFAULT) {
|
||||
$line .= OutputFormatter::escape($spanText);
|
||||
} else {
|
||||
$line .= \sprintf('<%s>%s</%s>', $spanType, OutputFormatter::escape($spanText), $spanType);
|
||||
}
|
||||
}
|
||||
|
||||
yield $lineNum => $line.\PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend line numbers to formatted lines.
|
||||
*
|
||||
* Lines must be in an associative array with the correct keys in order to be numbered properly.
|
||||
*
|
||||
* Optionally, pass $markLine to add a line marker.
|
||||
*
|
||||
* @param \Generator $lines Formatted lines
|
||||
* @param int|null $markLine
|
||||
*
|
||||
* @return \Generator Numbered, formatted lines
|
||||
*/
|
||||
private static function numberLines(\Generator $lines, ?int $markLine = null): \Generator
|
||||
{
|
||||
$lines = \iterator_to_array($lines);
|
||||
|
||||
// Figure out how much space to reserve for line numbers.
|
||||
\end($lines);
|
||||
$pad = \strlen(\key($lines));
|
||||
|
||||
// If $markLine is before or after our line range, don't bother reserving space for the marker.
|
||||
if ($markLine !== null) {
|
||||
if ($markLine > \key($lines)) {
|
||||
$markLine = null;
|
||||
}
|
||||
|
||||
\reset($lines);
|
||||
if ($markLine < \key($lines)) {
|
||||
$markLine = null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
$mark = '';
|
||||
if ($markLine !== null) {
|
||||
$mark = ($markLine === $lineNum) ? self::LINE_MARKER : self::NO_LINE_MARKER;
|
||||
}
|
||||
|
||||
yield \sprintf("%s<aside>%{$pad}s</aside>: %s", $mark, $lineNum, $line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a Reflector instance is reflectable by this formatter.
|
||||
*
|
||||
* @phpstan-assert-if-true \ReflectionClass|\ReflectionFunctionAbstract $reflector
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*/
|
||||
private static function isReflectable(\Reflector $reflector): bool
|
||||
{
|
||||
return ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) && \is_file($reflector->getFileName());
|
||||
}
|
||||
}
|
||||
166
vendor/psy/psysh/src/Formatter/DocblockFormatter.php
vendored
Normal file
166
vendor/psy/psysh/src/Formatter/DocblockFormatter.php
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Psy\Util\Docblock;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* A pretty-printer for docblocks.
|
||||
*/
|
||||
class DocblockFormatter implements ReflectorFormatter
|
||||
{
|
||||
private const VECTOR_PARAM_TEMPLATES = [
|
||||
'type' => 'info',
|
||||
'var' => 'strong',
|
||||
];
|
||||
|
||||
/**
|
||||
* Format a docblock.
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*
|
||||
* @return string Formatted docblock
|
||||
*/
|
||||
public static function format(\Reflector $reflector): string
|
||||
{
|
||||
$docblock = new Docblock($reflector);
|
||||
$chunks = [];
|
||||
|
||||
if (!empty($docblock->desc)) {
|
||||
$chunks[] = '<comment>Description:</comment>';
|
||||
$chunks[] = self::indent(OutputFormatter::escape($docblock->desc), ' ');
|
||||
$chunks[] = '';
|
||||
}
|
||||
|
||||
if (!empty($docblock->tags)) {
|
||||
foreach ($docblock::$vectors as $name => $vector) {
|
||||
if (isset($docblock->tags[$name])) {
|
||||
$chunks[] = \sprintf('<comment>%s:</comment>', self::inflect($name));
|
||||
$chunks[] = self::formatVector($vector, $docblock->tags[$name]);
|
||||
$chunks[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$tags = self::formatTags(\array_keys($docblock::$vectors), $docblock->tags);
|
||||
if (!empty($tags)) {
|
||||
$chunks[] = $tags;
|
||||
$chunks[] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return \rtrim(\implode("\n", $chunks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a docblock vector, for example, `@throws`, `@param`, or `@return`.
|
||||
*
|
||||
* @see DocBlock::$vectors
|
||||
*
|
||||
* @param array $vector
|
||||
* @param array $lines
|
||||
*/
|
||||
private static function formatVector(array $vector, array $lines): string
|
||||
{
|
||||
$template = [' '];
|
||||
foreach ($vector as $type) {
|
||||
$max = 0;
|
||||
foreach ($lines as $line) {
|
||||
$chunk = $line[$type];
|
||||
$cur = empty($chunk) ? 0 : \strlen($chunk) + 1;
|
||||
if ($cur > $max) {
|
||||
$max = $cur;
|
||||
}
|
||||
}
|
||||
|
||||
$template[] = self::getVectorParamTemplate($type, $max);
|
||||
}
|
||||
$template = \implode(' ', $template);
|
||||
|
||||
return \implode("\n", \array_map(function ($line) use ($template) {
|
||||
$escaped = \array_map(function ($l) {
|
||||
if ($l === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return OutputFormatter::escape($l);
|
||||
}, $line);
|
||||
|
||||
return \rtrim(\vsprintf($template, $escaped));
|
||||
}, $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format docblock tags.
|
||||
*
|
||||
* @param array $skip Tags to exclude
|
||||
* @param array $tags Tags to format
|
||||
*
|
||||
* @return string formatted tags
|
||||
*/
|
||||
private static function formatTags(array $skip, array $tags): string
|
||||
{
|
||||
$chunks = [];
|
||||
|
||||
foreach ($tags as $name => $values) {
|
||||
if (\in_array($name, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($values as $value) {
|
||||
$chunks[] = \sprintf('<comment>%s%s</comment> %s', self::inflect($name), empty($value) ? '' : ':', OutputFormatter::escape($value));
|
||||
}
|
||||
|
||||
$chunks[] = '';
|
||||
}
|
||||
|
||||
return \implode("\n", $chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a docblock vector template.
|
||||
*
|
||||
* @param string $type Vector type
|
||||
* @param int $max Pad width
|
||||
*/
|
||||
private static function getVectorParamTemplate(string $type, int $max): string
|
||||
{
|
||||
if (!isset(self::VECTOR_PARAM_TEMPLATES[$type])) {
|
||||
return \sprintf('%%-%ds', $max);
|
||||
}
|
||||
|
||||
return \sprintf('<%s>%%-%ds</%s>', self::VECTOR_PARAM_TEMPLATES[$type], $max, self::VECTOR_PARAM_TEMPLATES[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indent a string.
|
||||
*
|
||||
* @param string $text String to indent
|
||||
* @param string $indent (default: ' ')
|
||||
*/
|
||||
private static function indent(string $text, string $indent = ' '): string
|
||||
{
|
||||
return $indent.\str_replace("\n", "\n".$indent, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert underscored or whitespace separated words into sentence case.
|
||||
*
|
||||
* @param string $text
|
||||
*/
|
||||
private static function inflect(string $text): string
|
||||
{
|
||||
$words = \trim(\preg_replace('/[\s_-]+/', ' ', \preg_replace('/([a-z])([A-Z])/', '$1 $2', $text)));
|
||||
|
||||
return \implode(' ', \array_map('ucfirst', \explode(' ', $words)));
|
||||
}
|
||||
}
|
||||
106
vendor/psy/psysh/src/Formatter/LinkFormatter.php
vendored
Normal file
106
vendor/psy/psysh/src/Formatter/LinkFormatter.php
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
||||
|
||||
/**
|
||||
* Utility for creating terminal hyperlinks (OSC 8).
|
||||
*/
|
||||
class LinkFormatter
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private static $styles = [];
|
||||
|
||||
/**
|
||||
* Set styles for formatting hyperlinks.
|
||||
*
|
||||
* @param array $styles Map of style name to inline style string
|
||||
*/
|
||||
public static function setStyles(array $styles): void
|
||||
{
|
||||
self::$styles = $styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current Symfony Console version supports hyperlinks.
|
||||
*/
|
||||
public static function supportsLinks(): bool
|
||||
{
|
||||
static $supports = null;
|
||||
|
||||
if ($supports === null) {
|
||||
$supports = \method_exists(OutputFormatterStyle::class, 'setHref');
|
||||
}
|
||||
|
||||
return $supports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text in a style tag, optionally including an href.
|
||||
*
|
||||
* @param string $style The style name (e.g., 'class', 'function', 'info')
|
||||
* @param string $text The text to wrap
|
||||
* @param string|null $href Optional hyperlink
|
||||
*
|
||||
* @return string Formatted text with style and optional href
|
||||
*/
|
||||
public static function styleWithHref(string $style, string $text, ?string $href = null): string
|
||||
{
|
||||
$escapedText = OutputFormatter::escape($text);
|
||||
|
||||
if ($href !== null && self::supportsLinks()) {
|
||||
$href = self::encodeHrefForOsc8($href);
|
||||
$inline = self::$styles[$style] ?? '';
|
||||
$combinedStyle = $inline !== '' ? \sprintf('%s;href=%s', $inline, $href) : \sprintf('href=%s', $href);
|
||||
|
||||
return \sprintf('<%s>%s</>', $combinedStyle, $escapedText);
|
||||
}
|
||||
|
||||
return \sprintf('<%s>%s</%s>', $style, $escapedText, $style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the php.net manual URL for a given item.
|
||||
*
|
||||
* @param string $item Function or class name
|
||||
*
|
||||
* @return string URL to php.net manual
|
||||
*/
|
||||
public static function getPhpNetUrl(string $item): string
|
||||
{
|
||||
// Normalize the item name for URL (lowercase, replace :: with . and _ with -)
|
||||
$normalized = \str_replace('::', '.', $item);
|
||||
$normalized = \str_replace('_', '-', $normalized);
|
||||
$normalized = \strtolower($normalized);
|
||||
|
||||
return 'https://php.net/'.$normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string for use in OSC 8 hyperlink URIs.
|
||||
*
|
||||
* Per OSC 8 spec, URIs must only contain bytes in the 32-126 range.
|
||||
*
|
||||
* @param string $str String to encode
|
||||
*
|
||||
* @return string URI-encoded string safe for OSC 8
|
||||
*/
|
||||
public static function encodeHrefForOsc8(string $str): string
|
||||
{
|
||||
// Encode any character outside printable ASCII range (32-126)
|
||||
return \preg_replace_callback('/[^\x20-\x7E]/', function ($matches) {
|
||||
return \rawurlencode($matches[0]);
|
||||
}, $str);
|
||||
}
|
||||
}
|
||||
454
vendor/psy/psysh/src/Formatter/ManualFormatter.php
vendored
Normal file
454
vendor/psy/psysh/src/Formatter/ManualFormatter.php
vendored
Normal file
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Psy\Manual\ManualInterface;
|
||||
|
||||
/**
|
||||
* Formats structured manual data for display at runtime.
|
||||
*
|
||||
* Takes structured data from the v3 manual format and formats it for display,
|
||||
* adapting to terminal width and converting semantic tags to console styles.
|
||||
*/
|
||||
class ManualFormatter
|
||||
{
|
||||
// Maximum width for text wrapping, even on very wide terminals
|
||||
private const MAX_WIDTH = 120;
|
||||
|
||||
private ManualWrapper $wrapper;
|
||||
private int $width;
|
||||
private ?ManualInterface $manual;
|
||||
|
||||
/**
|
||||
* @param int $width Terminal width for text wrapping
|
||||
* @param ManualInterface|null $manual Optional manual for generating hyperlinks
|
||||
*/
|
||||
public function __construct(int $width = 100, ?ManualInterface $manual = null)
|
||||
{
|
||||
$this->wrapper = new ManualWrapper();
|
||||
// Cap width at MAX_WIDTH for readability on ultra-wide terminals
|
||||
$this->width = \min($width, self::MAX_WIDTH);
|
||||
$this->manual = $manual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format structured manual data for display.
|
||||
*
|
||||
* @param array $data Structured manual data
|
||||
*/
|
||||
public function format(array $data): string
|
||||
{
|
||||
$output = [];
|
||||
|
||||
// Format based on type
|
||||
switch ($data['type'] ?? '') {
|
||||
case 'function':
|
||||
$output[] = $this->formatFunction($data);
|
||||
break;
|
||||
case 'class':
|
||||
$output[] = $this->formatClass($data);
|
||||
break;
|
||||
case 'constant':
|
||||
$output[] = $this->formatConstant($data);
|
||||
break;
|
||||
default:
|
||||
// Generic fallback
|
||||
if (!empty($data['description'])) {
|
||||
$output[] = $this->formatDescription($data['description']);
|
||||
}
|
||||
}
|
||||
|
||||
return \implode("\n\n", \array_filter($output))."\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a function entry.
|
||||
*
|
||||
* @param array $data Function data
|
||||
*/
|
||||
private function formatFunction(array $data): string
|
||||
{
|
||||
$output = [];
|
||||
|
||||
if (!empty($data['description'])) {
|
||||
$output[] = $this->formatDescription($data['description']);
|
||||
}
|
||||
|
||||
if (!empty($data['params'])) {
|
||||
$output[] = $this->formatParameters($data['params']);
|
||||
}
|
||||
|
||||
if (!empty($data['return'])) {
|
||||
$output[] = $this->formatReturn($data['return']);
|
||||
}
|
||||
|
||||
if (!empty($data['seeAlso'])) {
|
||||
$output[] = $this->formatSeeAlso($data['seeAlso']);
|
||||
}
|
||||
|
||||
return \implode("\n\n", \array_filter($output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a class entry.
|
||||
*
|
||||
* @param array $data Class data
|
||||
*/
|
||||
private function formatClass(array $data): string
|
||||
{
|
||||
$output = [];
|
||||
|
||||
// Description
|
||||
if (!empty($data['description'])) {
|
||||
$output[] = $this->formatDescription($data['description']);
|
||||
}
|
||||
|
||||
// See also
|
||||
if (!empty($data['seeAlso'])) {
|
||||
$output[] = $this->formatSeeAlso($data['seeAlso']);
|
||||
}
|
||||
|
||||
return \implode("\n\n", \array_filter($output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a constant entry.
|
||||
*
|
||||
* @param array $data Constant data
|
||||
*/
|
||||
private function formatConstant(array $data): string
|
||||
{
|
||||
$output = [];
|
||||
|
||||
if (isset($data['value'])) {
|
||||
$output[] = '<strong>Value:</strong> '.$this->thunkTags($data['value']);
|
||||
}
|
||||
|
||||
if (!empty($data['description'])) {
|
||||
$output[] = $this->formatDescription($data['description']);
|
||||
}
|
||||
|
||||
if (!empty($data['seeAlso'])) {
|
||||
$output[] = $this->formatSeeAlso($data['seeAlso']);
|
||||
}
|
||||
|
||||
return \implode("\n\n", \array_filter($output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a description section.
|
||||
*
|
||||
* @param string $description Description text with semantic tags
|
||||
*
|
||||
* @return string Formatted description
|
||||
*/
|
||||
private function formatDescription(string $description): string
|
||||
{
|
||||
$output = ['<comment>Description:</comment>'];
|
||||
|
||||
$text = $this->thunkTags($description);
|
||||
$wrapped = $this->wrapper->wrap($text, $this->width - 2);
|
||||
|
||||
$output = \array_merge($output, $this->indentWrappedLines($wrapped, ' '));
|
||||
|
||||
return \implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format parameters section.
|
||||
*
|
||||
* @param array $params Parameter list
|
||||
*/
|
||||
private function formatParameters(array $params): string
|
||||
{
|
||||
// Decide layout based on terminal width
|
||||
// Use table layout for wide terminals (80+), stacked for narrow
|
||||
if ($this->width >= 80) {
|
||||
return $this->formatParametersTable($params);
|
||||
} else {
|
||||
return $this->formatParametersStacked($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format parameters as a table (for wide terminals).
|
||||
*
|
||||
* @param array $params Parameter list
|
||||
*/
|
||||
private function formatParametersTable(array $params): string
|
||||
{
|
||||
$output = ['<comment>Param:</comment>'];
|
||||
|
||||
// Calculate column widths (matching old format)
|
||||
$typeWidth = \max(\array_map(function ($param) {
|
||||
return \mb_strlen($param['type'] ?? 'mixed');
|
||||
}, $params));
|
||||
|
||||
$nameWidth = \max(\array_map(function ($param) {
|
||||
return \mb_strlen($param['name']);
|
||||
}, $params));
|
||||
|
||||
// Build columns with padding OUTSIDE style tags
|
||||
$indent = \str_repeat(' ', $typeWidth + $nameWidth + 6);
|
||||
$wrapWidth = $this->width - \mb_strlen($indent);
|
||||
|
||||
foreach ($params as $param) {
|
||||
$type = $param['type'] ?? 'mixed';
|
||||
$name = $param['name'];
|
||||
$desc = $this->thunkTags($param['description'] ?? '');
|
||||
|
||||
// Wrap in style tags first, THEN pad to avoid long color blocks
|
||||
$typeFormatted = '<info>'.$type.'</info>'.\str_repeat(' ', $typeWidth - \mb_strlen($type));
|
||||
$nameFormatted = '<strong>'.$name.'</strong>'.\str_repeat(' ', $nameWidth - \mb_strlen($name));
|
||||
|
||||
// Wrap description with proper indentation
|
||||
if (!empty($desc)) {
|
||||
$wrapped = $this->wrapper->wrap($desc, $wrapWidth);
|
||||
$firstLine = ' '.$typeFormatted.' '.$nameFormatted.' ';
|
||||
$output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent, $firstLine));
|
||||
} else {
|
||||
$output[] = ' '.$typeFormatted.' '.$nameFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
return \implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format parameters stacked (for narrow terminals).
|
||||
*
|
||||
* @param array $params Parameter list
|
||||
*/
|
||||
private function formatParametersStacked(array $params): string
|
||||
{
|
||||
$output = ['<comment>Param:</comment>'];
|
||||
|
||||
// Calculate type width for alignment
|
||||
$typeWidth = \max(\array_map(function ($param) {
|
||||
return \mb_strlen($param['type'] ?? 'mixed');
|
||||
}, $params));
|
||||
|
||||
foreach ($params as $param) {
|
||||
$type = \str_pad($param['type'] ?? 'mixed', $typeWidth);
|
||||
$name = $param['name'];
|
||||
|
||||
$output[] = \sprintf(' <info>%s</info> <strong>%s</strong>', $type, $name);
|
||||
|
||||
if (!empty($param['description'])) {
|
||||
$desc = $this->thunkTags($param['description']);
|
||||
$indent = \str_repeat(' ', $typeWidth + 4);
|
||||
$wrapped = $this->wrapper->wrap($desc, $this->width - \mb_strlen($indent));
|
||||
$output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent));
|
||||
}
|
||||
}
|
||||
|
||||
return \implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format return value section.
|
||||
*
|
||||
* @param array $return Return value data
|
||||
*/
|
||||
private function formatReturn(array $return): string
|
||||
{
|
||||
$output = ['<comment>Return:</comment>'];
|
||||
|
||||
$type = $return['type'] ?? 'unknown';
|
||||
$desc = $return['description'] ?? '';
|
||||
|
||||
$indent = \str_repeat(' ', \mb_strlen($type) + 4);
|
||||
$wrapWidth = $this->width - \mb_strlen($indent);
|
||||
|
||||
if (!empty($desc)) {
|
||||
$desc = $this->thunkTags($desc);
|
||||
$wrapped = $this->wrapper->wrap($desc, $wrapWidth);
|
||||
$firstLine = \sprintf(' <info>%s</info> ', $type);
|
||||
$output = \array_merge($output, $this->indentWrappedLines($wrapped, $indent, $firstLine));
|
||||
} else {
|
||||
$output[] = \sprintf(' <info>%s</info>', $type);
|
||||
}
|
||||
|
||||
return \implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format see also section.
|
||||
*
|
||||
* @param array $seeAlso List of related functions/classes
|
||||
*/
|
||||
private function formatSeeAlso(array $seeAlso): string
|
||||
{
|
||||
if (empty($seeAlso)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$output = ['<comment>See Also:</comment>'];
|
||||
|
||||
// Format items with hyperlinks if manual is available
|
||||
$items = \array_map(function ($item) {
|
||||
return $this->formatSeeAlsoItem($item);
|
||||
}, $seeAlso);
|
||||
|
||||
// Don't wrap - console tags need to stay intact
|
||||
// Just join with commas and indent
|
||||
$output[] = ' '.\implode(', ', $items);
|
||||
|
||||
return \implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a single see also item with hyperlink if available.
|
||||
*
|
||||
* @param string $item Function or class name (may contain XML tags)
|
||||
*/
|
||||
private function formatSeeAlsoItem(string $item): string
|
||||
{
|
||||
// Strip XML tags to get the actual function/class name
|
||||
$cleanItem = \strip_tags($item);
|
||||
|
||||
// Check if this item exists in the manual
|
||||
$href = null;
|
||||
if ($this->manual !== null && $this->manual->get($cleanItem) !== null) {
|
||||
$href = LinkFormatter::getPhpNetUrl($cleanItem);
|
||||
}
|
||||
|
||||
// Add parentheses to functions (like php.net and old manual format)
|
||||
// Items with <function> tags are functions, otherwise classes/constants
|
||||
$displayText = $cleanItem;
|
||||
if (\strpos($item, '<function>') !== false) {
|
||||
$displayText .= '()';
|
||||
}
|
||||
|
||||
if ($href !== null) {
|
||||
return LinkFormatter::styleWithHref('info', $displayText, $href);
|
||||
}
|
||||
|
||||
// No hyperlink; apply semantic tag formatting, then add parens if function
|
||||
$formatted = $this->thunkTags($item);
|
||||
if (\strpos($item, '<function>') !== false && \strpos($formatted, '()') === false) {
|
||||
$formatted .= '()';
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indent wrapped text lines.
|
||||
*
|
||||
* Takes wrapped text and adds indentation to each line.
|
||||
* The first line can have a different prefix than subsequent lines.
|
||||
*
|
||||
* @param string $wrapped Wrapped text (may contain newlines)
|
||||
* @param string $indent Indentation for continuation lines
|
||||
* @param string|null $firstIndent Optional different indentation for first line (defaults to $indent)
|
||||
*
|
||||
* @return array Lines with indentation applied
|
||||
*/
|
||||
private function indentWrappedLines(string $wrapped, string $indent, ?string $firstIndent = null): array
|
||||
{
|
||||
$firstIndent = $firstIndent ?? $indent;
|
||||
$lines = \explode("\n", $wrapped);
|
||||
$output = [];
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
$output[] = ($i === 0 ? $firstIndent : $indent).$line;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert semantic XML tags to Symfony Console format tags.
|
||||
*
|
||||
* @param string $text Text with semantic tags
|
||||
*
|
||||
* @return string Text with console format tags
|
||||
*/
|
||||
private function thunkTags(string $text): string
|
||||
{
|
||||
// First, escape any < and > that aren't part of our semantic tags
|
||||
// Protect our semantic tags by replacing them with placeholders
|
||||
$tagMap = [];
|
||||
$tagIndex = 0;
|
||||
|
||||
// Protect semantic tags
|
||||
$semanticTags = ['parameter', 'function', 'constant', 'classname', 'type', 'literal', 'class'];
|
||||
foreach ($semanticTags as $tag) {
|
||||
$text = \preg_replace_callback(
|
||||
"/<{$tag}>|<\/{$tag}>/",
|
||||
function ($matches) use (&$tagMap, &$tagIndex) {
|
||||
$placeholder = "\x00TAG{$tagIndex}\x00";
|
||||
$tagMap[$placeholder] = $matches[0];
|
||||
$tagIndex++;
|
||||
|
||||
return $placeholder;
|
||||
},
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
// Now escape any remaining < and > (these are content, not tags)
|
||||
$text = \str_replace(['<', '>'], ['\\<', '\\>'], $text);
|
||||
|
||||
// Restore protected tags
|
||||
$text = \str_replace(\array_keys($tagMap), \array_values($tagMap), $text);
|
||||
|
||||
// Handle parameters: add $ prefix and make bold
|
||||
$text = \preg_replace_callback(
|
||||
'/<parameter>([^<]+)<\/parameter>/',
|
||||
function ($matches) {
|
||||
$name = $matches[1];
|
||||
// Add $ if not already present
|
||||
if ($name[0] !== '$') {
|
||||
$name = '$'.$name;
|
||||
}
|
||||
|
||||
return '<strong>'.$name.'</strong>';
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
// Handle functions: add () suffix and make bold
|
||||
$text = \preg_replace_callback(
|
||||
'/<function>([^<]+)<\/function>/',
|
||||
function ($matches) {
|
||||
$name = $matches[1];
|
||||
// Add () if not already present
|
||||
if (\substr($name, -2) !== '()') {
|
||||
$name .= '()';
|
||||
}
|
||||
|
||||
return '<strong>'.$name.'</strong>';
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
// Map other semantic tags to corresponding formats
|
||||
$replacements = [
|
||||
'<constant>' => '<info>',
|
||||
'</constant>' => '</info>',
|
||||
'<classname>' => '<class>',
|
||||
'</classname>' => '</class>',
|
||||
'<class>' => '<class>',
|
||||
'</class>' => '</class>',
|
||||
'<type>' => '<info>',
|
||||
'</type>' => '</info>',
|
||||
'<literal>' => '<return>',
|
||||
'</literal>' => '</return>',
|
||||
];
|
||||
|
||||
$text = \str_replace(\array_keys($replacements), \array_values($replacements), $text);
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
216
vendor/psy/psysh/src/Formatter/ManualWrapper.php
vendored
Normal file
216
vendor/psy/psysh/src/Formatter/ManualWrapper.php
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
/**
|
||||
* A CJK and tag-aware text wrapper for manual content.
|
||||
*
|
||||
* Handles proper line breaking for:
|
||||
* - Full-width CJK characters (count as 2 characters wide)
|
||||
* - XML tags (ignored for width calculation)
|
||||
* - CJK typography rules (characters that can't start/end lines)
|
||||
*/
|
||||
class ManualWrapper
|
||||
{
|
||||
// Full-width character ranges
|
||||
// via http://www.localizingjapan.com/blog/2012/01/20/regular-expressions-for-japanese-text/
|
||||
private const HIRAGANA = '\p{Hiragana}';
|
||||
private const KATAKANA = '\p{Katakana}';
|
||||
private const HAN = '\p{Han}';
|
||||
private const HANGUL = '\p{Hangul}';
|
||||
private const RADICALS = '\x{2E80}-\x{2FD5}';
|
||||
private const PUNCTUATION = '\x{3000}-\x{303F}';
|
||||
private const SYMBOLS = '\x{31F0}-\x{31FF}\x{3220}-\x{3243}\x{3280}-\x{337F}';
|
||||
private const ASCII = '\x{FF01}-\x{FF5E}';
|
||||
|
||||
private static $fullWidthRanges = [
|
||||
self::HIRAGANA,
|
||||
self::KATAKANA,
|
||||
self::HAN,
|
||||
self::HANGUL,
|
||||
self::RADICALS,
|
||||
self::PUNCTUATION,
|
||||
self::SYMBOLS,
|
||||
self::ASCII,
|
||||
];
|
||||
|
||||
// CJK characters not allowed at the start of lines
|
||||
private static $notStart = [
|
||||
// mid-sentence and closing punctuation, hyphens, etc:
|
||||
// !,.:;?‐–—―‧‼⁇⁈⁉╴、。〜゠・︰﹐﹒﹔﹕﹖﹗﹘!",.:;?~
|
||||
'!,\\.:;\\?\x{2010}\x{2013}\x{2014}\x{2015}\x{2027}\x{203C}\x{2047}\x{2048}\x{2049}\x{2574}\x{3001}\x{3002}'
|
||||
.'\x{301C}\x{30A0}\x{30FB}\x{FE30}\x{FE50}\x{FE52}\x{FE54}\x{FE55}\x{FE56}\x{FE57}\x{FE58}\x{FF01}\x{FF02}'
|
||||
.'\x{FF0C}\x{FF0E}\x{FF1A}\x{FF1B}\x{FF1F}\x{FF5E}',
|
||||
|
||||
// closing brackets:
|
||||
// )>]}»'"„›〃〉》」』】〕〗〙〞〟︶︸︺︼︾﹀﹂﹑﹚﹜')]}⦆、
|
||||
'\\)>\\]\\}\x{00BB}\x{2019}\x{201D}\x{201E}\x{203A}\x{3003}\x{3009}\x{300B}\x{300D}\x{300F}\x{3011}\x{3015}'
|
||||
.'\x{3017}\x{3019}\x{301E}\x{301F}\x{FE36}\x{FE38}\x{FE3A}\x{FE3C}\x{FE3E}\x{FE40}\x{FE42}\x{FE51}\x{FE5A}'
|
||||
.'\x{FE5C}\x{FF07}\x{FF09}\x{FF3D}\x{FF5D}\x{FF60}\x{FF64}',
|
||||
|
||||
// misc symbols:
|
||||
// %¢¨°·ˇˉ‖†‡•‥℃∶、〆︱︲︳%|
|
||||
'%\x{00A2}\x{00A8}\x{00B0}\x{00B7}\x{02C7}\x{02C9}\x{2016}\x{2020}\x{2021}\x{2022}\x{2025}\x{2103}\x{2236}'
|
||||
.'\x{3001}\x{3006}\x{FE31}\x{FE32}\x{FE33}\x{FE53}\x{FF05}\x{FF5C}',
|
||||
|
||||
// misc japanese:
|
||||
// ヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻
|
||||
'\x{30FD}\x{30FE}\x{30FC}\x{30A1}\x{30A3}\x{30A5}\x{30A7}\x{30A9}\x{30C3}\x{30E3}\x{30E5}\x{30E7}\x{30EE}'
|
||||
.'\x{30F5}\x{30F6}\x{3041}\x{3043}\x{3045}\x{3047}\x{3049}\x{3063}\x{3083}\x{3085}\x{3087}\x{308E}\x{3095}'
|
||||
.'\x{3096}\x{31F0}\x{31F1}\x{31F2}\x{31F3}\x{31F4}\x{31F5}\x{31F6}\x{31F7}\x{31F8}\x{31F9}\x{31FA}\x{31FB}'
|
||||
.'\x{31FC}\x{31FD}\x{31FE}\x{31FF}\x{3005}\x{303B}',
|
||||
];
|
||||
|
||||
// CJK characters not allowed at the end of lines
|
||||
private static $notEnd = [
|
||||
// misc symbols, currency, etc:
|
||||
// #$*\£¥々〇$£¥₩
|
||||
'#\\$\\*\\\x{00A3}\x{00A5}\x{3005}\x{3007}\x{FF04}\x{FFE1}\x{FFE5}\x{FFE6}',
|
||||
|
||||
// opening brackets:
|
||||
// ([{'"‟‵〈《「『【〔〖〝︴︵︷︹︻︽︿﹁﹃﹏﹙﹛([`{
|
||||
'\\(\\[\\{\x{2018}\x{201C}\x{201F}\x{2035}\x{3008}\x{300A}\x{300C}\x{300E}\x{3010}\x{3014}\x{3016}\x{301D}'
|
||||
.'\x{FE34}\x{FE35}\x{FE37}\x{FE39}\x{FE3B}\x{FE3D}\x{FE3F}\x{FE41}\x{FE43}\x{FE4F}\x{FE59}\x{FE5B}\x{FF08}'
|
||||
.'\x{FF3B}\x{FF40}\x{FF5B}',
|
||||
];
|
||||
|
||||
// CJK characters not allowed at the start _or_ end of lines
|
||||
private static $notSplit = [
|
||||
// misc japanese:
|
||||
// —…‥〳〴〵
|
||||
'\x{2014}\x{2026}\x{2025}\x{3033}\x{3034}\x{3035}',
|
||||
];
|
||||
|
||||
/**
|
||||
* A tag-aware, CJK friendly version of wordwrap().
|
||||
*
|
||||
* @param string $text The input string
|
||||
* @param int $width The number of characters at which the string will be wrapped
|
||||
* @param string $break The line break character
|
||||
* @param bool $cut Wrap at or before the specified width (unused)
|
||||
*
|
||||
* @return string The wrapped text
|
||||
*/
|
||||
public function wrap(string $text, int $width = 100, string $break = "\n", bool $cut = false): string
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach (\explode($break, $text) as $line) {
|
||||
if (self::len($line) <= $width) {
|
||||
$lines[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
$buf = '';
|
||||
foreach ($this->words($line) as $word) {
|
||||
if (self::len($buf.$word) <= $width) {
|
||||
$buf .= $word;
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = \rtrim($buf);
|
||||
$buf = $word;
|
||||
}
|
||||
|
||||
if (self::len($buf) > 0) {
|
||||
$lines[] = $buf;
|
||||
}
|
||||
}
|
||||
|
||||
return \implode($break, $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the apparent length of a unicode string.
|
||||
*
|
||||
* Full-width CJK characters count as 2 characters wide.
|
||||
* Tags are ignored for width calculation.
|
||||
*
|
||||
* @param string $text The text to measure
|
||||
*
|
||||
* @return int The apparent width
|
||||
*/
|
||||
public static function len(string $text): int
|
||||
{
|
||||
// Match all full-width characters and replace them with 'xx', so that we can compute the _apparent_ length
|
||||
// of the rendered string.
|
||||
$isFullWidth = \sprintf('/[%s]/u', \implode('', self::$fullWidthRanges));
|
||||
|
||||
return \mb_strlen(\rtrim(\preg_replace($isFullWidth, 'xx', \strip_tags($text))));
|
||||
}
|
||||
|
||||
/**
|
||||
* A word generator.
|
||||
*
|
||||
* Returns actual (space delimited) words. It also returns CJK characters as "words", as long as they can be split
|
||||
* between lines.
|
||||
*
|
||||
* @param string $line The line to split into words
|
||||
*
|
||||
* @return \Generator<string>
|
||||
*/
|
||||
private function words(string $line): \Generator
|
||||
{
|
||||
$isCJK = \sprintf('/[%s]/u', \implode('', self::$fullWidthRanges));
|
||||
$notStart = \sprintf('/[%s%s]/u', \implode('', self::$notStart), \implode('', self::$notSplit));
|
||||
$notEnd = \sprintf('/[%s%s]/u', \implode('', self::$notEnd), \implode('', self::$notSplit));
|
||||
|
||||
$line = \rtrim($line);
|
||||
$i = 0;
|
||||
$len = \mb_strlen($line);
|
||||
$inTag = false;
|
||||
|
||||
do {
|
||||
$char = \mb_substr($line, $i, 1);
|
||||
switch ($char) {
|
||||
case '<':
|
||||
$inTag = true;
|
||||
break;
|
||||
|
||||
case '>':
|
||||
$inTag = false;
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
if (!$inTag) {
|
||||
yield \rtrim(\mb_substr($line, 0, $i)).' ';
|
||||
$line = \mb_substr($line, $i + 1);
|
||||
$len = \mb_strlen($line);
|
||||
$i = -1;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// if this is a CJK character...
|
||||
if (!$inTag && \preg_match($isCJK, $char)) {
|
||||
// … and it can end a line
|
||||
if (!\preg_match($notEnd, $char)) {
|
||||
// … and the next one can start a line
|
||||
$next = \mb_substr($line, $i + 1, 1);
|
||||
if (!\preg_match($notStart, $next)) {
|
||||
// we'll pretend it's a word :)
|
||||
yield \rtrim(\mb_substr($line, 0, $i + 1));
|
||||
$line = \mb_substr($line, $i + 1);
|
||||
$len = \mb_strlen($line);
|
||||
$i = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$i++;
|
||||
} while ($i < $len);
|
||||
|
||||
yield \rtrim($line);
|
||||
}
|
||||
}
|
||||
23
vendor/psy/psysh/src/Formatter/ReflectorFormatter.php
vendored
Normal file
23
vendor/psy/psysh/src/Formatter/ReflectorFormatter.php
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
/**
|
||||
* Reflector formatter interface.
|
||||
*/
|
||||
interface ReflectorFormatter
|
||||
{
|
||||
/**
|
||||
* @param \Reflector $reflector
|
||||
*/
|
||||
public static function format(\Reflector $reflector): string;
|
||||
}
|
||||
512
vendor/psy/psysh/src/Formatter/SignatureFormatter.php
vendored
Normal file
512
vendor/psy/psysh/src/Formatter/SignatureFormatter.php
vendored
Normal file
@@ -0,0 +1,512 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Psy\Manual\ManualInterface;
|
||||
use Psy\Reflection\ReflectionConstant;
|
||||
use Psy\Reflection\ReflectionLanguageConstruct;
|
||||
use Psy\Util\Json;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* An abstract representation of a function, class or property signature.
|
||||
*/
|
||||
class SignatureFormatter implements ReflectorFormatter
|
||||
{
|
||||
private static ?ManualInterface $manual = null;
|
||||
|
||||
/**
|
||||
* Set the manual interface for generating hyperlinks.
|
||||
*/
|
||||
public static function setManual(?ManualInterface $manual): void
|
||||
{
|
||||
self::$manual = $manual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set styles for formatting hyperlinks.
|
||||
*
|
||||
* @deprecated Use LinkFormatter::setStyles() instead
|
||||
*
|
||||
* @param array $styles Map of style name to inline style string
|
||||
*/
|
||||
public static function setStyles(array $styles): void
|
||||
{
|
||||
// Delegate to LinkFormatter which now handles this
|
||||
LinkFormatter::setStyles($styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the manual database for generating hyperlinks.
|
||||
*
|
||||
* @deprecated Manual database is now set via Configuration::setManual()
|
||||
*
|
||||
* @param \PDO|null $db
|
||||
*/
|
||||
public static function setManualDb(?\PDO $db): void
|
||||
{
|
||||
// The functionality is now handled through setManual()
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a signature for the given reflector.
|
||||
*
|
||||
* Defers to subclasses to do the actual formatting.
|
||||
* Automatically generates hyperlinks if manual database is set.
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
public static function format(\Reflector $reflector): string
|
||||
{
|
||||
switch (true) {
|
||||
case $reflector instanceof \ReflectionFunction:
|
||||
case $reflector instanceof ReflectionLanguageConstruct:
|
||||
return self::formatFunction($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionClass:
|
||||
// this case also covers \ReflectionObject
|
||||
return self::formatClass($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionClassConstant:
|
||||
return self::formatClassConstant($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionMethod:
|
||||
return self::formatMethod($reflector);
|
||||
|
||||
case $reflector instanceof \ReflectionProperty:
|
||||
return self::formatProperty($reflector);
|
||||
|
||||
case $reflector instanceof ReflectionConstant:
|
||||
return self::formatConstant($reflector);
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unexpected Reflector class: '.\get_class($reflector));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the signature name.
|
||||
*
|
||||
* @param \ReflectionClass|\ReflectionClassConstant|\ReflectionFunctionAbstract $reflector
|
||||
*
|
||||
* @return string Formatted name
|
||||
*/
|
||||
public static function formatName(\Reflector $reflector): string
|
||||
{
|
||||
return $reflector->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the method, property or class modifiers.
|
||||
*
|
||||
* @param \ReflectionMethod|\ReflectionProperty|\ReflectionClass $reflector
|
||||
*
|
||||
* @return string Formatted modifiers
|
||||
*/
|
||||
private static function formatModifiers(\Reflector $reflector): string
|
||||
{
|
||||
return \implode(' ', \array_map(function ($modifier) {
|
||||
return \sprintf('<keyword>%s</keyword>', $modifier);
|
||||
}, \Reflection::getModifierNames($reflector->getModifiers())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a class signature.
|
||||
*
|
||||
* @param \ReflectionClass $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatClass(\ReflectionClass $reflector): string
|
||||
{
|
||||
$chunks = [];
|
||||
|
||||
if ($modifiers = self::formatModifiers($reflector)) {
|
||||
$chunks[] = $modifiers;
|
||||
}
|
||||
|
||||
if ($reflector->isTrait()) {
|
||||
$chunks[] = 'trait';
|
||||
} else {
|
||||
$chunks[] = $reflector->isInterface() ? 'interface' : 'class';
|
||||
}
|
||||
|
||||
$chunks[] = LinkFormatter::styleWithHref('class', self::formatName($reflector), self::getManualHref($reflector));
|
||||
|
||||
if ($parent = $reflector->getParentClass()) {
|
||||
$chunks[] = 'extends';
|
||||
$parentHref = self::getManualHref($parent);
|
||||
$chunks[] = LinkFormatter::styleWithHref('class', $parent->getName(), $parentHref);
|
||||
}
|
||||
|
||||
$interfaces = $reflector->getInterfaceNames();
|
||||
if (!empty($interfaces)) {
|
||||
\sort($interfaces);
|
||||
|
||||
$chunks[] = $reflector->isInterface() ? 'extends' : 'implements';
|
||||
$chunks[] = \implode(', ', \array_map(function ($name) {
|
||||
try {
|
||||
$interfaceHref = self::getManualHref(new \ReflectionClass($name));
|
||||
} catch (\ReflectionException $e) {
|
||||
$interfaceHref = null;
|
||||
}
|
||||
|
||||
return LinkFormatter::styleWithHref('class', $name, $interfaceHref);
|
||||
}, $interfaces));
|
||||
}
|
||||
|
||||
return \implode(' ', $chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a constant signature.
|
||||
*
|
||||
* @param \ReflectionClassConstant $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatClassConstant($reflector): string
|
||||
{
|
||||
$value = $reflector->getValue();
|
||||
$style = self::getTypeStyle($value);
|
||||
|
||||
return \sprintf(
|
||||
'<keyword>const</keyword> %s = <%s>%s</%s>',
|
||||
LinkFormatter::styleWithHref('const', self::formatName($reflector), self::getManualHref($reflector)),
|
||||
$style,
|
||||
OutputFormatter::escape(Json::encode($value)),
|
||||
$style
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a constant signature.
|
||||
*
|
||||
* @param ReflectionConstant $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatConstant(ReflectionConstant $reflector): string
|
||||
{
|
||||
$value = $reflector->getValue();
|
||||
$style = self::getTypeStyle($value);
|
||||
|
||||
return \sprintf(
|
||||
'<keyword>define</keyword>(<string>%s</string>, <%s>%s</%s>)',
|
||||
OutputFormatter::escape(Json::encode($reflector->getName())),
|
||||
$style,
|
||||
OutputFormatter::escape(Json::encode($value)),
|
||||
$style
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getting output style for a given value's type.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function getTypeStyle($value): string
|
||||
{
|
||||
if (\is_int($value) || \is_float($value)) {
|
||||
return 'number';
|
||||
} elseif (\is_string($value)) {
|
||||
return 'string';
|
||||
} elseif (\is_bool($value) || $value === null) {
|
||||
return 'bool';
|
||||
} else {
|
||||
return 'strong'; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a property signature.
|
||||
*
|
||||
* @param \ReflectionProperty $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatProperty(\ReflectionProperty $reflector): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s <strong>$%s</strong>',
|
||||
self::formatModifiers($reflector),
|
||||
$reflector->getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a function signature.
|
||||
*
|
||||
* @param \ReflectionFunction $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatFunction(\ReflectionFunctionAbstract $reflector): string
|
||||
{
|
||||
return \sprintf(
|
||||
'<keyword>function</keyword> %s%s(%s)%s',
|
||||
$reflector->returnsReference() ? '&' : '',
|
||||
LinkFormatter::styleWithHref('function', self::formatName($reflector), self::getManualHref($reflector)),
|
||||
\implode(', ', self::formatFunctionParams($reflector)),
|
||||
self::formatFunctionReturnType($reflector)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a function signature's return type (if available).
|
||||
*
|
||||
* @param \ReflectionFunctionAbstract $reflector
|
||||
*
|
||||
* @return string Formatted return type
|
||||
*/
|
||||
private static function formatFunctionReturnType(\ReflectionFunctionAbstract $reflector): string
|
||||
{
|
||||
if (!\method_exists($reflector, 'hasReturnType') || !$reflector->hasReturnType()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \sprintf(': %s', self::formatReflectionType($reflector->getReturnType(), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a method signature.
|
||||
*
|
||||
* @param \ReflectionMethod $reflector
|
||||
*
|
||||
* @return string Formatted signature
|
||||
*/
|
||||
private static function formatMethod(\ReflectionMethod $reflector): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s %s',
|
||||
self::formatModifiers($reflector),
|
||||
self::formatFunction($reflector)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the function params.
|
||||
*
|
||||
* @param \ReflectionFunctionAbstract $reflector
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector): array
|
||||
{
|
||||
$params = [];
|
||||
foreach ($reflector->getParameters() as $param) {
|
||||
$hint = '';
|
||||
try {
|
||||
if (\method_exists($param, 'getType')) {
|
||||
// Only include the inquisitive nullable type iff param default value is not null.
|
||||
$defaultIsNull = $param->isOptional() && $param->isDefaultValueAvailable() && @$param->getDefaultValue() === null;
|
||||
$hint = self::formatReflectionType($param->getType(), !$defaultIsNull);
|
||||
} else {
|
||||
if ($param->isArray()) {
|
||||
$hint = '<keyword>array</keyword>';
|
||||
} elseif ($class = $param->getClass()) {
|
||||
$hint = LinkFormatter::styleWithHref('class', $class->getName(), self::getManualHref($class));
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// sometimes we just don't know...
|
||||
// bad class names, or autoloaded classes that haven't been loaded yet, or whathaveyou.
|
||||
// come to think of it, the only time I've seen this is with the intl extension.
|
||||
|
||||
// Hax: we'll try to extract it :P
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
$chunks = \explode('$'.$param->getName(), (string) $param);
|
||||
$chunks = \explode(' ', \trim($chunks[0]));
|
||||
$guess = \end($chunks);
|
||||
|
||||
$hint = \sprintf('<urgent>%s</urgent>', OutputFormatter::escape($guess));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($param->isOptional()) {
|
||||
if (!$param->isDefaultValueAvailable()) {
|
||||
$value = 'unknown';
|
||||
$typeStyle = 'urgent';
|
||||
} else {
|
||||
$value = @$param->getDefaultValue();
|
||||
$typeStyle = self::getTypeStyle($value);
|
||||
$value = \is_array($value) ? '[]' : ($value === null ? 'null' : \var_export($value, true));
|
||||
}
|
||||
$default = \sprintf(' = <%s>%s</%s>', $typeStyle, OutputFormatter::escape($value), $typeStyle);
|
||||
} else {
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$params[] = \sprintf(
|
||||
'%s%s%s<strong>$%s</strong>%s',
|
||||
$param->isPassedByReference() ? '&' : '',
|
||||
$hint,
|
||||
$hint !== '' ? ' ' : '',
|
||||
$param->getName(),
|
||||
$default
|
||||
);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print function param or return type(s).
|
||||
*
|
||||
* @param \ReflectionType|null $type
|
||||
*/
|
||||
private static function formatReflectionType(?\ReflectionType $type, bool $indicateNullable): string
|
||||
{
|
||||
if ($type === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
$delimeter = '|';
|
||||
} elseif ($type instanceof \ReflectionIntersectionType) {
|
||||
$delimeter = '&';
|
||||
} else {
|
||||
return self::formatReflectionNamedType($type, $indicateNullable);
|
||||
}
|
||||
|
||||
$formattedTypes = [];
|
||||
foreach ($type->getTypes() as $namedType) {
|
||||
$formattedTypes[] = self::formatReflectionNamedType($namedType, $indicateNullable);
|
||||
}
|
||||
|
||||
return \implode($delimeter, $formattedTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a single named type.
|
||||
*/
|
||||
private static function formatReflectionNamedType(\ReflectionNamedType $type, bool $indicateNullable): string
|
||||
{
|
||||
$nullable = $indicateNullable && $type->allowsNull() ? '?' : '';
|
||||
$typeName = $type->getName();
|
||||
|
||||
if ($type->isBuiltin()) {
|
||||
return \sprintf('<keyword>%s%s</keyword>', $nullable, OutputFormatter::escape($typeName));
|
||||
}
|
||||
|
||||
// Non-builtin type is a class - try to get href for it
|
||||
$href = null;
|
||||
try {
|
||||
$classReflector = new \ReflectionClass($typeName);
|
||||
$href = self::getManualHref($classReflector);
|
||||
} catch (\ReflectionException $e) {
|
||||
// Class doesn't exist or can't be reflected, no href
|
||||
}
|
||||
|
||||
return $nullable.LinkFormatter::styleWithHref('class', $typeName, $href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text in a style tag, optionally including an href.
|
||||
*
|
||||
* @deprecated use LinkFormatter::styleWithHref directly
|
||||
*
|
||||
* @param string $style The style name (e.g., 'class', 'function')
|
||||
* @param string $text The text to wrap
|
||||
* @param string|null $href Optional hyperlink URL
|
||||
*
|
||||
* @return string Formatted text with style and optional href
|
||||
*/
|
||||
private static function styleWithHref(string $style, string $text, ?string $href = null): string
|
||||
{
|
||||
return LinkFormatter::styleWithHref($style, $text, $href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hyperlink URL for a reflector if it's in the PHP manual.
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*
|
||||
* @return string|null URL to php.net or null if not in manual
|
||||
*/
|
||||
private static function getManualHref(\Reflector $reflector): ?string
|
||||
{
|
||||
// If it's not in the manual, assume it's not on php.net
|
||||
if (!self::getManualDoc($reflector)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (\get_class($reflector)) {
|
||||
case \ReflectionClass::class:
|
||||
case \ReflectionObject::class:
|
||||
case \ReflectionFunction::class:
|
||||
$query = $reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionMethod::class:
|
||||
$query = $reflector->class.'.'.$reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionProperty::class:
|
||||
case \ReflectionClassConstant::class:
|
||||
// No simple redirect URLs for properties/constants, link to class instead
|
||||
$query = $reflector->class;
|
||||
break;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return \sprintf('https://php.net/%s', $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manual documentation for a reflector.
|
||||
*
|
||||
* @param \Reflector $reflector
|
||||
*
|
||||
* @return string|array|false Documentation string or structured data, or false if not found
|
||||
*/
|
||||
private static function getManualDoc(\Reflector $reflector)
|
||||
{
|
||||
if (!self::$manual) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (\get_class($reflector)) {
|
||||
case \ReflectionClass::class:
|
||||
case \ReflectionObject::class:
|
||||
case \ReflectionFunction::class:
|
||||
$id = $reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionMethod::class:
|
||||
$id = $reflector->class.'::'.$reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionProperty::class:
|
||||
$id = $reflector->class.'::$'.$reflector->name;
|
||||
break;
|
||||
|
||||
case \ReflectionClassConstant::class:
|
||||
$id = $reflector->class.'::'.$reflector->name;
|
||||
break;
|
||||
|
||||
case ReflectionConstant::class:
|
||||
$id = $reflector->name;
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::$manual->get($id) ?? false;
|
||||
}
|
||||
}
|
||||
96
vendor/psy/psysh/src/Formatter/TraceFormatter.php
vendored
Normal file
96
vendor/psy/psysh/src/Formatter/TraceFormatter.php
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Psy Shell.
|
||||
*
|
||||
* (c) 2012-2025 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Psy\Formatter;
|
||||
|
||||
use Psy\Input\FilterOptions;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
|
||||
/**
|
||||
* Output formatter for exception traces.
|
||||
*/
|
||||
class TraceFormatter
|
||||
{
|
||||
/**
|
||||
* Format the trace of the given exception.
|
||||
*
|
||||
* @param \Throwable $throwable The error or exception with a backtrace
|
||||
* @param FilterOptions|null $filter (default: null)
|
||||
* @param int|null $count (default: PHP_INT_MAX)
|
||||
* @param bool $includePsy (default: true)
|
||||
*
|
||||
* @return string[] Formatted stacktrace lines
|
||||
*/
|
||||
public static function formatTrace(\Throwable $throwable, ?FilterOptions $filter = null, ?int $count = null, bool $includePsy = true): array
|
||||
{
|
||||
if ($cwd = \getcwd()) {
|
||||
$cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
if ($count === null) {
|
||||
$count = \PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
|
||||
$trace = $throwable->getTrace();
|
||||
\array_unshift($trace, [
|
||||
'function' => '',
|
||||
'file' => $throwable->getFile() !== null ? $throwable->getFile() : 'n/a',
|
||||
'line' => $throwable->getLine() !== null ? $throwable->getLine() : 'n/a',
|
||||
'args' => [],
|
||||
]);
|
||||
|
||||
if (!$includePsy) {
|
||||
for ($i = \count($trace) - 1; $i >= 0; $i--) {
|
||||
$thing = isset($trace[$i]['class']) ? $trace[$i]['class'] : $trace[$i]['function'];
|
||||
if (\preg_match('/\\\\?Psy\\\\/', $thing)) {
|
||||
$trace = \array_slice($trace, $i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0, $count = \min($count, \count($trace)); $i < $count; $i++) {
|
||||
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
|
||||
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
|
||||
$function = $trace[$i]['function'];
|
||||
$file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
|
||||
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
|
||||
|
||||
// Make file paths relative to cwd
|
||||
if ($cwd !== false) {
|
||||
$file = \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file);
|
||||
}
|
||||
|
||||
// Leave execution loop out of the `eval()'d code` lines
|
||||
if (\preg_match("#/src/Execution(?:Loop)?Closure.php\(\d+\) : eval\(\)'d code$#", \str_replace('\\', '/', $file))) {
|
||||
$file = "eval()'d code";
|
||||
}
|
||||
|
||||
// Skip any lines that don't match our filter options
|
||||
if ($filter !== null && !$filter->match(\sprintf('%s%s%s() at %s:%s', $class, $type, $function, $file, $line))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = \sprintf(
|
||||
' <class>%s</class>%s%s() at <info>%s:%s</info>',
|
||||
OutputFormatter::escape($class),
|
||||
OutputFormatter::escape($type),
|
||||
OutputFormatter::escape($function),
|
||||
OutputFormatter::escape($file),
|
||||
OutputFormatter::escape($line)
|
||||
);
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user