Skip to content

Commit

Permalink
Merge [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Oct 19, 2021
1 parent 510666b commit 446ab52
Show file tree
Hide file tree
Showing 5 changed files with 406 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/Neon/DiffElem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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;


/** @internal */
final class DiffElem
{
public const TYPE_KEEP = 0;
public const TYPE_REMOVE = 1;
public const TYPE_ADD = 2;

/** @var int One of the TYPE_* constants */
public $type;

/** @var ?Node\ArrayItemNode Is null for add operations */
public $old;

/** @var ?Node\ArrayItemNode Is null for remove operations */
public $new;


public function __construct(int $type, ?Node\ArrayItemNode $old, ?Node\ArrayItemNode $new)
{
$this->type = $type;
$this->old = $old;
$this->new = $new;
}
}
114 changes: 114 additions & 0 deletions src/Neon/Differ.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?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
{
private $isEqual;


public function __construct(callable $isEqual)
{
$this->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);
}
}
144 changes: 144 additions & 0 deletions src/Neon/Merge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

namespace Nette\Neon;

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


/** @internal */
class Merge
{
/** @var TokenStream */
private $tokens;

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

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

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


public function __construct(string $input)
{
$input = str_replace("\r", '', $input);
$lexer = new Lexer;
$parser = new Parser;
$this->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);
}
}
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
Loading

0 comments on commit 446ab52

Please sign in to comment.