diff --git a/src/Neon/DiffElem.php b/src/Neon/DiffElem.php new file mode 100644 index 00000000..9c49852b --- /dev/null +++ b/src/Neon/DiffElem.php @@ -0,0 +1,36 @@ +type = $type; + $this->old = $old; + $this->new = $new; + } +} diff --git a/src/Neon/Differ.php b/src/Neon/Differ.php new file mode 100644 index 00000000..fe028ec7 --- /dev/null +++ b/src/Neon/Differ.php @@ -0,0 +1,114 @@ +isEqual = $isEqual; + } + + + /** + * Calculates diff from $old to $new. + * @return DiffElem[] + */ + public function diff(array $old, array $new): array + { + [$trace, $x, $y] = $this->calculateTrace($old, $new); + return $this->extractDiff($trace, $x, $y, $old, $new); + } + + + private function calculateTrace(array $a, array $b): array + { + $n = \count($a); + $m = \count($b); + $max = $n + $m; + $v = [1 => 0]; + $trace = []; + for ($d = 0; $d <= $max; $d++) { + $trace[] = $v; + for ($k = -$d; $k <= $d; $k += 2) { + if ($k === -$d || ($k !== $d && $v[$k-1] < $v[$k+1])) { + $x = $v[$k+1]; + } else { + $x = $v[$k-1] + 1; + } + + $y = $x - $k; + while ($x < $n && $y < $m && ($this->isEqual)($a[$x], $b[$y])) { + $x++; + $y++; + } + + $v[$k] = $x; + if ($x >= $n && $y >= $m) { + return [$trace, $x, $y]; + } + } + } + throw new \Exception('Should not happen'); + } + + + private function extractDiff(array $trace, int $x, int $y, array $a, array $b): array + { + $result = []; + for ($d = \count($trace) - 1; $d >= 0; $d--) { + $v = $trace[$d]; + $k = $x - $y; + + if ($k === -$d || ($k !== $d && $v[$k-1] < $v[$k+1])) { + $prevK = $k + 1; + } else { + $prevK = $k - 1; + } + + $prevX = $v[$prevK]; + $prevY = $prevX - $prevK; + + while ($x > $prevX && $y > $prevY) { + $result[] = new DiffElem(DiffElem::TYPE_KEEP, $a[$x-1], $b[$y-1]); + $x--; + $y--; + } + + if ($d === 0) { + break; + } + + while ($x > $prevX) { + $result[] = new DiffElem(DiffElem::TYPE_REMOVE, $a[$x-1], null); + $x--; + } + + while ($y > $prevY) { + $result[] = new DiffElem(DiffElem::TYPE_ADD, null, $b[$y-1]); + $y--; + } + } + return array_reverse($result); + } +} diff --git a/src/Neon/Merge.php b/src/Neon/Merge.php new file mode 100644 index 00000000..0de933a2 --- /dev/null +++ b/src/Neon/Merge.php @@ -0,0 +1,144 @@ +tokens = $lexer->tokenize($input); + $this->node = $parser->parse($this->tokens); + } + + + public function merge($val): string + { + /* + normalization must take place and this notation must be removed: + + - a: + b: + + a: + - a + - b + */ + + $this->replacements = $this->appends = []; + $encoder = new Encoder; + $newNode = $encoder->valueToNode($val); + $this->replaceNode($this->node, $newNode); + $res = ''; + foreach ($this->tokens->getTokens() as $i => $token) { + $res .= $this->appends[$i] ?? ''; + $res .= $this->replacements[$i] ?? $token->value; + } + return $res; + } + + + private function replaceNode(Node $oldNode, Node $newNode, bool $blockMode = false): void + { + if ($oldNode->toValue() === $newNode->toValue()) { + return; + + } elseif ($oldNode instanceof ArrayNode && $newNode instanceof ArrayNode) { + $newNode->indent = $oldNode->indent; + if ($oldNode->indent !== null) { + $this->replaceArrayItems($oldNode->items, $newNode->items, $oldNode->indent !== null); + return; + } + } + + $newStr = $newNode instanceof ArrayNode && $newNode->indent !== null ? "\n" : ''; + $newStr .= $newNode->toString(); + $newStr = rtrim($newStr); + $indent = $this->tokens->setPos($oldNode->startPos)->getIndentation(); + $newStr = self::indent1($newStr, $indent . "\t"); + + $this->replaceWith($newStr, $oldNode->startPos, $oldNode->endPos); + } + + + /** + * @param ArrayItemNode[] $oldItems + * @param ArrayItemNode[] $newItems + */ + private function replaceArrayItems(array $oldItems, array $newItems): void + { + $differ = new Differ(function (ArrayItemNode $a, ArrayItemNode $b) { + if ($a->key || $b->key) { + return ($a->key ? $a->key->toValue() : null) === ($b->key ? $b->key->toValue() : null); + } else { + return $a->value->toValue() === $b->value->toValue(); + } + }); + $diff = $differ->diff($oldItems, $newItems); + $newPos = $this->tokens->skipLeft($oldItems[0]->startPos, Token::WHITESPACE); + $indent = $this->tokens->setPos($newPos)->getIndentation(); + + foreach ($diff as $elem) { + if ($elem->type === $elem::TYPE_REMOVE) { + $startPos = $this->tokens->skipLeft($elem->old->startPos, Token::WHITESPACE); + $endPos = $this->tokens->skipRight($elem->old->endPos, Token::WHITESPACE, Token::COMMENT); + $endPos = $this->tokens->skipRight($endPos, Token::NEWLINE); + $this->replaceWith('', $startPos, $endPos); + + } elseif ($elem->type === $elem::TYPE_KEEP) { // keys are same + $this->replaceNode($elem->old->value, $elem->new->value); + $newPos = $this->tokens->skipRight($elem->old->value->endPos, Token::WHITESPACE, Token::COMMENT); + $newPos = $this->tokens->skipRight($newPos, Token::NEWLINE); // jen jednou! + $newPos++; + + } elseif ($elem->type === $elem::TYPE_ADD) { + $newStr = Node\ArrayItemNode::itemsToBlockString([$elem->new]); + $newStr = self::indent($newStr, $indent); + @$this->appends[$newPos] .= $newStr; + } + } + } + + + private function replaceWith(string $new, int $startPos, int $endPos): void + { + for ($i = $startPos; $i <= $endPos; $i++) { + $this->replacements[$i] = $this->replacements[$i] ?? ''; + } + $this->replacements[$startPos] .= $new; + } + + + private static function indent(string $s, string $indentation = "\t"): string + { + return preg_replace('#^(?=.)#m', $indentation, $s); + } + + + private static function indent1(string $s, string $indentation = "\t"): string + { + return str_replace("\n", "\n" . $indentation, $s); + } +} diff --git a/src/Neon/TokenStream.php b/src/Neon/TokenStream.php index b30d046c..95e8e309 100644 --- a/src/Neon/TokenStream.php +++ b/src/Neon/TokenStream.php @@ -26,6 +26,13 @@ public function __construct(array $tokens) } + public function setPos(int $pos): self + { + $this->pos = $pos; + return $this; + } + + public function getPos(): int { return $this->pos; @@ -39,6 +46,24 @@ public function getTokens(): array } + public function skipLeft(int $pos, ...$types): int + { + while (in_array($this->tokens[$pos - 1]->type ?? null, $types, true)) { + $pos--; + } + return $pos; + } + + + public function skipRight(int $pos, ...$types): int + { + while (in_array($this->tokens[$pos + 1]->type ?? null, $types, true)) { + $pos++; + } + return $pos; + } + + public function isNext(...$types): bool { while (in_array($this->tokens[$this->pos]->type ?? null, [Token::COMMENT, Token::WHITESPACE], true)) { diff --git a/tests/Neon/Merge.phpt b/tests/Neon/Merge.phpt new file mode 100644 index 00000000..046afa75 --- /dev/null +++ b/tests/Neon/Merge.phpt @@ -0,0 +1,87 @@ +merge($b2) ); + +$b2 = Nette\Neon\Neon::decode($b);