Skip to content

Commit 33d262a

Browse files
committed
Encoder uses AST (Node::toString() methods)
1 parent d2e3c5c commit 33d262a

10 files changed

+396
-57
lines changed

src/Neon/Encoder.php

+43-57
Original file line numberDiff line numberDiff line change
@@ -22,73 +22,59 @@ final class Encoder
2222
/**
2323
* Returns the NEON representation of a value.
2424
*/
25-
public function encode($var, int $flags = 0): string
25+
public function encode($val, int $flags = 0): string
2626
{
27-
if ($var instanceof \DateTimeInterface) {
28-
return $var->format('Y-m-d H:i:s O');
27+
$node = $this->valueToNode($val, (bool) ($flags & self::BLOCK));
28+
return $node->toString();
29+
}
2930

30-
} elseif ($var instanceof Entity) {
31-
if ($var->value === Neon::CHAIN) {
32-
return implode('', array_map([$this, 'encode'], $var->attributes));
33-
}
34-
return $this->encode($var->value) . '('
35-
. (is_array($var->attributes) ? substr($this->encode($var->attributes), 1, -1) : '') . ')';
36-
}
3731

38-
if (is_object($var)) {
39-
$obj = $var;
40-
$var = [];
41-
foreach ($obj as $k => $v) {
42-
$var[$k] = $v;
43-
}
44-
}
32+
public function valueToNode($val, bool $blockMode = false): Node
33+
{
34+
if ($val instanceof \DateTimeInterface) {
35+
return new Node\LiteralNode($val);
4536

46-
if (is_array($var)) {
47-
$isList = !$var || array_keys($var) === range(0, count($var) - 1);
48-
$s = '';
49-
if ($flags & self::BLOCK) {
50-
if (count($var) === 0) {
51-
return '[]';
52-
}
53-
foreach ($var as $k => $v) {
54-
$v = $this->encode($v, self::BLOCK);
55-
$s .= ($isList ? '-' : $this->encode($k) . ':')
56-
. (strpos($v, "\n") === false
57-
? ' ' . $v . "\n"
58-
: "\n" . preg_replace('#^(?=.)#m', "\t", $v) . (substr($v, -2, 1) === "\n" ? '' : "\n"));
59-
}
60-
return $s;
61-
62-
} else {
63-
foreach ($var as $k => $v) {
64-
$s .= ($isList ? '' : $this->encode($k) . ': ') . $this->encode($v) . ', ';
65-
}
66-
return ($isList ? '[' : '{') . substr($s, 0, -2) . ($isList ? ']' : '}');
37+
} elseif ($val instanceof Entity && $val->value === Neon::CHAIN) {
38+
$node = new Node\EntityChainNode;
39+
foreach ($val->attributes as $entity) {
40+
$node->chain[] = $this->valueToNode($entity, $blockMode);
6741
}
42+
return $node;
6843

69-
} elseif (is_string($var)) {
70-
if (!Lexer::requiresDelimiters($var)) {
71-
return $var;
72-
}
44+
} elseif ($val instanceof Entity) {
45+
return new Node\EntityNode(
46+
$this->valueToNode($val->value),
47+
$this->arrayToNodes((array) $val->attributes)
48+
);
7349

74-
$res = json_encode($var, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
75-
if ($res === false) {
76-
throw new Exception('Invalid UTF-8 sequence: ' . $var);
77-
}
78-
if (strpos($var, "\n") !== false) {
79-
$res = preg_replace_callback('#[^\\\\]|\\\\(.)#s', function ($m) {
80-
return ['n' => "\n\t", 't' => "\t", '"' => '"'][$m[1] ?? ''] ?? $m[0];
81-
}, $res);
82-
$res = '"""' . "\n\t" . substr($res, 1, -1) . "\n" . '"""';
83-
}
84-
return $res;
50+
} elseif (is_object($val) || is_array($val)) {
51+
$node = new Node\ArrayNode($blockMode ? '' : null);
52+
$node->items = $this->arrayToNodes($val, $blockMode);
53+
return $node;
8554

86-
} elseif (is_float($var)) {
87-
$var = json_encode($var);
88-
return strpos($var, '.') === false ? $var . '.0' : $var;
55+
} elseif (is_string($val) && Lexer::requiresDelimiters($val)) {
56+
return new Node\StringNode($val);
8957

9058
} else {
91-
return json_encode($var);
59+
return new Node\LiteralNode($val);
60+
}
61+
}
62+
63+
64+
private function arrayToNodes($val, bool $blockMode = false): array
65+
{
66+
$res = [];
67+
$counter = 0;
68+
$hide = true;
69+
foreach ($val as $k => $v) {
70+
$res[] = $item = new Node\ArrayItemNode;
71+
$item->key = $hide && $k === $counter ? null : self::valueToNode($k);
72+
$item->value = self::valueToNode($v, $blockMode);
73+
if ($hide && is_int($k)) {
74+
$hide = $k === $counter;
75+
$counter = max($k + 1, $counter);
76+
}
9277
}
78+
return $res;
9379
}
9480
}

src/Neon/Node.php

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ abstract class Node
2424
abstract public function toValue();
2525

2626

27+
abstract public function toString(): string;
28+
29+
2730
/** @return self[] */
2831
public function getSubNodes(): array
2932
{

src/Neon/Node/ArrayItemNode.php

+34
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,46 @@ public static function itemsToArray(array $items): array
4343
}
4444

4545

46+
/** @param self[] $items */
47+
public static function itemsToInlineString(array $items): string
48+
{
49+
$res = '';
50+
foreach ($items as $item) {
51+
$res .= ($res === '' ? '' : ', ')
52+
. ($item->key ? $item->key->toString() . ': ' : '')
53+
. $item->value->toString();
54+
}
55+
return $res;
56+
}
57+
58+
59+
/** @param self[] $items */
60+
public static function itemsToBlockString(array $items): string
61+
{
62+
$res = '';
63+
foreach ($items as $item) {
64+
$v = $item->value->toString();
65+
$res .= ($item->key ? $item->key->toString() . ':' : '-')
66+
. (strpos($v, "\n") === false
67+
? ' ' . $v . "\n"
68+
: "\n" . preg_replace('#^(?=.)#m', "\t", $v) . (substr($v, -2, 1) === "\n" ? '' : "\n"));
69+
}
70+
return $res;
71+
}
72+
73+
4674
public function toValue()
4775
{
4876
throw new \LogicException;
4977
}
5078

5179

80+
public function toString(): string
81+
{
82+
throw new \LogicException;
83+
}
84+
85+
5286
public function getSubNodes(): array
5387
{
5488
return $this->key ? [$this->key, $this->value] : [$this->value];

src/Neon/Node/ArrayNode.php

+16
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public function toValue(): array
3535
}
3636

3737

38+
public function toString(): string
39+
{
40+
if ($this->indent === null) {
41+
$isList = !array_filter($this->items, function ($item) { return $item->key; });
42+
$res = ArrayItemNode::itemsToInlineString($this->items);
43+
return ($isList ? '[' : '{') . $res . ($isList ? ']' : '}');
44+
45+
} elseif (count($this->items) === 0) {
46+
return '[]';
47+
48+
} else {
49+
return ArrayItemNode::itemsToBlockString($this->items);
50+
}
51+
}
52+
53+
3854
public function getSubNodes(): array
3955
{
4056
return $this->items;

src/Neon/Node/EntityChainNode.php

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ public function toValue(): Neon\Entity
3838
}
3939

4040

41+
public function toString(): string
42+
{
43+
return implode('', array_map(function ($entity) { return $entity->toString(); }, $this->chain));
44+
}
45+
46+
4147
public function getSubNodes(): array
4248
{
4349
return $this->chain;

src/Neon/Node/EntityNode.php

+9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public function toValue(): Entity
4141
}
4242

4343

44+
public function toString(): string
45+
{
46+
return $this->value->toString()
47+
. '('
48+
. ($this->attributes ? ArrayItemNode::itemsToInlineString($this->attributes) : '')
49+
. ')';
50+
}
51+
52+
4453
public function getSubNodes(): array
4554
{
4655
return array_merge([$this->value], $this->attributes);

src/Neon/Node/LiteralNode.php

+21
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,25 @@ public function toValue()
3030
{
3131
return $this->value;
3232
}
33+
34+
35+
public function toString(): string
36+
{
37+
if ($this->value instanceof \DateTimeInterface) {
38+
return $this->value->format('Y-m-d H:i:s O');
39+
40+
} elseif (is_string($this->value)) {
41+
return $this->value;
42+
43+
} elseif (is_float($this->value)) {
44+
$res = json_encode($this->value);
45+
return strpos($res, '.') === false ? $res . '.0' : $res;
46+
47+
} elseif (is_int($this->value) || is_bool($this->value) || $this->value === null) {
48+
return json_encode($this->value);
49+
50+
} else {
51+
throw new \LogicException;
52+
}
53+
}
3354
}

src/Neon/Node/StringNode.php

+17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace Nette\Neon\Node;
1111

12+
use Nette;
1213
use Nette\Neon\Node;
1314

1415

@@ -30,4 +31,20 @@ public function toValue(): string
3031
{
3132
return $this->value;
3233
}
34+
35+
36+
public function toString(): string
37+
{
38+
$res = json_encode($this->value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
39+
if ($res === false) {
40+
throw new Nette\Neon\Exception('Invalid UTF-8 sequence: ' . $this->value);
41+
}
42+
if (strpos($this->value, "\n") !== false) {
43+
$res = preg_replace_callback('#[^\\\\]|\\\\(.)#s', function ($m) {
44+
return ['n' => "\n\t", 't' => "\t", '"' => '"'][$m[1] ?? ''] ?? $m[0];
45+
}, $res);
46+
$res = '"""' . "\n\t" . substr($res, 1, -1) . "\n" . '"""';
47+
}
48+
return $res;
49+
}
3350
}

tests/Neon/Encoder.nodes.phpt

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/** @phpVersion 7.2 */
4+
5+
declare(strict_types=1);
6+
7+
use Nette\Neon;
8+
use Nette\Neon\Entity;
9+
use Tester\Assert;
10+
11+
12+
require __DIR__ . '/../bootstrap.php';
13+
14+
15+
$input = [
16+
'map' => ['a' => 'b', 'c' => 'd'],
17+
'index' => ['a', 'b', 'c'],
18+
'mixed' => ['a', 'b', 4 => 'c', 'd'],
19+
'entity' => new Entity('ent', ['a', 'b']),
20+
'chain' => new Entity(Neon\Neon::CHAIN, [
21+
new Entity('first', ['a', 'b']),
22+
new Entity('second'),
23+
]),
24+
'multiline' => "hello\nworld",
25+
'date' => new DateTime('2016-06-03T19:00:00+02:00'),
26+
];
27+
28+
29+
$encoder = new Neon\Encoder;
30+
$node = $encoder->valueToNode($input);
31+
32+
Assert::matchFile(
33+
__DIR__ . '/fixtures/Encoder.nodes.txt',
34+
preg_replace('~ #\d+~', '', Tracy\Dumper::toText($node))
35+
);

0 commit comments

Comments
 (0)