Skip to content

Commit

Permalink
Updater [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Nov 25, 2021
1 parent 34eda04 commit 51037e4
Show file tree
Hide file tree
Showing 5 changed files with 443 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
* @author Nikita Popov
*/
final class Differ
{
public const KEEP = 0;
public const REMOVE = 1;
public const 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 @@ -59,4 +59,11 @@ public static function decodeFile(string $file)
}
return self::decode($input);
}


public static function update(string $input, $newValue): string
{
$updater = new Updater($input);
return $updater->updateValue($newValue);
}
}
25 changes: 25 additions & 0 deletions src/Neon/TokenStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)) {
Expand Down
173 changes: 173 additions & 0 deletions src/Neon/Updater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?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
{
/** @var TokenStream */
private $tokens;

/** @var Node */
private $node;

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

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


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


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->tokens->getTokens() as $i => $token) {
$res .= $this->appends[$i] ?? '';
$res .= $this->replacements[$i] ?? $token->value;
}
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, $oldNode->startPos, $oldNode->endPos);
}


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

foreach ($steps as [$type, $oldItem, $newItem]) {
if ($type === $differ::REMOVE) {
$startPos = $this->tokens->skipLeft($oldItem->startPos, Token::WHITESPACE);
$endPos = $this->tokens->skipRight($oldItem->endPos, Token::WHITESPACE, Token::COMMENT);
$endPos = $this->tokens->skipRight($endPos, Token::NEWLINE);
$this->replaceWith('', $startPos, $endPos);

} elseif ($type === $differ::KEEP) {
$this->replaceNode($oldItem->value, $newItem->value, $indentation);
$newPos = $this->tokens->skipRight($oldItem->value->endPos, Token::WHITESPACE, Token::COMMENT);
$newPos = $this->tokens->skipRight($newPos, Token::NEWLINE); // jen jednou!
$newPos++;

} elseif ($type === $differ::ADD) {
$newStr = Node\ArrayItemNode::itemsToBlockString([$newItem], $indentation);
@$this->appends[$newPos] .= $indentation . $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 indent1(string $s, string $indentation = "\t"): string
{
return str_replace("\n", "\n" . $indentation, $s);
}
}
Loading

0 comments on commit 51037e4

Please sign in to comment.