First commit

This commit is contained in:
Cody Cook 2024-04-29 19:04:51 -07:00
commit d5bb2f19fa
117 changed files with 68604 additions and 0 deletions

View file

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
abstract class AbstractParser
{
protected $lexer;
/**
* Constructor
*
* @param LexerInterface $lexer The lexer
*/
public function __construct(LexerInterface $lexer)
{
$this->lexer = $lexer;
}
/**
* Parse a given input
*
* @param string $input
*
* @return mixed
*/
public function parse(string $input)
{
$ts = $this->lexer->tokenize($input);
$parseResult = $this->parseImplementation($ts);
if ($ts->hasPendingTokens()) {
throw new SyntaxErrorException('There are tokens not processed.');
}
return $parseResult;
}
/**
* Do the real parsing
*
* @param TokenStream $stream The token stream returned by the lexer
*
* @return mixed
*/
abstract protected function parseImplementation(TokenStream $stream);
}

View file

@ -0,0 +1,175 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
class BasicLexer implements LexerInterface
{
protected $newlineTokenName = 'T_NEWLINE';
protected $eosTokenName = 'T_EOS';
protected $activateNewlineToken = false;
protected $activateEOSToken = false;
protected $terminals = [];
/**
* Constructor
*
* @param array $terminal List of terminals
* e.g:
* [
* "/^([)/" => "T_BRAKET_BEGIN"
* ]
*/
public function __construct(array $terminals)
{
$this->terminals = $terminals;
}
/**
* Generates an special "T_NEWLINE" for each line of the input
*
* @return BasicLexer The BasicLexer itself
*/
public function generateNewlineTokens() : BasicLexer
{
$this->activateNewlineToken = true;
return $this;
}
/**
* Generates an special "T_EOS" at the end of the input string
*
* @return BasicLexer The BasicLexer itself
*/
public function generateEosToken() : BasicLexer
{
$this->activateEOSToken = true;
return $this;
}
/**
* Sets the name of the newline token
*
* @param string $name The name of the token
*
* @return BasicLexer The BasicLexer itself
*
* @throws InvalidArgumentException If the name is empty
*/
public function setNewlineTokenName(string $name) : BasicLexer
{
if (strlen($name) == 0) {
throw new \InvalidArgumentException('The name of the newline token must be not empty.');
}
$this->newlineTokenName = $name;
return $this;
}
/**
* Sets the name of the end-of-string token
*
* @param string $name The name of the token
*
* @return BasicLexer The BasicLexer itself
*
* @throws InvalidArgumentException If the name is empty
*/
public function setEosTokenName(string $name) : BasicLexer
{
if (strlen($name) == 0) {
throw new \InvalidArgumentException('The name of the EOS token must be not empty.');
}
$this->eosTokenName = $name;
return $this;
}
/**
* {@inheritdoc}
*/
public function tokenize(string $input) : TokenStream
{
$counter = 0;
$tokens = [];
$lines = explode("\n", $input);
$totalLines = count($lines);
foreach ($lines as $number => $line) {
$offset = 0;
$lineNumber = $number + 1;
while ($offset < strlen($line)) {
list($name, $matches) = $this->match($line, $lineNumber, $offset);
if (isset($matches[1])) {
$token = new Token($matches[1], $name, $lineNumber);
$this->processToken($token, $matches);
$tokens[] = $token;
}
$offset += strlen($matches[0]);
}
if ($this->activateNewlineToken && ++$counter < $totalLines) {
$tokens[] = new Token("\n", $this->newlineTokenName, $lineNumber);
}
}
if ($this->activateEOSToken) {
$tokens[] = new Token('', $this->eosTokenName, $lineNumber);
}
return new TokenStream($tokens);
}
/**
* Returns the first match with the list of terminals
*
* @return array An array with the following keys:
* [0] (string): name of the token
* [1] (array): matches of the regular expression
*
* @throws SyntaxErrorException If the line does not contain any token
*/
protected function match(string $line, int $lineNumber, int $offset) : array
{
$restLine = substr($line, $offset);
foreach ($this->terminals as $pattern => $name) {
if (preg_match($pattern, $restLine, $matches)) {
return [
$name,
$matches,
];
}
}
throw new SyntaxErrorException(sprintf('Lexer error: unable to parse "%s" at line %s.', $line, $lineNumber));
}
/**
* Applies additional actions over a token.
*
* Implement this method if you need to do changes after a token was found.
* This method is invoked for each token found
*
* @param Token $token The token
* @param string[] $matches Set of matches from the regular expression
*
* @return void
*/
protected function processToken(Token $token, array $matches) : void
{
}
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
/**
* Interface for a lexer
*/
interface LexerInterface
{
/**
* Returns the tokens found
*
* @param string $input The input to be tokenized
*
* @return TokenStream The stream of tokens
*/
public function tokenize(string $input) : TokenStream;
}

View file

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
/**
* Exception thrown when an error occurs during parsing or tokenizing
*/
class SyntaxErrorException extends \RuntimeException
{
protected $token;
/**
* Constructor
*
* @param string $message The error messsage
* @param Token|null $token The token
* @param \Exception|null $previous The previous exceptio
*/
public function __construct(string $message, Token $token = null, \Exception $previous = null)
{
parent::__construct($message, 0, $previous);
}
/**
* Sets the token associated to the exception
*
* @param Token $token The token
*/
public function setToken(Token $token) : void
{
$this->token = $token;
}
/**
* Returns the token associated to the exception
*
* @return Token|null
*/
public function getToken() : ?Token
{
return $this->token;
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
class Token
{
protected $value;
protected $name;
protected $line;
/**
* Constructor.
*
* @param string $value The value of the token
* @param string $name The name of the token. e.g: T_BRAKET_BEGIN
* @param int $line Line of the code in where the token is found
*/
public function __construct(string $value, string $name, int $line)
{
$this->value = $value;
$this->name = $name;
$this->line = $line;
}
/**
* Returns the value (the match term)
*
* @return string
*/
public function getValue() : string
{
return $this->value;
}
/**
* Returns the name of the token
*
* @return string
*/
public function getName() : string
{
return $this->name;
}
/**
* Returns the line of the code in where the token is found
*
* @return int
*/
public function getLine() : int
{
return $this->line;
}
public function __toString() : string
{
return sprintf(
"[\n name: %s\n value:%s\n line: %s\n]",
$this->name,
$this->value,
$this->line
);
}
}

View file

@ -0,0 +1,163 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
class TokenStream implements TokenStreamInterface
{
protected $tokens;
protected $index = -1;
/**
* Constructor
*
* @param Token[] List of tokens
*/
public function __construct(array $tokens)
{
$this->tokens = $tokens;
}
/**
* {@inheritdoc}
*/
public function moveNext() : ?Token
{
return $this->tokens[++$this->index] ?? null;
}
/**
* {@inheritdoc}
*/
public function matchNext(string $tokenName) : string
{
$token = $this->moveNext();
--$this->index;
if ($token->getName() == $tokenName) {
return $this->moveNext()->getValue();
}
throw new SyntaxErrorException(sprintf(
'Syntax error: expected token with name "%s" instead of "%s" at line %s.',
$tokenName,
$token->getName(),
$token->getLine()
));
}
/**
* {@inheritdoc}
*/
public function skipWhile(string $tokenName) : void
{
$this->skipWhileAny([$tokenName]);
}
/**
* {@inheritdoc}
*/
public function skipWhileAny(array $tokenNames) : void
{
while ($this->isNextAny($tokenNames)) {
$this->moveNext();
}
}
/**
* {@inheritdoc}
*/
public function isNext(string $tokenName) : bool
{
$token = $this->moveNext();
--$this->index;
if ($token === null) {
return false;
}
return $token->getName() == $tokenName;
}
/**
* {@inheritdoc}
*/
public function isNextSequence(array $tokenNames) : bool
{
$result = true;
$currentIndex = $this->index;
foreach ($tokenNames as $tokenName) {
$token = $this->moveNext();
if ($token === null || $token->getName() != $tokenName) {
$result = false;
break;
}
}
$this->index = $currentIndex;
return $result;
}
/**
* {@inheritdoc}
*/
public function isNextAny(array $tokenNames) : bool
{
$token = $this->moveNext();
--$this->index;
if ($token === null) {
return false;
}
foreach ($tokenNames as $tokenName) {
if ($tokenName === $token->getName()) {
return true;
}
}
return false;
}
/**
* Returns all tokens
*
* @return token[] List of tokens
*/
public function getAll() : array
{
return $this->tokens;
}
/**
* {@inheritdoc}
*/
public function hasPendingTokens() :bool
{
$tokenCount = count($this->tokens);
if ($tokenCount == 0) {
return false;
}
return $this->index < ($tokenCount - 1);
}
/**
* {@inheritdoc}
*/
public function reset() : void
{
$this->index = -1;
}
}

View file

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Yosymfony\ParserUtils package.
*
* (c) YoSymfony <http://github.com/yosymfony>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Yosymfony\ParserUtils;
interface TokenStreamInterface
{
/**
* Moves the pointer one token forward
*
* @return Token|null The token or null if there are not more tokens
*/
public function moveNext() : ?Token;
/**
* Matches the next token. This method moves the pointer one token forward
* if an error does not occur
*
* @param string $tokenName The name of the token
*
* @return string The value of the token
*
* @throws SyntaxErrorException If the next token does not match
*/
public function matchNext(string $tokenName) : string;
/**
* Skips tokens while they match with the token name passed as argument.
* This method moves the pointer "n" tokens forward until the last one
* that match with the token name
*
* @param string $tokenName The name of the token
*/
public function skipWhile(string $tokenName) : void;
/**
* Skips tokens while they match with one of the token names passed as
* argument. This method moves the pointer "n" tokens forward until the
* last one that match with one of the token names
*
* @param string[] $tokenNames List of token names
*/
public function skipWhileAny(array $tokenNames) : void;
/**
* Checks if the next token matches with the token name passed as argument
*
* @param string $tokenName The name of the token
*
* @return bool
*/
public function isNext(string $tokenName) : bool;
/**
* Checks if the following tokens in the stream match with the sequence of tokens
*
* @param string[] $tokenNames Sequence of token names
*
* @return bool
*/
public function isNextSequence(array $tokenNames) : bool;
/**
* Checks if one of the tokens passed as argument is the next token
*
* @param string[] $tokenNames List of token names. e.g: 'T_PLUS', 'T_SUB'
*
* @return bool
*/
public function isNextAny(array $tokenNames) : bool;
/**
* Has pending tokens?
*
* @return bool
*/
public function hasPendingTokens() :bool;
/**
* Resets the stream to the beginning
*/
public function reset() : void;
}