From 44595cd813f015401608b2b80c552fa23a2bf6b2 Mon Sep 17 00:00:00 2001 From: asccc Date: Sun, 12 Jul 2020 01:42:44 +0200 Subject: [PATCH 01/27] had to reset my branch --- app/CodeFormatterApp.php | 68 ++++++++++----- app/Parser/ElemAttrs.php | 73 ++++++++++++++++ app/Parser/ElemNode.php | 72 ++++++++++++++++ app/Parser/Node.php | 52 ++++++++++++ app/Parser/Parser.php | 164 +++++++++++++++++++++++++++++++++--- app/Parser/TextNode.php | 20 +++++ composer.json | 1 + tests/Parser/ParserTest.php | 125 +++++++++++++++++++++++++++ tests/Parser/input-0.txt | 3 + 9 files changed, 545 insertions(+), 33 deletions(-) create mode 100644 app/Parser/ElemAttrs.php create mode 100644 app/Parser/ElemNode.php create mode 100644 app/Parser/Node.php create mode 100644 app/Parser/TextNode.php create mode 100644 tests/Parser/ParserTest.php create mode 100644 tests/Parser/input-0.txt diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index a632189..2d98e6e 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -3,9 +3,10 @@ namespace DevCommunityDE\CodeFormatter; use DevCommunityDE\CodeFormatter\CodeFormatter\CodeFormatter; +use DevCommunityDE\CodeFormatter\Parser\ElemNode; +use DevCommunityDE\CodeFormatter\Parser\Node; use DevCommunityDE\CodeFormatter\Parser\Parser; -use DevCommunityDE\CodeFormatter\Parser\PregParser; -use DevCommunityDE\CodeFormatter\Parser\Token; +use DevCommunityDE\CodeFormatter\Parser\TextNode; /** * Class CodeFormatterApp. @@ -22,7 +23,7 @@ class CodeFormatterApp */ public function __construct(Parser $parser = null) { - $this->parser = $parser ?? new PregParser(); + $this->parser = $parser ?? new Parser(); } /** @@ -32,17 +33,17 @@ public function __construct(Parser $parser = null) */ public function run() { - $tokens = $this->parseInput(); + $nodes = $this->parseInput(); - foreach ($tokens as $token) { - echo $this->formatToken($token); + foreach ($nodes as $node) { + echo $this->formatNode($node); } } /** * parses the code from stdin. * - * @return iterable + * @return iterable */ private function parseInput(): iterable { @@ -52,39 +53,64 @@ private function parseInput(): iterable /** * formats a token based on its type and language. * - * @param Token $token + * @param Node $node * * @return string */ - private function formatToken(Token $token): string + private function formatNode(Node $node): string { - if ($token->isText()) { - return $token->getBody(); + if ($node instanceof TextNode) { + return $node->getBody(); } - $language = $token->getAttribute('lang'); - \assert(null !== $language); + \assert($node instanceof ElemNode); + if (!$node->isCode()) { + // export node as-is (PLAIN bbcode) + return $this->exportNode($node, null); + } + + $language = $this->detectLang($node); $formatter = CodeFormatter::create($language); if (null === $formatter) { - // no formatter found, return token as is - return $this->exportToken($token, null); + // no formatter found, return node as is + return $this->exportNode($node, null); + } + + $result = $formatter->exec($node->getBody()); + return $this->exportNode($node, $result); + } + + /** + * detect a code-language form the given node. + * + * @param ElemNode $node + * + * @return string + */ + private function detectLang(ElemNode $node): string + { + $value = $node->getAttr('lang'); + + if (empty($value)) { + // check if a immediate value is set + // (this is the [code=lang] notation) + $value = $node->getAttr('@value'); } - $result = $formatter->exec($token->getBody()); - return $this->exportToken($token, $result); + return $value ?: 'text'; } /** - * exports a token. + * exports a node. * - * @param Token $token + * @param Node $Noe * @param string|null $body * * @return string */ - private function exportToken(Token $token, ?string $body): string + private function exportNode(Node $node, ?string $body): string { - return $this->parser->exportToken($token, $body); + return $this->parser->exportNode($node, $body); } } diff --git a/app/Parser/ElemAttrs.php b/app/Parser/ElemAttrs.php new file mode 100644 index 0000000..9afeba6 --- /dev/null +++ b/app/Parser/ElemAttrs.php @@ -0,0 +1,73 @@ + $pairs + */ + public function __construct(string $match, array $pairs) + { + $this->match = $match; + $this->pairs = $pairs; + } + + /** + * returns a value. + * + * @param string $name + * + * @return string|null + */ + public function getValue(string $name): ?string + { + return $this->pairs[$name] ?? null; + } + + /** + * returns the matched attribute string. + * + * @return string + */ + public function getMatch(): string + { + return $this->match; + } + + /** + * checks if some attributes are set. + * + * @return bool + */ + public function hasMatch(): bool + { + return !empty($this->match); + } + + /** + * returns the matched attributes as string. + * + * @return string + */ + public function __toString(): string + { + return $this->match; + } +} diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php new file mode 100644 index 0000000..9344af8 --- /dev/null +++ b/app/Parser/ElemNode.php @@ -0,0 +1,72 @@ +name = $name; + $this->attrs = $attrs; + } + + /** + * checks if this node represents a [CODE] bbcode. + * + * @return bool + */ + public function isCode(): bool + { + return !strcasecmp($this->name, 'code'); + } + + /** + * returns the element name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * returns the node attributes. + * + * @return ElemAttrs + */ + public function getAttrs(): ElemAttrs + { + return $this->attrs; + } + + /** + * returns a single node attribute. + * + * @param string $name + * + * @return string|null + */ + public function getAttr(string $name): ?string + { + return $this->attrs->getValue($name); + } +} diff --git a/app/Parser/Node.php b/app/Parser/Node.php new file mode 100644 index 0000000..e2923fb --- /dev/null +++ b/app/Parser/Node.php @@ -0,0 +1,52 @@ +kind = $kind; + $this->body = $body; + } + + /** + * @see Token::getKind() + * + * @return int + */ + public function getKind(): int + { + return $this->kind; + } + + /** + * @see Token::getBody() + * + * @return string + */ + public function getBody(): string + { + return $this->body; + } +} diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 6d414dd..d5dfd0f 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -4,33 +4,173 @@ namespace DevCommunityDE\CodeFormatter\Parser; -interface Parser +use DevCommunityDE\CodeFormatter\Exceptions\Exception; + +/** + * this parser uses pcre (regular expressions) + * to parse incoming code. + */ +final class Parser { + /** pattern used to tokenize bbcode tags */ + private const RE_TAGS = '/\G\[(code|plain)(=\w+|(?:\s+\w+=(?:"[^"]*"|\S+))*)\]/i'; + + /** pattern used to tokenize attributes */ + private const RE_ATTR = '/\G\s*(\w+)=("[^"]*"|\S+)/'; + /** - * parses a file. - * * @param string $filePath * - * @return iterable + * @return iterable */ - public function parseFile(string $filePath): iterable; + public function parseFile(string $filePath): iterable + { + $textData = file_get_contents($filePath); + + if (false === $textData) { + throw new Exception('unable to parse file: ' . $filePath); + } + + return $this->parseText($textData); + } /** - * parses a string (text). - * * @param string $textData * - * @return iterable + * @return iterable */ - public function parseText(string $textData): iterable; + public function parseText(string $textData): iterable + { + $length = \strlen($textData); + $offset = 0; + while ($offset < $length) { + if (!preg_match(self::RE_TAGS, $textData, $m, 0, $offset)) { + // not a bbcode at the current offset + $span = strcspn($textData, '[', $offset); + if (0 === $span) { + // we're looking at a '[' but did not match a bbcode + // this algorithm optimizes consecutive '[' tokens + $start = $offset; + while (($offset + 2) < $length) { + if ('[' !== $textData[$offset + 2]) { + // stop folding '[' tokens, because the next + // iteration has to check for a bbcode again + break; + } + ++$offset; + } + // we add 1 to the span, because we know that + // at least the current '[' did not start a bbcode + $span = $offset - $start + 1; + if ($offset + 2 === $length) { + // edge case: + // last token in input is a '[' + // there is no need to check if this token + // starts a bbcode + ++$span; + } + $offset = $start; + } + $body = substr($textData, $offset, $span); + $offset += $span; + yield new TextNode($body); + continue; + } + // read from $textData until we see a closing tag + $name = $m[1]; + $attrs = $this->parseAttrs($m[2]); + $start = $offset + \strlen($m[0]); + $needle = "[/{$name}]"; + $position = stripos($textData, $needle, $start); + if (false === $position) { + // edge case: + // no closing tag found... tread the rest of the input + // as bbcode-body and stop the iteration + $body = substr($textData, $start); + yield new ElemNode($name, $attrs, $body); + break; + } + $body = substr($textData, $start, ($position - $start)); + $offset = $position + \strlen($needle); + yield new ElemNode($name, $attrs, $body); + } + } /** - * exports a parsed token to its string-form. + * parses an attribute string from a bbcode. + * + * @param string $attrs * - * @param Token $token + * @return ElemAttrs + */ + private function parseAttrs(string $attrs): ElemAttrs + { + $pairs = []; + $match = $attrs; + + if (empty($attrs = trim($attrs))) { + return new ElemAttrs($match, $pairs); + } + + if ('=' === substr($attrs, 0, 1)) { + // [bbcode=value] notation + $pairs['@value'] = substr($attrs, 1); + return new ElemAttrs($match, $pairs); + } + + $offset = 0; + $length = \strlen($attrs); + while ($offset < $length) { + if (!preg_match(self::RE_ATTR, $attrs, $m, 0, $offset)) { + // we could throw here, but the text-input is in most + // cases provided by users. so we just discard it + break; + } + $pairs[$m[1]] = $this->parseAttr($m[2]); + $offset += \strlen($m[0]); + } + + return new ElemAttrs($match, $pairs); + } + + /** + * parses an attribute (value). + * + * @param string $value + * + * @return string + */ + private function parseAttr(string $value): string + { + switch (substr($value, 0, 1)) { + case '"': + case "'": + return substr($value, 1, -1) ?: ''; + default: + return $value; + } + } + + /** + * exports node. + * + * @param Node $node * @param string|null $body * * @return string */ - public function exportToken(Token $token, ?string $body): string; + public function exportNode(Node $node, ?string $body): string + { + if ($node instanceof TextNode) { + return $body ?: $node->getBody(); + } + + \assert($node instanceof ElemNode); + $bbcode = $node->getName(); + $buffer = "[{$bbcode}"; + $buffer .= $node->getAttrs(); + $buffer .= ']'; + $buffer .= $body ?: $node->getBody(); + return $buffer . "[/{$bbcode}]"; + } } diff --git a/app/Parser/TextNode.php b/app/Parser/TextNode.php new file mode 100644 index 0000000..74903dc --- /dev/null +++ b/app/Parser/TextNode.php @@ -0,0 +1,20 @@ +parseFile($input)); + } + + private function parseTextToArray(string $input) + { + $parser = new Parser(); + return iterator_to_array($parser->parseText($input)); + } + + public function testParsingAFileBehavesTheSameAsParsingAText() + { + $input = __DIR__ . '/input-0.txt'; + $fromText = $this->parseTextToArray(file_get_contents($input)); + $fromFile = $this->parseFileToArray($input); + $this->assertEquals($fromText, $fromFile); + } + + public function testPlainBbcodeConsumesOtherBbcodes() + { + $nodes = $this->parseTextToArray('before plain bbcode[plain][code]test[/code][/plain]'); + $this->assertCount(2, $nodes); + $this->assertInstanceof(TextNode::class, $nodes[0]); + $this->assertEquals(Node::KIND_TEXT, $nodes[0]->getKind()); + $this->assertEquals('before plain bbcode', $nodes[0]->getBody()); + $this->assertInstanceof(ElemNode::class, $nodes[1]); + $this->assertNotTrue($nodes[1]->isCode()); + $this->assertEquals('[code]test[/code]', $nodes[1]->getBody()); + } + + public function testBracketsInsideTextAreHandledCorrectly() + { + $nodes = $this->parseTextToArray('hello[[[[['); + $this->assertCount(2, $nodes); + $this->assertInstanceof(TextNode::class, $nodes[0]); + $this->assertEquals('hello', $nodes[0]->getBody()); + $this->assertInstanceof(TextNode::class, $nodes[1]); + $this->assertEquals('[[[[[', $nodes[1]->getBody()); + + $nodes = $this->parseTextToArray('hello[[[[[code]test[/code]'); + $this->assertCount(3, $nodes); + $this->assertInstanceof(TextNode::class, $nodes[0]); + $this->assertEquals('hello', $nodes[0]->getBody()); + $this->assertInstanceof(TextNode::class, $nodes[1]); + $this->assertEquals('[[[[', $nodes[1]->getBody()); + $this->assertInstanceof(ElemNode::class, $nodes[2]); + $this->assertTrue($nodes[2]->isCode()); + $this->assertEquals('test', $nodes[2]->getBody()); + } + + public function testNestedCodeBbcodesBehaveTheSameAsInXenforo() + { + $nodes = $this->parseTextToArray('[code][code]test[/code][/code]'); + $this->assertCount(3, $nodes); + $this->assertInstanceof(ElemNode::class, $nodes[0]); + $this->assertTrue($nodes[0]->isCode()); + $this->assertEquals('[code]test', $nodes[0]->getBody()); + + // TODO could be optimized as single text-node "[/code]" + $this->assertInstanceof(TextNode::class, $nodes[1]); + $this->assertEquals('[', $nodes[1]->getBody()); + $this->assertInstanceof(TextNode::class, $nodes[2]); + $this->assertEquals('/code]', $nodes[2]->getBody()); + } + + public function testUnclosedBbcodesGetParsedCorrectly() + { + $nodes = $this->parseTextToArray('[code]test'); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertTrue($nodes[0]->isCode()); + $this->assertEquals('test', $nodes[0]->getBody()); + } + + public function testAttributesGetMatchedCorrectly() + { + $nodes = $this->parseTextToArray('[code=css]test{}[/code]'); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertEquals('=css', $nodes[0]->getAttrs()->getMatch()); + $this->assertEquals('=css', (string) $nodes[0]->getAttrs()); + $this->assertEquals('css', $nodes[0]->getAttr('@value')); + + $nodes = $this->parseTextToArray('[code lang="css" title="Test 123"]test{}[/code]'); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertEquals(' lang="css" title="Test 123"', (string) $nodes[0]->getAttrs()); + $this->assertEquals('css', $nodes[0]->getAttr('lang')); + $this->assertEquals('Test 123', $nodes[0]->getAttr('title')); + } + + public function testParsedBbcodesAreExportedAsIs() + { + $parser = new Parser(); + $inputs = [ + '[code=css]test{}[/code]', + '[code lang=css title="Test"]test{}[/code]', + '[plain][code=css]test{}[/code][/plain]', + '[CODE]test[/CODE]', + ]; + + foreach ($inputs as $input) { + $nodes = $this->parseTextToArray($input); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $export = $parser->exportNode($nodes[0], null); + $this->assertEquals($input, $export); + } + } +} diff --git a/tests/Parser/input-0.txt b/tests/Parser/input-0.txt new file mode 100644 index 0000000..37571d2 --- /dev/null +++ b/tests/Parser/input-0.txt @@ -0,0 +1,3 @@ +hello +[code]world[/code] +[plain][code]testing[/code][/plain] From 2b42b5bd376f9189ee67e8b14fb0aa78fa47917e Mon Sep 17 00:00:00 2001 From: asccc Date: Sun, 12 Jul 2020 01:50:54 +0200 Subject: [PATCH 02/27] remove unused files in parser-directory after reset --- app/CodeFormatterApp.php | 2 +- app/Parser/Parser.php | 6 +- app/Parser/PregParser.php | 159 -------------------------------------- app/Parser/Token.php | 92 ---------------------- 4 files changed, 4 insertions(+), 255 deletions(-) delete mode 100644 app/Parser/PregParser.php delete mode 100644 app/Parser/Token.php diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 2d98e6e..b100fef 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -104,7 +104,7 @@ private function detectLang(ElemNode $node): string /** * exports a node. * - * @param Node $Noe + * @param Node $node * @param string|null $body * * @return string diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index d5dfd0f..f44579b 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -71,7 +71,7 @@ public function parseText(string $textData): iterable } $offset = $start; } - $body = substr($textData, $offset, $span); + $body = substr($textData, $offset, $span) ?: ''; $offset += $span; yield new TextNode($body); continue; @@ -86,11 +86,11 @@ public function parseText(string $textData): iterable // edge case: // no closing tag found... tread the rest of the input // as bbcode-body and stop the iteration - $body = substr($textData, $start); + $body = substr($textData, $start) ?: ''; yield new ElemNode($name, $attrs, $body); break; } - $body = substr($textData, $start, ($position - $start)); + $body = substr($textData, $start, ($position - $start)) ?: ''; $offset = $position + \strlen($needle); yield new ElemNode($name, $attrs, $body); } diff --git a/app/Parser/PregParser.php b/app/Parser/PregParser.php deleted file mode 100644 index df8533c..0000000 --- a/app/Parser/PregParser.php +++ /dev/null @@ -1,159 +0,0 @@ - - */ - public function parseFile(string $filePath): iterable - { - $textData = file_get_contents($filePath); - - if (false === $textData) { - throw new Exception('unable to parse file: ' . $filePath); - } - - yield from $this->parseText($textData); - } - - /** - * {@inheritdoc} - * - * @param string $textData - * - * @return @return iterable - */ - public function parseText(string $textData): iterable - { - // the structure looks like this after the split: - // 0 => text - // 1 => code-tag attributes - // 2 => code-tag body - // 3 => text - // 4 => ... - - $split = preg_split(self::RE_SPLIT, $textData, -1, - PREG_SPLIT_DELIM_CAPTURE); - - if (0 === ($count = \count($split))) { - return; - } - - $index = 0; - while ($index < $count) { - $attrs = null; - - if (0 !== $index % 3) { - $attrs = $this->parseAttrs($split[$index++]); - $kind = Token::T_CODE; - } else { - $kind = Token::T_TEXT; - } - - $body = $split[$index++]; - yield new Token($kind, $body, $attrs); - } - } - - /** - * parses an attribute string from a code-tag. - * - * @param string $attrs - * - * @return array|null - */ - private function parseAttrs(string $attrs): ?array - { - // ensure a language - $attrMap = ['lang' => 'text']; - - if (empty($attrs = trim($attrs))) { - return $attrMap; - } - - if ('=' === substr($attrs, 0, 1)) { - // [code=lang] notation - $attrMap['lang'] = substr($attrs, 1); - return $attrMap; - } - - $offset = 0; - $length = \strlen($attrs); - while ($offset < $length) { - if (!preg_match(self::RE_ATTRS, $attrs, $m, 0, $offset)) { - // we could throw here, but the text-input is in most - // cases provided by users. so we just discard it - break; - } - - $attrMap[$m[1]] = $this->parseAttr($m[2]); - $offset += \strlen($m[0]); - } - - return $attrMap; - } - - /** - * parses an attribute (value). - * - * @param string $value - * - * @return string - */ - private function parseAttr(string $value): string - { - switch (substr($value, 0, 1)) { - case '"': - case "'": - return substr($value, 1, -1) ?: ''; - default: - return $value; - } - } - - /** - * {@inheritdoc} - * - * @param Token $token - * @param string|null $body - * - * @return string - */ - public function exportToken(Token $token, ?string $body): string - { - if ($token->isText()) { - return $body ?: $token->getBody(); - } - - $buffer = '[CODE'; - $attrMap = $token->getAttributes(); - - foreach ($attrMap as $name => $value) { - $buffer .= ' ' . $name . '="' . $value . '"'; - } - - $buffer .= ']'; - $buffer .= $body ?: $token->getBody(); - return $buffer . '[/CODE]'; - } -} diff --git a/app/Parser/Token.php b/app/Parser/Token.php deleted file mode 100644 index 41ec9c1..0000000 --- a/app/Parser/Token.php +++ /dev/null @@ -1,92 +0,0 @@ - */ - private $attrs = []; - - /** - * constructor. - * - * @param int $kind - * @param string $body - * @param array|null $attrs - */ - public function __construct(int $kind, string $body, ?array $attrs) - { - $this->kind = $kind; - $this->body = $body; - - if (null !== $attrs) { - $this->attrs = $attrs; - } - } - - /** - * returns true if this token represents a text-token. - * - * @return bool - */ - public function isText(): bool - { - return self::T_TEXT === $this->kind; - } - - /** - * returns true if this token represents a code-token. - * - * @return bool - */ - public function isCode(): bool - { - return self::T_CODE === $this->kind; - } - - /** - * returns the token body. - * - * @return string - */ - public function getBody(): string - { - return $this->body; - } - - /** - * returns a single attribute. - * - * @param string $name - * - * @return string|null - */ - public function getAttribute(string $name): ?string - { - return $this->attrs[$name] ?? null; - } - - /** - * returns a COW reference to this tokens attributes. - * - * @return array - */ - public function getAttributes(): array - { - return $this->attrs; - } -} From aed88386c24a73fc17d7f10fd4166e8bfcf421c1 Mon Sep 17 00:00:00 2001 From: JR Cologne Date: Mon, 13 Jul 2020 12:50:25 +0200 Subject: [PATCH 03/27] Fix typos/mistakes in code comments --- app/CodeFormatterApp.php | 4 ++-- app/Parser/Parser.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index b100fef..19a0eea 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -51,7 +51,7 @@ private function parseInput(): iterable } /** - * formats a token based on its type and language. + * formats a node based on its type and language. * * @param Node $node * @@ -82,7 +82,7 @@ private function formatNode(Node $node): string } /** - * detect a code-language form the given node. + * detect a code-language from the given node. * * @param ElemNode $node * diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index f44579b..7703ef4 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -84,7 +84,7 @@ public function parseText(string $textData): iterable $position = stripos($textData, $needle, $start); if (false === $position) { // edge case: - // no closing tag found... tread the rest of the input + // no closing tag found... treat the rest of the input // as bbcode-body and stop the iteration $body = substr($textData, $start) ?: ''; yield new ElemNode($name, $attrs, $body); From 59244e552193c04930fd7a003fc73e66b502398e Mon Sep 17 00:00:00 2001 From: JR Cologne Date: Mon, 13 Jul 2020 12:51:36 +0200 Subject: [PATCH 04/27] Properly set up phpunit testsuite and extract helper methods to trait --- composer.json | 5 + composer.lock | 1841 ++++++++++++++++++-- phpunit.xml | 11 + tests/Parser/Helpers/ParserTestHelpers.php | 27 + tests/Parser/ParserTest.php | 15 +- 5 files changed, 1726 insertions(+), 173 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/Parser/Helpers/ParserTestHelpers.php diff --git a/composer.json b/composer.json index fda7526..dcecc59 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,11 @@ "DevCommunityDE\\CodeFormatter\\": "app" } }, + "autoload-dev": { + "psr-4": { + "DevCommunityDE\\CodeFormatter\\Tests\\": "tests/" + } + }, "require-dev": { "symfony/var-dumper": "^5.0", "phpunit/phpunit": "^9.2", diff --git a/composer.lock b/composer.lock index 49b022a..69502d7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8cc714456f7638686a09ab023631b60d", + "content-hash": "8b81cfd21bdd0ccc280ba04b9c0152c9", "packages": [], "packages-dev": [ { @@ -113,6 +113,76 @@ ], "time": "2020-06-04T11:16:35+00:00" }, + { + "name": "doctrine/instantiator", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" + }, { "name": "felixfbecker/advanced-json-rpc", "version": "v3.1.1", @@ -195,6 +265,60 @@ "description": "Tolerant PHP-to-AST parser designed for IDE usage scenarios", "time": "2020-02-18T02:57:19+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" + }, { "name": "netresearch/jsonmapper", "version": "v2.1.0", @@ -293,300 +417,1656 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "MIT" + ], + "authors": [ + { + "name": "Tyson Andre" + }, + { + "name": "Rasmus Lerdorf" + }, + { + "name": "Andrew S. Morrison" + } + ], + "description": "A static analyzer for PHP", + "keywords": [ + "analyzer", + "php", + "static" + ], + "time": "2020-07-03T23:01:25+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "shasum": "" + }, + "require": { + "ext-filter": "^7.1", + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0", + "phpdocumentor/type-resolver": "^1.0", + "webmozart/assert": "^1" + }, + "require-dev": { + "doctrine/instantiator": "^1", + "mockery/mockery": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-02-22T12:28:44+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-06-27T10:12:23+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-07-08T12:44:21+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca6647ffddd2add025ab3f21644a441d7c146cdc", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.3", + "phpunit/php-file-iterator": "^3.0", + "phpunit/php-text-template": "^2.0", + "phpunit/php-token-stream": "^4.0", + "sebastian/code-unit-reverse-lookup": "^2.0", + "sebastian/environment": "^5.0", + "sebastian/version": "^3.0", + "theseer/tokenizer": "^1.1.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-05-23T08:02:54+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-11T05:18:21+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:53:53+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:58:13+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-27T06:36:25+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.2.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ad7cc5ec3ab2597b329880e30442d9054526023b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad7cc5ec3ab2597b329880e30442d9054526023b", + "reference": "ad7cc5ec3ab2597b329880e30442d9054526023b", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.1", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.3", + "phpspec/prophecy": "^1.8.1", + "phpunit/php-code-coverage": "^8.0.1", + "phpunit/php-file-iterator": "^3.0", + "phpunit/php-invoker": "^3.0", + "phpunit/php-text-template": "^2.0", + "phpunit/php-timer": "^5.0", + "sebastian/code-unit": "^1.0.2", + "sebastian/comparator": "^4.0", + "sebastian/diff": "^4.0", + "sebastian/environment": "^5.0.1", + "sebastian/exporter": "^4.0", + "sebastian/global-state": "^4.0", + "sebastian/object-enumerator": "^4.0", + "sebastian/resource-operations": "^3.0", + "sebastian/type": "^2.1", + "sebastian/version": "^3.0" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-22T07:10:55+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/log", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", + "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2020-03-23T09:12:05+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "d00a17507af0e7544cfe17096372f5d733e3b276" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/d00a17507af0e7544cfe17096372f5d733e3b276", + "reference": "d00a17507af0e7544cfe17096372f5d733e3b276", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.16.1", + "phpunit/phpunit": "^7 || ^8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\Event\\": "lib/" + }, + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "time": "2020-01-31T18:52:29+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:50:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-30T04:46:02+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:07:24+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" + }, + { + "name": "sebastian/global-state", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bdb1e7c79e592b8c82cb1699be3c8743119b8a72", + "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72", + "shasum": "" + }, + "require": { + "php": "^7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], "authors": [ { - "name": "Tyson Andre" - }, - { - "name": "Rasmus Lerdorf" - }, - { - "name": "Andrew S. Morrison" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "A static analyzer for PHP", + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", "keywords": [ - "analyzer", - "php", - "static" + "global state" ], - "time": "2020-07-03T23:01:25+00:00" + "time": "2020-02-07T06:11:37+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "sebastian/object-enumerator", + "version": "4.0.2", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.3 || ^8.0", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2020-06-27T09:03:43+00:00" + "time": "2020-06-26T12:11:32+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.1.0", + "name": "sebastian/object-reflector", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "127a46f6b057441b201253526f81d5406d6c7840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", "shasum": "" }, "require": { - "ext-filter": "^7.1", - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" + "php": "^7.3 || ^8.0" }, "require-dev": { - "doctrine/instantiator": "^1", - "mockery/mockery": "^1" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "funding": [ { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-02-22T12:28:44+00:00" + "time": "2020-06-26T12:12:55+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "name": "sebastian/recursion-context", + "version": "4.0.2", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" + "php": "^7.3 || ^8.0" }, "require-dev": { - "ext-tokenizer": "*" + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-1.x": "1.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" }, { - "name": "psr/container", - "version": "1.0.0", + "name": "sebastian/resource-operations", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0653718a5a629b065e91f774595267f8dc32e213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-02-14T16:28:37+00:00" + "time": "2020-06-26T12:16:22+00:00" }, { - "name": "psr/log", - "version": "1.1.3", + "name": "sebastian/type", + "version": "2.2.1", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.2-dev" } }, "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2020-03-23T09:12:05+00:00" + "time": "2020-07-05T08:31:53+00:00" }, { - "name": "sabre/event", - "version": "5.1.0", + "name": "sebastian/version", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/sabre-io/event.git", - "reference": "d00a17507af0e7544cfe17096372f5d733e3b276" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/event/zipball/d00a17507af0e7544cfe17096372f5d733e3b276", - "reference": "d00a17507af0e7544cfe17096372f5d733e3b276", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", "shasum": "" }, "require": { - "php": "^7.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.16.1", - "phpunit/phpunit": "^7 || ^8" + "php": "^7.3 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { - "psr-4": { - "Sabre\\Event\\": "lib/" - }, - "files": [ - "lib/coroutine.php", - "lib/Loop/functions.php", - "lib/Promise/functions.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -595,27 +2075,20 @@ ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "sabre/event is a library for lightweight event-based programming", - "homepage": "http://sabre.io/event/", - "keywords": [ - "EventEmitter", - "async", - "coroutine", - "eventloop", - "events", - "hooks", - "plugin", - "promise", - "reactor", - "signal" + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2020-01-31T18:52:29+00:00" + "time": "2020-06-26T12:18:43+00:00" }, { "name": "symfony/console", @@ -1317,6 +2790,52 @@ ], "time": "2020-05-30T20:35:19+00:00" }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, { "name": "webmozart/assert", "version": "1.9.1", @@ -1373,7 +2892,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.3" + "php": "^7.4" }, "platform-dev": [], "plugin-api-version": "1.1.0" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e59fb05 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + diff --git a/tests/Parser/Helpers/ParserTestHelpers.php b/tests/Parser/Helpers/ParserTestHelpers.php new file mode 100644 index 0000000..c5f1ded --- /dev/null +++ b/tests/Parser/Helpers/ParserTestHelpers.php @@ -0,0 +1,27 @@ +parseFile($input)); + } + + /** + * @param string $input + */ + private function parseTextToArray(string $input) + { + $parser = new Parser(); + return iterator_to_array($parser->parseText($input)); + } +} diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 7dfba24..3bec671 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -1,27 +1,18 @@ parseFile($input)); - } - - private function parseTextToArray(string $input) - { - $parser = new Parser(); - return iterator_to_array($parser->parseText($input)); - } + use ParserTestHelpers; public function testParsingAFileBehavesTheSameAsParsingAText() { From 0ec4dd4e7e696c6ef9ea21ca378838ca0baae8c9 Mon Sep 17 00:00:00 2001 From: JR Cologne Date: Mon, 13 Jul 2020 12:57:12 +0200 Subject: [PATCH 05/27] Add phpunit to phan github action --- .github/workflows/{phan.yml => phan-phpunit.yml} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename .github/workflows/{phan.yml => phan-phpunit.yml} (85%) diff --git a/.github/workflows/phan.yml b/.github/workflows/phan-phpunit.yml similarity index 85% rename from .github/workflows/phan.yml rename to .github/workflows/phan-phpunit.yml index 1a77d0a..a18dbe4 100644 --- a/.github/workflows/phan.yml +++ b/.github/workflows/phan-phpunit.yml @@ -1,4 +1,4 @@ -name: Static analysis (phan) +name: Static analysis (phan) & Unit Testing (phpunit) on: pull_request: @@ -6,7 +6,7 @@ on: - "**.php" jobs: - phan: + phan-phpunit: runs-on: ubuntu-latest steps: - name: Install PHP and dependencies @@ -26,3 +26,5 @@ jobs: run: composer install - name: Run phan from vendor folder run: vendor/bin/phan -S -k ./.phan/config.php + - name: Run phpunit from vendor folder + run: vendor/bin/phpunit From 0f57d541b345c4b6c82e676eb185019c2e36cf40 Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 00:21:34 +0200 Subject: [PATCH 06/27] simplify the parser and make sure text-nodes are buffered --- app/Parser/ElemAttrs.php | 10 -- app/Parser/ElemNode.php | 24 ++-- app/Parser/Node.php | 35 ++++++ app/Parser/Parser.php | 49 ++++---- phpunit.xml | 3 +- tests/Parser/Helpers/ParserTestHelpers.php | 27 ---- tests/Parser/ParserTest.php | 137 ++++++++++++++++----- tests/Parser/{input-0.txt => input.txt} | 0 8 files changed, 181 insertions(+), 104 deletions(-) delete mode 100644 tests/Parser/Helpers/ParserTestHelpers.php rename tests/Parser/{input-0.txt => input.txt} (100%) diff --git a/app/Parser/ElemAttrs.php b/app/Parser/ElemAttrs.php index 9afeba6..8da7f4e 100644 --- a/app/Parser/ElemAttrs.php +++ b/app/Parser/ElemAttrs.php @@ -60,14 +60,4 @@ public function hasMatch(): bool { return !empty($this->match); } - - /** - * returns the matched attributes as string. - * - * @return string - */ - public function __toString(): string - { - return $this->match; - } } diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php index 9344af8..c68d855 100644 --- a/app/Parser/ElemNode.php +++ b/app/Parser/ElemNode.php @@ -31,6 +31,8 @@ public function __construct(string $name, ElemAttrs $attrs, string $body) /** * checks if this node represents a [CODE] bbcode. * + * @override + * * @return bool */ public function isCode(): bool @@ -49,24 +51,28 @@ public function getName(): string } /** - * returns the node attributes. + * returns a single node attribute. + * + * @override * - * @return ElemAttrs + * @param string $name + * + * @return string|null */ - public function getAttrs(): ElemAttrs + public function getAttr(string $name): ?string { - return $this->attrs; + return $this->attrs->getValue($name); } /** - * returns a single node attribute. + * returns the node attributes. * - * @param string $name + * @override * - * @return string|null + * @return string */ - public function getAttr(string $name): ?string + public function getAttrMatch(): string { - return $this->attrs->getValue($name); + return $this->attrs->getMatch(); } } diff --git a/app/Parser/Node.php b/app/Parser/Node.php index e2923fb..9b32d8d 100644 --- a/app/Parser/Node.php +++ b/app/Parser/Node.php @@ -30,6 +30,41 @@ public function __construct(int $kind, string $body) $this->body = $body; } + /** + * checks if this node represents [CODE] node. + * returns false by default. + * + * @return bool + */ + public function isCode(): bool + { + return false; + } + + /** + * returns a attribute assigned to this node. + * returns null by default. + * + * @param string $name + * + * @return string|null + */ + public function getAttr(string $name): ?string + { + return null; + } + + /** + * returns all matched attributes as string. + * returns an empty string by default. + * + * @return string + */ + public function getAttrMatch(): string + { + return ''; + } + /** * @see Token::getKind() * diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 7703ef4..defe7d4 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -43,45 +43,33 @@ public function parseText(string $textData): iterable { $length = \strlen($textData); $offset = 0; + $buffer = ''; while ($offset < $length) { if (!preg_match(self::RE_TAGS, $textData, $m, 0, $offset)) { // not a bbcode at the current offset - $span = strcspn($textData, '[', $offset); - if (0 === $span) { - // we're looking at a '[' but did not match a bbcode - // this algorithm optimizes consecutive '[' tokens - $start = $offset; - while (($offset + 2) < $length) { - if ('[' !== $textData[$offset + 2]) { - // stop folding '[' tokens, because the next - // iteration has to check for a bbcode again - break; - } - ++$offset; - } - // we add 1 to the span, because we know that - // at least the current '[' did not start a bbcode - $span = $offset - $start + 1; - if ($offset + 2 === $length) { - // edge case: - // last token in input is a '[' - // there is no need to check if this token - // starts a bbcode - ++$span; - } - $offset = $start; - } + // we know for sure that a leading '[' did not start + // a bbcode, so we can safely assume that the span + // has to be at least 1 + $span = max(1, strcspn($textData, '[', $offset)); $body = substr($textData, $offset, $span) ?: ''; $offset += $span; - yield new TextNode($body); + $buffer .= $body; continue; } + + if ('' !== $buffer) { + // yield a single text-node using the current buffer + yield new TextNode($buffer); + $buffer = ''; + } + // read from $textData until we see a closing tag $name = $m[1]; $attrs = $this->parseAttrs($m[2]); $start = $offset + \strlen($m[0]); $needle = "[/{$name}]"; $position = stripos($textData, $needle, $start); + if (false === $position) { // edge case: // no closing tag found... treat the rest of the input @@ -90,10 +78,17 @@ public function parseText(string $textData): iterable yield new ElemNode($name, $attrs, $body); break; } + $body = substr($textData, $start, ($position - $start)) ?: ''; $offset = $position + \strlen($needle); yield new ElemNode($name, $attrs, $body); } + + // check if we still have some content in the buffer + if ('' !== $buffer) { + // yield the remaining buffer as text-node + yield new TextNode($buffer); + } } /** @@ -168,7 +163,7 @@ public function exportNode(Node $node, ?string $body): string \assert($node instanceof ElemNode); $bbcode = $node->getName(); $buffer = "[{$bbcode}"; - $buffer .= $node->getAttrs(); + $buffer .= $node->getAttrMatch(); $buffer .= ']'; $buffer .= $body ?: $node->getBody(); return $buffer . "[/{$bbcode}]"; diff --git a/phpunit.xml b/phpunit.xml index e59fb05..6fab679 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,8 @@ + colors="true" + testdox="true"> tests diff --git a/tests/Parser/Helpers/ParserTestHelpers.php b/tests/Parser/Helpers/ParserTestHelpers.php deleted file mode 100644 index c5f1ded..0000000 --- a/tests/Parser/Helpers/ParserTestHelpers.php +++ /dev/null @@ -1,27 +0,0 @@ -parseFile($input)); - } - - /** - * @param string $input - */ - private function parseTextToArray(string $input) - { - $parser = new Parser(); - return iterator_to_array($parser->parseText($input)); - } -} diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 3bec671..141ee30 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -7,22 +7,55 @@ use DevCommunityDE\CodeFormatter\Parser\Node; use DevCommunityDE\CodeFormatter\Parser\Parser; use DevCommunityDE\CodeFormatter\Parser\TextNode; -use DevCommunityDE\CodeFormatter\Tests\Parser\Helpers\ParserTestHelpers; use PHPUnit\Framework\TestCase; final class ParserTest extends TestCase { - use ParserTestHelpers; + /** + * parses a file and returns it as array of nodes. + * + * @param string $input + * + * @return Node[] + */ + private function parseFileToArray(string $input): array + { + $parser = new Parser(); + return iterator_to_array($parser->parseFile($input)); + } - public function testParsingAFileBehavesTheSameAsParsingAText() + /** + * parses a text and returns it as array of nodes. + * + * @param string $input + * + * @return Node[] + */ + private function parseTextToArray(string $input): array { - $input = __DIR__ . '/input-0.txt'; + $parser = new Parser(); + return iterator_to_array($parser->parseText($input)); + } + + /** + * @testdox Parsing a file should behaves the same as parsing text + * + * @return void + */ + public function testFileTextParsing() + { + $input = __DIR__ . '/input.txt'; $fromText = $this->parseTextToArray(file_get_contents($input)); $fromFile = $this->parseFileToArray($input); $this->assertEquals($fromText, $fromFile); } - public function testPlainBbcodeConsumesOtherBbcodes() + /** + * @testdox PLAIN bbcodes should consume other bbcodes + * + * @return void + */ + public function testPlainElement() { $nodes = $this->parseTextToArray('before plain bbcode[plain][code]test[/code][/plain]'); $this->assertCount(2, $nodes); @@ -34,68 +67,112 @@ public function testPlainBbcodeConsumesOtherBbcodes() $this->assertEquals('[code]test[/code]', $nodes[1]->getBody()); } - public function testBracketsInsideTextAreHandledCorrectly() + /** + * @testdox Brackets inside text shouldn't yield additional text-nodes + * + * @return void + */ + public function testBracketsInsideText() { $nodes = $this->parseTextToArray('hello[[[[['); - $this->assertCount(2, $nodes); + $this->assertCount(1, $nodes); $this->assertInstanceof(TextNode::class, $nodes[0]); - $this->assertEquals('hello', $nodes[0]->getBody()); - $this->assertInstanceof(TextNode::class, $nodes[1]); - $this->assertEquals('[[[[[', $nodes[1]->getBody()); + $this->assertEquals('hello[[[[[', $nodes[0]->getBody()); $nodes = $this->parseTextToArray('hello[[[[[code]test[/code]'); - $this->assertCount(3, $nodes); + $this->assertCount(2, $nodes); $this->assertInstanceof(TextNode::class, $nodes[0]); - $this->assertEquals('hello', $nodes[0]->getBody()); - $this->assertInstanceof(TextNode::class, $nodes[1]); - $this->assertEquals('[[[[', $nodes[1]->getBody()); - $this->assertInstanceof(ElemNode::class, $nodes[2]); - $this->assertTrue($nodes[2]->isCode()); - $this->assertEquals('test', $nodes[2]->getBody()); + $this->assertEquals('hello[[[[', $nodes[0]->getBody()); + $this->assertInstanceof(ElemNode::class, $nodes[1]); + $this->assertTrue($nodes[1]->isCode()); + $this->assertEquals('test', $nodes[1]->getBody()); } - public function testNestedCodeBbcodesBehaveTheSameAsInXenforo() + /** + * @testdox Unmatched closing bbcodes should be treated as text + * + * @return void + */ + public function testUnmatchedClosingElements() + { + $nodes = $this->parseTextToArray('[code]a[/code]b[/code]'); + $this->assertCount(2, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertTrue($nodes[0]->isCode()); + $this->assertEquals('a', $nodes[0]->getBody()); + $this->assertInstanceOf(TextNode::class, $nodes[1]); + $this->assertEquals('b[/code]', $nodes[1]->getBody()); + } + + /** + * @testdox Nested CODE bbcodes should behave the same as in XenForo + * + * @return void + */ + public function testNestedCodeElements() { $nodes = $this->parseTextToArray('[code][code]test[/code][/code]'); - $this->assertCount(3, $nodes); + $this->assertCount(2, $nodes); $this->assertInstanceof(ElemNode::class, $nodes[0]); $this->assertTrue($nodes[0]->isCode()); $this->assertEquals('[code]test', $nodes[0]->getBody()); - - // TODO could be optimized as single text-node "[/code]" $this->assertInstanceof(TextNode::class, $nodes[1]); - $this->assertEquals('[', $nodes[1]->getBody()); - $this->assertInstanceof(TextNode::class, $nodes[2]); - $this->assertEquals('/code]', $nodes[2]->getBody()); + $this->assertEquals('[/code]', $nodes[1]->getBody()); } - public function testUnclosedBbcodesGetParsedCorrectly() + /** + * @testdox Unclosed bbcodes should be matched correctly + * + * @return void + */ + public function testUnclosedElements() { $nodes = $this->parseTextToArray('[code]test'); $this->assertCount(1, $nodes); $this->assertInstanceOf(ElemNode::class, $nodes[0]); $this->assertTrue($nodes[0]->isCode()); $this->assertEquals('test', $nodes[0]->getBody()); + + // TODO we could add an algorithm that enables this: + // + // $nodes = $this->parseTextToArray('[code]a[code]b[/code]'); + // //=> [ code(a), code(b) ]; + // $this->assertCount(2, $nodes); + // $this->assertInstanceOf(ElemNode::class, $nodes[0]); + // $this->assertTrue($nodes[0]->isCode()); + // $this->assertEquals('a', $nodes[0]->getBody()); + // $this->assertInstanceOf(ElemNode::class, $nodes[1]); + // $this->assertTrue($nodes[1]->isCode()); + // $this->assertEquals('b', $nodes[1]->getBody()); } - public function testAttributesGetMatchedCorrectly() + /** + * @testdox Attributes are parsed correctly and are exported as-is + * + * @return void + */ + public function testAttributeBehaviour() { $nodes = $this->parseTextToArray('[code=css]test{}[/code]'); $this->assertCount(1, $nodes); $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertEquals('=css', $nodes[0]->getAttrs()->getMatch()); - $this->assertEquals('=css', (string) $nodes[0]->getAttrs()); + $this->assertEquals('=css', $nodes[0]->getAttrMatch()); $this->assertEquals('css', $nodes[0]->getAttr('@value')); $nodes = $this->parseTextToArray('[code lang="css" title="Test 123"]test{}[/code]'); $this->assertCount(1, $nodes); $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertEquals(' lang="css" title="Test 123"', (string) $nodes[0]->getAttrs()); + $this->assertEquals(' lang="css" title="Test 123"', $nodes[0]->getAttrMatch()); $this->assertEquals('css', $nodes[0]->getAttr('lang')); $this->assertEquals('Test 123', $nodes[0]->getAttr('title')); } - public function testParsedBbcodesAreExportedAsIs() + /** + * @testdox Verify that parsed bbcodes are exported as-is + * + * @return void + */ + public function testElementExports() { $parser = new Parser(); $inputs = [ diff --git a/tests/Parser/input-0.txt b/tests/Parser/input.txt similarity index 100% rename from tests/Parser/input-0.txt rename to tests/Parser/input.txt From 5387689cbf35d289557881dc0ab05909a18134e7 Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 00:28:11 +0200 Subject: [PATCH 07/27] stop parsing after unclosed bbcode there is no reason the check the text-buffer after a unclosed bbcode, because the unclosed bbcode consumes the rest of the input anyway. --- app/Parser/Parser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index defe7d4..7fae724 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -73,10 +73,10 @@ public function parseText(string $textData): iterable if (false === $position) { // edge case: // no closing tag found... treat the rest of the input - // as bbcode-body and stop the iteration + // as bbcode-body and stop the parsing entirely $body = substr($textData, $start) ?: ''; yield new ElemNode($name, $attrs, $body); - break; + return; } $body = substr($textData, $start, ($position - $start)) ?: ''; From 57538a653972f67c6253f5d7357169aaed5146ab Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 00:42:14 +0200 Subject: [PATCH 08/27] fix wording in test --- tests/Parser/ParserTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 141ee30..f407aac 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -147,7 +147,7 @@ public function testUnclosedElements() } /** - * @testdox Attributes are parsed correctly and are exported as-is + * @testdox Verify that attributes are parsed correctly and are exported as-is * * @return void */ From cbbb93dcc33bf999140ee4542b6434f6384a0e0d Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 00:43:01 +0200 Subject: [PATCH 09/27] fix wording in test #2 --- tests/Parser/ParserTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index f407aac..8b1ec00 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -38,7 +38,7 @@ private function parseTextToArray(string $input): array } /** - * @testdox Parsing a file should behaves the same as parsing text + * @testdox Parsing a file should behave the same as parsing text * * @return void */ From 741d6caa4976c04f9424a536e2ae64c35856074b Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 00:51:37 +0200 Subject: [PATCH 10/27] add comment about nested bbcodes in test --- tests/Parser/ParserTest.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 8b1ec00..5a7cebb 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -121,7 +121,7 @@ public function testNestedCodeElements() } /** - * @testdox Unclosed bbcodes should be matched correctly + * @testdox Unclosed bbcodes should consume the rest of the input as body * * @return void */ @@ -133,17 +133,26 @@ public function testUnclosedElements() $this->assertTrue($nodes[0]->isCode()); $this->assertEquals('test', $nodes[0]->getBody()); - // TODO we could add an algorithm that enables this: + // it would be possible to treat any opening bbcode + // as an implicit closing bbcode (if another bbcode is open). + // currently, the parser just consumes everything ignoring + // other bbcodes inside. + $nodes = $this->parseTextToArray('[code]a[plain]b[/plain]'); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertTrue($nodes[0]->isCode()); + $this->assertEquals('a[plain]b[/plain]', $nodes[0]->getBody()); + + // an optimized algorithm could produce the following: // - // $nodes = $this->parseTextToArray('[code]a[code]b[/code]'); - // //=> [ code(a), code(b) ]; + // $nodes = $this->parseTextToArray('[code]a[plain]b[/plain]'); // $this->assertCount(2, $nodes); // $this->assertInstanceOf(ElemNode::class, $nodes[0]); // $this->assertTrue($nodes[0]->isCode()); // $this->assertEquals('a', $nodes[0]->getBody()); // $this->assertInstanceOf(ElemNode::class, $nodes[1]); - // $this->assertTrue($nodes[1]->isCode()); - // $this->assertEquals('b', $nodes[1]->getBody()); + // $this->assertEqualsIgnoringCase('plain', $nodes[1]->getName()); + // $this->assertEqual('b', $nodes[1]->getBody()); } /** From e3f7f5e6d9c00aa8ab4fdcac1ed47830e56ccf25 Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 01:13:02 +0200 Subject: [PATCH 11/27] added parser-helpers trait again --- phpunit.xml | 2 +- tests/Parser/Helpers/ParserTestHelpers.php | 36 ++++++++++++++++++++++ tests/Parser/ParserTest.php | 27 ++-------------- 3 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 tests/Parser/Helpers/ParserTestHelpers.php diff --git a/phpunit.xml b/phpunit.xml index 6fab679..49331cc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,7 +1,7 @@ diff --git a/tests/Parser/Helpers/ParserTestHelpers.php b/tests/Parser/Helpers/ParserTestHelpers.php new file mode 100644 index 0000000..84a9b50 --- /dev/null +++ b/tests/Parser/Helpers/ParserTestHelpers.php @@ -0,0 +1,36 @@ +parseFile($input)); + } + + /** + * parses a text and returns it as array of nodes. + * + * @param string $input + * + * @return Node[] + */ + private function parseTextToArray(string $input): array + { + $parser = new Parser(); + return iterator_to_array($parser->parseText($input)); + } +} diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 5a7cebb..b2ceffc 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -7,35 +7,12 @@ use DevCommunityDE\CodeFormatter\Parser\Node; use DevCommunityDE\CodeFormatter\Parser\Parser; use DevCommunityDE\CodeFormatter\Parser\TextNode; +use DevCommunityDE\CodeFormatter\Tests\Parser\Helpers\ParserTestHelpers; use PHPUnit\Framework\TestCase; final class ParserTest extends TestCase { - /** - * parses a file and returns it as array of nodes. - * - * @param string $input - * - * @return Node[] - */ - private function parseFileToArray(string $input): array - { - $parser = new Parser(); - return iterator_to_array($parser->parseFile($input)); - } - - /** - * parses a text and returns it as array of nodes. - * - * @param string $input - * - * @return Node[] - */ - private function parseTextToArray(string $input): array - { - $parser = new Parser(); - return iterator_to_array($parser->parseText($input)); - } + use ParserTestHelpers; /** * @testdox Parsing a file should behave the same as parsing text From 8e12dd503ade5852f408cbbd95aba00c7b919a4e Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 01:25:49 +0200 Subject: [PATCH 12/27] used more descriptive variable-names in parser --- app/Parser/Parser.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 7fae724..a3a321e 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -64,24 +64,25 @@ public function parseText(string $textData): iterable } // read from $textData until we see a closing tag - $name = $m[1]; - $attrs = $this->parseAttrs($m[2]); - $start = $offset + \strlen($m[0]); - $needle = "[/{$name}]"; - $position = stripos($textData, $needle, $start); + $tagName = $m[1]; + $tagAttrs = $this->parseAttrs($m[2]); + $bodyStart = $offset + \strlen($m[0]); + $closeTag = "[/{$tagName}]"; + $closePos = stripos($textData, $closeTag, $bodyStart); - if (false === $position) { + if (false === $closePos) { // edge case: // no closing tag found... treat the rest of the input // as bbcode-body and stop the parsing entirely - $body = substr($textData, $start) ?: ''; - yield new ElemNode($name, $attrs, $body); + $tagBody = substr($textData, $bodyStart) ?: ''; + yield new ElemNode($tagName, $tagAttrs, $tagBody); return; } - $body = substr($textData, $start, ($position - $start)) ?: ''; - $offset = $position + \strlen($needle); - yield new ElemNode($name, $attrs, $body); + $bodySpan = $closePos - $bodyStart; + $tagBody = substr($textData, $bodyStart, $bodySpan) ?: ''; + $offset = $closePos + \strlen($closeTag); + yield new ElemNode($tagName, $tagAttrs, $tagBody); } // check if we still have some content in the buffer From 94f9f70112b2939a4696388f6845de1b86e596c9 Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 01:25:49 +0200 Subject: [PATCH 13/27] used more descriptive variable-names in parser --- app/Parser/Parser.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 7fae724..ff00534 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -50,10 +50,10 @@ public function parseText(string $textData): iterable // we know for sure that a leading '[' did not start // a bbcode, so we can safely assume that the span // has to be at least 1 - $span = max(1, strcspn($textData, '[', $offset)); - $body = substr($textData, $offset, $span) ?: ''; - $offset += $span; - $buffer .= $body; + $textSpan = max(1, strcspn($textData, '[', $offset)); + $textBody = substr($textData, $offset, $textSpan) ?: ''; + $offset += $textSpan; + $buffer .= $textBody; continue; } @@ -64,24 +64,25 @@ public function parseText(string $textData): iterable } // read from $textData until we see a closing tag - $name = $m[1]; - $attrs = $this->parseAttrs($m[2]); - $start = $offset + \strlen($m[0]); - $needle = "[/{$name}]"; - $position = stripos($textData, $needle, $start); + $tagName = $m[1]; + $tagAttrs = $this->parseAttrs($m[2]); + $bodyStart = $offset + \strlen($m[0]); + $closeTag = "[/{$tagName}]"; + $closePos = stripos($textData, $closeTag, $bodyStart); - if (false === $position) { + if (false === $closePos) { // edge case: // no closing tag found... treat the rest of the input // as bbcode-body and stop the parsing entirely - $body = substr($textData, $start) ?: ''; - yield new ElemNode($name, $attrs, $body); + $tagBody = substr($textData, $bodyStart) ?: ''; + yield new ElemNode($tagName, $tagAttrs, $tagBody); return; } - $body = substr($textData, $start, ($position - $start)) ?: ''; - $offset = $position + \strlen($needle); - yield new ElemNode($name, $attrs, $body); + $bodySpan = $closePos - $bodyStart; + $tagBody = substr($textData, $bodyStart, $bodySpan) ?: ''; + $offset = $closePos + \strlen($closeTag); + yield new ElemNode($tagName, $tagAttrs, $tagBody); } // check if we still have some content in the buffer From 036e423f17924f9def98f7956d02260ace9e5d92 Mon Sep 17 00:00:00 2001 From: asccc Date: Tue, 14 Jul 2020 01:53:42 +0200 Subject: [PATCH 14/27] hide phan progress-bar, because github cannot render it --- .github/workflows/phan-phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phan-phpunit.yml b/.github/workflows/phan-phpunit.yml index a18dbe4..db90187 100644 --- a/.github/workflows/phan-phpunit.yml +++ b/.github/workflows/phan-phpunit.yml @@ -25,6 +25,6 @@ jobs: - name: Install composer packages run: composer install - name: Run phan from vendor folder - run: vendor/bin/phan -S -k ./.phan/config.php + run: vendor/bin/phan -S -k ./.phan/config.php --no-progress-bar - name: Run phpunit from vendor folder run: vendor/bin/phpunit From 706b7a54190f4ba7c2aa6a10d6bbeeae5cd10703 Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 11:40:18 +0200 Subject: [PATCH 15/27] (wip) added support for nested code-blocks --- app/Parser/ElemNode.php | 8 +- app/Parser/Node.php | 16 +-- app/Parser/Parser.php | 224 ++++++++++++++++++++++++++++++++++------ 3 files changed, 205 insertions(+), 43 deletions(-) diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php index c68d855..e15883c 100644 --- a/app/Parser/ElemNode.php +++ b/app/Parser/ElemNode.php @@ -17,11 +17,11 @@ final class ElemNode extends Node /** * constructs a new code-token. * - * @param string $name - * @param ElemAttrs $attrs - * @param string $body + * @param string $name + * @param ElemAttrs $attrs + * @param NodeList|string $body */ - public function __construct(string $name, ElemAttrs $attrs, string $body) + public function __construct(string $name, ElemAttrs $attrs, $body) { parent::__construct(Node::KIND_ELEM, $body); $this->name = $name; diff --git a/app/Parser/Node.php b/app/Parser/Node.php index 9b32d8d..6f215ac 100644 --- a/app/Parser/Node.php +++ b/app/Parser/Node.php @@ -15,16 +15,16 @@ abstract class Node /** @var int */ private $kind; - /** @var string */ + /** @var Node[]|string */ private $body; /** * constructs a new token. * - * @param int $kind - * @param string $body + * @param int $kind + * @param Node[]|string $body */ - public function __construct(int $kind, string $body) + public function __construct(int $kind, $body) { $this->kind = $kind; $this->body = $body; @@ -66,7 +66,7 @@ public function getAttrMatch(): string } /** - * @see Token::getKind() + * returns the node kind. * * @return int */ @@ -76,11 +76,11 @@ public function getKind(): int } /** - * @see Token::getBody() + * returns the node body. * - * @return string + * @return Node[]|string */ - public function getBody(): string + public function getBody() { return $this->body; } diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index ff00534..aebeafd 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -6,14 +6,63 @@ use DevCommunityDE\CodeFormatter\Exceptions\Exception; +/** + * uses as a parse-state. + * + * @internal + */ +final class State +{ + /** @var string */ + public string $input; + + /** @var int */ + public int $length; + + /** @var int */ + public int $offset; + + /** + * constructs a new state. + * + * @param string $input + */ + public function __construct(string $input) + { + $this->input = $input; + $this->length = \strlen($input); + $this->offset = 0; + } + + /** + * checks if this state is still valid. + * + * @return bool + */ + public function valid(): bool + { + return $this->offset < $this->length; + } + + /** + * marks the state a finished. + * + * @return void + */ + public function finish() + { + $this->offset = $this->length; + } +} + /** * this parser uses pcre (regular expressions) * to parse incoming code. */ final class Parser { - /** pattern used to tokenize bbcode tags */ - private const RE_TAGS = '/\G\[(code|plain)(=\w+|(?:\s+\w+=(?:"[^"]*"|\S+))*)\]/i'; + /** pattern used to tokenize bbcode elements */ + private const RE_ELEM = '/\G\[(code|plain)(=\w+|(?:\s+\w+=(?:"[^"]*"|\S+))*)\]/i'; /** pattern used to tokenize attributes */ private const RE_ATTR = '/\G\s*(\w+)=("[^"]*"|\S+)/'; @@ -35,61 +84,174 @@ public function parseFile(string $filePath): iterable } /** + * parses the given text. + * * @param string $textData * * @return iterable */ public function parseText(string $textData): iterable { - $length = \strlen($textData); - $offset = 0; - $buffer = ''; - while ($offset < $length) { - if (!preg_match(self::RE_TAGS, $textData, $m, 0, $offset)) { - // not a bbcode at the current offset - // we know for sure that a leading '[' did not start - // a bbcode, so we can safely assume that the span - // has to be at least 1 - $textSpan = max(1, strcspn($textData, '[', $offset)); - $textBody = substr($textData, $offset, $textSpan) ?: ''; - $offset += $textSpan; - $buffer .= $textBody; + $state = new State($textData); + return $this->parseNorm($state); + } + + /** + * parses and normalizes a prepared state. + * + * @param State $state + * + * @return iterable + */ + private function parseNorm(State $state): iterable + { + $textBuffer = ''; + while ($state->valid()) { + $node = $this->parseNode($state); + + if ($node instanceof ElemNode) { + // yield a buffered text-node first (if any) + if (!empty($textBuffer)) { + yield new TextNode($textBuffer); + $textBuffer = ''; + } + + yield $node; continue; } - if ('' !== $buffer) { - // yield a single text-node using the current buffer - yield new TextNode($buffer); - $buffer = ''; - } + $textBody = $node->getBody(); + \assert(\is_string($textBody)); + $textBuffer .= $textBody; + } + if (!empty($textBuffer)) { + // yield a buffered text-node (if any) + yield new TextNode($textBuffer); + } + } + + /** + * parses a node. + * + * @param State $state + * + * @return Node + */ + private function parseNode(State $state): Node + { + $input = $state->input; + $offset = $state->offset; + + if (preg_match(self::RE_ELEM, $input, $m, 0, $offset)) { // read from $textData until we see a closing tag $tagName = $m[1]; $tagAttrs = $this->parseAttrs($m[2]); $bodyStart = $offset + \strlen($m[0]); + + if ($this->isRichCode($tagName, $tagAttrs)) { + // CODE=rich allows nested CODE bbcodes ... yay. + // switch to the recursive parser and collect all + // child-nodes as tag-body + $state->offset = $bodyStart; + $tagBody = $this->parseRich($state); + return new ElemNode($tagName, $tagAttrs, $tagBody); + } + $closeTag = "[/{$tagName}]"; - $closePos = stripos($textData, $closeTag, $bodyStart); + $closePos = stripos($input, $closeTag, $bodyStart); if (false === $closePos) { // edge case: // no closing tag found... treat the rest of the input // as bbcode-body and stop the parsing entirely - $tagBody = substr($textData, $bodyStart) ?: ''; - yield new ElemNode($tagName, $tagAttrs, $tagBody); - return; + $tagBody = substr($input, $bodyStart) ?: ''; + $state->finish(); + return new ElemNode($tagName, $tagAttrs, $tagBody); } $bodySpan = $closePos - $bodyStart; - $tagBody = substr($textData, $bodyStart, $bodySpan) ?: ''; - $offset = $closePos + \strlen($closeTag); - yield new ElemNode($tagName, $tagAttrs, $tagBody); + $tagBody = substr($input, $bodyStart, $bodySpan) ?: ''; + $state->offset = $closePos + \strlen($closeTag); + return new ElemNode($tagName, $tagAttrs, $tagBody); + } + + $textSpan = max(1, strcspn($input, '[', $offset)); + $textBody = substr($input, $offset, $textSpan); + $state->offset += $textSpan; + return new TextNode($textBody); + } + + /** + * parses a CODE=rich body. + * + * @param State $state + * + * @return Node[] + */ + private function parseRich(State $state): array + { + $nodeList = []; + $textBuffer = ''; + while ($state->valid()) { + $node = $this->parseNode($state); + + if ($node instanceof ElemNode) { + // yield a buffered text-node first (if any) + if (!empty($textBuffer)) { + $nodes[] = new TextNode($textBuffer); + $textBuffer = ''; + } + + $nodes[] = $node; + continue; + } + + $textBody = $node->getBody(); + \assert(\is_string($textBody)); + + if ('[' === $textBody) { + // a single bracket was parsed a text-node + $input = $state->input; + $offset = $state->offset; + $closePos = stripos($input, '/code]', $offset); + + if ($closePos === $offset) { + // found a closing tag, stop rich-parsing + $state->offset += \strlen('/code]'); + break; + } + } + + $textBuffer .= $textBody; + } + + if (!empty($textBuffer)) { + // append a buffered text-node (if any) + $nodeList[] = new TextNode($textBuffer); } - // check if we still have some content in the buffer - if ('' !== $buffer) { - // yield the remaining buffer as text-node - yield new TextNode($buffer); + return $nodeList; + } + + /** + * checks if the given node is a CODE=rich node. + * + * @param string $tagName + * @param ElemAttrs $tagAttrs + * + * @return bool + */ + private function isRichCode(string $tagName, ElemAttrs $tagAttrs): bool + { + if (0 !== strcasecmp($tagName, 'code')) { + return false; } + + $lang = $tagAttrs->getValue('lang') ?: + $tagAttrs->getValue('@value'); + + return $lang && 0 === strcasecmp($lang, 'rich'); } /** From eb85f55b867a8f8c382744ada62a3c7f46105952 Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 11:57:21 +0200 Subject: [PATCH 16/27] some fixes and adjusted node-export logic --- app/CodeFormatterApp.php | 16 +++++++- app/Parser/ElemNode.php | 6 +-- app/Parser/Parser.php | 81 +++++++++++++--------------------------- app/Parser/State.php | 53 ++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 60 deletions(-) create mode 100644 app/Parser/State.php diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 19a0eea..22564b8 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -60,7 +60,7 @@ private function parseInput(): iterable private function formatNode(Node $node): string { if ($node instanceof TextNode) { - return $node->getBody(); + return $this->exportNode($node, null); } \assert($node instanceof ElemNode); @@ -70,14 +70,26 @@ private function formatNode(Node $node): string return $this->exportNode($node, null); } + $nodeBody = $node->getBody(); + + if (\is_array($nodeBody)) { + $buffer = ''; + foreach ($nodeBody as $subNode) { + $buffer .= $this->formatNode($subNode); + } + return $buffer; + } + $language = $this->detectLang($node); $formatter = CodeFormatter::create($language); + if (null === $formatter) { // no formatter found, return node as is return $this->exportNode($node, null); } - $result = $formatter->exec($node->getBody()); + \assert(\is_string($nodeBody)); + $result = $formatter->exec($nodeBody); return $this->exportNode($node, $result); } diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php index e15883c..20a9a3e 100644 --- a/app/Parser/ElemNode.php +++ b/app/Parser/ElemNode.php @@ -17,9 +17,9 @@ final class ElemNode extends Node /** * constructs a new code-token. * - * @param string $name - * @param ElemAttrs $attrs - * @param NodeList|string $body + * @param string $name + * @param ElemAttrs $attrs + * @param Node[]|string $body */ public function __construct(string $name, ElemAttrs $attrs, $body) { diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index aebeafd..7a178fd 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -6,55 +6,6 @@ use DevCommunityDE\CodeFormatter\Exceptions\Exception; -/** - * uses as a parse-state. - * - * @internal - */ -final class State -{ - /** @var string */ - public string $input; - - /** @var int */ - public int $length; - - /** @var int */ - public int $offset; - - /** - * constructs a new state. - * - * @param string $input - */ - public function __construct(string $input) - { - $this->input = $input; - $this->length = \strlen($input); - $this->offset = 0; - } - - /** - * checks if this state is still valid. - * - * @return bool - */ - public function valid(): bool - { - return $this->offset < $this->length; - } - - /** - * marks the state a finished. - * - * @return void - */ - public function finish() - { - $this->offset = $this->length; - } -} - /** * this parser uses pcre (regular expressions) * to parse incoming code. @@ -177,8 +128,8 @@ private function parseNode(State $state): Node } $textSpan = max(1, strcspn($input, '[', $offset)); - $textBody = substr($input, $offset, $textSpan); - $state->offset += $textSpan; + $textBody = substr($input, $offset, $textSpan) ?: ''; + $state->offset += (int) $textSpan; return new TextNode($textBody); } @@ -199,11 +150,11 @@ private function parseRich(State $state): array if ($node instanceof ElemNode) { // yield a buffered text-node first (if any) if (!empty($textBuffer)) { - $nodes[] = new TextNode($textBuffer); + $nodeList[] = new TextNode($textBuffer); $textBuffer = ''; } - $nodes[] = $node; + $nodeList[] = $node; continue; } @@ -320,7 +271,7 @@ private function parseAttr(string $value): string public function exportNode(Node $node, ?string $body): string { if ($node instanceof TextNode) { - return $body ?: $node->getBody(); + return $body ?: $this->exportBody($node->getBody()); } \assert($node instanceof ElemNode); @@ -328,7 +279,27 @@ public function exportNode(Node $node, ?string $body): string $buffer = "[{$bbcode}"; $buffer .= $node->getAttrMatch(); $buffer .= ']'; - $buffer .= $body ?: $node->getBody(); + $buffer .= $body ?: $this->exportBody($node->getBody()); return $buffer . "[/{$bbcode}]"; } + + /** + * exports a node body. + * + * @param Node[]|string $nodeBody + * + * @return string + */ + private function exportBody($nodeBody): string + { + if (\is_string($nodeBody)) { + return $nodeBody; + } + + $buffer = ''; + foreach ($nodeBody as $subNode) { + $buffer .= $this->exportNode($subNode, null); + } + return $buffer; + } } diff --git a/app/Parser/State.php b/app/Parser/State.php new file mode 100644 index 0000000..0be887d --- /dev/null +++ b/app/Parser/State.php @@ -0,0 +1,53 @@ +input = $input; + $this->length = \strlen($input); + $this->offset = 0; + } + + /** + * checks if this state is still valid. + * + * @return bool + */ + public function valid(): bool + { + return $this->offset < $this->length; + } + + /** + * marks the state a finished. + * + * @return void + */ + public function finish() + { + $this->offset = $this->length; + } +} From 3ba42f4020534d6a5a65ef0e35c5d1d1d861aff9 Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 12:05:03 +0200 Subject: [PATCH 17/27] tests added --- tests/Parser/ParserTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index b2ceffc..3bc97c3 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -176,4 +176,32 @@ public function testElementExports() $this->assertEquals($input, $export); } } + + /** + * @testdox Verifies that CODE=rich can contain CODE elements + * + * @return void + */ + public function testRichCode() + { + $nodes = $this->parseTextToArray('[code=rich]hello[code=css].test { color: red; }[/code]world[/code]'); + $this->assertCount(1, $nodes); + $this->assertInstanceOf(ElemNode::class, $nodes[0]); + $this->assertEquals('rich', $nodes[0]->getAttr('@value')); + + $nodeBody = $nodes[0]->getBody(); + $this->assertIsArray($nodeBody); + $this->assertCount(3, $nodeBody); + + $this->assertInstanceOf(TextNode::class, $nodeBody[0]); + $this->assertEquals('hello', $nodeBody[0]->getBody()); + + $codeNode = $nodeBody[1]; + $this->assertInstanceOf(ElemNode::class, $codeNode); + $this->assertEquals('css', $codeNode->getAttr('@value')); + $this->assertEquals('.test { color: red; }', $codeNode->getBody()); + + $this->assertInstanceOf(TextNode::class, $nodeBody[2]); + $this->assertEquals('world', $nodeBody[2]->getBody()); + } } From 149a1f382208b3bb9fdffe5dbcf11a6f8f3f5bcc Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 12:18:28 +0200 Subject: [PATCH 18/27] fix code=rich export issue --- app/CodeFormatterApp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 22564b8..88af36b 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -77,7 +77,7 @@ private function formatNode(Node $node): string foreach ($nodeBody as $subNode) { $buffer .= $this->formatNode($subNode); } - return $buffer; + return $this->exportNode($node, $buffer); } $language = $this->detectLang($node); From 40c28034cdac188f0b1e0fdbe4bec4618ef8fc76 Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 18:33:33 +0200 Subject: [PATCH 19/27] small adjustments in architecture + saner api --- app/CodeFormatterApp.php | 30 +++------------- app/Parser/ElemNode.php | 68 +++++++++++++++++++++++++++++++------ app/Parser/Node.php | 54 ++--------------------------- app/Parser/NodeList.php | 65 +++++++++++++++++++++++++++++++++++ app/Parser/Parser.php | 45 ++++++++++++------------ app/Parser/TextNode.php | 16 ++++++++- tests/Parser/ParserTest.php | 36 +++++++++++++++++--- 7 files changed, 198 insertions(+), 116 deletions(-) create mode 100644 app/Parser/NodeList.php diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 88af36b..5079801 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -70,17 +70,16 @@ private function formatNode(Node $node): string return $this->exportNode($node, null); } - $nodeBody = $node->getBody(); - - if (\is_array($nodeBody)) { + if ($node->isRich()) { + // iterate over all child-nodes in this case $buffer = ''; - foreach ($nodeBody as $subNode) { + foreach ($node->getBody() as $subNode) { $buffer .= $this->formatNode($subNode); } return $this->exportNode($node, $buffer); } - $language = $this->detectLang($node); + $language = $node->getLang() ?: 'text'; $formatter = CodeFormatter::create($language); if (null === $formatter) { @@ -88,31 +87,12 @@ private function formatNode(Node $node): string return $this->exportNode($node, null); } + $nodeBody = $node->getBody(); \assert(\is_string($nodeBody)); $result = $formatter->exec($nodeBody); return $this->exportNode($node, $result); } - /** - * detect a code-language from the given node. - * - * @param ElemNode $node - * - * @return string - */ - private function detectLang(ElemNode $node): string - { - $value = $node->getAttr('lang'); - - if (empty($value)) { - // check if a immediate value is set - // (this is the [code=lang] notation) - $value = $node->getAttr('@value'); - } - - return $value ?: 'text'; - } - /** * exports a node. * diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php index 20a9a3e..791e6eb 100644 --- a/app/Parser/ElemNode.php +++ b/app/Parser/ElemNode.php @@ -14,30 +14,70 @@ final class ElemNode extends Node /** @var ElemAttrs */ private $attrs; + /** @var NodeList|string */ + private $body; + /** * constructs a new code-token. * - * @param string $name - * @param ElemAttrs $attrs - * @param Node[]|string $body + * @param string $name + * @param ElemAttrs $attrs + * @param NodeList|string $body */ public function __construct(string $name, ElemAttrs $attrs, $body) { - parent::__construct(Node::KIND_ELEM, $body); + \assert(\is_string($body) || $body instanceof NodeList); + parent::__construct(Node::KIND_ELEM); $this->name = $name; $this->attrs = $attrs; + $this->body = $body; } /** * checks if this node represents a [CODE] bbcode. * - * @override - * * @return bool */ public function isCode(): bool { - return !strcasecmp($this->name, 'code'); + return 0 === strcasecmp($this->name, 'code'); + } + + /** + * checks if this node represents a [CODE=rich] bbcode. + * + * @return bool + */ + public function isRich(): bool + { + if (0 !== strcasecmp($this->name, 'code')) { + return false; + } + + $langAttr = $this->getLang(); + + if (empty($langAttr)) { + /* defaults to "text" */ + return false; + } + + return 0 === strcasecmp($langAttr, 'rich'); + } + + /** + * utility method to retrieve a lang-attribute. + * + * @return string|null + */ + public function getLang(): ?string + { + $langAttr = $this->getAttr('lang'); + + if (empty($langAttr) && $this->isCode()) { + $langAttr = $this->getAttr('@value'); + } + + return $langAttr; } /** @@ -53,8 +93,6 @@ public function getName(): string /** * returns a single node attribute. * - * @override - * * @param string $name * * @return string|null @@ -67,12 +105,20 @@ public function getAttr(string $name): ?string /** * returns the node attributes. * - * @override - * * @return string */ public function getAttrMatch(): string { return $this->attrs->getMatch(); } + + /** + * returns the node-body. + * + * @return NodeList|string + */ + public function getBody() + { + return $this->body; + } } diff --git a/app/Parser/Node.php b/app/Parser/Node.php index 6f215ac..5179c7d 100644 --- a/app/Parser/Node.php +++ b/app/Parser/Node.php @@ -15,54 +15,14 @@ abstract class Node /** @var int */ private $kind; - /** @var Node[]|string */ - private $body; - /** * constructs a new token. * - * @param int $kind - * @param Node[]|string $body + * @param int $kind */ - public function __construct(int $kind, $body) + public function __construct(int $kind) { $this->kind = $kind; - $this->body = $body; - } - - /** - * checks if this node represents [CODE] node. - * returns false by default. - * - * @return bool - */ - public function isCode(): bool - { - return false; - } - - /** - * returns a attribute assigned to this node. - * returns null by default. - * - * @param string $name - * - * @return string|null - */ - public function getAttr(string $name): ?string - { - return null; - } - - /** - * returns all matched attributes as string. - * returns an empty string by default. - * - * @return string - */ - public function getAttrMatch(): string - { - return ''; } /** @@ -74,14 +34,4 @@ public function getKind(): int { return $this->kind; } - - /** - * returns the node body. - * - * @return Node[]|string - */ - public function getBody() - { - return $this->body; - } } diff --git a/app/Parser/NodeList.php b/app/Parser/NodeList.php new file mode 100644 index 0000000..fd74c84 --- /dev/null +++ b/app/Parser/NodeList.php @@ -0,0 +1,65 @@ +nodes = $nodes; + } + + /** + * appends a node. + * + * @param Node|string $node + * + * @return void + */ + public function append($node) + { + if (\is_string($node)) { + $this->nodes[] = new TextNode($node); + return; + } + + \assert($node instanceof Node); + $this->nodes[] = $node; + } + + /** + * returns the node-list as array. + * this method is mainly used for testing. + * + * @return array + */ + public function toArray(): array + { + return $this->nodes; + } + + /** + * returns the node-list as iterable list. + * + * (phan has issues with returning an array as iterable) + * + * @phan-suppress PhanParamSignatureMismatchInternal + * + * @return Node[] + */ + public function getIterator(): iterable + { + return $this->nodes; + } +} diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 7a178fd..f28897c 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -71,9 +71,8 @@ private function parseNorm(State $state): iterable continue; } - $textBody = $node->getBody(); - \assert(\is_string($textBody)); - $textBuffer .= $textBody; + \assert($node instanceof TextNode); + $textBuffer .= $node->getBody(); } if (!empty($textBuffer)) { @@ -96,35 +95,35 @@ private function parseNode(State $state): Node if (preg_match(self::RE_ELEM, $input, $m, 0, $offset)) { // read from $textData until we see a closing tag - $tagName = $m[1]; - $tagAttrs = $this->parseAttrs($m[2]); + $elemName = $m[1]; + $elemAttrs = $this->parseAttrs($m[2]); $bodyStart = $offset + \strlen($m[0]); - if ($this->isRichCode($tagName, $tagAttrs)) { + if ($this->isRichCode($elemName, $elemAttrs)) { // CODE=rich allows nested CODE bbcodes ... yay. // switch to the recursive parser and collect all // child-nodes as tag-body $state->offset = $bodyStart; - $tagBody = $this->parseRich($state); - return new ElemNode($tagName, $tagAttrs, $tagBody); + $elemBody = $this->parseRich($state); + return new ElemNode($elemName, $elemAttrs, $elemBody); } - $closeTag = "[/{$tagName}]"; + $closeTag = "[/{$elemName}]"; $closePos = stripos($input, $closeTag, $bodyStart); if (false === $closePos) { // edge case: // no closing tag found... treat the rest of the input // as bbcode-body and stop the parsing entirely - $tagBody = substr($input, $bodyStart) ?: ''; + $elemBody = substr($input, $bodyStart) ?: ''; $state->finish(); - return new ElemNode($tagName, $tagAttrs, $tagBody); + return new ElemNode($elemName, $elemAttrs, $elemBody); } $bodySpan = $closePos - $bodyStart; - $tagBody = substr($input, $bodyStart, $bodySpan) ?: ''; + $elemBody = substr($input, $bodyStart, $bodySpan) ?: ''; $state->offset = $closePos + \strlen($closeTag); - return new ElemNode($tagName, $tagAttrs, $tagBody); + return new ElemNode($elemName, $elemAttrs, $elemBody); } $textSpan = max(1, strcspn($input, '[', $offset)); @@ -138,11 +137,11 @@ private function parseNode(State $state): Node * * @param State $state * - * @return Node[] + * @return NodeList */ - private function parseRich(State $state): array + private function parseRich(State $state): NodeList { - $nodeList = []; + $nodeList = new NodeList(); $textBuffer = ''; while ($state->valid()) { $node = $this->parseNode($state); @@ -150,16 +149,16 @@ private function parseRich(State $state): array if ($node instanceof ElemNode) { // yield a buffered text-node first (if any) if (!empty($textBuffer)) { - $nodeList[] = new TextNode($textBuffer); + $nodeList->append($textBuffer); $textBuffer = ''; } - $nodeList[] = $node; + $nodeList->append($node); continue; } + \assert($node instanceof TextNode); $textBody = $node->getBody(); - \assert(\is_string($textBody)); if ('[' === $textBody) { // a single bracket was parsed a text-node @@ -179,7 +178,7 @@ private function parseRich(State $state): array if (!empty($textBuffer)) { // append a buffered text-node (if any) - $nodeList[] = new TextNode($textBuffer); + $nodeList->append($textBuffer); } return $nodeList; @@ -271,7 +270,7 @@ private function parseAttr(string $value): string public function exportNode(Node $node, ?string $body): string { if ($node instanceof TextNode) { - return $body ?: $this->exportBody($node->getBody()); + return $body ?: $node->getBody(); } \assert($node instanceof ElemNode); @@ -286,7 +285,7 @@ public function exportNode(Node $node, ?string $body): string /** * exports a node body. * - * @param Node[]|string $nodeBody + * @param NodeList|string $nodeBody * * @return string */ @@ -296,6 +295,8 @@ private function exportBody($nodeBody): string return $nodeBody; } + \assert($nodeBody instanceof NodeList); + $buffer = ''; foreach ($nodeBody as $subNode) { $buffer .= $this->exportNode($subNode, null); diff --git a/app/Parser/TextNode.php b/app/Parser/TextNode.php index 74903dc..8dfb662 100644 --- a/app/Parser/TextNode.php +++ b/app/Parser/TextNode.php @@ -8,6 +8,9 @@ */ final class TextNode extends Node { + /** @var string */ + private $body; + /** * constructs a new text-node. * @@ -15,6 +18,17 @@ final class TextNode extends Node */ public function __construct(string $body) { - parent::__construct(Node::KIND_TEXT, $body); + parent::__construct(Node::KIND_TEXT); + $this->body = $body; + } + + /** + * returns the node-body. + * + * @return string + */ + public function getBody(): string + { + return $this->body; } } diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index 3bc97c3..e74c973 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -184,12 +184,17 @@ public function testElementExports() */ public function testRichCode() { - $nodes = $this->parseTextToArray('[code=rich]hello[code=css].test { color: red; }[/code]world[/code]'); - $this->assertCount(1, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertEquals('rich', $nodes[0]->getAttr('@value')); + $nodes = $this->parseTextToArray('before[code=rich]hello[code=css].test { color: red; }[/code]world[/code]after'); + $this->assertCount(3, $nodes); + $this->assertInstanceOf(TextNode::class, $nodes[0]); + $this->assertEquals('before', $nodes[0]->getBody()); + $this->assertInstanceOf(TextNode::class, $nodes[2]); + $this->assertEquals('after', $nodes[2]->getBody()); - $nodeBody = $nodes[0]->getBody(); + $this->assertInstanceOf(ElemNode::class, $nodes[1]); + $this->assertEquals('rich', $nodes[1]->getLang()); + + $nodeBody = $nodes[1]->getBody()->toArray(); $this->assertIsArray($nodeBody); $this->assertCount(3, $nodeBody); @@ -204,4 +209,25 @@ public function testRichCode() $this->assertInstanceOf(TextNode::class, $nodeBody[2]); $this->assertEquals('world', $nodeBody[2]->getBody()); } + + /** + * @testdox Check if nodes inside CODE=rich elements are parsed correctly + * + * @return void + */ + public function testRichCodeInner() + { + $nodes = $this->parseTextToArray('[code=rich]hello[[[[[world]]]][code][/code][[[[[/code]'); + $this->assertCount(1, $nodes); + $nodeBody = $nodes[0]->getBody()->toArray(); + $this->assertIsArray($nodeBody); + $this->assertCount(3, $nodeBody); + $this->assertInstanceOf(TextNode::class, $nodeBody[0]); + $this->assertEquals('hello[[[[[world]]]]', $nodeBody[0]->getBody()); + $this->assertInstanceOf(ElemNode::class, $nodeBody[1]); + $this->assertEquals(null, $nodeBody[1]->getLang()); + $this->assertEquals('', $nodeBody[1]->getBody()); + $this->assertInstanceOf(TextNode::class, $nodeBody[2]); + $this->assertEquals('[[[[', $nodeBody[2]->getBody()); + } } From 17af5c0bd3a87c2a2353379dceb7be24f71af00a Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 18:46:13 +0200 Subject: [PATCH 20/27] test if pr is broken --- app/CodeFormatterApp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 5079801..62ae18d 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -72,6 +72,7 @@ private function formatNode(Node $node): string if ($node->isRich()) { // iterate over all child-nodes in this case + // test $buffer = ''; foreach ($node->getBody() as $subNode) { $buffer .= $this->formatNode($subNode); From 2e664b0e0707691f770d1023768112e565641bea Mon Sep 17 00:00:00 2001 From: asccc Date: Wed, 15 Jul 2020 18:47:09 +0200 Subject: [PATCH 21/27] yep, pr is broken --- app/CodeFormatterApp.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 62ae18d..5079801 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -72,7 +72,6 @@ private function formatNode(Node $node): string if ($node->isRich()) { // iterate over all child-nodes in this case - // test $buffer = ''; foreach ($node->getBody() as $subNode) { $buffer .= $this->formatNode($subNode); From 8fadf3da46204c98584f4711a5ef6f5f56555c91 Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 02:24:14 +0200 Subject: [PATCH 22/27] fix IteratorAggregate usage --- app/Parser/NodeList.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/Parser/NodeList.php b/app/Parser/NodeList.php index fd74c84..2fe9a19 100644 --- a/app/Parser/NodeList.php +++ b/app/Parser/NodeList.php @@ -4,6 +4,7 @@ namespace DevCommunityDE\CodeFormatter\Parser; use IteratorAggregate; +use Traversable; final class NodeList implements IteratorAggregate { @@ -50,16 +51,12 @@ public function toArray(): array } /** - * returns the node-list as iterable list. + * returns the node-list as traversable. * - * (phan has issues with returning an array as iterable) - * - * @phan-suppress PhanParamSignatureMismatchInternal - * - * @return Node[] + * @return Traversable */ - public function getIterator(): iterable + public function getIterator(): Traversable { - return $this->nodes; + yield from $this->nodes; } } From 463e6086740899c0d52c394b5366263f23d93e1c Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 03:21:53 +0200 Subject: [PATCH 23/27] make tests type-safe --- tests/Parser/ParserTest.php | 246 +++++++++++++++++++++++------------- 1 file changed, 160 insertions(+), 86 deletions(-) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index e74c973..cc5d18d 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -5,10 +5,12 @@ use DevCommunityDE\CodeFormatter\Parser\ElemNode; use DevCommunityDE\CodeFormatter\Parser\Node; +use DevCommunityDE\CodeFormatter\Parser\NodeList; use DevCommunityDE\CodeFormatter\Parser\Parser; use DevCommunityDE\CodeFormatter\Parser\TextNode; use DevCommunityDE\CodeFormatter\Tests\Parser\Helpers\ParserTestHelpers; use PHPUnit\Framework\TestCase; +use Traversable; final class ParserTest extends TestCase { @@ -36,12 +38,23 @@ public function testPlainElement() { $nodes = $this->parseTextToArray('before plain bbcode[plain][code]test[/code][/plain]'); $this->assertCount(2, $nodes); - $this->assertInstanceof(TextNode::class, $nodes[0]); - $this->assertEquals(Node::KIND_TEXT, $nodes[0]->getKind()); - $this->assertEquals('before plain bbcode', $nodes[0]->getBody()); - $this->assertInstanceof(ElemNode::class, $nodes[1]); - $this->assertNotTrue($nodes[1]->isCode()); - $this->assertEquals('[code]test[/code]', $nodes[1]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals(Node::KIND_TEXT, $node0->getKind()); + $this->assertEquals('before plain bbcode', $node0->getBody()); + + $node1 = $nodes[1]; + \assert($node1 instanceof ElemNode); + $this->assertNotTrue($node1->isCode()); + $this->assertNotTrue($node1->isRich()); + $this->assertNull($node1->getLang()); + $this->assertEquals('plain', $node1->getName()); + $this->assertEquals('', $node1->getAttrMatch()); + + $elemBody = $node1->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('[code]test[/code]', $elemBody); } /** @@ -53,16 +66,28 @@ public function testBracketsInsideText() { $nodes = $this->parseTextToArray('hello[[[[['); $this->assertCount(1, $nodes); - $this->assertInstanceof(TextNode::class, $nodes[0]); - $this->assertEquals('hello[[[[[', $nodes[0]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals('hello[[[[[', $node0->getBody()); $nodes = $this->parseTextToArray('hello[[[[[code]test[/code]'); $this->assertCount(2, $nodes); - $this->assertInstanceof(TextNode::class, $nodes[0]); - $this->assertEquals('hello[[[[', $nodes[0]->getBody()); - $this->assertInstanceof(ElemNode::class, $nodes[1]); - $this->assertTrue($nodes[1]->isCode()); - $this->assertEquals('test', $nodes[1]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals('hello[[[[', $node0->getBody()); + + $node1 = $nodes[1]; + \assert($node1 instanceof ElemNode); + $this->assertTrue($node1->isCode()); + $this->assertFalse($node1->isRich()); + $this->assertNull($node1->getLang()); + $this->assertEquals('', $node1->getAttrMatch()); + + $elemBody = $node1->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('test', $elemBody); } /** @@ -74,11 +99,21 @@ public function testUnmatchedClosingElements() { $nodes = $this->parseTextToArray('[code]a[/code]b[/code]'); $this->assertCount(2, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertTrue($nodes[0]->isCode()); - $this->assertEquals('a', $nodes[0]->getBody()); - $this->assertInstanceOf(TextNode::class, $nodes[1]); - $this->assertEquals('b[/code]', $nodes[1]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + $this->assertNull($node0->getLang()); + $this->assertEquals('', $node0->getAttrMatch()); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('a', $elemBody); + + $node1 = $nodes[1]; + \assert($node1 instanceof TextNode); + $this->assertEquals('b[/code]', $node1->getBody()); } /** @@ -90,11 +125,21 @@ public function testNestedCodeElements() { $nodes = $this->parseTextToArray('[code][code]test[/code][/code]'); $this->assertCount(2, $nodes); - $this->assertInstanceof(ElemNode::class, $nodes[0]); - $this->assertTrue($nodes[0]->isCode()); - $this->assertEquals('[code]test', $nodes[0]->getBody()); - $this->assertInstanceof(TextNode::class, $nodes[1]); - $this->assertEquals('[/code]', $nodes[1]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + $this->assertNull($node0->getLang()); + $this->assertEquals('', $node0->getAttrMatch()); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('[code]test', $elemBody); + + $node1 = $nodes[1]; + \assert($node1 instanceof TextNode); + $this->assertEquals('[/code]', $node1->getBody()); } /** @@ -106,9 +151,17 @@ public function testUnclosedElements() { $nodes = $this->parseTextToArray('[code]test'); $this->assertCount(1, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertTrue($nodes[0]->isCode()); - $this->assertEquals('test', $nodes[0]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + $this->assertNull($node0->getLang()); + $this->assertEquals('', $node0->getAttrMatch()); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('test', $elemBody); // it would be possible to treat any opening bbcode // as an implicit closing bbcode (if another bbcode is open). @@ -116,20 +169,14 @@ public function testUnclosedElements() // other bbcodes inside. $nodes = $this->parseTextToArray('[code]a[plain]b[/plain]'); $this->assertCount(1, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertTrue($nodes[0]->isCode()); - $this->assertEquals('a[plain]b[/plain]', $nodes[0]->getBody()); - - // an optimized algorithm could produce the following: - // - // $nodes = $this->parseTextToArray('[code]a[plain]b[/plain]'); - // $this->assertCount(2, $nodes); - // $this->assertInstanceOf(ElemNode::class, $nodes[0]); - // $this->assertTrue($nodes[0]->isCode()); - // $this->assertEquals('a', $nodes[0]->getBody()); - // $this->assertInstanceOf(ElemNode::class, $nodes[1]); - // $this->assertEqualsIgnoringCase('plain', $nodes[1]->getName()); - // $this->assertEqual('b', $nodes[1]->getBody()); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('a[plain]b[/plain]', $elemBody); } /** @@ -137,20 +184,38 @@ public function testUnclosedElements() * * @return void */ - public function testAttributeBehaviour() + public function testAttributeParsing() { $nodes = $this->parseTextToArray('[code=css]test{}[/code]'); $this->assertCount(1, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertEquals('=css', $nodes[0]->getAttrMatch()); - $this->assertEquals('css', $nodes[0]->getAttr('@value')); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + $this->assertEquals('css', $node0->getLang()); + $this->assertEquals('=css', $node0->getAttrMatch()); + $this->assertEquals('css', $node0->getAttr('@value')); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('test{}', $elemBody); $nodes = $this->parseTextToArray('[code lang="css" title="Test 123"]test{}[/code]'); $this->assertCount(1, $nodes); - $this->assertInstanceOf(ElemNode::class, $nodes[0]); - $this->assertEquals(' lang="css" title="Test 123"', $nodes[0]->getAttrMatch()); - $this->assertEquals('css', $nodes[0]->getAttr('lang')); - $this->assertEquals('Test 123', $nodes[0]->getAttr('title')); + + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + $this->assertEquals('css', $node0->getLang()); + $this->assertEquals(' lang="css" title="Test 123"', $node0->getAttrMatch()); + $this->assertEquals('css', $node0->getAttr('lang')); + $this->assertEquals('Test 123', $node0->getAttr('title')); + + $elemBody = $node0->getBody(); + $this->assertIsString($elemBody); + $this->assertEquals('test{}', $elemBody); } /** @@ -166,6 +231,9 @@ public function testElementExports() '[code lang=css title="Test"]test{}[/code]', '[plain][code=css]test{}[/code][/plain]', '[CODE]test[/CODE]', + '[code]test[/code]', + '[code=rich][code]test[/code]test[/code]', + '[CODE=css]test[[[[/CODE]', ]; foreach ($inputs as $input) { @@ -178,7 +246,7 @@ public function testElementExports() } /** - * @testdox Verifies that CODE=rich can contain CODE elements + * @testdox Verify that CODE=rich is parsed correctly * * @return void */ @@ -186,48 +254,54 @@ public function testRichCode() { $nodes = $this->parseTextToArray('before[code=rich]hello[code=css].test { color: red; }[/code]world[/code]after'); $this->assertCount(3, $nodes); - $this->assertInstanceOf(TextNode::class, $nodes[0]); - $this->assertEquals('before', $nodes[0]->getBody()); - $this->assertInstanceOf(TextNode::class, $nodes[2]); - $this->assertEquals('after', $nodes[2]->getBody()); - $this->assertInstanceOf(ElemNode::class, $nodes[1]); - $this->assertEquals('rich', $nodes[1]->getLang()); + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals('before', $node0->getBody()); - $nodeBody = $nodes[1]->getBody()->toArray(); - $this->assertIsArray($nodeBody); - $this->assertCount(3, $nodeBody); + $node2 = $nodes[2]; + \assert($node2 instanceof TextNode); + $this->assertEquals('after', $node2->getBody()); - $this->assertInstanceOf(TextNode::class, $nodeBody[0]); - $this->assertEquals('hello', $nodeBody[0]->getBody()); + $node1 = $nodes[1]; + \assert($node1 instanceof ElemNode); + $this->assertTrue($node1->isCode()); + $this->assertTrue($node1->isRich()); + $this->assertEquals('rich', $node1->getLang()); + $this->assertEquals('=rich', $node1->getAttrMatch()); + $this->assertEquals('rich', $node1->getAttr('@value')); - $codeNode = $nodeBody[1]; - $this->assertInstanceOf(ElemNode::class, $codeNode); - $this->assertEquals('css', $codeNode->getAttr('@value')); - $this->assertEquals('.test { color: red; }', $codeNode->getBody()); + $nodeList = $node1->getBody(); + \assert($nodeList instanceof NodeList); + $nodeIter = $nodeList->getIterator(); + $this->assertInstanceOf(Traversable::class, $nodeIter); - $this->assertInstanceOf(TextNode::class, $nodeBody[2]); - $this->assertEquals('world', $nodeBody[2]->getBody()); - } + $nodeListArray = iterator_to_array($nodeIter); + $this->assertIsArray($nodeListArray); - /** - * @testdox Check if nodes inside CODE=rich elements are parsed correctly - * - * @return void - */ - public function testRichCodeInner() - { - $nodes = $this->parseTextToArray('[code=rich]hello[[[[[world]]]][code][/code][[[[[/code]'); - $this->assertCount(1, $nodes); - $nodeBody = $nodes[0]->getBody()->toArray(); - $this->assertIsArray($nodeBody); - $this->assertCount(3, $nodeBody); - $this->assertInstanceOf(TextNode::class, $nodeBody[0]); - $this->assertEquals('hello[[[[[world]]]]', $nodeBody[0]->getBody()); - $this->assertInstanceOf(ElemNode::class, $nodeBody[1]); - $this->assertEquals(null, $nodeBody[1]->getLang()); - $this->assertEquals('', $nodeBody[1]->getBody()); - $this->assertInstanceOf(TextNode::class, $nodeBody[2]); - $this->assertEquals('[[[[', $nodeBody[2]->getBody()); + $elemBody = $nodeList->toArray(); + $this->assertIsArray($elemBody); + $this->assertCount(3, $elemBody); + $this->assertEquals($elemBody, $nodeListArray); + + $subNode0 = $elemBody[0]; + \assert($subNode0 instanceof TextNode); + $this->assertEquals('hello', $subNode0->getBody()); + + $subNode1 = $elemBody[1]; + \assert($subNode1 instanceof ElemNode); + $this->assertTrue($subNode1->isCode()); + $this->assertFalse($subNode1->isRich()); + $this->assertEquals('css', $subNode1->getLang()); + $this->assertEquals('=css', $subNode1->getAttrMatch()); + $this->assertEquals('css', $subNode1->getAttr('@value')); + + $subElemBody = $subNode1->getBody(); + $this->assertIsString($subElemBody); + $this->assertEquals('.test { color: red; }', $subElemBody); + + $subNode2 = $elemBody[2]; + \assert($subNode2 instanceof TextNode); + $this->assertEquals('world', $subNode2->getBody()); } } From cfb84cb77de71feb8ebfec2a3d0fd84f630f8b49 Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 04:02:17 +0200 Subject: [PATCH 24/27] added more tests and fixed some associated issues --- app/Parser/ElemNode.php | 2 +- app/Parser/Parser.php | 13 +++++++-- tests/Parser/ParserTest.php | 57 +++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/Parser/ElemNode.php b/app/Parser/ElemNode.php index 791e6eb..01e2197 100644 --- a/app/Parser/ElemNode.php +++ b/app/Parser/ElemNode.php @@ -73,7 +73,7 @@ public function getLang(): ?string { $langAttr = $this->getAttr('lang'); - if (empty($langAttr) && $this->isCode()) { + if (null === $langAttr && $this->isCode()) { $langAttr = $this->getAttr('@value'); } diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index f28897c..372ac98 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -13,7 +13,7 @@ final class Parser { /** pattern used to tokenize bbcode elements */ - private const RE_ELEM = '/\G\[(code|plain)(=\w+|(?:\s+\w+=(?:"[^"]*"|\S+))*)\]/i'; + private const RE_ELEM = '/\G\[(code|plain)(=\w+|(?:\s+\w+=(?:"[^"]*"|\S+?))*)\]/i'; /** pattern used to tokenize attributes */ private const RE_ATTR = '/\G\s*(\w+)=("[^"]*"|\S+)/'; @@ -25,7 +25,16 @@ final class Parser */ public function parseFile(string $filePath): iterable { - $textData = file_get_contents($filePath); + // note: we cannot stat php://input + // therefore we have to give it a try + $fileHandle = @fopen($filePath, 'r'); + + if (false === $fileHandle) { + throw new Exception('unable to open file: ' . $filePath); + } + + $textData = stream_get_contents($fileHandle); + fclose($fileHandle); if (false === $textData) { throw new Exception('unable to parse file: ' . $filePath); diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index cc5d18d..ad09d30 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace DevCommunityDE\CodeFormatter\Tests\Parser; +use DevCommunityDE\CodeFormatter\Exceptions\Exception; use DevCommunityDE\CodeFormatter\Parser\ElemNode; use DevCommunityDE\CodeFormatter\Parser\Node; use DevCommunityDE\CodeFormatter\Parser\NodeList; @@ -29,6 +30,24 @@ public function testFileTextParsing() $this->assertEquals($fromText, $fromFile); } + /** + * @testdox Parsing a non-existent file should throw + * + * @return void + */ + public function testFileParsing() + { + $input = __DIR__ . 'non-existent.txt'; + $this->assertFalse(file_exists($input)); + + try { + $this->parseFileToArray($input); + $this->assertTrue(false); + } catch (Exception $ex) { + $this->assertTrue(true); + } + } + /** * @testdox PLAIN bbcodes should consume other bbcodes * @@ -304,4 +323,42 @@ public function testRichCode() \assert($subNode2 instanceof TextNode); $this->assertEquals('world', $subNode2->getBody()); } + + /** + * @testdox The parser should make the best out of invalid inputs + * + * @return void + */ + public function testInvalidInputs() + { + $nodes = $this->parseTextToArray('[code lang=""""title="test"ing][/code]'); + $this->assertCount(1, $nodes); + + // TODO XenForo does not recognize this as a bbcode ... + $node0 = $nodes[0]; + \assert($node0 instanceof ElemNode); + $this->assertTrue($node0->isCode()); + $this->assertFalse($node0->isRich()); + + // the `lang=""` part gets parsed correctly, because the parser + // matched the attribute string not containing white-space (\S+) + $this->assertEquals('', $node0->getLang()); + $this->assertEquals('', $node0->getAttr('lang')); + + $this->assertEquals(' lang=""""title="test"ing', $node0->getAttrMatch()); + $this->assertNull($node0->getAttr('@value')); + $this->assertNull($node0->getAttr('title')); + + $parser = new Parser(); + $export = $parser->exportNode($node0, null); + $this->assertEquals('[code lang=""""title="test"ing][/code]', $export); + + $nodes = $this->parseTextToArray('[code lang= """"title="test"ing][/code]'); + // note the white-space ---------------------^ + $this->assertCount(1, $nodes); + + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals('[code lang= """"title="test"ing][/code]', $node0->getBody()); + } } From 516a86d2580132d441f88df6b030fa3ef588b077 Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 04:03:33 +0200 Subject: [PATCH 25/27] fix typo in file-path --- tests/Parser/ParserTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index ad09d30..e9c9009 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -37,7 +37,7 @@ public function testFileTextParsing() */ public function testFileParsing() { - $input = __DIR__ . 'non-existent.txt'; + $input = __DIR__ . '/non-existent.txt'; $this->assertFalse(file_exists($input)); try { From 37824dcdce5961e997a079852d01b0e79ba63efc Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 05:16:05 +0200 Subject: [PATCH 26/27] fixed some typos and added more tests --- app/CodeFormatterApp.php | 2 +- app/Parser/Parser.php | 11 +--- app/Parser/State.php | 2 +- tests/Parser/ParserTest.php | 120 ++++++++++++++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 16 deletions(-) diff --git a/app/CodeFormatterApp.php b/app/CodeFormatterApp.php index 5079801..158188e 100644 --- a/app/CodeFormatterApp.php +++ b/app/CodeFormatterApp.php @@ -83,7 +83,7 @@ private function formatNode(Node $node): string $formatter = CodeFormatter::create($language); if (null === $formatter) { - // no formatter found, return node as is + // no formatter found, return node as-is return $this->exportNode($node, null); } diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 372ac98..7ebd2b3 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -6,10 +6,6 @@ use DevCommunityDE\CodeFormatter\Exceptions\Exception; -/** - * this parser uses pcre (regular expressions) - * to parse incoming code. - */ final class Parser { /** pattern used to tokenize bbcode elements */ @@ -170,14 +166,13 @@ private function parseRich(State $state): NodeList $textBody = $node->getBody(); if ('[' === $textBody) { - // a single bracket was parsed a text-node + // a single bracket was parsed as text-node $input = $state->input; $offset = $state->offset; - $closePos = stripos($input, '/code]', $offset); - if ($closePos === $offset) { + if (0 === substr_compare($input, '/code]', $offset, 6, true)) { // found a closing tag, stop rich-parsing - $state->offset += \strlen('/code]'); + $state->offset += 6; break; } } diff --git a/app/Parser/State.php b/app/Parser/State.php index 0be887d..ab01f7e 100644 --- a/app/Parser/State.php +++ b/app/Parser/State.php @@ -4,7 +4,7 @@ namespace DevCommunityDE\CodeFormatter\Parser; /** - * uses as a parse-state. + * used internally as a parse-state. * * @internal */ diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index e9c9009..e55a466 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -5,7 +5,6 @@ use DevCommunityDE\CodeFormatter\Exceptions\Exception; use DevCommunityDE\CodeFormatter\Parser\ElemNode; -use DevCommunityDE\CodeFormatter\Parser\Node; use DevCommunityDE\CodeFormatter\Parser\NodeList; use DevCommunityDE\CodeFormatter\Parser\Parser; use DevCommunityDE\CodeFormatter\Parser\TextNode; @@ -55,13 +54,17 @@ public function testFileParsing() */ public function testPlainElement() { - $nodes = $this->parseTextToArray('before plain bbcode[plain][code]test[/code][/plain]'); - $this->assertCount(2, $nodes); + $nodes = $this->parseTextToArray( + 'before' . + '[plain][code]test[/code][/plain]' . + 'after' + ) + ; + $this->assertCount(3, $nodes); $node0 = $nodes[0]; \assert($node0 instanceof TextNode); - $this->assertEquals(Node::KIND_TEXT, $node0->getKind()); - $this->assertEquals('before plain bbcode', $node0->getBody()); + $this->assertEquals('before', $node0->getBody()); $node1 = $nodes[1]; \assert($node1 instanceof ElemNode); @@ -74,6 +77,10 @@ public function testPlainElement() $elemBody = $node1->getBody(); $this->assertIsString($elemBody); $this->assertEquals('[code]test[/code]', $elemBody); + + $node2 = $nodes[2]; + \assert($node2 instanceof TextNode); + $this->assertEquals('after', $node2->getBody()); } /** @@ -271,7 +278,16 @@ public function testElementExports() */ public function testRichCode() { - $nodes = $this->parseTextToArray('before[code=rich]hello[code=css].test { color: red; }[/code]world[/code]after'); + $nodes = $this->parseTextToArray( + 'before' . + '[code=rich]' . + 'hello' . + '[code=css].test { color: red; }[/code]' . + 'world' . + '[/code]' . + 'after' + ); + $this->assertCount(3, $nodes); $node0 = $nodes[0]; @@ -324,6 +340,98 @@ public function testRichCode() $this->assertEquals('world', $subNode2->getBody()); } + /** + * @testdox Check nested CODE=rich elements + * + * @return void + */ + public function testNestedRichCode() + { + $nodes = $this->parseTextToArray( + 'before' . + '[code=rich]' . + '[code lang="rich" title="inner"]' . + '[code]test[/code]' . + '[/code]' . + 'between' . + '[code=rich][b]test2[/b][/code]' . + '[/code]' . + 'after' + ); + + $this->assertCount(3, $nodes); + + $node0 = $nodes[0]; + \assert($node0 instanceof TextNode); + $this->assertEquals('before', $node0->getBody()); + + $node1 = $nodes[1]; + \assert($node1 instanceof ElemNode); + $this->assertTrue($node1->isCode()); + $this->assertTrue($node1->isRich()); + $this->assertEquals('rich', $node1->getLang()); + $this->assertEquals('=rich', $node1->getAttrMatch()); + $this->assertEquals('rich', $node1->getAttr('@value')); + + $nodeList = $node1->getBody(); + \assert($nodeList instanceof NodeList); + + $elemBody = $nodeList->toArray(); + $this->assertCount(3, $elemBody); + + $subNode0 = $elemBody[0]; + \assert($subNode0 instanceof ElemNode); + $this->assertTrue($subNode0->isCode()); + $this->assertTrue($subNode0->isRich()); + $this->assertEquals('rich', $subNode0->getLang()); + $this->assertEquals('rich', $subNode0->getAttr('lang')); + $this->assertEquals('inner', $subNode0->getAttr('title')); + $this->assertEquals(' lang="rich" title="inner"', $subNode0->getAttrMatch()); + $this->assertNull($subNode0->getAttr('@value')); + + $subNodeList = $subNode0->getBody(); + \assert($subNodeList instanceof NodeList); + + $subElemBody = $subNodeList->toArray(); + $this->assertCount(1, $subElemBody); + + $subSubNode0 = $subElemBody[0]; + \assert($subSubNode0 instanceof ElemNode); + $this->assertTrue($subSubNode0->isCode()); + $this->assertFalse($subSubNode0->isRich()); + + $subSubElemBody = $subSubNode0->getBody(); + $this->assertIsString($subSubElemBody); + $this->assertEquals('test', $subSubElemBody); + + $subNode1 = $elemBody[1]; + \assert($subNode1 instanceof TextNode); + $this->assertEquals('between', $subNode1->getBody()); + + $subNode2 = $elemBody[2]; + \assert($subNode2 instanceof ElemNode); + $this->assertTrue($subNode2->isCode()); + $this->assertTrue($subNode2->isRich()); + $this->assertEquals('rich', $subNode2->getLang()); + $this->assertNull($subNode2->getAttr('lang')); + $this->assertEquals('=rich', $subNode2->getAttrMatch()); + $this->assertEquals('rich', $subNode2->getAttr('@value')); + + $subNodeList = $subNode2->getBody(); + \assert($subNodeList instanceof NodeList); + + $subElemBody = $subNodeList->toArray(); + $this->assertCount(1, $subElemBody); + + $subSubNode0 = $subElemBody[0]; + \assert($subSubNode0 instanceof TextNode); + $this->assertEquals('[b]test2[/b]', $subSubNode0->getBody()); + + $node2 = $nodes[2]; + \assert($node2 instanceof TextNode); + $this->assertEquals('after', $node2->getBody()); + } + /** * @testdox The parser should make the best out of invalid inputs * From 79466ef03de0eb4c4149647fd578d8f91a73010c Mon Sep 17 00:00:00 2001 From: asccc Date: Thu, 16 Jul 2020 06:06:27 +0200 Subject: [PATCH 27/27] use expectException in tests --- app/Parser/Parser.php | 2 +- tests/Parser/ParserTest.php | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/Parser/Parser.php b/app/Parser/Parser.php index 7ebd2b3..4e0c0c0 100644 --- a/app/Parser/Parser.php +++ b/app/Parser/Parser.php @@ -33,7 +33,7 @@ public function parseFile(string $filePath): iterable fclose($fileHandle); if (false === $textData) { - throw new Exception('unable to parse file: ' . $filePath); + throw new Exception('unable to read file: ' . $filePath); } return $this->parseText($textData); diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php index e55a466..26f0238 100644 --- a/tests/Parser/ParserTest.php +++ b/tests/Parser/ParserTest.php @@ -38,13 +38,9 @@ public function testFileParsing() { $input = __DIR__ . '/non-existent.txt'; $this->assertFalse(file_exists($input)); - - try { - $this->parseFileToArray($input); - $this->assertTrue(false); - } catch (Exception $ex) { - $this->assertTrue(true); - } + $this->expectException(Exception::class); + $this->expectExceptionMessage('unable to open file: ' . $input); + $this->parseFileToArray($input); } /**