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,128 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\alphaChar;
use function Parsica\Parsica\char;
use function Parsica\Parsica\digitChar;
use function Parsica\Parsica\either;
use function Parsica\Parsica\eof;
use function Parsica\Parsica\ignore;
use function Parsica\Parsica\keepFirst;
use function Parsica\Parsica\many;
use function Parsica\Parsica\punctuationChar;
use function Parsica\Parsica\some;
use function Parsica\Parsica\string;
final class AlternativeTest extends TestCase
{
use ParserAssertions;
/** @test */
public function or()
{
$parser = char('a')->or(char('b'));
$this->assertParses("a123", $parser, "a");
$this->assertParses("b123", $parser, "b");
$this->assertParseFails("123", $parser);
}
/** @test */
public function alternatives_for_strings_with_similar_starts()
{
$jan =
either(
string("Jan")->thenIgnore(eof()),
string("January")->thenIgnore(eof()),
);
$this->assertParses("Jan", $jan, "Jan");
$this->assertParses("January", $jan, "January");
// Reverse order
$jan =
either(
string("January")->thenIgnore(eof()),
string("Jan")->thenIgnore(eof()),
);
$this->assertParses("Jan", $jan, "Jan");
$this->assertParses("January", $jan, "January");
}
/** @test */
public function or_order_matters()
{
// The order of clauses in an or() matters. If we do the following parser definition, the parser will consume
// "http", even if the strings starts with "https", leaving "s://..." as the remainder.
$parser = string('http')->or(string('https'));
$input = "https://verraes.net";
$this->assertRemainder($input, $parser, "s://verraes.net");
// The solution is to consider the order of or clauses:
$parser = string('https')->or(string('http'));
$input = "https://verraes.net";
$this->assertParses($input, $parser, "https");
$this->assertRemainder($input, $parser, "://verraes.net");
}
/** @test */
public function optional()
{
$parser = char('a')->optional();
$this->assertParses("", $parser, null, "EOF");
$this->assertParses("abc", $parser, "a");
$this->assertRemainder("abc", $parser, "bc");
$this->assertParses("bc", $parser, null);
$this->assertRemainder("bc", $parser, "bc");
}
/** @test */
public function many()
{
$parser = many(alphaChar());
$this->assertParses("123", $parser, []);
$this->assertParses("Hello", $parser, ["H", "e", "l", "l", "o"]);
$parser = many(alphaChar()->append(digitChar()));
$this->assertParses("1a2b3c", $parser, []);
$this->assertParses("a1b2c3", $parser, ["a1", "b2", "c3"]);
}
/** @test */
public function some()
{
$parser = many(
keepFirst(
some(alphaChar())->map(fn($a) => implode('', $a)),
punctuationChar()->optional()
)
);
$input = "abc,def,ghi";
$expected = ["abc","def","ghi"];
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function some_2()
{
$parser = some(string("foo"));
$this->assertParseFails("bla", $parser);
$this->assertParses("foo", $parser, ["foo"]);
$this->assertParses("foobar", $parser, ["foo"]);
$this->assertParses("foofoo", $parser, ["foo", "foo"]);
$this->assertParses("foofoobar", $parser, ["foo", "foo"]);
}
}

View File

@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use Exception;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
final class AppendTest extends TestCase
{
use ParserAssertions;
/** @test */
public function append_strings()
{
$parser = char('a')->append(char('b'));
$this->assertParses("abc", $parser, "ab");
}
/** @test */
public function append_array()
{
$a = char('a')->map(fn($x) => [$x]);
$b = char('b')->map(fn($x) => [$x]);
$this->assertParses("abc", $a->append($b), ['a', 'b']);
}
/** @test */
public function append_non_semigroup()
{
$a = char('a')->map(fn($v)=> new NotASemigroup($v));
$b = char('b')->map(fn($v)=> new NotASemigroup($v));
$this->expectException(Exception::class);
$a->append($b)->run(new StringStream('abc'));
}
}
final class NotASemigroup
{
public function __construct($_)
{
}
}

View File

@@ -0,0 +1,261 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use \InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{alphaChar,
anything,
atLeastOne,
char,
Curry\curry,
digitChar,
keepFirst,
keepSecond,
pure,
repeat,
repeatList,
sepBy,
sepBy1,
sepBy2,
skipSpace,
string};
final class ApplicativeTest extends TestCase
{
use ParserAssertions;
/** @test */
public function pure()
{
$parser = pure("<3");
$this->assertParses("(╯°□°)╯", $parser, "<3");
}
/** @test */
public function sequential_application()
{
$upper = pure(fn(string $v) => strtoupper($v));
$hello = string('hello');
// Parser<callable(a):b> -> Parser<a> -> Parser<b>
$parser = $upper->apply($hello);
$this->assertParses("hello", $parser, "HELLO");
}
/** @test */
public function sequential_application_2()
{
$multiply = curry(fn($x, $y) => $x * $y);
$number = digitChar()->map(fn($s) => intval($s));
// Parser<callable(a, b):c> -> Parser<a> -> Parser<b> -> Parser<c>
$parser = pure($multiply)->apply($number)->apply($number);
$input = "35";
$this->assertParses($input, $parser, 15);
}
/** @test */
public function sequential_application_3()
{
$sort3 = curry(function($x, $y, $z) {
$arr = [$x, $y, $z];
sort($arr);
return implode('', $arr);
});
$parser = pure($sort3)->apply(anything())->apply(anything())->apply(anything());
$this->assertParses("735", $parser, "357");
$this->assertParses("cba", $parser, "abc");
}
/** @test */
public function sequential_application_throws_when_not_a_callable()
{
$parser = pure("ceci n'est pas un callable")->apply(anything());
$this->expectException(InvalidArgumentException::class);
$parser->tryString("foo");
}
/** @test */
public function keepFirst()
{
$parser = keepFirst(char('a'), char('b'));
$this->assertParses("abc", $parser, "a");
$this->assertRemainder("abc", $parser, "c");
$this->assertParseFails("ac", $parser);
}
/** @test */
public function keepFirst_with_ignore()
{
$parser = keepFirst(char('a'), skipSpace());
$this->assertParses("a ", $parser, "a");
}
/** @test */
public function keepSecond()
{
$parser = keepSecond(char('a'), char('b'));
$this->assertParses("abc", $parser, "b");
$this->assertRemainder("abc", $parser, "c");
$this->assertParseFails("ac", $parser);
}
/** @test */
public function sepBy()
{
$parser = sepBy(string('||'), atLeastOne(alphaChar()));
$input = "";
$expected = [];
$this->assertParses($input, $parser, $expected);
$input = "foo";
$expected = ["foo"];
$this->assertParses($input, $parser, $expected);
$input = "foo||";
$expected = ["foo"];
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "||");
$input = "foo||bar";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$input = "foo||bar||";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "||");
$input = "foo||bar||baz";
$expected = ["foo", "bar", "baz"];
$this->assertParses($input, $parser, $expected);
$input = "||";
$this->assertParses($input, $parser, [], "The sepBy parser always succeed, even if it doesn't find anything");
$this->assertRemainder($input, $parser, $input);
$input = "||bar||baz";
$this->assertParses($input, $parser, []);
$this->assertRemainder($input, $parser, $input);
$input = "||bar||";
$this->assertParses($input, $parser, []);
$this->assertRemainder($input, $parser, $input);
$input = "||bar";
$this->assertParses($input, $parser, []);
$this->assertRemainder($input, $parser, $input);
}
/** @test */
public function sepBy1()
{
$parser = sepBy1(string('||'), atLeastOne(alphaChar()));
$input = "";
$this->assertParseFails($input, $parser, "at least one A-Z or a-z, separated by '||'");
$input = "||";
$this->assertParseFails($input, $parser);
$input = "||bar||baz";
$this->assertParseFails($input, $parser);
$input = "||bar||";
$this->assertParseFails($input, $parser);
$input = "||bar";
$this->assertParseFails($input, $parser);
$input = "foo";
$expected = ["foo"];
$this->assertParses($input, $parser, $expected);
$input = "foo||";
$expected = ["foo"];
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "||");
$input = "foo||bar";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$input = "foo||bar||";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "||");
$input = "foo||bar||baz";
$expected = ["foo", "bar", "baz"];
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function sepBy2()
{
$parser = sepBy2(string('||'), atLeastOne(alphaChar()));
$input = "";
$this->assertParseFails($input, $parser, "at least two of (at least one A-Z or a-z), separated by '||'");
$input = "||";
$this->assertParseFails($input, $parser);
$input = "||bar||baz";
$this->assertParseFails($input, $parser);
$input = "||bar||";
$this->assertParseFails($input, $parser);
$input = "||bar";
$this->assertParseFails($input, $parser);
$input = "foo";
$this->assertParseFails($input, $parser);
$input = "foo||";
$this->assertParseFails($input, $parser);
$input = "foo||bar";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$input = "foo||bar||";
$expected = ["foo", "bar"];
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "||");
$input = "foo||bar||baz";
$expected = ["foo", "bar", "baz"];
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function repeat_vs_repeatList()
{
$parser = repeat(5, alphaChar());
$this->assertParses("hello", $parser, "hello");
$parser = repeatList(5, alphaChar());
$this->assertParses("hello", $parser, ["h", "e", "l", "l", "o"]);
$parser = repeatList(3, repeat(3, alphaChar()));
$this->assertParses("EURUSDGBP", $parser, ["EUR", "USD", "GBP"]);
}
}

View File

@@ -0,0 +1,67 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\char;
use function Parsica\Parsica\float;
use function Parsica\Parsica\sequence;
final class FunctorTest extends TestCase
{
use ParserAssertions;
/** @test */
public function map()
{
$parser =
char('a')->followedBy(char('b'))
->map('strtoupper');
$expected = "B";
$this->assertParses("abca", $parser, $expected);
}
/** @test */
public function simple_eur()
{
$parser = sequence(
char('€'),
float()->map(fn($v)=>new SimpleEur((float) $v))
);
$this->assertParses("€1.25", $parser, new SimpleEur(1.25));
}
}
class MyType1
{
private $val;
public function __construct($val)
{
$this->val = $val;
}
}
final class SimpleEur
{
private float $val;
public function __construct(float $val)
{
$this->val = $val;
}
}

View File

@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\any;
use function Parsica\Parsica\char;
use function Parsica\Parsica\fail;
use function Parsica\Parsica\string;
final class LabelTest extends TestCase
{
/** @test */
public function or_label()
{
$parser = char('a')->or(char('b'));
$input = "c";
$result = $parser->run(new StringStream($input));
$this->assertEquals("'a' or 'b'", $result->expected());
}
/** @test */
public function or_label2()
{
$parser = string('hello')->or(string('world'));
$input = "foo";
$result = $parser->run(new StringStream($input));
$this->assertEquals("'hello' or 'world'", $result->expected());
}
/** @test */
public function any_label()
{
$parser = any(char('a'), char('b'), string("hello"));
$input = "foo";
$result = $parser->run(new StringStream($input));
$this->assertEquals("'a' or 'b' or 'hello'", $result->expected());
}
/** @test */
public function failure_label()
{
$parser = fail("reason");
$result = $parser->run(new StringStream("foo"));
$this->assertEquals("reason", $result->expected());
}
}

View File

@@ -0,0 +1,80 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\anySingle;
use function Parsica\Parsica\bind;
use function Parsica\Parsica\char;
use function Parsica\Parsica\pure;
use function Parsica\Parsica\sequence;
final class MonadTest extends TestCase
{
use ParserAssertions;
/** @test */
public function bind()
{
// This parser checks if the second character is the same as the first, by taking the output of the first
// parser and binding it to a function that produces the second parser from that output.
$parser = anySingle()->bind(fn(string $c) => char($c));
$this->assertParses("aa", $parser, "a");
$this->assertParses("bb", $parser, "b");
$this->assertParseFails("ab", $parser);
$parser = bind(anySingle(), fn(string $c) => char($c));
$this->assertParses("aa", $parser, "a");
$this->assertParses("bb", $parser, "b");
$this->assertParseFails("ab", $parser);
}
/** @test */
public function bind_fails()
{
// If the first parser fails, bind() returns the first one.
$parser = char('x')->bind(fn(string $c) => char($c));
$this->assertParses("xx", $parser, "x");
$this->assertParseFails("yx", $parser);
}
/** @test */
public function sequence()
{
$parser = char('a')->sequence(char('b'));
$this->assertParses("ab", $parser, "b");
$this->assertParseFails("aa", $parser);
$parser = sequence(char('a'), char('b'));
$this->assertParses("ab", $parser, "b");
$this->assertParseFails("aa", $parser);
}
/** @test */
public function sequence_error_should_show_the_label_of_the_failing_parser()
{
$parser = char('a')->sequence(char('b'));
$this->assertParseFails("X", $parser, "'a'");
$this->assertParseFails("aX", $parser, "'b'");
}
/** @test */
public function pure()
{
$parser = pure("hi");
$this->assertParses("something else", $parser, "hi");
}
}

View File

@@ -0,0 +1,38 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\char;
use function Parsica\Parsica\string;
final class ParserTest extends TestCase
{
use ParserAssertions;
/** @test */
public function label()
{
$parser = string(":-)");
$this->assertParseFails("x", $parser, "':-)'");
$labeled = $parser->label("smiley");
$this->assertParseFails("x", $labeled, "smiley");
}
/** @test */
public function followedBy()
{
$parser = char('a')->followedBy(char('b'));
$this->assertParses("abc", $parser, "b");
}
}

View File

@@ -0,0 +1,43 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\ParserHasFailed;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
use function Parsica\Parsica\skipSpace;
use function Parsica\Parsica\string;
final class RunningParsersTest extends TestCase
{
/** @test */
public function try_throws()
{
$parser = char('a');
$result = $parser->try(new StringStream("a"));
$this->assertSame("a", $result->output());
$this->expectException(ParserHasFailed::class);
$result = $parser->try(new StringStream("b"));
}
/** @test */
public function continueFrom()
{
$parser = string('hello')->sequence(skipSpace());
$result = $parser->try(new StringStream("hello world!"));
$parser2 = string("world");
$result2 = $parser2->continueFrom($result);
$this->assertEquals("world", $result2->output());
$this->assertEquals("!", $result2->remainder());
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types=1);
/**
* This file is part of the Parsica library.
*
* Copyright (c) 2020 Mathias Verraes <mathias@verraes.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tests\Parsica\Parsica\Parser;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\char;
final class unicodeTest extends TestCase
{
use ParserAssertions;
/** @test */
public function mbstring_must_be_installed()
{
$this->assertTrue(function_exists('mb_detect_encoding'), "ext-mbstring must be installed.");
}
/** @test */
public function parses_unicode()
{
$parser = char("🥰");
$this->assertParses("🥰 hello", $parser, "🥰");
}
}