Commit a895424b authored by Emma's avatar Emma

add diff functionality to wiki

parent 31e22300
Pipeline #18136422 failed with stage
in 4 minutes and 48 seconds
@import '../settings';
.diff-table {
border-collapse: collapse;
margin-bottom: 1rem;
width: 100%;
&__line-group {
border-bottom: solid 0.5rem @diff-table-line-group-border-color;
}
&__line-no {
background: @diff-table-line-no-background-color;
padding: 0.25rem 0.5rem;
text-align: right;
vertical-align: top;
width: 5%;
}
&__content {
padding: 0.25rem 0.5rem;
min-width: 4rem;
vertical-align: top;
width: 45%;
&--changed {
background: @diff-table-changed-background-color;
}
&--removed {
background: @diff-table-removed-background-color;
}
&--added {
background: @diff-table-added-background-color;
}
}
}
......@@ -9,6 +9,7 @@
@import 'components/alert';
@import 'components/button';
@import 'components/clear-notification-button';
@import 'components/diff-table';
@import 'components/form';
@import 'components/icon';
@import 'components/markdown-input';
......
......@@ -34,5 +34,11 @@
@table-header-separator-color: #666;
@table-alternate-row-background-color: #1a1a1a;
// diff table
@diff-table-added-background-color: #030;
@diff-table-removed-background-color: #300;
@diff-table-changed-background-color: #003;
@diff-table-line-no-background-color: lightslategray;
// misc
@markdown-preview-background-color: #422;
......@@ -80,3 +80,10 @@
@tab-active-background-color: #efefef;
@tab-active-border-color: #aaa;
@tab-active-color: unset;
// Diff tables
@diff-table-added-background-color: #dfd;
@diff-table-removed-background-color: #fdd;
@diff-table-changed-background-color: #ddf;
@diff-table-line-group-border-color: @page-background-color;
@diff-table-line-no-background-color: lightgrey;
......@@ -35,6 +35,7 @@
"ramsey/uuid": "^3.7",
"ramsey/uuid-doctrine": "^1.4",
"sabberworm/php-css-parser": "^8.1",
"sebastian/diff": "^3.0",
"sensio/framework-extra-bundle": "^5.1",
"symfony/asset": "^4.0",
"symfony/cache": "^4.0",
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "973023e25f8bb085634a8058b7ad8838",
"content-hash": "f52782d720dcf77a08ce73b67040e9a1",
"packages": [
{
"name": "composer/ca-bundle",
......@@ -2808,6 +2808,62 @@
],
"time": "2016-07-19T19:14:21+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8",
"reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
"symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"time": "2018-02-01T13:45:15+00:00"
},
{
"name": "sensio/framework-extra-bundle",
"version": "v5.1.6",
......
......@@ -44,6 +44,11 @@ wiki_history:
methods: [GET]
requirements: { path: "%wiki_page_regex%" }
wiki_diff:
controller: App\Controller\WikiController::diff
path: /wiki/_diff
methods: [GET]
wiki_all:
controller: App\Controller\WikiController::all
defaults: { page: 1 }
......
......@@ -8,7 +8,9 @@ use App\Form\Model\WikiData;
use App\Form\WikiType;
use App\Repository\WikiPageRepository;
use App\Repository\WikiRevisionRepository;
use App\Utils\Differ;
use Doctrine\ORM\EntityManager;
use Ramsey\Uuid\Uuid;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\HttpFoundation\Request;
......@@ -148,6 +150,32 @@ final class WikiController extends AbstractController {
]);
}
public function diff(Request $request, WikiRevisionRepository $repository) {
/* @var WikiRevision $from
* @var WikiRevision $to */
[$from, $to] = array_map(function ($q) use ($request, $repository) {
$id = $request->query->get($q);
if (!Uuid::isValid($id)) {
throw $this->createNotFoundException();
}
$revision = $repository->find($id);
if (!$revision) {
throw $this->createNotFoundException();
}
return $revision;
}, ['from', 'to']);
return $this->render('wiki/diff.html.twig', [
'diff' => Differ::diff($from->getBody(), $to->getBody()),
'from' => $from,
'to' => $to,
]);
}
/**
* @param WikiRevision $revision
*
......
<?php
namespace App\Utils;
use SebastianBergmann\Diff\Differ as BaseDiffer;
final class Differ {
// the version of sebastian/diff included in the phpunit's .phar doesn't
// define these constants in the Differ class, so we redefine them here for
// the unit tests
private const B_OLD = 0;
private const B_ADDED = 1;
private const B_REMOVED = 2;
private function __construct() {
}
/**
* Diff in a format that's easy to work with in templates, and contains only
* what we want (changed lines).
*
* @param string $from
* @param string $to
* @param int $context
*
* @return array[]
*/
public static function diff(string $from, string $to, int $context = 0): array {
$from = preg_split('/\R/', $from);
$to = preg_split('/\R/', $to);
$output = [];
$oldLineNo = 0;
$newLineNo = 0;
$diff = (new BaseDiffer())->diffToArray($from, $to);
for ($i = 0, $len = count($diff); $i < $len; $i++) {
switch ($diff[$i][1]) {
case self::B_OLD:
$oldLineNo++;
$newLineNo++;
break;
case self::B_ADDED:
if ($i > 0 && $diff[$i - 1][1] == self::B_REMOVED) {
$newLineNo++;
$oldLineNo++;
$includedIndexes[$i] = true;
$output[] = [
'type' => 'changed',
'oldLineNo' => $oldLineNo,
'newLineNo' => $newLineNo,
'old' => $diff[$i - 1][0],
'new' => $diff[$i][0],
];
} else {
$newLineNo++;
$includedIndexes[$i] = true;
$output[] = [
'type' => 'added',
'newLineNo' => $newLineNo,
'new' => $diff[$i][0],
];
}
break;
case self::B_REMOVED:
if ($i == $len - 1 || $diff[$i + 1][1] != self::B_ADDED) {
$oldLineNo++;
$includedIndexes[$i] = true;
$output[] = [
'type' => 'removed',
'oldLineNo' => $oldLineNo,
'old' => $diff[$i][0],
];
}
break;
default:
throw new \UnexpectedValueException(sprintf(
'Differ: Unknown operator (%s)',
var_export($diff[$i][1], true)
));
}
}
return $output;
}
}
{% extends 'base.html.twig' %}
{% block page_classes 'wiki-diff-page' %}
{% block title 'title.wiki_diff'|trans({
'from': from.id[0:8],
'to': to.id[0:8],
}) %}
{% block body %}
{% from _self import time_ago, time_since %}
<h1 class="page-heading">
{{ 'title.wiki_diff'|trans({
'%from%': '<a href="%s">%s</a>'|format(
path('wiki_revision', {id: from.id})|e,
from.id[0:8]
),
'%to%': '<a href="%s">%s</a>'|format(
path('wiki_revision', {id: to.id})|e,
to.id[0:8]
)
})|raw }}
</h1>
<table class="diff-table">
<thead>
<tr>
<th class="diff-table__header diff-table__header--from" colspan="2">
{% with { date: from.timestamp|localizeddate('long', 'short') } %}
<time datetime="{{ from.timestamp|date('c') }}" class="relative-time" title="{{ date }}">
{{- date -}}
</time>
{% endwith %}
</th>
<th class="diff-table__header diff-table__header--to" colspan="2">
{% with { date: to.timestamp|localizeddate('long', 'short') } %}
<time datetime="{{ to.timestamp|date('c') }}"
class="relative-time-diff"
data-compare-to="{{ from.timestamp|date('c') }}"
title="{{ date }}">
{{- date -}}
</time>
{% endwith %}
</th>
</tr>
</thead>
<tbody class="diff-table__line-group">
{% with { lastOldLineNo: 0, lastNewLineNo: 0 } %}
{% for entry in diff %}
{% if not loop.first and
(entry.oldLineNo ?? lastOldLineNo) != lastOldLineNo + 1 and
(entry.newLineNo ?? lastNewLineNo) != lastNewLineNo + 1 %}
</tbody><tbody class="diff-table__line-group">
{% endif %}
{% set lastOldLineNo = entry.oldLineNo ?? lastOldLineNo %}
{% set lastNewLineNo = entry.newLineNo ?? lastNewLineNo %}
<tr>
{% if entry.oldLineNo is defined %}
<th class="diff-table__line-no diff-table__line-no--{{ entry.type }}">{{ entry.oldLineNo }}</th>
<td class="diff-table__content diff-table__content--{{ entry.type }}"><code>{{ entry.old }}</code></td>
{% else %}
<td class="diff-table__col-spacer" colspan="2"></td>
{% endif %}
{% if entry.newLineNo is defined %}
<th class="diff-table__line-no diff-table__line-no--{{ entry.type }}">{{ entry.newLineNo }}</th>
<td class="diff-table__content diff-table__content--{{ entry.type }}"><code>{{ entry.new }}</code></td>
{% else %}
<td class="diff-table__col-spacer" colspan="2"></td>
{% endif %}
</tr>
{% endfor %}
{% endwith %}
</tbody>
</table>
{% endblock %}
......@@ -13,32 +13,47 @@
'%path%': '<a href="%s">%s</a>'|format(path('wiki', {path: page.path})|escape, page.path|escape)
})|raw }}</h1>
<table class="table">
<thead>
<tr>
<th>{{ 'wiki.id_label'|trans }}</th>
<th>{{ 'wiki.revision_time'|trans }}</th>
<th>{{ 'wiki.user'|trans }}</th>
</tr>
</thead>
<tbody>
{% for revision in revisions %}
<form action="{{ path('wiki_diff') }}">
<table class="table">
<thead>
<tr>
<td>
<a href="{{ path('wiki_revision', {id: revision.id}) }}">{{ revision.id.hex|slice(0, 8) }}</a>
</td>
<td>
{% with {date: revision.timestamp|localizeddate('long', 'short')} %}
<time datetime="{{ revision.timestamp|date('c') }}" class="relative-time" title="{{ date }}">
{{- date -}}
</time>
{% endwith %}
</td>
<td><a href="{{ path('user', {username: revision.user.username}) }}">{{ revision.user.username }}</a></td>
<th colspan="2">{{ 'label.compare'|trans }}</th>
<th>{{ 'wiki.id_label'|trans }}</th>
<th>{{ 'wiki.revision_time'|trans }}</th>
<th>{{ 'wiki.user'|trans }}</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for revision in revisions %}
<tr>
<td><input type="radio"
name="from"
value="{{ revision.id }}"
{{ loop.index == 2 ? 'checked' }}></td>
<td><input type="radio"
name="to"
value="{{ revision.id }}"
{{ loop.index == 1 ? 'checked' }}></td>
<td>
<a href="{{ path('wiki_revision', {id: revision.id}) }}">{{ revision.id.hex|slice(0, 8) }}</a>
</td>
<td>
{% with {date: revision.timestamp|localizeddate('long', 'short')} %}
<time datetime="{{ revision.timestamp|date('c') }}" class="relative-time" title="{{ date }}">
{{- date -}}
</time>
{% endwith %}
</td>
<td><a href="{{ path('user', {username: revision.user.username}) }}">{{ revision.user.username }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form__row form__button-row">
<button class="button">{{ 'action.compare'|trans }}</button>
</div>
</form>
{% include '_includes/pagination.html.twig' with {pager: revisions} only %}
{% endblock %}
<?php
namespace App\Tests\Utils;
use App\Utils\Differ;
use PHPUnit\Framework\TestCase;
class DifferTest extends TestCase {
/**
* @dataProvider provideDiffs
*/
public function testDiff(array $expected, string $from, string $to) {
$this->assertEquals($expected, Differ::diff($from, $to), '', 0.0, 10, true);
}
public function provideDiffs() {
yield [[], '', ''];
yield [
[
[
'type' => 'added',
'newLineNo' => 5,
'new' => 'e',
],
],
"a\nb\nc\nd",
"a\nb\nc\nd\ne",
];
yield [
[
[
'type' => 'changed',
'oldLineNo' => 2,
'newLineNo' => 2,
'old' => 'b',
'new' => 'e',
],
],
"a\nb\nc\nd",
"a\ne\nc\nd",
];
yield [
[
[
'type' => 'removed',
'oldLineNo' => 4,
'old' => 'd',
],
],
"a\nb\nc\nd",
"a\nb\nc",
];
yield [
[
[
'type' => 'removed',
'oldLineNo' => 3,
'old' => 'c',
],
[
'type' => 'added',
'newLineNo' => 5,
'new' => 'i',
],
[
'type' => 'added',
'newLineNo' => 6,
'new' => 'ii',
],
[
'type' => 'changed',
'oldLineNo' => 7,
'old' => 'g',
'newLineNo' => 8,
'new' => 'z',
],
],
"a\nb\nc\nd\ne\nf\ng\nh\ni\nj",
"a\nb\nd\ne\ni\nii\nf\nz\nh\ni\nj",
];
}
}
......@@ -24,6 +24,7 @@ action:
global_ban: Global ban
filter: Filter
create: Create
compare: Compare
add_moderator:
title: Add moderator to %forum%
......@@ -277,6 +278,7 @@ label:
registration_date: Registration date
posting: Posting
auto_fetch_submission_titles: Auto-fetch submission titles
compare: Compare
log:
comment_deletion: '%user% deleted comment by %author% in "%submission%"'
......@@ -449,6 +451,7 @@ title:
manage_forum_categories: Manage forum categories
create_forum_category: Create forum category
editing_forum_category: 'Editing forum category: %category%'
wiki_diff: Showing difference between revisions %from% and %to%
user:
submissions: Submissions
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment