refactor: Cleanup git state - commit all staged changes

Major refactoring cleanup:
- Add new controller architecture (class-controller-*.php)
- Add new settings-v2 UI (views/settings-v2/)
- Add new CSS architecture (agentic-sidebar.css, tokens)
- Add esbuild build pipeline (scripts/build.js, package.json)
- Add composer dependencies (vendor/)
- Add frontend src directory (assets/js/src/index.jsx)
- Add documentation files
- Remove old/obsolete files (class-settings.php, old CSS)

This commits all pending changes from previous refactoring efforts.
This commit is contained in:
Dwindi Ramadhana
2026-06-17 05:27:58 +07:00
parent d3f142222c
commit 690991c526
7963 changed files with 941566 additions and 67372 deletions

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
use Flyfinder\Specification\CompositeSpecification;
use Flyfinder\Specification\SpecificationInterface;
use Generator;
use League\Flysystem\File;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\PluginInterface;
/**
* Flysystem plugin to add file finding capabilities to the filesystem entity.
*
* Note that found *directories* are **not** returned... only found *files*.
*/
class Finder implements PluginInterface
{
/**
* @var FilesystemInterface
* @psalm-suppress PropertyNotSetInConstructor
*/
private $filesystem;
/**
* Get the method name.
*/
public function getMethod() : string
{
return 'find';
}
/**
* Set the Filesystem object.
*/
public function setFilesystem(FilesystemInterface $filesystem) : void
{
$this->filesystem = $filesystem;
}
/**
* Find the specified files
*
* Note that only found *files* are yielded at this level,
* which go back to the caller.
*
* @see File
*
* @return Generator<mixed>
*/
public function handle(SpecificationInterface $specification) : Generator
{
foreach ($this->yieldFilesInPath($specification, '') as $path) {
if (isset($path['type']) && $path['type'] === 'file') {
yield $path;
}
}
}
/**
* Recursively yield files that meet the specification
*
* Note that directories are also yielded at this level,
* since they have to be recursed into. Yielded directories
* will not make their way back to the caller, as they are filtered out
* by {@link handle()}.
*
* @return Generator<mixed>
*
* @psalm-return Generator<array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string}>
*/
private function yieldFilesInPath(SpecificationInterface $specification, string $path) : Generator
{
$listContents = $this->filesystem->listContents($path);
/** @psalm-var array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $location */
foreach ($listContents as $location) {
if ($specification->isSatisfiedBy($location)) {
yield $location;
}
if ($location['type'] !== 'dir'
|| !CompositeSpecification::thatCanBeSatisfiedBySomethingBelow($specification, $location)
) {
continue;
}
yield from $this->yieldFilesInPath($specification, $location['path']);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* phpDocumentor
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder;
/**
* Value Object for paths.
* This can be absolute or relative.
*/
final class Path
{
/**
* file path
*
* @var string
*/
private $path;
/**
* Initializes the path.
*/
public function __construct(string $path)
{
$this->path = $path;
}
/**
* returns a string representation of the path.
*/
public function __toString() : string
{
return $this->path;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class AndSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $one;
/** @var SpecificationInterface */
private $other;
/**
* Initializes the AndSpecification object
*/
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return $this->one->isSatisfiedBy($value) && $this->other->isSatisfiedBy($value);
}
/** {@inheritDoc} */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return self::thatCanBeSatisfiedBySomethingBelow($this->one, $value)
&& self::thatCanBeSatisfiedBySomethingBelow($this->other, $value);
}
/** {@inheritDoc} */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return self::thatWillBeSatisfiedByEverythingBelow($this->one, $value)
&& self::thatWillBeSatisfiedByEverythingBelow($this->other, $value);
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* Base class for specifications, allows for combining specifications
*
* @psalm-immutable
*/
abstract class CompositeSpecification implements SpecificationInterface, PrunableInterface
{
/**
* Returns a specification that satisfies the original specification
* as well as the other specification
*/
public function andSpecification(SpecificationInterface $other) : AndSpecification
{
return new AndSpecification($this, $other);
}
/**
* Returns a specification that satisfies the original specification
* or the other specification
*/
public function orSpecification(SpecificationInterface $other) : OrSpecification
{
return new OrSpecification($this, $other);
}
/**
* Returns a specification that is the inverse of the original specification
* i.e. does not meet the original criteria
*/
public function notSpecification() : NotSpecification
{
return new NotSpecification($this);
}
/** {@inheritDoc} */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return true;
}
/** {@inheritDoc} */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return false;
}
/**
* Provide default {@see canBeSatisfiedBySomethingBelow()} logic for specification classes
* that don't implement PrunableInterface
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
* @psalm-mutation-free
*/
public static function thatCanBeSatisfiedBySomethingBelow(SpecificationInterface $that, array $value) : bool
{
return $that instanceof PrunableInterface
? $that->canBeSatisfiedBySomethingBelow($value)
: true;
}
/**
* Provide default {@see willBeSatisfiedByEverythingBelow()} logic for specification classes
* that don't implement PrunableInterface
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
* @psalm-mutation-free
*/
public static function thatWillBeSatisfiedByEverythingBelow(SpecificationInterface $that, array $value) : bool
{
return $that instanceof PrunableInterface
&& $that->willBeSatisfiedByEverythingBelow($value);
}
}

View File

@@ -0,0 +1,392 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* Many thanks to webmozart by providing the original code in webmozart/glob
*
* @link https://github.com/webmozart/glob/blob/master/src/Glob.php
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use InvalidArgumentException;
use function array_slice;
use function count;
use function explode;
use function implode;
use function max;
use function min;
use function preg_match;
use function rtrim;
use function sprintf;
use function strlen;
use function strpos;
use function substr;
/**
* Glob specification class
*
* @psalm-immutable
*/
final class Glob extends CompositeSpecification
{
/** @var string */
private $regex;
/**
* The "static prefix" is the part of the glob up to the first wildcard "*".
* If the glob does not contain wildcards, the full glob is returned.
*
* @var string
*/
private $staticPrefix;
/**
* The "bounded prefix" is the part of the glob up to the first recursive wildcard "**".
* It is the longest prefix for which the number of directory segments in the partial match
* is known. If the glob does not contain the recursive wildcard "**", the full glob is returned.
*
* @var string
*/
private $boundedPrefix;
/**
* The "total prefix" is the part of the glob before the trailing catch-all wildcard sequence if the glob
* ends with one, otherwise null. It is needed for implementing the A-quantifier pruning hint.
*
* @var string|null
*/
private $totalPrefix;
public function __construct(string $glob)
{
$this->regex = self::toRegEx($glob);
$this->staticPrefix = self::getStaticPrefix($glob);
$this->boundedPrefix = self::getBoundedPrefix($glob);
$this->totalPrefix = self::getTotalPrefix($glob);
}
/**
* @inheritDoc
*/
public function isSatisfiedBy(array $value) : bool
{
//Flysystem paths are not absolute, so make it that way.
$path = '/' . $value['path'];
if (strpos($path, $this->staticPrefix) !== 0) {
return false;
}
return preg_match($this->regex, $path) === 1;
}
/**
* Returns the static prefix of a glob.
*
* The "static prefix" is the part of the glob up to the first wildcard "*".
* If the glob does not contain wildcards, the full glob is returned.
*
* @param string $glob The canonical glob. The glob should contain forward
* slashes as directory separators only. It must not
* contain any "." or ".." segments.
*
* @return string The static prefix of the glob.
*
* @psalm-pure
*/
private static function getStaticPrefix(string $glob) : string
{
self::assertValidGlob($glob);
$prefix = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '/':
$prefix .= '/';
if (self::isRecursiveWildcard($glob, $i)) {
break 2;
}
break;
case '*':
case '?':
case '{':
case '[':
break 2;
case '\\':
[$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i);
$prefix .= $unescaped;
$i += $consumedChars;
break;
default:
$prefix .= $c;
break;
}
}
return $prefix;
}
private static function getBoundedPrefix(string $glob) : string
{
self::assertValidGlob($glob);
$prefix = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '/':
$prefix .= '/';
if (self::isRecursiveWildcard($glob, $i)) {
break 2;
}
break;
case '\\':
[$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i);
$prefix .= $unescaped;
$i += $consumedChars;
break;
default:
$prefix .= $c;
break;
}
}
return $prefix;
}
private static function getTotalPrefix(string $glob) : ?string
{
self::assertValidGlob($glob);
$matches = [];
return preg_match('~(?<!\\\\)/\\*\\*(?:/\\*\\*?)+$~', $glob, $matches)
? substr($glob, 0, strlen($glob)-strlen($matches[0]))
: null;
}
/**
* @return mixed[]
*
* @psalm-return array{0: string, 1:int}
* @psalm-pure
*/
private static function scanBackslashSequence(string $glob, int $offset) : array
{
$startOffset = $offset;
$result = '';
switch ($c = $glob[$offset + 1] ?? '') {
case '*':
case '?':
case '{':
case '}':
case '[':
case ']':
case '-':
case '^':
case '$':
case '~':
case '\\':
$result .= $c;
++$offset;
break;
default:
$result .= '\\';
}
return [$result, $offset - $startOffset];
}
/**
* Asserts that glob is well formed
*
* @psalm-pure
*/
private static function assertValidGlob(string $glob) : void
{
if (strpos($glob, '/') !== 0 && strpos($glob, '://') === false) {
throw new InvalidArgumentException(sprintf(
'The glob "%s" is not absolute and not a URI.',
$glob
));
}
}
/**
* Checks if the current position the glob is start of a Recursive directory wildcard
*
* @psalm-pure
*/
private static function isRecursiveWildcard(string $glob, int $i) : bool
{
return isset($glob[$i + 3]) && $glob[$i + 1] . $glob[$i + 2] . $glob[$i + 3] === '**/';
}
/**
* Converts a glob to a regular expression.
*
* @param string $glob The canonical glob. The glob should contain forward
* slashes as directory separators only. It must not
* contain any "." or ".." segments.
*
* @return string The regular expression for matching the glob.
*
* @psalm-pure
*/
private static function toRegEx(string $glob) : string
{
$delimiter = '~';
$inSquare = false;
$curlyLevels = 0;
$regex = '';
$length = strlen($glob);
for ($i = 0; $i < $length; ++$i) {
$c = $glob[$i];
switch ($c) {
case '.':
case '(':
case ')':
case '|':
case '+':
case '^':
case '$':
case $delimiter:
$regex .= '\\' . $c;
break;
case '/':
if (self::isRecursiveWildcard($glob, $i)) {
$regex .= '/([^/]+/)*';
$i += 3;
} else {
$regex .= '/';
}
break;
case '*':
$regex .= '[^/]*';
break;
case '?':
$regex .= '.';
break;
case '{':
$regex .= '(';
++$curlyLevels;
break;
case '}':
if ($curlyLevels > 0) {
$regex .= ')';
--$curlyLevels;
} else {
$regex .= '}';
}
break;
case ',':
$regex .= $curlyLevels > 0 ? '|' : ',';
break;
case '[':
$regex .= '[';
$inSquare = true;
if (isset($glob[$i + 1]) && $glob[$i + 1] === '^') {
$regex .= '^';
++$i;
}
break;
case ']':
$regex .= $inSquare ? ']' : '\\]';
$inSquare = false;
break;
case '-':
$regex .= $inSquare ? '-' : '\\-';
break;
case '\\':
if (isset($glob[$i + 1])) {
switch ($glob[$i + 1]) {
case '*':
case '?':
case '{':
case '}':
case '[':
case ']':
case '-':
case '^':
case '$':
case '~':
case '\\':
$regex .= '\\' . $glob[$i + 1];
++$i;
break;
default:
$regex .= '\\\\';
}
}
break;
default:
$regex .= $c;
break;
}
}
if ($inSquare) {
throw new InvalidArgumentException(sprintf(
'Invalid glob: missing ] in %s',
$glob
));
}
if ($curlyLevels > 0) {
throw new InvalidArgumentException(sprintf(
'Invalid glob: missing } in %s',
$glob
));
}
return $delimiter . '^' . $regex . '$' . $delimiter;
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
$valueSegments = explode('/', '/' . $value['path']);
$boundedPrefixSegments = explode('/', rtrim($this->boundedPrefix, '/'));
$howManySegmentsToConsider = min(count($valueSegments), count($boundedPrefixSegments));
$boundedPrefixGlob = implode('/', array_slice($boundedPrefixSegments, 0, $howManySegmentsToConsider));
$valuePathPrefix = implode('/', array_slice($valueSegments, 1, max($howManySegmentsToConsider-1, 0)));
$prefixValue = $value;
$prefixValue['path'] = $valuePathPrefix;
if ($boundedPrefixGlob === '') {
$boundedPrefixGlob = '/';
}
$spec = new Glob($boundedPrefixGlob);
return $spec->isSatisfiedBy($prefixValue);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
if ($this->totalPrefix === null) {
return false;
}
$spec = new Glob(rtrim($this->totalPrefix, '/') . '/**/*');
$terminatedValue = $value;
$terminatedValue['path'] = rtrim($terminatedValue['path'], '/') . '/x/x';
return $spec->isSatisfiedBy($terminatedValue);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use function in_array;
/**
* Files and directories meet the specification if they have the given extension
*
* @psalm-immutable
*/
class HasExtension extends CompositeSpecification
{
/** @var string[] */
private $extensions;
/**
* Receives the file extensions you want to find
*
* @param string[] $extensions
*/
public function __construct(array $extensions)
{
$this->extensions = $extensions;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return isset($value['extension']) && in_array($value['extension'], $this->extensions, false);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use Flyfinder\Path;
use function array_slice;
use function count;
use function explode;
use function implode;
use function in_array;
use function min;
use function preg_match;
use function str_replace;
/**
* Files *and directories* meet the specification if they are in the given path.
* Note this behavior is different than in Finder, in that directories *can* meet the spec,
* whereas Finder would never return a directory as "found".
*
* @psalm-immutable
*/
class InPath extends CompositeSpecification
{
/** @var Path */
private $path;
/**
* Initializes the InPath specification
*/
public function __construct(Path $path)
{
$this->path = $path;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
if (in_array($this->path, ['', '.', './'], false)) {
/*
* since flysystem stuff is always relative to the filesystem object's root,
* a spec of "current" dir should always be a match anything being considered
*/
return true;
}
$path = (string) $this->path;
$validChars = '[a-zA-Z0-9\\\/\.\<\>\,\|\:\(\)\&\;\#]';
/*
* a FILE spec would have to match on 'path',
* e.g. value path 'src/Cilex/Provider/MonologServiceProvider.php' should match FILE spec of same path...
* this should also hit a perfect DIR=DIR_SPEC match,
* e.g. value path 'src/Cilex/Provider' should match DIR spec of 'src/Cilex/Provider'
*/
if (isset($value['path'])) {
$pattern = '(^(?!\/)'
. str_replace(['?', '*'], [$validChars . '{1}', $validChars . '*'], $path)
. '(?:/' . $validChars . '*)?$)';
if (preg_match($pattern, $value['path'])) {
return true;
}
}
/* a DIR spec that wasn't an exact match should be able to match on dirname,
* e.g. value dirname 'src' of path 'src/Cilex' should match DIR spec of 'src'
*/
if (isset($value['dirname'])) {
$pattern = '(^(?!\/)'
. str_replace(['?', '*'], [$validChars . '{1}', $validChars . '*'], $path . '/')
. $validChars . '*)';
if (preg_match($pattern, $value['dirname'] . '/')) {
return true;
}
}
return false;
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
$pathSegments = explode('/', (string) $this->path);
$valueSegments = explode('/', $value['path']);
$pathPrefixSegments = array_slice($pathSegments, 0, min(count($pathSegments), count($valueSegments)));
$spec = new InPath(new Path(implode('/', $pathPrefixSegments)));
return $spec->isSatisfiedBy($value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return $this->isSatisfiedBy($value);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
use function substr;
/**
* Files or directories meet the specification if they are hidden
*
* @psalm-immutable
*/
class IsHidden extends CompositeSpecification
{
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return isset($value['basename']) && substr($value['basename'], 0, 1) === '.';
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class NotSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $wrapped;
/**
* Initializes the NotSpecification object
*/
public function __construct(SpecificationInterface $wrapped)
{
$this->wrapped = $wrapped;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return !$this->wrapped->isSatisfiedBy($value);
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return !self::thatWillBeSatisfiedByEverythingBelow($this->wrapped, $value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return !self::thatCanBeSatisfiedBySomethingBelow($this->wrapped, $value);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* @psalm-immutable
*/
final class OrSpecification extends CompositeSpecification
{
/** @var SpecificationInterface */
private $one;
/** @var SpecificationInterface */
private $other;
/**
* Initializes the OrSpecification object
*/
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(array $value) : bool
{
return $this->one->isSatisfiedBy($value) || $this->other->isSatisfiedBy($value);
}
/** @inheritDoc */
public function canBeSatisfiedBySomethingBelow(array $value) : bool
{
return self::thatCanBeSatisfiedBySomethingBelow($this->one, $value)
|| self::thatCanBeSatisfiedBySomethingBelow($this->other, $value);
}
/** @inheritDoc */
public function willBeSatisfiedByEverythingBelow(array $value) : bool
{
return self::thatWillBeSatisfiedByEverythingBelow($this->one, $value)
|| self::thatWillBeSatisfiedByEverythingBelow($this->other, $value);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Flyfinder\Specification;
/**
* Interface PrunableInterface
*
* @psalm-immutable
*/
interface PrunableInterface
{
/**
* Checks if anything under the directory path in value can possibly satisfy the specification.
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function canBeSatisfiedBySomethingBelow(array $value) : bool;
/**
* Returns true if it is known or can be deduced that everything under the directory path in value
* will certainly satisfy the specification.
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function willBeSatisfiedByEverythingBelow(array $value) : bool;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of phpDocumentor.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @link http://phpdoc.org
*/
namespace Flyfinder\Specification;
/**
* Interface for FlyFinder specifications
*
* @psalm-immutable
*/
interface SpecificationInterface
{
/**
* Checks if the value meets the specification
*
* @param mixed[] $value
*
* @psalm-param array{basename: string, path: string, stream: resource, dirname: string, type: string, extension: string} $value
*/
public function isSatisfiedBy(array $value) : bool;
}