Skip to content

Commit

Permalink
Updater [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Sep 27, 2023
1 parent b0dc312 commit 9024868
Show file tree
Hide file tree
Showing 4 changed files with 434 additions and 0 deletions.
121 changes: 121 additions & 0 deletions src/Neon/Differ.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Neon;


/**
* Implements the Myers diff algorithm.
*
* Myers, Eugene W. "An O (ND) difference algorithm and its variations."
* Algorithmica 1.1 (1986): 251-266.
*
* @internal
*/
final class Differ
{
public const
Keep = 0,
Remove = 1,
Add = 2;

private $isEqual;


public function __construct(callable $isEqual)
{
$this->isEqual = $isEqual;
}


/**
* Calculates diff from $old to $new.
* @template T
* @param T[] $old
* @param T[] $new
* @return array{self::Keep|self::Remove|self::Add, ?T, ?T}[]
*/
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[] = [self::Keep, $a[$x - 1], $b[$y - 1]];
$x--;
$y--;
}

if ($d === 0) {
break;
}

while ($x > $prevX) {
$result[] = [self::Remove, $a[$x - 1], null];
$x--;
}

while ($y > $prevY) {
$result[] = [self::Add, null, $b[$y - 1]];
$y--;
}
}
return array_reverse($result);
}
}
7 changes: 7 additions & 0 deletions src/Neon/Neon.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,11 @@ public static function decodeFile(string $file): mixed

return self::decode($input);
}


public static function update(string $input, $newValue): string
{
$updater = new Updater($input);
return $updater->updateValue($newValue);
}
}
189 changes: 189 additions & 0 deletions src/Neon/Updater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

declare(strict_types=1);

namespace Nette\Neon;

use Nette\Neon\Node\ArrayItemNode;
use Nette\Neon\Node\ArrayNode;
use Nette\Neon\Node\BlockArrayNode;


/*
normalization must take place and this notation must be removed:
- a:
b:
a:
- a
- b
*/

/** @internal */
class Updater
{
private TokenStream $stream;
private Node $node;

/** @var string[] */
private array $replacements;

/** @var string[] */
private array $appends;


public function __construct(string $input)
{
$this->stream = (new Lexer)->tokenize($input);
$this->node = (new Parser)->parse($this->stream);
}


public function getNodeClone(): Node
{
return (new Traverser)->traverse($this->node, function (Node $node) {
$dolly = clone $node;
$dolly->data['originalNode'] = $node;
return $dolly;
});
}


public function updateValue($newValue): string
{
$newNode = (new Encoder)->valueToNode($newValue);
$this->guessOriginalNodes($this->node, $newNode);
return $this->updateNode($newNode);
}


public function updateNode(Node $newNode): string
{
$this->replacements = $this->appends = [];
$this->replaceNode($this->node, $newNode);
$res = '';
foreach ($this->stream->tokens as $i => $token) {
$res .= $this->appends[$i] ?? '';
$res .= $this->replacements[$i] ?? $token->text;
}
return $res;
}


public function guessOriginalNodes(Node $oldNode, Node $newNode): void
{
if ($oldNode instanceof BlockArrayNode && $newNode instanceof ArrayNode) {
$newNode->data['originalNode'] = $oldNode;
$differ = new Differ(function (ArrayItemNode $old, ArrayItemNode $new) {
if ($old->key || $new->key) {
return ($old->key ? $old->key->toValue() : null) === ($new->key ? $new->key->toValue() : null);
} else {
return $old->value->toValue() === $new->value->toValue();
}
});
$steps = $differ->diff($oldNode->items, $newNode->items);
foreach ($steps as [$type, $oldItem, $newItem]) {
if ($type === $differ::Keep) { // keys are same
$newItem->data['originalNode'] = $oldItem;
// TODO: original for keys?
$this->guessOriginalNodes($oldItem->value, $newItem->value);
}
}
} elseif ($oldNode->toValue() === $newNode->toValue()) {
$newNode->data['originalNode'] = $oldNode;
}
}


private function replaceNode(Node $oldNode, Node &$newNode, string $indentation = null): void
{
// assumes that $oldNode->data['originalNode'] === $newNode
if ($oldNode->toValue() === $newNode->toValue()) {
return;

} elseif ($oldNode instanceof ArrayNode && $newNode instanceof ArrayNode) {
$tmp = $newNode->items;
$newNode = clone $oldNode;
$newNode->items = $tmp;
if ($oldNode instanceof BlockArrayNode) {
$this->replaceArrayItems($oldNode->items, $newNode->items, $indentation . $oldNode->indentation);
return;
}
}

$newStr = $newNode instanceof BlockArrayNode ? "\n" : '';
$newStr .= $newNode->toString();
$newStr = rtrim($newStr);
$newStr = self::indent1($newStr, $indentation . "\t");

$this->replaceWith($newStr, $this->stream->findIndex($oldNode->start), $this->stream->findIndex($oldNode->end));
}


/**
* @param ArrayItemNode[] $oldItems
* @param ArrayItemNode[] $newItems
*/
private function replaceArrayItems(array $oldItems, array $newItems, string $indentation): void
{
$differ = new Differ(fn(Node $old, Node $new) => $old === ($new->data['originalNode'] ?? null));
$steps = $differ->diff($oldItems, $newItems);
$newPos = $this->skipLeft($this->stream->findIndex($oldItems[0]->start), Token::Whitespace, $this->stream);

foreach ($steps as [$type, $oldItem, $newItem]) {
if ($type === $differ::Remove) {
$startPos = $this->skipLeft($this->stream->findIndex($oldItem->start), Token::Whitespace, $this->stream);
$endPos = $this->skipRight($this->stream->findIndex($oldItem->end), Token::Whitespace, $this->stream,
Token::Comment
);
$endPos = $this->skipRight($endPos, Token::Newline, $this->stream);
$this->replaceWith('', $startPos, $endPos);

} elseif ($type === $differ::Keep) {
$this->replaceNode($oldItem->value, $newItem->value, $indentation);
$newPos = $this->skipRight($this->stream->findIndex($oldItem->value->end), Token::Whitespace, $this->stream, Token::Comment);
$newPos = $this->skipRight($newPos, Token::Newline, $this->stream); // jen jednou!
$newPos++;

} elseif ($type === $differ::Add) {
$newStr = Node\ArrayItemNode::itemsToBlockString([$newItem], $indentation);
@$this->appends[$newPos] .= $indentation . $newStr;
}
}
}


private function replaceWith(string $new, int $start, int $end): void
{
for ($i = $start; $i <= $end; $i++) {
$this->replacements[$i] ??= '';
}
$this->replacements[$start] .= $new;
}


private static function indent1(string $s, string $indentation = "\t"): string
{
return str_replace("\n", "\n" . $indentation, $s);
}


public function skipRight(int $index, ...$types): int
{
while (in_array($this->stream->tokens[$index + 1]->type, $types, true)) {
$index++;
}
return $index;
}


public function skipLeft(int $index, ...$types): int
{
while (in_array($this->stream->tokens[$index - 1]->type ?? null, $types, true)) {
$index--;
}
return $index;
}
}
Loading

0 comments on commit 9024868

Please sign in to comment.