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,290 @@
<?php declare(strict_types=1);
/**
* This code is forked from https://github.com/matteosister/php-curry, which is abandoned. It could be integrated into
* the rest of Parsica.
*/
namespace Tests\Parsica\Parsica\Curry;
use PHPUnit\Framework\TestCase;
use function Parsica\Parsica\Curry\__;
use function Parsica\Parsica\Curry\_is_fullfilled;
use function Parsica\Parsica\Curry\_rest;
use function Parsica\Parsica\Curry\curry;
use function Parsica\Parsica\Curry\curry_args;
use function Parsica\Parsica\Curry\curry_right;
use function Parsica\Parsica\Curry\curry_right_args;
final class CurryTest extends TestCase
{
/**
* @test
*/
public function curry_without_params()
{
$simpleFunction = curry(function () {
return 1;
});
$this->assertEquals(1, $simpleFunction());
}
/**
* @test
*/
public function curry_identity()
{
$identity = curry([new TestSubject(), 'identity'], 1);
$this->assertEquals(1, $identity(1));
}
/**
* @test
*/
public function curry_identity_function()
{
$func = curry(function ($v) {
return $v;
}, 'test string');
$this->assertEquals('test string', $func());
}
/**
* @test
*/
public function curry_with_one_later_param()
{
$curriedOne = curry([new TestSubject(), 'add2'], 1);
$this->assertInstanceOf('Closure', $curriedOne);
$this->assertEquals(2, $curriedOne(1));
}
/**
* @test
*/
public function curry_with_two_later_param()
{
$curriedTwo = curry([new TestSubject(), 'add4'], 1, 1);
$this->assertInstanceOf('Closure', $curriedTwo);
$this->assertEquals(4, $curriedTwo(1, 1));
}
/**
* @test
*/
public function curry_with_successive_calls()
{
$curriedTwo = curry([new TestSubject(), 'add4'], 1, 1);
$curriedThree = $curriedTwo(1);
$this->assertEquals(4, $curriedThree(1));
}
/**
* @test
*/
public function curry_right()
{
$divideBy10 = curry_right([new TestSubject(), 'divide2'], 10);
$this->assertInstanceOf('Closure', $divideBy10);
$this->assertEquals(10, $divideBy10(100));
}
/**
* @test
*/
public function curry_right_immediate()
{
$divide3 = curry_right([new TestSubject(), 'divide3'], 5, 2, 20);
$this->assertEquals(2, $divide3());
}
/**
* @test
*/
public function curry_left_immediate()
{
$divide3 = curry([new TestSubject(), 'divide3'], 20, 2, 4);
$this->assertEquals(2.5, $divide3());
}
/**
* @test
*/
public function curry_three_times()
{
$divideBy5 = curry([new TestSubject(), 'divide3'], 100);
$divideBy10And5 = $divideBy5(10);
$this->assertEquals(2, $divideBy10And5(5));
}
/**
* @test
*/
public function curry_right_three_times()
{
$divideBy5 = curry_right([new TestSubject(), 'divide3'], 5);
$divideBy10And5 = $divideBy5(10);
$this->assertEquals(2, $divideBy10And5(100));
}
/**
* @test
*/
public function curry_using_func_get_args()
{
$fnNoArgs = function () {
return func_get_args();
};
$curried = curry($fnNoArgs);
$curriedRight = curry_right($fnNoArgs);
$this->assertEquals([], $fnNoArgs());
$this->assertEquals([], $curried());
$this->assertEquals([], $curriedRight());
$this->assertEquals([1], $fnNoArgs(1));
$this->assertEquals([1], $curried(1));
$this->assertEquals([1], $curriedRight(1));
$this->assertEquals([1, 2, 'three'], $fnNoArgs(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curried(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curriedRight(1, 2, 'three'));
$fnOneArg = function ($x) {
return func_get_args();
};
$curried = curry($fnOneArg);
$curriedRight = curry_right($fnOneArg);
$this->assertEquals([1], $fnOneArg(1));
$this->assertEquals([1], $curried(1));
$this->assertEquals([1], $curriedRight(1));
$this->assertEquals([1, 2, 'three'], $fnOneArg(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curried(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curriedRight(1, 2, 'three'));
$fnTwoArgs = function ($x, $y) {
return func_get_args();
};
$curried = curry($fnTwoArgs);
$curriedRight = curry_right($fnTwoArgs);
$curriedOne = $curried(1);
$curriedRightOne = $curriedRight(2);
$curriedRightTwo = $curriedRight('three');
$this->assertEquals([1, 2], $fnTwoArgs(1, 2));
$this->assertEquals([1, 2], $curried(1, 2));
$this->assertEquals([1, 2], $curriedRight(2, 1));
$this->assertEquals([1, 2, 'three'], $fnTwoArgs(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curried(1, 2, 'three'));
$this->assertEquals([1, 2, 'three'], $curriedRight('three', 2, 1));
$this->assertEquals([1, 2], $curriedOne(2));
$this->assertEquals([1, 2], $curriedRightOne(1));
$this->assertEquals([1, 2, 'three'], $curriedOne(2, 'three'));
$this->assertEquals([1, 2, 'three'], $curriedRightTwo(2, 1));
}
/**
* @test
*/
public function curry_with_placeholders()
{
$minus = curry(function ($x, $y) {
return $x - $y;
});
$decrement = $minus(__(), 1);
$this->assertEquals(9, $decrement(10));
$introduce = curry(function ($name, $age, $job, $details = '') {
return "{$name}, {$age} years old, is a {$job} {$details}";
});
$introduceDeveloper = $introduce(__(), __(), 'Developer');
$this->assertEquals("Foo, 20 years old, is a Developer ", $introduceDeveloper('Foo', 20));
$introduceOld = $introduce(__(), 99, __());
$this->assertEquals("Foo, 99 years old, is a Developer and Cooker as well", $introduceOld('Foo', 'Developer', 'and Cooker as well'));
$introduceSkipName = $introduce(__());
$introduceSkipJob = $introduceSkipName(99, __());
$this->assertEquals("Foo, 99 years old, is a Cooker ", $introduceSkipJob('Foo', 'Cooker'));
$this->assertEquals("Foo, 99 years old, is a Cooker yumm !", $introduceSkipJob('Foo', 'Cooker', 'yumm !'));
$reduce = curry('array_reduce');
$add = function ($x, $y) {
return $x + $y;
};
$sum = $reduce(__(), $add);
$this->assertEquals(10, $sum([1, 2, 3, 4], 0));
}
/**
* @test
*/
public function rest()
{
$this->assertEquals([1], _rest([1, 1]));
$this->assertEquals(['a', 'b'], _rest([1, 'a', 'b']));
$this->assertEquals([], _rest([1]));
$this->assertEquals([], _rest([]));
}
/**
* @test
* @dataProvider provider_is_fullfilled
*/
public function is_fullfilled($isFullfilled, $args, $callable)
{
$this->assertSame($isFullfilled, _is_fullfilled($callable, $args));
}
public function provider_is_fullfilled()
{
return [[false, [], function ($a) {
}], [true, [], function () {
}], [true, [1], function ($a) {
}], [false, [1], function ($a, $b) {
}], [false, [1], [new TestSubject(), 'add2']], [true, [1, 2], [new TestSubject(), 'add2']], [true, ['aaa', 'a'], 'strpos'],];
}
}
final class TestSubject
{
public function identity($a)
{
return $a;
}
public function add2($a, $b)
{
return $a + $b;
}
public function divide2($a, $b)
{
return $a / $b;
}
public function divide3($a, $b, $c)
{
return $a / $b / $c;
}
public function add3($a, $b, $c)
{
return $a + $b + $c;
}
public function add4($a, $b, $c, $d)
{
return $a + $b + $c + $d;
}
}

View File

@@ -0,0 +1,130 @@
<?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\Examples;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{between, char, choice, keepFirst, recursive, skipHSpace, string};
use function Parsica\Parsica\Expression\{binaryOperator, expression, leftAssoc, prefix, unaryOperator};
final class BooleanExpressionsTest extends TestCase
{
use ParserAssertions;
/** @test */
public function booleanExpressions()
{
$token = fn(Parser $parser) : Parser => keepFirst($parser, skipHSpace());
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
$term = fn(): Parser => $token(choice(
string("TRUE")->map(fn($v) => new True_),
string("FALSE")->map(fn($v) => new False_),
));
$NOT = unaryOperator($token(string("NOT")), fn($v) => new Not_($v));
$AND = binaryOperator($token(string("AND")), fn($l, $r) => new And_($l, $r));
$OR = binaryOperator($token(string("OR")), fn($l, $r) => new Or_($l, $r));
$expr = recursive();
$expr->recurse(expression(
$parens($expr)->or($term()),
[
prefix($NOT),
leftAssoc($AND),
leftAssoc($OR),
]
));
$parser = $expr->thenEof();
$input = "TRUE AND NOT (FALSE AND FALSE)";
$expected =
new And_(
new True_(),
new Not_(
new And_(
new False_(),
new False_()
)
)
);
$this->assertParses($input, $parser, $expected);
$parser = $expr->thenEof();
$input = "TRUE AND NOT (FALSE OR TRUE AND FALSE)";
$expected =
new And_(
new True_,
new Not_(
new Or_(
new False_,
new And_(
new True_,
new False_
)
)
)
);
$this->assertParses($input, $parser, $expected);
// Now swapping precedence of AND and OR
$expr = recursive();
$expr->recurse(expression(
$parens($expr)->or($term()),
[
prefix($NOT),
leftAssoc($OR),
leftAssoc($AND),
]
));
$parser = $expr->thenEof();
$input = "TRUE AND NOT (FALSE OR TRUE AND FALSE)";
$expected =
new And_(
new True_,
new Not_(
new And_(
new Or_(
new False_,
new True_
),
new False_
)
)
);
$this->assertParses($input, $parser, $expected);
}
}
interface Boolean_ {}
class True_ implements Boolean_ {}
class False_ implements Boolean_ {}
class Not_ implements Boolean_ {
private Boolean_ $boolean;
function __construct(Boolean_ $boolean){$this->boolean = $boolean;}
}
class And_ implements Boolean_ {
private Boolean_ $l, $r;
function __construct(Boolean_ $l, Boolean_ $r){
$this->l = $l;
$this->r = $r;
}
}
class Or_ implements Boolean_ {
private Boolean_ $l, $r;
function __construct(Boolean_ $l, Boolean_ $r){
$this->l = $l;
$this->r = $r;
}
}

View File

@@ -0,0 +1,63 @@
<?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\Examples;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use function Parsica\Parsica\{atLeastOne, between, char, digitChar, keepFirst, recursive, skipHSpace, string};
use function Parsica\Parsica\Expression\{binaryOperator,
expression,
leftAssoc,
postfix,
prefix,
unaryOperator};
/**
* Parse expressions and calculate the result
*/
final class CalculatorTest extends TestCase
{
/** @test */
public function calculator()
{
$token = fn(Parser $parser) => keepFirst($parser, skipHSpace());
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
$term = fn(): Parser => $token(atLeastOne(digitChar()));
$expr = recursive();
$expr->recurse(expression(
$parens($expr)->or($term()),
[
prefix(
unaryOperator(char('-'), fn($v) => -$v),
unaryOperator(char('+'), fn($v) => $v),
),
postfix(
unaryOperator($token(string('--')), fn($v) => $v - 1),
unaryOperator($token(string('++')), fn($v) => $v + 1),
),
leftAssoc(
binaryOperator($token(char('*')), fn($l, $r) => $l * $r),
binaryOperator($token(char('/')), fn($l, $r) => $l / $r),
),
leftAssoc(
binaryOperator($token(char('+')), fn($l, $r) => $l + $r),
binaryOperator($token(char('-')), fn($l, $r) => $l - $r),
),
]
));
$parser = $expr->thenEof();
$result = $parser->tryString("(3 - 2) + -1 - 3 * (1 + 1) / 6");
$this->assertEquals(-1, (string)$result->output());
}
}

View File

@@ -0,0 +1,131 @@
<?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\Examples;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{alphaChar, between, char, collect, digitChar, skipHSpace1, space, string};
final class ExcelTest extends TestCase
{
use ParserAssertions;
/** @test */
public function spaceOrOperatorDependingOnContext()
{
// https://twitter.com/Mark_Baker/status/1309919606887374849?s=20
// and https://twitter.com/Mark_Baker/status/1309960902482026498?s=20
// `=SUM(B7:D7 C6:C8)` where space is the intersection operator for the
// intersection between the two ranges B7:D7 and C6:C8 (ie. C7),
// and `=A1 & B1` where the space is simply whitespace and should be ignored
$parser = $this->excelParser();
$input = "=SUM(B7:D7 C6:C8)";
$expected = new Sum(
new Intersection(
new Range(new Cell("B", "7"), new Cell("D", "7")),
new Range(new Cell("C", "6"), new Cell("C", "8")),
)
);
$this->assertParses($input, $parser, $expected);
$input = "=A1 & B1";
$expected = new Ampersand(
new Cell("A", "1"),
new Cell("B", "1"),
);
$this->assertParses($input, $parser, $expected);
}
private function excelParser(): Parser
{
$parens = fn(Parser $p): Parser => between(char('('), char(')'), $p);
$cell = collect(alphaChar(), digitChar())
->map(fn($o) => new Cell($o[0], $o[1]));
$range = collect($cell, char(':'), $cell)
->map(fn($o) => new Range($o[0], $o[2]));
$intersection = collect($range, space(), $range)
->map(fn($o) => new Intersection($o[0], $o[2]));
$sum = (string('=SUM')->followedBy($parens($intersection)))
->map(fn($o) => new Sum($o));
// consumes space before and after Parser $p
$token = fn(Parser $p): Parser => between(skipHSpace1(), skipHSpace1(), $p);
$ampersand = char('=')->followedBy(collect(
$cell,
$token(char('&')),
$cell
))->map(fn($o) => new Ampersand($o[0], $o[2]));
return $sum->or($ampersand);
}
}
class Cell
{
private $col;
private $row;
function __construct($col, $row)
{
$this->col = $col;
$this->row = $row;
}
}
class Range
{
private Cell $from;
private Cell $to;
function __construct(Cell $from, Cell $to)
{
$this->from = $from;
$this->to = $to;
}
}
class Intersection
{
private Range $l;
private Range $r;
function __construct(Range $l, Range $r)
{
$this->l = $l;
$this->r = $r;
}
}
class Sum
{
private Intersection $intersection;
function __construct(Intersection $intersection)
{
$this->intersection = $intersection;
}
}
class Ampersand
{
private Cell $l;
private Cell $r;
function __construct(Cell $l, Cell $r)
{
$this->l = $l;
$this->r = $r;
}
}

View File

@@ -0,0 +1,47 @@
<?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\Examples;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use PHPUnit\Framework\TestCase;
use function Parsica\Parsica\any;
use function Parsica\Parsica\collect;
use function Parsica\Parsica\digitChar;
use function Parsica\Parsica\repeat;
use function Parsica\Parsica\skipSpace;
use function Parsica\Parsica\string;
final class SimpleDateTest extends TestCase
{
use ParserAssertions;
/** @test */
public function simple_date()
{
$jan = (string("January")->or(string("Jan")))->map(fn($v) => 1);
$feb = (string("February")->or(string("Feb")))->map(fn($v) => 2);
$mar = (string("March")->or(string("Mar")))->map(fn($v) => 3);
// ... you get the gist
$month = any($jan, $feb, $mar);
$day = repeat(2, digitChar())->map('intval');
$p1 = collect(
$month->thenIgnore(skipSpace()),
$day
);
$this->assertParses("January 28", $p1, [1, 28]);
$this->assertParses("Jan 28", $p1, [1, 28]);
$this->assertParses("February 28", $p1, [2, 28]);
$this->assertParses("Feb 28", $p1, [2, 28]);
}
}

View File

@@ -0,0 +1,172 @@
<?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\Expression;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Expression\{LeftAssoc, NonAssoc, Operator, Postfix, Prefix, RightAssoc};
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\atLeastOne;
use function Parsica\Parsica\between;
use function Parsica\Parsica\char;
use function Parsica\Parsica\collect;
use function Parsica\Parsica\digitChar;
use function Parsica\Parsica\eof;
use function Parsica\Parsica\Expression\binaryOperator;
use function Parsica\Parsica\Expression\expression;
use function Parsica\Parsica\Expression\leftAssoc;
use function Parsica\Parsica\Expression\nonAssoc;
use function Parsica\Parsica\Expression\operator;
use function Parsica\Parsica\Expression\postfix;
use function Parsica\Parsica\Expression\prefix;
use function Parsica\Parsica\Expression\rightAssoc;
use function Parsica\Parsica\Expression\unaryOperator;
use function Parsica\Parsica\keepFirst;
use function Parsica\Parsica\recursive;
use function Parsica\Parsica\skipHSpace;
use function Parsica\Parsica\string;
final class ExpressionsTest extends TestCase
{
use ParserAssertions;
private Parser $expression;
protected function setUp() : void
{
/** Consumes whitespace */
$token = fn(Parser $parser) => keepFirst($parser, skipHSpace());
$parens = fn (Parser $parser): Parser => $token(between($token(char('(')), $token(char(')')), $parser));
$term = fn(): Parser => $token(atLeastOne(digitChar()));
$expr = recursive();
$primaryTermParser = $parens($expr)->or($term());
$expr->recurse(expression(
$primaryTermParser,
[
prefix(
unaryOperator(char('-'), fn($v) => "(-$v)"),
unaryOperator(char('+'), fn($v) => "(+$v)"),
),
postfix(
unaryOperator($token(string('--')), fn($v) => "($v--)"),
unaryOperator($token(string('++')), fn($v) => "($v++)"),
),
leftAssoc(
binaryOperator($token(char('*')), fn($l, $r) => "($l * $r)"),
binaryOperator($token(char('/')), fn($l, $r) => "($l / $r)"),
),
rightAssoc(
// imaginary right associative operator
binaryOperator($token(char('R')), fn($l, $r) => "($l R $r)"),
binaryOperator($token(string('R2')), fn($l, $r) => "($l R2 $r)"),
),
leftAssoc(
binaryOperator($token(char('-')), fn($l, $r) => "($l - $r)"),
binaryOperator($token(char('+')), fn($l, $r) => "($l + $r)"),
),
nonAssoc(
// imaginary non-associative operator
binaryOperator($token(char('§')), fn($l, $r) => "($l § $r)"),
)
]
));
$this->expression = $expr;
}
/**
* @test
* @dataProvider examples
*/
public function expression(string $input, string $expected)
{
$parser = $this->expression->thenEof();
$result = $parser->tryString($input);
$this->assertEquals($expected, (string)$result->output());
}
public function examples()
{
$examples = [
["1", "1"],
["1 + 1", "(1 + 1)"],
["1 * 1", "(1 * 1)"],
["(1 + 1) + 1", "((1 + 1) + 1)"],
["1 + (1 + 1)", "(1 + (1 + 1))"],
["1 * (1 + 1)", "(1 * (1 + 1))"],
["1 + (1 * 1)", "(1 + (1 * 1))"],
["(1 * 2) + (1 * 1)", "((1 * 2) + (1 * 1))"],
["1 + 2 + 3", "((1 + 2) + 3)"],
["1 * 2 * 3", "((1 * 2) * 3)"],
["1 * 2 + 3", "((1 * 2) + 3)"],
["1 + 2 * 3", "(1 + (2 * 3))"],
["4 + 5 + 2 * 3", "((4 + 5) + (2 * 3))"],
["4 + 5 * 2 * 3", "(4 + ((5 * 2) * 3))"],
["1 * 2 * 3 / 4 * 5", "((((1 * 2) * 3) / 4) * 5)"],
["1 / 2 / 3 * 4", "(((1 / 2) / 3) * 4)"],
["1 - 2 + 3", "((1 - 2) + 3)"],
["1 - 2 * 3", "(1 - (2 * 3))"],
["1 + 5 - 2 * 3 - 6", "(((1 + 5) - (2 * 3)) - 6)"],
["-1", "(-1)"],
["-1 + -2", "((-1) + (-2))"],
["-(-1)", "(-(-1))"],
["-(-(1))", "(-(-1))"],
// @todo crazy slow for some reason
// ["(-(-(1)))", "(-(-1))"],
["-1 * +1", "((-1) * (+1))"],
["1 § 2", "(1 § 2)"],
["1 + 5 § 2 * 3 - 6", "((1 + 5) § ((2 * 3) - 6))"],
["1 R 2 R 3", "(1 R (2 R 3))"],
["1 R 2 R 3 R 4", "(1 R (2 R (3 R 4)))"],
["1 - 2 * 3 R 4", "(1 - ((2 * 3) R 4))"],
["1 - 2 * 3 R 4 R 5", "(1 - ((2 * 3) R (4 R 5)))"],
["1++", "(1++)"],
["1++ + 2++", "((1++) + (2++))"],
["1--", "(1--)"],
["1-- + 2--", "((1--) + (2--))"],
["1++ + 2--", "((1++) + (2--))"],
["1-- + 2++", "((1--) + (2++))"],
];
return array_combine(array_column($examples, 0), $examples);
}
/**
* @test
* @dataProvider unparsableExamples
*/
public function unparsableExpressions(string $input)
{
$parser = $this->expression->thenEof();
$this->assertParseFails($input, $parser);
}
public function unparsableExamples()
{
$examples = [
["--1"],
["1--++"],
["1++--"],
["1 § 2 § 3"],
["1 § 2 * 3 § 4"],
["1 § 2 * 3 § 4 § 5"],
];
return array_combine(array_column($examples, 0), $examples);
}
}

View File

@@ -0,0 +1,47 @@
<?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\Internal\FP;
use PHPUnit\Framework\TestCase;
use function Parsica\Parsica\Curry\curry;
final class CurryTest extends TestCase
{
/** @test */
public function curry()
{
$f = fn($a, $b, $c) => $a + $b + $c;
$curried = curry($f);
$this->assertIsCallable($curried);
$this->assertIsCallable($curried(1));
$this->assertIsCallable($curried(1)(2));
$this->assertIsCallable($curried(1)(2));
$this->assertEquals(6, $curried(1)(2)(3));
}
/** @test */
public function partial_application()
{
$f = fn($a, $b, $c) => $a + $b + $c;
$this->assertIsCallable(curry($f, 1));
$this->assertIsCallable(curry($f, 1, 2));
// I would expect this:
// $this->assertEquals(6, curry($f, 1, 2, 3));
// But we must add a () at the end, which I feel is a bug:
$this->assertIsCallable(curry($f, 1, 2, 3));
$this->assertEquals(6, curry($f, 1, 2, 3)());
}
}

View File

@@ -0,0 +1,59 @@
<?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\Internal\FP;
use PHPUnit\Framework\TestCase;
use function Parsica\Parsica\Internal\FP\foldr;
final class FoldrTest extends TestCase
{
/** @test */
public function sum_implemented_as_foldr()
{
$actual = foldr([1, 2, 3], fn ($x, $y) => $x + $y, 0);
$this->assertSame(6, $actual);
}
/** @test */
public function associativity_is_correct()
{
$minus = fn($x, $y) => $x - $y;
$input = [1, 2, 3, 4, 5];
$init = 0;
// foldl: ((((0 - 1) - 2) - 3) - 4) - 5) = -15
// foldr: (1 - (2 - (3 - (4 - (5 - 0))))) = 3
$actual = array_reduce($input, $minus, $init);
$this->assertSame(-15, $actual);
$actual = foldr($input, $minus, $init);
$this->assertSame(3, $actual);
}
/** @test */
public function x()
{
$concat = fn($x, $y) => "$x$y";
$input = [1, 2, 3, 4, 5];
$init = "0";
// foldl: 012345
// foldr: 123450
$actual = array_reduce($input, $concat, $init);
$this->assertSame("012345", $actual);
$actual = foldr($input, $concat, $init);
$this->assertSame("123450", $actual);
}
}

View File

@@ -0,0 +1,65 @@
<?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\Internal;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Internal\Position;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
final class PositionTest extends TestCase
{
/** @test */
public function update()
{
$position = Position::initial();
$this->assertEquals(1, $position->line());
$this->assertEquals(1, $position->column());
$position = $position->advance("a");
$this->assertEquals(1, $position->line());
$this->assertEquals(2, $position->column());
$position = $position->advance("\n");
$this->assertEquals(2, $position->line());
$this->assertEquals(1, $position->column());
$position = $position->advance("\n\n\nabc");
$this->assertEquals(5, $position->line());
$this->assertEquals(4, $position->column());
}
/** @test */
public function position_in_sequence()
{
$parser = char('a')->followedBy(char('b'));
$input = new StringStream("abc", Position::initial());
$result = $parser->run($input);
$expectedColumn = 3;
$actualColumn = $result->remainder()->position()->column();
$this->assertEquals($expectedColumn, $actualColumn);
}
/** @test */
public function position_with_tabs()
{
$expected = 10;
// All of these move the column position to 10
$position = Position::initial()->advance("123456789");
$this->assertEquals($expected, $position->column());
$position = Position::initial()->advance("\t56789");
$this->assertEquals($expected, $position->column());
$position = Position::initial()->advance("\t\t9");
$this->assertEquals($expected, $position->column());
$position = Position::initial()->advance("1\t56789");
$this->assertEquals($expected, $position->column());
$position = Position::initial()->advance("123\t56789");
$this->assertEquals($expected, $position->column());
}
}

View File

@@ -0,0 +1,52 @@
<?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\Internal;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Internal\Position;
use Parsica\Parsica\StringStream;
final class StringStreamTest extends TestCase
{
/** @test */
public function take1()
{
$s = new StringStream("abc");
$t = $s->take1();
$this->assertEquals("a", $t->chunk());
$expectedPosition = new Position("<input>", 1, 2);
$expectedStream = new StringStream("bc", $expectedPosition);
$this->assertEquals($expectedStream, $t->stream());
}
/** @test */
public function takeN()
{
$s = new StringStream("abcde");
$t = $s->takeN(3);
$this->assertEquals("abc", $t->chunk());
$expectedPosition = new Position("<input>", 1, 4);
$expectedStream = new StringStream("de", $expectedPosition);
$this->assertEquals($expectedStream, $t->stream());
}
/** @test */
public function takeWhile()
{
$s = new StringStream("abc\nde");
$t = $s->takeWhile(fn($c) => $c !== "\n");
$this->assertEquals("abc", $t->chunk());
$expectedPosition = new Position("<input>", 1, 4);
$expectedStream = new StringStream("\nde", $expectedPosition);
$this->assertEquals($expectedStream, $t->stream());
}
}

View File

@@ -0,0 +1,161 @@
<?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\Issues;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\alphaNumChar;
use function Parsica\Parsica\any;
use function Parsica\Parsica\atLeastOne;
use function Parsica\Parsica\between;
use function Parsica\Parsica\char;
use function Parsica\Parsica\either;
use function Parsica\Parsica\emit;
use function Parsica\Parsica\eof;
use function Parsica\Parsica\fail;
use function Parsica\Parsica\many;
use function Parsica\Parsica\succeed;
/**
* https://github.com/mathiasverraes/parsica/issues/6
*/
final class GH26_Test extends TestCase
{
use ParserAssertions;
private static function pathParser(): Parser
{
$sep = char('/')
->label('directory separator');
// unix supports other characters, such as space, so adapt if needed
$name = atLeastOne(char('.')->or(char('_'))->or(alphaNumChar()))
->label("directory or filename");
$parser = many($sep->followedBy($name));
return $parser;
}
/** @test */
public function parsing_a_simple_path()
{
$parser = self::pathParser();
$input = "/a/b/c/file1";
$expected = ["a", "b", "c", "file1"];
$this->assertParses($input, $parser, $expected);
}
/**
* https://github.com/mathiasverraes/parsica/issues/6#issuecomment-653772920
*
* @test
*/
public function only_the_first_successful_parser_in_an_either_should_call_emit()
{
$x = new class {
public bool $first = false;
public bool $second = false;
};
$parser = either(
emit(
succeed(),
function ($output) use ($x) {
$x->first = true; // is called
}
),
emit(
succeed(),
function ($output) use ($x) {
$x->second = true; // is not called
}
)
);
$result = $parser->tryString('test');
$this->assertEquals(true, $x->first);
$this->assertEquals(false, $x->second, "Either should only call emit on the first successful parser");
}
/**
* @TODO Set $repeatParser at 500 and fix the performance issues.
*
* https://github.com/mathiasverraes/parsica/issues/6#issuecomment-653772920
*
* @test
*/
public function it_should_parse_500_times_in_under_100_ms()
{
// Number of times we run the parser
$repeatParser = 1;//500;
$propertyName = atLeastOne(alphaNumChar());
$type = emit(
either(
eof(),
char('@')
->followedBy($propertyName)
->thenIgnore(eof()),
),
function () {}
);
$map = emit(
char('.')->followedBy($propertyName),
function () {}
);
$list = emit(
between(
char('['),
char(']'),
either(
char('@')
->followedBy($propertyName)
->map(fn($value) => [
'discriminatorName' => $value,
'keepKeys' => true
]),
$propertyName
->map(fn($value) => [
'discriminatorName' => $value,
'keepKeys' => false
]),
)
),
function () {}
);
$root = emit(
char('$'),
function () {}
);
$rest = many(any($map, $list))->followedBy($type);
$parser = either(
fail("message"), // $context->preflightCacheParser(),
$root
)->followedBy($rest);
$start = microtime(true);
for ($i = 0; $i < $repeatParser; $i++) {
$parser->tryString('$.q.w[@1].e[2]@int');
}
$end = microtime(true);
$this->assertLessThan(0.1, $end - $start);
}
}

View File

@@ -0,0 +1,46 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\JSON\key_value;
final class ArrayTest extends TestCase
{
use ParserAssertions;
/**
* @test
* @dataProvider examples
*/
public function array(string $input, $expected)
{
$parser = JSON::array();
$this->assertParses($input, $parser, $expected);
}
public function examples()
{
return [
['[]', []],
['[ ] ', []],
['[ 1 ] ', [1.0]],
['[ true ] ', [true]],
['[ 1.23, "abc", null, false ] ', [ 1.23, "abc", null, false]],
];
}
}

View File

@@ -0,0 +1,64 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
final class JSONTest extends TestCase
{
public static function examples(): array
{
return [
['true'],
['false'],
['null'],
['"abc"'],
['{"a b":"c d"}'],
[' { " a b " : " c d " } '],
[' [ { " a b " : " c d " } ] '],
[' [ { " a b " : " c d " } , { "ef" : "gh" } ] '],
['"some weird chars \\n in \\t strings \\u9999 should do it"'],
['"this \\\\ is just a backslash"'],
[<<<JSON
[
-1.23,
null,
true,
[
[
{
"a": true
},
{
"b": false,
"c": -1.23456789E+123
}
]
]
]
JSON,
],
[file_get_contents(__DIR__ . '/../../composer.json')],
];
}
/**
* @test
* @dataProvider examples
*/
public function compare_to_json_decode(string $input)
{
$native = json_decode($input);
$parsica = JSON::json()->tryString($input)->output();
$this->assertEquals($native, $parsica);
}
}

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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
final class NumberTest extends TestCase
{
use ParserAssertions;
/** @test */
public function number()
{
$this->assertParses("0", JSON::number(), 0.0);
$this->assertParses("0.1", JSON::number(), 0.1);
$this->assertParses("0.15", JSON::number(), 0.15);
$this->assertParses("0.10", JSON::number(), 0.1);
$this->assertParses("-0.1", JSON::number(), -0.1);
$this->assertParses("1.2345678", JSON::number(), 1.2345678);
$this->assertParses("-1.2345678", JSON::number(), -1.2345678);
$this->assertParses("-1.23456789E+123", JSON::number(), -1.23456789E+123);
$this->assertParses("-1.23456789e-123", JSON::number(), -1.23456789E-123);
$this->assertParses("-1E-123", JSON::number(), -1E-123);
$this->assertParses("-1E-123 ", JSON::number(), -1E-123);
}
}

View File

@@ -0,0 +1,45 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
final class ObjectTest extends TestCase
{
use ParserAssertions;
/** @test */
public function member()
{
$input = '"foo":"bar"';
$parser = JSON::member();
$this->assertParses($input, $parser, ["foo" => "bar"]);
}
/** @test */
public function object()
{
$input = '{"foo":"bar","bar":"foo"}';
$parser = JSON::object();
$result = $parser->run(new StringStream($input));
$this->assertParses($input, $parser, (object)["foo" => "bar", "bar" => "foo"]);
}
}

View File

@@ -0,0 +1,61 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
final class StringLiteralTest extends TestCase
{
use ParserAssertions;
public static function escapedChars(): array
{
return [
// label => [literal that will appear in json, expected character it results in]
"quotation mark" => ["\\\"", '"'],
"reverse solidus" => ['\\\\', '\\'],
"solidus" => ["\\/", '/'],
"backspace" => ["\\b", mb_chr(8)],
"formfeed" => ["\\f", mb_chr(12)],
"linefeed" => ["\\n", "\n"],
"carriage return" => ["\\r", "\r"],
"horizontal tab" => ["\\t", "\t"],
];
}
/** @test */
public function empty()
{
$this->assertParses('""', JSON::stringLiteral(), "");
}
/**
* @test
* @dataProvider escapedChars
*/
public function escapes(string $input, string $expected)
{
$this->assertParses('"' . $input . '"', JSON::stringLiteral(), $expected);
$this->assertParses('"a' . $input . '"', JSON::stringLiteral(), "a" . $expected);
$this->assertParses('"' . $input . 'a"', JSON::stringLiteral(), $expected . "a");
}
/** @test */
public function escape_hex()
{
$input = '"\\u0BB9\\u0BB2\\u0BCB\\u0020\\u0B89\\u0BB2\\u0B95\\u0BAE\\u0BCD"';
$this->assertParses($input, JSON::stringLiteral(), "ஹலோ உலகம்");
}
}

View File

@@ -0,0 +1,32 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\char;
use function Parsica\Parsica\JSON\token;
final class TokenTest extends TestCase
{
use ParserAssertions;
/** @test */
public function token()
{
$parser = JSON::token(char('a'));
$input = "a \n \tb";
$this->assertRemainder($input, $parser, "b");
}
}

View File

@@ -0,0 +1,79 @@
<?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\JSON;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\JSON\JSON;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\JSON\ws;
final class WhitespaceTest extends TestCase
{
use ParserAssertions;
/** @test */
public function ws_empty()
{
$expected = null;
$input = "";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function ws_space()
{
$expected = null;
$input = " ";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function ws_tab()
{
$expected = null;
$input = "\t";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function ws_newline()
{
$expected = null;
$input = "\n";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function ws_carriage_return()
{
$expected = null;
$input = "\r";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function a_bunch_of_whitespace()
{
$expected = null;
$input = " \n \r \t a";
$parser = JSON::ws();
$this->assertParses($input, $parser, $expected);
$this->assertRemainder($input, $parser, "a");
}
}

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\PHPUnit;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
final class ParserTestCaseTest extends TestCase
{
use ParserAssertions;
/** @test */
public function strict_equality()
{
$this->assertEquals(1.23, "1.23",
"A string and a float are equal in php");
$this->assertNotSame(1.23, "1.23",
"A string and a float are not the same in php");
$this->assertSame(1.23, 1.23,
"Primitives are compared by value");
$this->assertEquals(new MyType(1.23), new MyType(1.23),
"Weak equality works for objects");
$this->assertNotSame(new MyType(1.23), new MyType(1.23),
"...but value object instances with the same value do not have equality");
$this->assertTrue((new MyType(1.23))->equals(new MyType(1.23)),
"We can solve it with an equals() method, but the user doesn't always have "
. "control of the types.");
$this->assertTrue(true,
"Therefore, we need something that will behave like assertSame for primitives, "
. "like assertEquals for objects of the same type,"
. "and fail for everything else.");
$this->assertStrictlyEquals(1.23, 1.23);
$this->assertStrictlyEquals(new MyType(1.23), new MyType(1.23));
/*
$this->assertStrictlyEquals(1.23, "1.23",
"should fail");
$this->assertStrictlyEquals("1.23", 1.23,
"should fail");
$this->assertStrictlyEquals(new MyType(1.23), new MyType(7.89),
"should fail");
*/
}
/** @test */
public function strictlyEquals_for_arrays()
{
$this->assertStrictlyEquals(
[1, new MyType(5.0)],
[1, new MyType(5.0)]
);
}
}
final class MyType
{
private float $x;
public function __construct(float $x)
{
$this->x = $x;
}
public function equals(MyType $other): bool
{
return $this->x === $other->x;
}
}

View File

@@ -0,0 +1,53 @@
<?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\ParseResult;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Internal\Fail;
use Parsica\Parsica\Internal\Succeed;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
final class AppendTest extends TestCase
{
use ParserAssertions;
/** @test */
public function append_strings()
{
$remainder = new StringStream("");
$succeed1 = new Succeed("Parsed1", new StringStream("Remain1"));
$succeed2 = new Succeed("Parsed2", new StringStream("Remain2"));
$fail1 = new Fail("Expected1", new StringStream("Got1"));
$fail2 = new Fail("Expected2", new StringStream("Got2"));
$this->assertStrictlyEquals(new Succeed("Parsed1Parsed2", new StringStream("Remain2")), $succeed1->append($succeed2));
$this->assertStrictlyEquals(new Fail("Expected1", new StringStream("Got1")), $succeed1->append($fail1));
$this->assertStrictlyEquals(new Fail("Expected1", new StringStream("Got1")), $fail1->append($succeed2));
$this->assertStrictlyEquals(new Fail("Expected1", new StringStream("Got1")), $fail1->append($fail2));
}
/** @test */
public function append_with_null()
{
$null1 = new Succeed(null, new StringStream("Remain Null 1"));
$null2 = new Succeed(null, new StringStream("Remain Null 2"));
$string = new Succeed("String", new StringStream("Remain String"));
$first = $string->append($null1);
$this->assertStrictlyEquals(new Succeed("String", new StringStream("Remain Null 1")), $first);
$second = $null1->append($string);
$this->assertStrictlyEquals(new Succeed("String", new StringStream("Remain String")), $second);
$both = $null1->append($null2);
$this->assertStrictlyEquals(new Succeed(null, new StringStream("Remain Null 2")), $both);
}
}

View File

@@ -0,0 +1,277 @@
<?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\ParseResult;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Internal\Position;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
use function Parsica\Parsica\many;
use function Parsica\Parsica\newline;
use function Parsica\Parsica\repeat;
use function Parsica\Parsica\skipSpace;
use function Parsica\Parsica\string;
use function Parsica\Parsica\whitespace;
final class ErrorReportingTest extends TestCase
{
use ParserAssertions;
/** @test */
public function failing_on_the_first_token()
{
$parser = char('a');
$input = new StringStream("bcd");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:1
|
1 | bcd
| ^— column 1
Unexpected 'b'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function failing_with_an_advanced_position()
{
$parser = char('a');
$input = new StringStream("bcd", new Position("/path/to/file", 5, 10));
$result = $parser->run($input);
$expected =
<<<ERROR
/path/to/file:5:10
|
5 | ...bcd
| ^— column 10
Unexpected 'b'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function works_for_parsers_with_more_than_one_character()
{
$parser = string("abc");
$input = new StringStream("xyz", Position::initial("/path/to/file"));
$result = $parser->run($input);
$expected =
<<<ERROR
/path/to/file:1:1
|
1 | xyz
| ^— column 1
Unexpected 'x'
Expecting 'abc'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function advance_the_column_with_followedBy()
{
$parser = char('a')->sequence(char('b'));
$input = new StringStream("axy");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:2
|
1 | ...xy
| ^— column 2
Unexpected 'x'
Expecting 'b'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function works_with_custom_labels()
{
$parser = char('a')->sequence(char('b'))->label("a followed by b");
$input = new StringStream("axy");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:2
|
1 | ...xy
| ^— column 2
Unexpected 'x'
Expecting a followed by b
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function tabs_move_column_position()
{
$parser = skipSpace()->sequence(char('a'));
$input = new StringStream("\t\tbcdefgh");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:9
|
1 | ...bcdefgh
| ^— column 9
Unexpected 'b'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function line_numbers_space_out()
{
$parser = skipSpace()->sequence(char('a'));
$input = new StringStream(str_repeat("\n", 99) . "b");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:100:1
|
100 | b
| ^— column 1
Unexpected 'b'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function multiline_input()
{
$parser = many(newline())->sequence(char('a'));
$input = new StringStream("\n\n\nbcd\nxyz", Position::initial("/path/to/file"));
$result = $parser->run($input);
$expected =
<<<ERROR
/path/to/file:4:1
|
4 | bcd
| ^— column 1
Unexpected 'b'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function indicate_position()
{
$parser = repeat(5, char('a'))->sequence(char('b'));
$input = new StringStream("aaaaaXYZ");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:6
|
1 | ...XYZ
| ^— column 6
Unexpected 'X'
Expecting 'b'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function repeatN()
{
$parser = repeat(5, char('a'))->sequence(char('b'));
$input = new StringStream("aaaaXYZ");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:5
|
1 | ...XYZ
| ^— column 5
Unexpected 'X'
Expecting 5 times 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function indicate_shorter_position()
{
$parser = string("aa")->sequence(char('b'));
$input = new StringStream("aaXYZ");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:3
|
1 | ...XYZ
| ^— column 3
Unexpected 'X'
Expecting 'b'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function truncate_long_lines()
{
$parser = skipSpace()->sequence(string("Hello"))->sequence(char(','))->sequence(whitespace())->sequence(string("World"));
$input = new StringStream("\n\n\n\n\n\n\n\n\nHello World! This is a really long line of more than 80 characters, if you count the spaces.");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:10:6
|
10 | ... World! This is a really long line of more than 80 characters, if you...
| ^— column 6
Unexpected <space>
Expecting ','
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
/** @test */
public function dont_truncate_short_enough_lines()
{
$parser = char('a');
$input = new StringStream("1234567890123456789012345678901234567890123456789012345678901234567890123456");
$result = $parser->run($input);
$expected =
<<<ERROR
<input>:1:1
|
1 | 1234567890123456789012345678901234567890123456789012345678901234567890123456
| ^— column 1
Unexpected '1'
Expecting 'a'
ERROR;
$this->assertEquals($expected, $result->errorMessage());
}
}

View File

@@ -0,0 +1,40 @@
<?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\ParseResult;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Internal\Fail;
use Parsica\Parsica\Internal\Succeed;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
final class FunctorTest extends TestCase
{
use ParserAssertions;
/** @test */
public function map_over_ParseSuccess()
{
$succeed = new Succeed("parsed", new StringStream("remainder"));
$expected = new Succeed("PARSED", new StringStream("remainder"));
$this->assertEquals($expected, $succeed->map('strtoupper'));
}
/** @test */
public function map_over_ParseFailure()
{
$remainder = new StringStream("");
$fail = new Fail("expected", new StringStream("got"));
$expected = new Fail("expected", new StringStream("got"));
$this->assertEquals($expected, $fail->map('strtoupper'));
}
}

View File

@@ -0,0 +1,39 @@
<?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\ParseResult;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
final class ParseResultTest extends TestCase
{
/** @test */
public function ParseSuccess_continueWith()
{
$input = new StringStream("abc");
$success = char('a')->run($input);
$result = $success->continueWith(char('b'));
$this->assertTrue($result->isSuccess());
$this->assertEquals("c", $result->remainder());
}
/** @test */
public function ParseFailure_continueWith()
{
$input = new StringStream("abc");
$fail = char('x')->run($input);
$result = $fail->continueWith(char('a'));
$this->assertTrue($result->isFail());
}
}

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, "🥰");
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Tests\Parsica\Parsica;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\between;
use function Parsica\Parsica\char;
use function Parsica\Parsica\collect;
use function Parsica\Parsica\digitChar;
use function Parsica\Parsica\eof;
use function Parsica\Parsica\keepFirst;
use function Parsica\Parsica\recursive;
use function Parsica\Parsica\skipHSpace;
final class PolishNotationTest extends TestCase
{
use ParserAssertions;
/**
* @var Parser
*/
private Parser $expr;
public function setUp(): void
{
$token = fn(Parser $parser) => keepFirst($parser, skipHSpace());
$term = digitChar();
$parens = fn(Parser $parser)
: Parser => $token(between($token(char('(')), $token(char(')')), $parser));
$expr = recursive();
$plus = collect(
$token(char('+')),
$token($expr),
$token($expr)
)->map(fn($o) => "(+ {$o[1]} {$o[2]})");
$times = collect(
$token(char('*')),
$token($expr),
$token($expr)
)->map(fn($o) => "(* {$o[1]} {$o[2]})");
$expr->recurse($term->or($plus)->or($times)->or($parens($expr)));
$this->expr = $expr->thenEof();
}
/**
* @test
* @dataProvider polishExamples
*/
public function polishNotation(string $input, $output)
{
$this->assertParses($input, $this->expr, $output);
}
public function polishExamples()
{
$examples = [
['1', '1'],
['+ 1 2', '(+ 1 2)'],
['+ 1 + 2 3', '(+ 1 (+ 2 3))'],
['+ + 1 2 + 3 4', '(+ (+ 1 2) (+ 3 4))'],
['+ 1 (+ 2 3)', '(+ 1 (+ 2 3))'],
['((+ 1 + 2 (+ 3 4)))', '(+ 1 (+ 2 (+ 3 4)))'],
['(1)', '1'],
['((1))', '1'],
['(((1)))', '1'],
['* 1 2', '(* 1 2)'],
['+ 1 * 2 3', '(+ 1 (* 2 3))'],
['* 1 + 2 3', '(* 1 (+ 2 3))'],
['* 1 * 2 3', '(* 1 (* 2 3))'],
['((+ 1 * 2 (+ 3 4)))', '(+ 1 (* 2 (+ 3 4)))'],
];
return array_combine(array_column($examples, 0), $examples);
}
/**
* @test
* @dataProvider badExamples
*/
public function theseAreNotPolishNotation(string $input)
{
$this->assertParseFails($input, $this->expr);
}
public function badExamples()
{
$examples = [
[''],
['()'],
['1 2'],
['(1 2'],
['1 2)'],
['(1 2)'],
['(+ 2)'],
['+ 1 2)'],
['(1 + 2)'],
['1 + 2)'],
['(1 2 +)'],
];
return array_combine(array_column($examples, 0), $examples);
}
}

View File

@@ -0,0 +1,93 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\assemble;
use function Parsica\Parsica\char;
use function Parsica\Parsica\string;
final class assembleTest extends TestCase
{
use ParserAssertions;
/** @test */
public function assemble_string()
{
$parser = assemble(
string('first'),
string('second'),
);
$this->assertParses("firstsecond", $parser, "firstsecond");
$this->assertRemainder("firstsecond", $parser, "");
}
/** @test */
public function assemble_string_ignore()
{
$parser = assemble(
string('first')->thenIgnore(char('-')),
string('second'),
);
$this->assertParses("first-second", $parser, "firstsecond");
$this->assertRemainder("first-second", $parser, "");
}
/** @test */
public function assemble_arrays()
{
$toArray = fn($v) => [$v];
$parser = assemble(
string('first')->map($toArray),
string('second')->map($toArray),
);
$input = "firstsecond";
$expected = ["first", "second"];
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function assemble_different_types_but_the_others_are_ignored()
{
// @todo this could be more elegant
$toArray = fn($v) => [$v];
$parser = assemble(
char('[')->sequence(
string('first')->map($toArray)
),
char(',')->sequence(
string('second')->map($toArray)
)->thenIgnore(char(']')),
);
$input = "[first,second]";
$expected = ["first", "second"];
$this->assertParses($input, $parser, $expected);
}
/** @test */
public function label()
{
$parser = assemble(
string('first'),
string('second'),
);
$this->assertParseFails("X", $parser, "'first'");
$this->assertParseFails("firsX", $parser, "'first'");
$this->assertParseFails("firstX", $parser, "'second'");
}
}

View File

@@ -0,0 +1,125 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\Parser;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{alphaChar,
alphaNumChar,
binDigitChar,
blank,
char,
charI,
controlChar,
digitChar,
hexDigitChar,
lowerChar,
octDigitChar,
printChar,
punctuationChar,
space,
string,
stringI,
tab,
upperChar,
whitespace};
final class charactersTest extends TestCase
{
use ParserAssertions;
/** @test */
public function char()
{
$this->assertParses("abc", char('a'), "a");
$this->assertRemainder("abc", char('a'), "bc");
$this->assertParseFails("bc", char('a'), "'a'");
}
/** @test */
public function charI()
{
$this->assertParses("abc", charI('a'), "a");
$this->assertParses("ABC", charI('a'), "A");
}
/** @test */
public function charI_label()
{
$this->assertParseFails("foo", charI('a'), "'a' or 'A'");
$this->assertParseFails("foo", charI('%'), "'%'");
}
/** @test */
public function string()
{
$this->assertParses("abcde", string('abc'), "abc");
$this->assertParseFails("babc", string('abc'), "'abc'");
}
/** @test */
public function stringI()
{
$parser = stringI('hello world');
$input = "hElLO WoRlD!!1!";
$expected = "hElLO WoRlD";
$this->assertParses($input, $parser, $expected, "stringI() should be case-preserving");
$this->assertRemainder($input, $parser, "!!1!");
}
public function characterParsers(): array
{
$tests = [
// dataSet => [Parser, example character]
'controlChar' => [controlChar(), mb_chr(0x05)],
'printChar_a' => [printChar(), "a"],
'printChar_%' => [printChar(), "%"],
];
$types = [
// dataSet => [Parser, [example character]]
'upperChar' => [upperChar(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
'lowerChar' => [lowerChar(), "abcdefghijklmnopqrstuvwxyz"],
'alphaChar' => [alphaChar(), "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"],
'alphaNumChar' => [alphaNumChar(), "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"],
'digitChar' => [digitChar(), "0123456789"],
'binDigitChar' => [binDigitChar(), "01"],
'octDigitChar' => [octDigitChar(), "01234567"],
'hexDigitChar' => [hexDigitChar(), "0123456789abcdefABCDEF"],
'punctuationChar' => [punctuationChar(), "!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"],
'whitespace' => [whitespace(), " \t\n\r\f\v"],
'space' => [space(), " "],
'tab' => [tab(), "\t"],
'blank' => [blank(), "\t "],
];
foreach ($types as $name => [$parser, $chars]) {
foreach (mb_str_split($chars) as $char) {
$tests["{$name}: {$char}"] = [$parser, $char];
}
}
return $tests;
}
/**
* @test
* @dataProvider characterParsers
*/
public function character_parsers(Parser $parser, string $example)
{
$this->assertParses($example, $parser, $example);
}
}

View File

@@ -0,0 +1,462 @@
<?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;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{alphaChar,
alphaNumChar,
any,
anySingle,
anySingleBut,
atLeastOne,
between,
char,
choice,
collect,
digitChar,
either,
float,
identity,
keepFirst,
lookAhead,
many,
noneOf,
noneOfS,
notFollowedBy,
oneOf,
oneOfS,
optional,
punctuationChar,
repeat,
repeatList,
sepBy,
sepBy1,
sequence,
skipSpace,
string,
stringI,
takeRest,
whitespace};
final class combinatorsTest extends TestCase
{
use ParserAssertions;
/** @test */
public function identity()
{
$parser = identity(char('a'));
$this->assertParses("abc", $parser, "a");
$this->assertRemainder("abc", $parser, "bc");
}
/** @test */
public function identity_does_not_show_up_in_error_messages()
{
$parser = identity(char('a'));
$this->assertParseFails("bc", $parser, "'a'");
}
/** @test */
public function thenIgnore()
{
$parser = string('abcd')
->thenIgnore(char('-'))
->append(string('efgh'));
$this->assertParses("abcd-efgh", $parser, "abcdefgh");
$this->assertParseFails("abcdefgh", $parser);
// smae with optional dash
$parser = string('abcd')
->thenIgnore(optional(char('-')))
->append(string('efgh'));
$this->assertParses("abcdefgh", $parser, "abcdefgh");
$this->assertParses("abcd-efgh", $parser, "abcdefgh");
}
/** @test */
public function anySingle()
{
$parser = anySingle();
$this->assertFailOnEOF($parser);
$this->assertParses("a", $parser, "a");
$this->assertParses("abc", $parser, "a");
$this->assertParses(":", $parser, ":");
$this->assertParses(":-)", $parser, ":");
}
/** @test */
public function anySingleBut()
{
$parser = anySingleBut("x");
$this->assertFailOnEOF($parser);
$this->assertParses("a", $parser, "a");
$this->assertRemainder("a", $parser, "");
$this->assertParses("abc", $parser, "a");
$this->assertRemainder("abc", $parser, "bc");
$this->assertParseFails("x", $parser);
$this->assertParseFails("xxx", $parser);
}
/** @test */
public function oneOf()
{
$parser = oneOf(['a', 'b', 'c']);
$this->assertFailOnEOF($parser);
$this->assertParses("a", $parser, "a");
$this->assertParses("ax", $parser, "a");
$this->assertParses("b", $parser, "b");
$this->assertParses("c", $parser, "c");
$this->assertParseFails("xyz", $parser);
}
/** @test */
public function oneOf_expects_single_chars()
{
$this->expectException(InvalidArgumentException::class);
$parser = oneOf(['a', "long", "c"]);
}
/** @test */
public function oneOfS()
{
$parser = oneOfS("abc");
$this->assertParses("ax", $parser, "a");
$this->assertParseFails("xyz", $parser);
}
/** @test */
public function noneOf()
{
$parser = noneOf(['a', 'b', 'c']);
$this->assertFailOnEOF($parser);
$this->assertParseFails("a", $parser);
$this->assertParseFails("ax", $parser);
$this->assertParseFails("b", $parser);
$this->assertParses("xyz", $parser, "x");
$this->assertRemainder("xyz", $parser, "yz");
}
/** @test */
public function noneOfS()
{
$parser = noneOfS("abc");
$this->assertParseFails("ax", $parser);
$this->assertParses("xyz", $parser, "x");
}
/** @test */
public function takeRest()
{
$parser = takeRest();
$this->assertSucceedOnEOF($parser);
$this->assertParses("xyz", $parser, "xyz");
$this->assertRemainder("xyz", $parser, "");
}
/** @test */
public function either()
{
$parser = either(char('a'), char('b'));
$this->assertFailOnEOF($parser);
$this->assertParses("abc", $parser, "a");
$this->assertRemainder("abc", $parser, "bc");
$this->assertParses("bc", $parser, "b");
$this->assertRemainder("bc", $parser, "c");
$this->assertParseFails("cd", $parser);
}
/** @test */
public function either_with_mixed_type()
{
$parser = either(
atLeastOne(digitChar())->map(fn(string $o)=> intval($o))->thenEof(),
atLeastOne(alphaNumChar())->thenEof(),
);
$actual = $parser->tryString("123")->output();
$this->assertIsInt($actual);
$this->assertEquals("123", $actual);
$actual = $parser->tryString("123a")->output();
$this->assertIsString($actual);
$this->assertEquals("123a", $actual);
}
/** @test */
public function sequence()
{
$parser = sequence(char('a'), char('b'));
$this->assertFailOnEOF($parser);
$this->assertParses("abc", $parser, "b");
$this->assertRemainder("abc", $parser, "c");
$this->assertParseFails("acc", $parser);
$this->assertParseFails("cab", $parser);
}
/** @test */
public function collect()
{
$parser =
collect(
string("Hello")
->append(skipSpace())->thenIgnore(char(','))
->append(skipSpace()),
string("world")
->thenIgnore(char('!'))
);
$expected = ["Hello", "world"];
$this->assertFailOnEOF($parser);
$this->assertParses("Hello , world!", $parser, $expected);
$this->assertParses("Hello,world!", $parser, $expected);
}
/** @test */
public function collectFails()
{
$parser =
collect(
string("Hello"),
string("World")
);
$this->assertFailOnEOF($parser);
$this->assertParseFails("HiWorld", $parser, "'Hello'");
$this->assertParseFails("HelloPlanet", $parser, "'World'");
}
/**
* @test
*/
public function atLeastOne()
{
$parser = atLeastOne(char('a'));
$this->assertFailOnEOF($parser);
$this->assertParses("a", $parser, "a");
$this->assertParses("aa", $parser, "aa");
$this->assertParses("aaaaa", $parser, "aaaaa");
$this->assertParses("aaabb", $parser, "aaa");
$this->assertParseFails("bb", $parser);
}
/** @test */
public function any_()
{
$symbol = any(string(""), string("$"));
$amount = float()->map('floatval');
$money = collect($symbol, $amount);
$this->assertFailOnEOF($money);
$this->assertParses("", $symbol, "");
$this->assertParses("15.23", $amount, 15.23);
$this->assertParses("€15.23", $money, ["", 15.23]);
$this->assertParses("$15", $money, ["$", 15.0]);
$this->assertParseFails("£12.13", $money);
}
/** @test */
public function choice()
{
$symbol = choice(string(""), string("$"));
$amount = float()->map('floatval');
$money = collect($symbol, $amount);
$this->assertFailOnEOF($money);
$this->assertParses("", $symbol, "");
$this->assertParses("15.23", $amount, 15.23);
$this->assertParses("€15.23", $money, ["", 15.23]);
$this->assertParses("$15", $money, ["$", 15.0]);
$this->assertParseFails("£12.13", $money);
}
/** @test */
public function skipMany()
{
//skipMany p applies the parser p zero or more times, skipping its result.
self::markTestIncomplete();
}
/** @test */
public function keepFirst__inside_a_nested_parser()
{
$movies = any(stringI('movie'), stringI('movies'), stringI('film'), stringI('films'))->followedBy(skipSpace());
$number = atLeastOne(digitChar())->map('intval');
$words = many(any(alphaChar(), punctuationChar(), whitespace()));
$parser = $words->followedBy(keepFirst($number, skipSpace()->followedBy($movies)));
$input = "I watched 23 MOVIES this week ";
$this->assertParses($input, $parser, 23);
}
/** @test */
public function between()
{
$parser = between(char('{'), char('}'), atLeastOne(alphaNumChar()));
$input = "{foo}";
$this->assertParses($input, $parser, "foo");
}
/** @test */
public function between_failure()
{
$parser = between(char('{'), char('}'), atLeastOne(alphaNumChar()));
$this->assertParseFails("foo}", $parser, "'{'");
$this->assertParseFails("{foo", $parser, "'}'");
$this->assertParseFails("{}", $parser, "A-Z or a-z or 0-9");
}
/** @test */
public function notFollowedBy()
{
$print = string("print");
$this->assertParses("print('Hello World');", $print, "print");
// This also outputs "print", but it wasn't our intention, because "printXYZ" is not a valid keyword:
$this->assertParses("printXYZ('Hello World');", $print, "print");
// with notFollowedBy:
$print = keepFirst(string("print"), notFollowedBy(alphaNumChar()));
$this->assertParses("print('Hello World');", $print, "print");
$this->assertParseFails("printXYZ('Hello World');", $print);
}
/** @test */
public function notFollowedBy_fluent()
{
$print = string("print")->notFollowedBy(alphaNumChar());
$this->assertParses("print('Hello World');", $print, "print");
$this->assertParseFails("printXYZ('Hello World');", $print);
}
/** @test */
public function lookAhead()
{
$parser = lookAhead(stringI("hello"));
// On success, lookAhead succeeds without consuming input
$this->assertParses("Hello, world!", $parser, "Hello");
$this->assertRemainder("Hello, world!", $parser, "Hello, world!");
// On fail, lookAhead fails without consuming input
$this->assertParseFails("Hi, world!", $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 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,105 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\float;
use function Parsica\Parsica\integer;
use function Parsica\Parsica\keepFirst;
use function Parsica\Parsica\skipSpace1;
final class numericTest extends TestCase
{
use ParserAssertions;
/** @test */
public function integer()
{
$parser = integer();
$this->assertParses("0", $parser, "0");
$this->assertParses("1", $parser, "1");
$this->assertParses("10", $parser, "10");
$this->assertParses("972115541", $parser, "972115541");
$this->assertParses("-0", $parser, "-0");
$this->assertParses("-1", $parser, "-1");
$this->assertParses("-10", $parser, "-10");
$this->assertParses("-972115541", $parser, "-972115541");
}
/** @test */
public function integer_maps_to_int()
{
$parser = integer()->map('intval');
$this->assertParses("0", $parser, 0);
$this->assertParses("1", $parser, 1);
$this->assertParses("10", $parser, 10);
$this->assertParses("972115541", $parser, 972115541);
$this->assertParses("-0", $parser, 0);
$this->assertParses("-1", $parser, -1);
$this->assertParses("-10", $parser, -10);
$this->assertParses("-972115541", $parser, -972115541);
}
/** @test */
public function integer_fails()
{
$parser = keepFirst(integer(), skipSpace1());
$this->assertParseFails("00", $parser);
$this->assertParseFails("01", $parser);
$this->assertParseFails("+1", $parser);
}
/** @test */
public function float()
{
$parser = float();
$this->assertParses("0", $parser, "0");
$this->assertParses("0.1", $parser, "0.1");
$this->assertParses("0.15", $parser, "0.15");
$this->assertParses("0.10", $parser, "0.10");
$this->assertParses("123.456", $parser, "123.456");
$this->assertParses("1.2345678", $parser, "1.2345678");
$this->assertParses("-1.2345678", $parser, "-1.2345678");
$this->assertParses("-1.23456789E+123", $parser, "-1.23456789E+123");
$this->assertParses("-1.23456789e-123", $parser, "-1.23456789E-123");
$this->assertParses("-1E-123", $parser, "-1E-123");
}
/** @test */
public function float_fails()
{
$parser = keepFirst(float(), skipSpace1()); // avoid false positives
$this->assertParseFails("00", $parser);
$this->assertParseFails("0. 15", $parser);
$this->assertParseFails("00.10", $parser);
$this->assertParseFails(" + 00.10", $parser);
$this->assertParseFails(" + 123.456", $parser);
$this->assertParseFails("1.234.5678", $parser);
$this->assertParseFails(" - 1,2345678", $parser);
$this->assertParseFails("--1.234E123", $parser);
$this->assertParseFails(" - 1.234e", $parser);
$this->assertParseFails(" - 1E-123E - 456", $parser);
}
/** @test */
public function float_maps_correctly()
{
$parser = float()->map('floatval');
$this->assertParses("123.456", $parser, 123.456);
$this->assertParses("-0.1", $parser, -0.1);
$this->assertParses("1.2345678", $parser, 1.2345678);
$this->assertParses("-1.2345678", $parser, -1.2345678);
$this->assertParses("-1.23456789E+123", $parser, -1.23456789E+123);
$this->assertParses("-1.23456789e-123", $parser, -1.23456789E-123);
$this->assertParses("-1E-123", $parser, -1E-123);
}
}

View File

@@ -0,0 +1,57 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{andPred, isEqual, notPred, orPred, satisfy};
final class predicatesTest extends TestCase
{
use ParserAssertions;
/** @test */
public function isEqual()
{
$this->assertTrue(isEqual('x')('x'));
$this->assertFalse(isEqual('x')('y'));
}
/** @test */
public function notPred()
{
$this->assertFalse(notPred(isEqual('x'))('x'));
$this->assertTrue(notPred(isEqual('x'))('y'));
$parser = satisfy(notPred(isEqual('x')));
$this->assertParseFails("xyz", $parser);
$this->assertParses("yz", $parser, "y");
$this->assertParseFails("", $parser, "satisfy(predicate)");
}
/** @test */
public function orPred()
{
$predicate = orPred(isEqual('x'), isEqual('y'));
$this->assertTrue($predicate('x'));
$this->assertTrue($predicate('y'));
$this->assertFalse($predicate('z'));
}
/** @test */
public function andPred()
{
$predicate = andPred(isEqual('x'), isEqual('x'));
$this->assertTrue($predicate('x'));
$this->assertFalse($predicate('y'));
}
}

View File

@@ -0,0 +1,185 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{anything,
eof,
everything,
fail,
nothing,
satisfy,
skipWhile,
skipWhile1,
succeed,
takeWhile,
takeWhile1};
use function Parsica\Parsica\{isEqual, notPred};
final class primitivesTest extends TestCase
{
use ParserAssertions;
/** @test */
public function satisfy()
{
$parser = satisfy(isEqual('x'));
$this->assertParses("xyz", $parser, "x");
$this->assertRemainder("xyz", $parser, "yz");
$this->assertParseFails("yz", $parser, "satisfy(predicate)");
$this->assertParseFails("", $parser, "satisfy(predicate)");
}
/** @test */
public function anything_()
{
$this->assertParses("xyz", anything(), "x");
$this->assertRemainder("xyz", anything(), "yz");
$this->assertParses(":-)", anything(), ":");
$this->assertRemainder(":-)", anything(), "-)");
$this->assertParseFails("", anything(), "anything");
}
/** @test */
public function nothing()
{
$this->assertRemainder("xyz", nothing(), "xyz");
$this->assertRemainder(":-)", nothing(), ":-)");
}
/** @test */
public function everything()
{
$this->assertParses("xyz", everything(), "xyz");
$this->assertRemainder("xyz", everything(), "");
$this->assertParses(":-)", everything(), ":-)");
$this->assertRemainder(":-)", everything(), "");
$this->assertParses("", everything(), "");
}
/** @test */
public function eof()
{
$this->assertParses("", eof(), "");
$this->assertParseFails("xyz", eof(), "<EOF>");
}
/** @test */
public function takeWhile()
{
$parser = takeWhile(isEqual('a'));
$this->assertParses("xyz", $parser, "");
$this->assertParses("xaaa", $parser, "");
$this->assertParses("axyz", $parser, "a");
$this->assertParses("aaaxyz", $parser, "aaa");
$this->assertParses("aaa", $parser, "aaa");
}
/** @test */
public function takeWhile_using_not()
{
$parser = takeWhile(notPred(isEqual('a')));
$this->assertParses("xyza", $parser, "xyz");
$this->assertParses("xyz", $parser, "xyz");
$this->assertParses("xaaa", $parser, "x");
$this->assertParses("axyz", $parser, "");
$this->assertParses("aaaxyz", $parser, "");
$this->assertParses("aaa", $parser, "");
}
/** @test */
public function takeWile_succeeds_on_EOF()
{
$parser = takeWhile(isEqual('a'));
$this->assertSucceedOnEOF($parser);
$parser = takeWhile(notPred(isEqual('a')));
$this->assertSucceedOnEOF($parser);
}
/** @test */
public function takeWhile1()
{
$parser = takeWhile1(isEqual('a'));
$this->assertFailOnEOF($parser);
$this->assertParseFails("xyz", $parser, "takeWhile1(predicate)");
$this->assertParseFails("takeWhile1(predicate)", $parser);
$this->assertParses("axyz", $parser, "a");
$this->assertParses("aaaxyz", $parser, "aaa");
$this->assertParses("aaa", $parser, "aaa");
$this->assertParseFails("", $parser, "takeWhile1(predicate)");
}
/** @test */
public function success_and_failure()
{
$this->assertParses("doesn't matter what we put in here", succeed(), null);
$this->assertRemainder("no input is consumed", succeed(), "no input is consumed");
$this->assertParseFails("doesn't matter what we put in here", fail("reason for failure"));
$or = fail("")->or(succeed());
$this->assertParses("failure or success is success", $or, null);
}
/** @test */
public function skipWhile()
{
$parser = skipWhile(isEqual('a'));
$this->assertParses("xyz", $parser, null);
$this->assertRemainder("xyz", $parser, "xyz");
$this->assertParses("xaaa", $parser, null);
$this->assertRemainder("xaaa", $parser, "xaaa");
$this->assertParses("axyz", $parser, null);
$this->assertRemainder("axyz", $parser, "xyz");
$this->assertParses("aaaxyz", $parser, null);
$this->assertRemainder("aaaxyz", $parser, "xyz");
$this->assertParses("aaa", $parser, null);
$this->assertRemainder("aaa", $parser, "");
}
/** @test */
public function skipWhile_using_not()
{
$parser = skipWhile(notPred(isEqual('a')));
$this->assertParses("xyz", $parser, null);
$this->assertRemainder("xyz", $parser, "");
$this->assertParses("xaaa", $parser, null);
$this->assertRemainder("xaaa", $parser, "aaa");
$this->assertParses("axyz", $parser, null);
$this->assertRemainder("axyz", $parser, "axyz");
$this->assertParses("aaaxyz", $parser, null);
$this->assertRemainder("aaaxyz", $parser, "aaaxyz");
$this->assertParses("aaa", $parser, null);
$this->assertRemainder("aaa", $parser, "aaa");
}
/** @test */
public function skipWhile1()
{
$parser = skipWhile1(isEqual('a'));
$this->assertFailOnEOF($parser);
$this->assertParseFails("xyz", $parser);
$this->assertParses("axyz", $parser, null);
$this->assertRemainder("axyz", $parser, "xyz");
$this->assertParses("aaaxyz", $parser, null);
$this->assertRemainder("aaaxyz", $parser, "xyz");
$this->assertParses("aaa", $parser, null);
$this->assertRemainder("aaa", $parser, "");
$this->assertParseFails("", $parser);
}
}

View File

@@ -0,0 +1,126 @@
<?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;
use Exception;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\{between, char, collect, digitChar, recursive};
final class recursionTest extends TestCase
{
use ParserAssertions;
/** @test */
public function recursion_on_nested_structures()
{
$opening = char('[');
$closing = char(']');
$comma = char(',');
$digit = digitChar()->map('intval');
$pair = recursive();
$pair->recurse(
between(
$opening, $closing, collect(
$digit->or($pair)->thenIgnore($comma),
$digit->or($pair)
)
)
);
$input = "[1,2]";
$this->assertParses($input, $pair, [1, 2]);
$input = "[1,[2,[3,4]]]";
$this->assertParses($input, $pair, [1, [2, [3, 4]]]);
$input = "[[[4,3],2],1]";
$this->assertParses($input, $pair, [[[4, 3], 2], 1]);
}
/** @test */
public function nesting_multiple_recursive_parsers()
{
$openingSquare = char('[');
$closingSquare = char(']');
$openingCurly = char('{');
$closingCurly = char('}');
$comma = char(',');
$digit = digitChar()->map('intval');
$curlyPair = recursive();
$squarePair = recursive();
$anyPair = $curlyPair->or($squarePair);
$expr = $digit->or($anyPair);
$inner = collect($expr->thenIgnore($comma), $expr);
$curlyPair->recurse(
between($openingCurly, $closingCurly, $inner)
);
$squarePair->recurse(
between($openingSquare, $closingSquare, $inner)
);
$input = "[1,{2,[{3,4},{5,6}]}]";
$this->assertParses($input, $anyPair, [1, [2, [[3, 4], [5, 6]]]]);
}
/** @test */
public function throw_on_multiple_calls_to_recurse()
{
$parser = recursive();
$parser->recurse(char('a'));
$this->expectException(Exception::class);
$parser->recurse(char('b'));
}
/** @test */
public function throw_when_recursing_non_recursive_parsers()
{
$parser = char('a');
$this->expectException(Exception::class);
$parser->recurse(char('b'));
}
/** @test */
public function throw_for_nested_recursive_parsers_that_arent_completely_setup()
{
$p1 = recursive();
$p2 = recursive();
$p1->recurse($p2);
$this->expectException(Exception::class);
$p1->run(new StringStream("test"));
}
/** @test */
public function using_a_recursive_parser_like_a_regular_one_after_it_was_setup()
{
$parser = recursive();
$parser->recurse(char('a'));
$labeledParser = $parser->label("test");
$this->assertParses("abc", $labeledParser, "a");
$this->assertParseFails("bc", $labeledParser, "test");
}
/** @test */
public function calling_combinators_on_a_recursive_parser_before_it_is_setup()
{
$p1 = recursive();
$p2 = char('a')->followedBy($p1->label("test"));
$this->expectException(Exception::class);
$this->assertParses("abc", $p2, "a");
}
}

View File

@@ -0,0 +1,53 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\StringStream;
use function Parsica\Parsica\char;
use function Parsica\Parsica\emit;
final class sideEffectsTest extends TestCase
{
/** @test */
public function emit()
{
$cache = new Cache();
$addToCache = fn($output) => $cache->add($output, $output);
$parser = emit(char('a'), $addToCache);
$input = "a";
$parser->run(new StringStream($input));
$this->assertEquals("a", $cache->get('a'));
$input = "b";
$parser->run(new StringStream($input));
$this->assertNull($cache->get('b'));
}
}
class Cache
{
private $items = [];
function add($key, $value)
{
$this->items[$key] = $value;
}
function get($key)
{
return array_key_exists($key, $this->items)
? $this->items[$key]
: null;
}
}

View File

@@ -0,0 +1,89 @@
<?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;
use PHPUnit\Framework\TestCase;
use Parsica\Parsica\PHPUnit\ParserAssertions;
use function Parsica\Parsica\{crlf, eol, newline, skipHSpace, skipHSpace1, skipSpace, skipSpace1, tab};
final class spaceTest extends TestCase
{
use ParserAssertions;
/** @test */
public function newline()
{
$this->assertParses("\nabc", newline(), "\n");
$this->assertParseFails("\rabc", newline());
}
/** @test */
public function crlf()
{
$this->assertParses("\r\nabc", crlf(), "\r\n");
$this->assertParseFails("\rabc", crlf(), "<crlf>");
$this->assertParseFails("\rabc", crlf(), "<crlf>");
}
/** @test */
public function eol()
{
$this->assertParses("\nabc", eol(), "\n");
$this->assertParses("\r\nabc", eol(), "\r\n");
$this->assertParseFails("\rabc", eol(), "<EOL>");
}
/** @test */
public function tab()
{
$this->assertParses("\tabc", tab(), "\t");
$this->assertParseFails("abc", tab(), "<tab>");
}
/** @test */
public function skipSpace()
{
$this->assertRemainder("no space", skipSpace(), "no space");
$this->assertRemainder(" 1 space", skipSpace(), "1 space");
$this->assertRemainder("\ttab", skipSpace(), "tab");
$this->assertRemainder("\nnewline", skipSpace(), "newline");
$this->assertRemainder("\t \n \r\n abc", skipSpace(), "abc");
}
/** @test */
public function skipHSpace()
{
$this->assertRemainder("abc", skipHSpace(), "abc");
$this->assertRemainder("\t abc", skipHSpace(), "abc");
$this->assertRemainder("\t \nabc", skipHSpace(), "\nabc");
}
/** @test */
public function skipSpace1()
{
$this->assertParseFails("no space", skipSpace1(), "<space>");
$this->assertRemainder(" 1 space", skipSpace1(), "1 space");
$this->assertRemainder("\ttab", skipSpace1(), "tab");
$this->assertRemainder("\nnewline", skipSpace1(), "newline");
$this->assertRemainder("\t \n \r\n abc", skipSpace1(), "abc");
}
/** @test */
public function skipHSpace1()
{
$this->assertParseFails("no space", skipHSpace1(), "<space>");
$this->assertRemainder("\t some space", skipHSpace1(), "some space");
$this->assertRemainder("\t abc", skipHSpace1(), "abc");
$this->assertRemainder("\t \nabc", skipHSpace1(), "\nabc");
}
}