From 33d262a0c4fb6c6371385f6dc8532f4e32c20ebc Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 19 Oct 2021 18:06:05 +0200 Subject: [PATCH] Encoder uses AST (Node::toString() methods) --- src/Neon/Encoder.php | 100 ++++++------ src/Neon/Node.php | 3 + src/Neon/Node/ArrayItemNode.php | 34 +++++ src/Neon/Node/ArrayNode.php | 16 ++ src/Neon/Node/EntityChainNode.php | 6 + src/Neon/Node/EntityNode.php | 9 ++ src/Neon/Node/LiteralNode.php | 21 +++ src/Neon/Node/StringNode.php | 17 +++ tests/Neon/Encoder.nodes.phpt | 35 +++++ tests/Neon/fixtures/Encoder.nodes.txt | 212 ++++++++++++++++++++++++++ 10 files changed, 396 insertions(+), 57 deletions(-) create mode 100644 tests/Neon/Encoder.nodes.phpt create mode 100644 tests/Neon/fixtures/Encoder.nodes.txt diff --git a/src/Neon/Encoder.php b/src/Neon/Encoder.php index f0404eb6..be6fefa1 100644 --- a/src/Neon/Encoder.php +++ b/src/Neon/Encoder.php @@ -22,73 +22,59 @@ final class Encoder /** * Returns the NEON representation of a value. */ - public function encode($var, int $flags = 0): string + public function encode($val, int $flags = 0): string { - if ($var instanceof \DateTimeInterface) { - return $var->format('Y-m-d H:i:s O'); + $node = $this->valueToNode($val, (bool) ($flags & self::BLOCK)); + return $node->toString(); + } - } elseif ($var instanceof Entity) { - if ($var->value === Neon::CHAIN) { - return implode('', array_map([$this, 'encode'], $var->attributes)); - } - return $this->encode($var->value) . '(' - . (is_array($var->attributes) ? substr($this->encode($var->attributes), 1, -1) : '') . ')'; - } - if (is_object($var)) { - $obj = $var; - $var = []; - foreach ($obj as $k => $v) { - $var[$k] = $v; - } - } + public function valueToNode($val, bool $blockMode = false): Node + { + if ($val instanceof \DateTimeInterface) { + return new Node\LiteralNode($val); - if (is_array($var)) { - $isList = !$var || array_keys($var) === range(0, count($var) - 1); - $s = ''; - if ($flags & self::BLOCK) { - if (count($var) === 0) { - return '[]'; - } - foreach ($var as $k => $v) { - $v = $this->encode($v, self::BLOCK); - $s .= ($isList ? '-' : $this->encode($k) . ':') - . (strpos($v, "\n") === false - ? ' ' . $v . "\n" - : "\n" . preg_replace('#^(?=.)#m', "\t", $v) . (substr($v, -2, 1) === "\n" ? '' : "\n")); - } - return $s; - - } else { - foreach ($var as $k => $v) { - $s .= ($isList ? '' : $this->encode($k) . ': ') . $this->encode($v) . ', '; - } - return ($isList ? '[' : '{') . substr($s, 0, -2) . ($isList ? ']' : '}'); + } elseif ($val instanceof Entity && $val->value === Neon::CHAIN) { + $node = new Node\EntityChainNode; + foreach ($val->attributes as $entity) { + $node->chain[] = $this->valueToNode($entity, $blockMode); } + return $node; - } elseif (is_string($var)) { - if (!Lexer::requiresDelimiters($var)) { - return $var; - } + } elseif ($val instanceof Entity) { + return new Node\EntityNode( + $this->valueToNode($val->value), + $this->arrayToNodes((array) $val->attributes) + ); - $res = json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if ($res === false) { - throw new Exception('Invalid UTF-8 sequence: ' . $var); - } - if (strpos($var, "\n") !== false) { - $res = preg_replace_callback('#[^\\\\]|\\\\(.)#s', function ($m) { - return ['n' => "\n\t", 't' => "\t", '"' => '"'][$m[1] ?? ''] ?? $m[0]; - }, $res); - $res = '"""' . "\n\t" . substr($res, 1, -1) . "\n" . '"""'; - } - return $res; + } elseif (is_object($val) || is_array($val)) { + $node = new Node\ArrayNode($blockMode ? '' : null); + $node->items = $this->arrayToNodes($val, $blockMode); + return $node; - } elseif (is_float($var)) { - $var = json_encode($var); - return strpos($var, '.') === false ? $var . '.0' : $var; + } elseif (is_string($val) && Lexer::requiresDelimiters($val)) { + return new Node\StringNode($val); } else { - return json_encode($var); + return new Node\LiteralNode($val); + } + } + + + private function arrayToNodes($val, bool $blockMode = false): array + { + $res = []; + $counter = 0; + $hide = true; + foreach ($val as $k => $v) { + $res[] = $item = new Node\ArrayItemNode; + $item->key = $hide && $k === $counter ? null : self::valueToNode($k); + $item->value = self::valueToNode($v, $blockMode); + if ($hide && is_int($k)) { + $hide = $k === $counter; + $counter = max($k + 1, $counter); + } } + return $res; } } diff --git a/src/Neon/Node.php b/src/Neon/Node.php index 8bbd2253..b047a8c3 100644 --- a/src/Neon/Node.php +++ b/src/Neon/Node.php @@ -24,6 +24,9 @@ abstract class Node abstract public function toValue(); + abstract public function toString(): string; + + /** @return self[] */ public function getSubNodes(): array { diff --git a/src/Neon/Node/ArrayItemNode.php b/src/Neon/Node/ArrayItemNode.php index c4b237a5..13f0dac8 100644 --- a/src/Neon/Node/ArrayItemNode.php +++ b/src/Neon/Node/ArrayItemNode.php @@ -43,12 +43,46 @@ public static function itemsToArray(array $items): array } + /** @param self[] $items */ + public static function itemsToInlineString(array $items): string + { + $res = ''; + foreach ($items as $item) { + $res .= ($res === '' ? '' : ', ') + . ($item->key ? $item->key->toString() . ': ' : '') + . $item->value->toString(); + } + return $res; + } + + + /** @param self[] $items */ + public static function itemsToBlockString(array $items): string + { + $res = ''; + foreach ($items as $item) { + $v = $item->value->toString(); + $res .= ($item->key ? $item->key->toString() . ':' : '-') + . (strpos($v, "\n") === false + ? ' ' . $v . "\n" + : "\n" . preg_replace('#^(?=.)#m', "\t", $v) . (substr($v, -2, 1) === "\n" ? '' : "\n")); + } + return $res; + } + + public function toValue() { throw new \LogicException; } + public function toString(): string + { + throw new \LogicException; + } + + public function getSubNodes(): array { return $this->key ? [$this->key, $this->value] : [$this->value]; diff --git a/src/Neon/Node/ArrayNode.php b/src/Neon/Node/ArrayNode.php index cf0ff071..42404ac0 100644 --- a/src/Neon/Node/ArrayNode.php +++ b/src/Neon/Node/ArrayNode.php @@ -35,6 +35,22 @@ public function toValue(): array } + public function toString(): string + { + if ($this->indent === null) { + $isList = !array_filter($this->items, function ($item) { return $item->key; }); + $res = ArrayItemNode::itemsToInlineString($this->items); + return ($isList ? '[' : '{') . $res . ($isList ? ']' : '}'); + + } elseif (count($this->items) === 0) { + return '[]'; + + } else { + return ArrayItemNode::itemsToBlockString($this->items); + } + } + + public function getSubNodes(): array { return $this->items; diff --git a/src/Neon/Node/EntityChainNode.php b/src/Neon/Node/EntityChainNode.php index b4c91481..2e5fdff3 100644 --- a/src/Neon/Node/EntityChainNode.php +++ b/src/Neon/Node/EntityChainNode.php @@ -38,6 +38,12 @@ public function toValue(): Neon\Entity } + public function toString(): string + { + return implode('', array_map(function ($entity) { return $entity->toString(); }, $this->chain)); + } + + public function getSubNodes(): array { return $this->chain; diff --git a/src/Neon/Node/EntityNode.php b/src/Neon/Node/EntityNode.php index 6c0ff722..4bd0ba82 100644 --- a/src/Neon/Node/EntityNode.php +++ b/src/Neon/Node/EntityNode.php @@ -41,6 +41,15 @@ public function toValue(): Entity } + public function toString(): string + { + return $this->value->toString() + . '(' + . ($this->attributes ? ArrayItemNode::itemsToInlineString($this->attributes) : '') + . ')'; + } + + public function getSubNodes(): array { return array_merge([$this->value], $this->attributes); diff --git a/src/Neon/Node/LiteralNode.php b/src/Neon/Node/LiteralNode.php index a28d1be8..95d82e0a 100644 --- a/src/Neon/Node/LiteralNode.php +++ b/src/Neon/Node/LiteralNode.php @@ -30,4 +30,25 @@ public function toValue() { return $this->value; } + + + public function toString(): string + { + if ($this->value instanceof \DateTimeInterface) { + return $this->value->format('Y-m-d H:i:s O'); + + } elseif (is_string($this->value)) { + return $this->value; + + } elseif (is_float($this->value)) { + $res = json_encode($this->value); + return strpos($res, '.') === false ? $res . '.0' : $res; + + } elseif (is_int($this->value) || is_bool($this->value) || $this->value === null) { + return json_encode($this->value); + + } else { + throw new \LogicException; + } + } } diff --git a/src/Neon/Node/StringNode.php b/src/Neon/Node/StringNode.php index 73d3c0ef..ec4be166 100644 --- a/src/Neon/Node/StringNode.php +++ b/src/Neon/Node/StringNode.php @@ -9,6 +9,7 @@ namespace Nette\Neon\Node; +use Nette; use Nette\Neon\Node; @@ -30,4 +31,20 @@ public function toValue(): string { return $this->value; } + + + public function toString(): string + { + $res = json_encode($this->value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($res === false) { + throw new Nette\Neon\Exception('Invalid UTF-8 sequence: ' . $this->value); + } + if (strpos($this->value, "\n") !== false) { + $res = preg_replace_callback('#[^\\\\]|\\\\(.)#s', function ($m) { + return ['n' => "\n\t", 't' => "\t", '"' => '"'][$m[1] ?? ''] ?? $m[0]; + }, $res); + $res = '"""' . "\n\t" . substr($res, 1, -1) . "\n" . '"""'; + } + return $res; + } } diff --git a/tests/Neon/Encoder.nodes.phpt b/tests/Neon/Encoder.nodes.phpt new file mode 100644 index 00000000..12aac219 --- /dev/null +++ b/tests/Neon/Encoder.nodes.phpt @@ -0,0 +1,35 @@ + ['a' => 'b', 'c' => 'd'], + 'index' => ['a', 'b', 'c'], + 'mixed' => ['a', 'b', 4 => 'c', 'd'], + 'entity' => new Entity('ent', ['a', 'b']), + 'chain' => new Entity(Neon\Neon::CHAIN, [ + new Entity('first', ['a', 'b']), + new Entity('second'), + ]), + 'multiline' => "hello\nworld", + 'date' => new DateTime('2016-06-03T19:00:00+02:00'), +]; + + +$encoder = new Neon\Encoder; +$node = $encoder->valueToNode($input); + +Assert::matchFile( + __DIR__ . '/fixtures/Encoder.nodes.txt', + preg_replace('~ #\d+~', '', Tracy\Dumper::toText($node)) +); diff --git a/tests/Neon/fixtures/Encoder.nodes.txt b/tests/Neon/fixtures/Encoder.nodes.txt new file mode 100644 index 00000000..7a42ea85 --- /dev/null +++ b/tests/Neon/fixtures/Encoder.nodes.txt @@ -0,0 +1,212 @@ +Nette\Neon\Node\ArrayNode + items: array (7) + | 0 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'map' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\ArrayNode + | | | items: array (2) + | | | | 0 => Nette\Neon\Node\ArrayItemNode + | | | | | key: Nette\Neon\Node\LiteralNode + | | | | | | value: 'a' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'b' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 1 => Nette\Neon\Node\ArrayItemNode + | | | | | key: Nette\Neon\Node\LiteralNode + | | | | | | value: 'c' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'd' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | indent: null + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 1 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'index' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\ArrayNode + | | | items: array (3) + | | | | 0 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'a' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 1 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'b' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 2 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'c' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | indent: null + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 2 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'mixed' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\ArrayNode + | | | items: array (4) + | | | | 0 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'a' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 1 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'b' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 2 => Nette\Neon\Node\ArrayItemNode + | | | | | key: Nette\Neon\Node\LiteralNode + | | | | | | value: 4 + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'c' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 3 => Nette\Neon\Node\ArrayItemNode + | | | | | key: Nette\Neon\Node\LiteralNode + | | | | | | value: 5 + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'd' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | indent: null + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 3 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'entity' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\EntityNode + | | | value: Nette\Neon\Node\LiteralNode + | | | | value: 'ent' + | | | | startPos: null + | | | | endPos: null + | | | attributes: array (2) + | | | | 0 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'a' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | | 1 => Nette\Neon\Node\ArrayItemNode + | | | | | key: null + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'b' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | startPos: null + | | | | | endPos: null + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 4 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'chain' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\EntityChainNode + | | | chain: array (2) + | | | | 0 => Nette\Neon\Node\EntityNode + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'first' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | attributes: array (2) + | | | | | | 0 => Nette\Neon\Node\ArrayItemNode ... + | | | | | | 1 => Nette\Neon\Node\ArrayItemNode ... + | | | | | startPos: null + | | | | | endPos: null + | | | | 1 => Nette\Neon\Node\EntityNode + | | | | | value: Nette\Neon\Node\LiteralNode + | | | | | | value: 'second' + | | | | | | startPos: null + | | | | | | endPos: null + | | | | | attributes: array (0) + | | | | | startPos: null + | | | | | endPos: null + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 5 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'multiline' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\StringNode + | | | value: string + | | | | 'hello\n + | | | | world' + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + | 6 => Nette\Neon\Node\ArrayItemNode + | | key: Nette\Neon\Node\LiteralNode + | | | value: 'date' + | | | startPos: null + | | | endPos: null + | | value: Nette\Neon\Node\LiteralNode + | | | value: DateTime + | | | | date: '2016-06-03 19:00:00.000000' + | | | | timezone_type: 1 + | | | | timezone: '+02:00' + | | | startPos: null + | | | endPos: null + | | startPos: null + | | endPos: null + indent: null + startPos: null + endPos: null