Add compare function between snipsContents
This commit is contained in:
parent
28a2706525
commit
cc3e050304
41
src/Controller/SnipContentController.php
Normal file
41
src/Controller/SnipContentController.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\SnipContent;
|
||||||
|
use App\Security\Voter\SnipVoter;
|
||||||
|
use App\Service\SnipContent\MyersDiff;
|
||||||
|
use App\Service\SnipContent\SnipContentService;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/content', name: 'content')]
|
||||||
|
class SnipContentController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SnipContentService $contentService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/compare/{to}/{from}', name: '_compare')]
|
||||||
|
public function compare(SnipContent $to, ?SnipContent $from = null): Response
|
||||||
|
{
|
||||||
|
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $to->getSnip());
|
||||||
|
|
||||||
|
if ($from === null) {
|
||||||
|
$from = $to->getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = MyersDiff::buildDiffLines(
|
||||||
|
$this->contentService->rebuildText($from),
|
||||||
|
$this->contentService->rebuildText($to),
|
||||||
|
);
|
||||||
|
|
||||||
|
dump($diff);
|
||||||
|
|
||||||
|
return $this->render('content/compare.html.twig', [
|
||||||
|
'snip' => $to->getSnip(),
|
||||||
|
'diff' => $diff,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
15
src/Service/SnipContent/DiffTypeEnum.php
Normal file
15
src/Service/SnipContent/DiffTypeEnum.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\SnipContent;
|
||||||
|
|
||||||
|
enum DiffTypeEnum: string
|
||||||
|
{
|
||||||
|
case INSERT = 'I';
|
||||||
|
case DELETE = 'D';
|
||||||
|
case KEEP = 'K';
|
||||||
|
|
||||||
|
public function is(string $diffType): bool
|
||||||
|
{
|
||||||
|
return $this->value === $diffType;
|
||||||
|
}
|
||||||
|
}
|
@ -52,7 +52,7 @@ class MyersDiff
|
|||||||
$x++;
|
$x++;
|
||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
$solution[] = ['D', $count];
|
$solution[] = [DiffTypeEnum::DELETE->value, $count];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insertions
|
// Insertions
|
||||||
@ -63,10 +63,10 @@ class MyersDiff
|
|||||||
$y++;
|
$y++;
|
||||||
}
|
}
|
||||||
$solutionKey = count($solution) - 1;
|
$solutionKey = count($solution) - 1;
|
||||||
if ($solutionKey >= 0 && $solution[$solutionKey][0] === 'I') {
|
if ($solutionKey >= 0 && DiffTypeEnum::INSERT->is($solution[$solutionKey][0])) {
|
||||||
$solution[$solutionKey][1] = array_merge($solution[$solutionKey][1], $values);
|
$solution[$solutionKey][1] = array_merge($solution[$solutionKey][1], $values);
|
||||||
} else {
|
} else {
|
||||||
$solution[] = ['I', $values];
|
$solution[] = [DiffTypeEnum::INSERT->value, $values];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ class MyersDiff
|
|||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
if ($count > 0) {
|
if ($count > 0) {
|
||||||
$solution[] = ['K', $count];
|
$solution[] = [DiffTypeEnum::KEEP->value, $count];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,16 +88,24 @@ class MyersDiff
|
|||||||
/**
|
/**
|
||||||
* Calculate the shortest edit sequence to convert $x into $y.
|
* Calculate the shortest edit sequence to convert $x into $y.
|
||||||
*
|
*
|
||||||
* @param string $textFrom - tokens (characters, words or lines)
|
* @param string|array $textFrom - tokens (characters, words or lines)
|
||||||
* @param string $textTo - tokens (characters, words or lines)
|
* @param string|array $textTo - tokens (characters, words or lines)
|
||||||
* @param ?callable $compare - comparison function for tokens. Signature is compare($x, $y):bool. If null, === is used.
|
* @param ?callable $compare - comparison function for tokens. Signature is compare($x, $y):bool. If null, === is used.
|
||||||
*
|
*
|
||||||
* @return array[] - pairs of token and edit (-1 for delete, 0 for keep, +1 for insert)
|
* @return array[] - pairs of token and edit (-1 for delete, 0 for keep, +1 for insert)
|
||||||
*/
|
*/
|
||||||
public static function calculate(string $textFrom, string $textTo, ?callable $compare = null): array
|
public static function calculate(string|array $textFrom, string|array $textTo, ?callable $compare = null): array
|
||||||
{
|
{
|
||||||
$a = self::explode($textFrom);
|
if (is_string($textFrom)) {
|
||||||
$b = self::explode($textTo);
|
$a = self::explode($textFrom);
|
||||||
|
} else {
|
||||||
|
$a = $textFrom;
|
||||||
|
}
|
||||||
|
if (is_string($textTo)) {
|
||||||
|
$b = self::explode($textTo);
|
||||||
|
} else {
|
||||||
|
$b = $textTo;
|
||||||
|
}
|
||||||
|
|
||||||
if ($compare === null) {
|
if ($compare === null) {
|
||||||
$compare = function ($x, $y) {
|
$compare = function ($x, $y) {
|
||||||
@ -144,22 +152,75 @@ class MyersDiff
|
|||||||
$x = 0;
|
$x = 0;
|
||||||
|
|
||||||
foreach ($diff as [$op, $data]) {
|
foreach ($diff as [$op, $data]) {
|
||||||
if ($op === 'K') {
|
switch ($op) {
|
||||||
for ($i = 0; $i < $data; $i++) {
|
case DiffTypeEnum::KEEP->value:
|
||||||
$b[] = $a[$x++];
|
for ($i = 0; $i < $data; $i++) {
|
||||||
}
|
$b[] = $a[$x++];
|
||||||
} elseif ($op === 'D') {
|
}
|
||||||
$x += $data; // skip deleted
|
break;
|
||||||
} elseif ($op === 'I') {
|
case DiffTypeEnum::DELETE->value:
|
||||||
foreach ($data as $v) {
|
$x += $data; // skip deleted
|
||||||
$b[] = $v;
|
break;
|
||||||
}
|
case DiffTypeEnum::INSERT->value:
|
||||||
|
foreach ($data as $v) {
|
||||||
|
$b[] = $v;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('Invalid diff operation');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::implode($b);
|
return self::implode($b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function buildDiffLines(string $textFrom, string $textTo): array
|
||||||
|
{
|
||||||
|
$a = self::explode($textFrom);
|
||||||
|
$b = self::explode($textTo);
|
||||||
|
$diff = MyersDiff::calculate($a, $b);
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
$x = 0;
|
||||||
|
foreach ($diff as [$op, $data]) {
|
||||||
|
switch ($op) {
|
||||||
|
case DiffTypeEnum::KEEP->value:
|
||||||
|
for ($i = 0; $i < $data; $i++) {
|
||||||
|
$lines[] = [
|
||||||
|
'type' => 'keep',
|
||||||
|
'from' => $a[$x],
|
||||||
|
'to' => $a[$x],
|
||||||
|
];
|
||||||
|
$x++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DiffTypeEnum::DELETE->value:
|
||||||
|
for ($i = 0; $i < $data; $i++) {
|
||||||
|
$lines[] = [
|
||||||
|
'type' => 'delete',
|
||||||
|
'from' => $a[$x],
|
||||||
|
'to' => '',
|
||||||
|
];
|
||||||
|
$x++;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DiffTypeEnum::INSERT->value:
|
||||||
|
foreach ($data as $v) {
|
||||||
|
$lines[] = [
|
||||||
|
'type' => 'insert',
|
||||||
|
'from' => '',
|
||||||
|
'to' => $v,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \InvalidArgumentException('Invalid diff operation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
private static function explode(string $text): array
|
private static function explode(string $text): array
|
||||||
{
|
{
|
||||||
return explode(self::NEWLINE, $text);
|
return explode(self::NEWLINE, $text);
|
||||||
|
@ -41,8 +41,7 @@ readonly class SnipContentService
|
|||||||
|
|
||||||
public function getActiveText(Snip $snip): string
|
public function getActiveText(Snip $snip): string
|
||||||
{
|
{
|
||||||
$contentRepo = $this->em->getRepository(SnipContent::class);
|
return $this->rebuildText($snip->getActiveVersion());
|
||||||
return $this->rebuildText($contentRepo->find($snip->getActiveVersion()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rebuildText(?SnipContent $snipContent): string
|
public function rebuildText(?SnipContent $snipContent): string
|
||||||
|
38
templates/content/compare.html.twig
Normal file
38
templates/content/compare.html.twig
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% extends 'base/single.column.html.twig' %}
|
||||||
|
|
||||||
|
{% set title = 'Snip compare ' ~ snip %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<a href="{{ path('snip_single', {snip: snip.id}) }}" class="btn btn-primary">
|
||||||
|
<i class="fa fa-arrow-left"></i> Back
|
||||||
|
</a>
|
||||||
|
<br><br>
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Line</th>
|
||||||
|
<th>Old</th>
|
||||||
|
<th>New</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for line in diff %}
|
||||||
|
<tr>
|
||||||
|
<td class="table-{{ line.type == 'insert' ? 'success' : (line.type == 'delete' ? 'danger' : 'info') }}">
|
||||||
|
{{ line.type }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if line.from %}
|
||||||
|
{{ line.from }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if line.to %}
|
||||||
|
{{ line.to }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
@ -26,6 +26,9 @@
|
|||||||
<a href="{{ path('snip_raw', {snip: snip.id}) }}" class="btn btn-danger">
|
<a href="{{ path('snip_raw', {snip: snip.id}) }}" class="btn btn-danger">
|
||||||
<i class="fa fa-eye"></i> Raw
|
<i class="fa fa-eye"></i> Raw
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ path('content_compare', {to: snip.activeVersion.id}) }}" class="btn btn-warning">
|
||||||
|
<i class="fa fa-eye"></i> Compare
|
||||||
|
</a>
|
||||||
<br><br>
|
<br><br>
|
||||||
<div class="card" style="width: 100%;">
|
<div class="card" style="width: 100%;">
|
||||||
<h4 class="card-header">
|
<h4 class="card-header">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user