Skip to content
This repository was archived by the owner on Oct 15, 2025. It is now read-only.

Commit 19987b2

Browse files
committed
feat(challenge): Add "diff presenter" back
1 parent f7bb767 commit 19987b2

File tree

4 files changed

+295
-1
lines changed

4 files changed

+295
-1
lines changed

src/Twig/Components/Challenge/Tabs.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class Tabs
2121
* @var string[]
2222
*/
2323
public array $tabs = [
24-
'result', 'answer', 'events',
24+
'result', 'answer', 'diff', 'events',
2525
];
2626

2727
/**
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Twig\Components\Challenge\Tabs;
6+
7+
use App\Entity\Question;
8+
use App\Entity\User;
9+
use App\Repository\SolutionEventRepository;
10+
use App\Service\QuestionDbRunnerService;
11+
use jblond\Diff;
12+
use jblond\Diff\Renderer\Html\SideBySide;
13+
use Psr\Log\LoggerInterface;
14+
use Symfony\Component\Serializer\SerializerInterface;
15+
use Symfony\Contracts\Translation\TranslatorInterface;
16+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
17+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
18+
use Symfony\UX\LiveComponent\DefaultActionTrait;
19+
use Symfony\UX\TwigComponent\Attribute\PostMount;
20+
21+
#[AsLiveComponent]
22+
final class DiffPresenter
23+
{
24+
use DefaultActionTrait;
25+
26+
public function __construct(
27+
private readonly QuestionDbRunnerService $questionDbRunnerService,
28+
private readonly SolutionEventRepository $solutionEventRepository,
29+
private readonly TranslatorInterface $translator,
30+
private readonly SerializerInterface $serializer,
31+
private readonly LoggerInterface $logger,
32+
) {
33+
}
34+
35+
#[LiveProp]
36+
public Question $question;
37+
38+
#[LiveProp]
39+
public User $user;
40+
41+
#[LiveProp(writable: true)]
42+
public ?string $query = null;
43+
44+
#[PostMount]
45+
public function postMount(): void
46+
{
47+
$this->query = $this->solutionEventRepository->getLatestQuery($this->question, $this->user)?->getQuery();
48+
}
49+
50+
public function getAnswerResult(): ?string
51+
{
52+
try {
53+
$resultDto = $this->questionDbRunnerService->getAnswerResult($this->question);
54+
55+
return $this->serializer->serialize($resultDto->getResult(), 'csv', [
56+
'csv_delimiter' => "\t",
57+
'csv_enclosure' => ' ',
58+
]);
59+
} catch (\Throwable $e) {
60+
$this->logger->debug('Failed to get the answer result', [
61+
'exception' => $e,
62+
]);
63+
64+
return null;
65+
}
66+
}
67+
68+
public function getUserResult(): ?string
69+
{
70+
if (null === $this->query) {
71+
return null;
72+
}
73+
74+
try {
75+
$resultDto = $this->questionDbRunnerService->getQueryResult($this->question, $this->query);
76+
77+
return $this->serializer->serialize($resultDto->getResult(), 'csv', [
78+
'csv_delimiter' => "\t",
79+
'csv_enclosure' => ' ',
80+
]);
81+
} catch (\Throwable $e) {
82+
$this->logger->debug('Failed to get the user result', [
83+
'exception' => $e,
84+
]);
85+
86+
return null;
87+
}
88+
}
89+
90+
/**
91+
* @return ?string The HTML string of the diff.
92+
* "" if the diff is empty.
93+
* Null if the diff cannot be calculated, for example, no results.
94+
*/
95+
public function getDiff(): ?string
96+
{
97+
$leftQueryResult = $this->getUserResult();
98+
$rightQueryResult = $this->getAnswerResult();
99+
100+
if (null === $leftQueryResult || null === $rightQueryResult) {
101+
return null;
102+
}
103+
104+
$diff = new Diff(explode("\n", $leftQueryResult), explode("\n", $rightQueryResult));
105+
$renderer = new SideBySide([
106+
'title1' => $this->translator->trans('diff.answer'),
107+
'title2' => $this->translator->trans('diff.yours'),
108+
]);
109+
110+
$result = $diff->render($renderer);
111+
if (null === $result || false === $result) {
112+
return '';
113+
}
114+
115+
\assert(\is_string($result));
116+
117+
return $result;
118+
}
119+
}

templates/components/Challenge/Tabs.html.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<twig:Challenge:Tabs:AnswerQueryResult :question="question" />
1818
{% elseif currentTab == 'events' %}
1919
<twig:Challenge:Tabs:Events :question="question" :user="user" />
20+
{% elseif currentTab == 'diff' %}
21+
<twig:Challenge:Tabs:DiffPresenter :question="question" :user="user" />
2022
{% else %}
2123
<twig:Challenge:Tabs:UserQueryResult :question="question" :user="user" loading="defer" />
2224
{% endif %}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<div{{ attributes }}>
2+
{% set diff = this.diff %}
3+
4+
{% if diff %}
5+
{{ diff|raw }}
6+
7+
<style>
8+
/*
9+
* HTML Renderers - General
10+
*/
11+
.Differences {
12+
border-collapse: collapse;
13+
border-spacing: 0;
14+
empty-cells: show;
15+
width: 100%;
16+
overflow: scroll;
17+
}
18+
19+
.Differences thead th {
20+
background: #AAAAAA;
21+
border-bottom: 1px solid #000000;
22+
color: #000000;
23+
padding: 4px;
24+
text-align: left;
25+
}
26+
27+
.Differences tbody th {
28+
background: #CCCCCC;
29+
border-right: 1px solid #000000;
30+
font-size: 13px;
31+
padding: 1px 2px;
32+
text-align: right;
33+
vertical-align: top;
34+
width: 4em;
35+
}
36+
37+
.Differences td {
38+
font-family: Consolas, monospace;
39+
font-size: 13px;
40+
padding: 1px 2px;
41+
vertical-align: top;
42+
}
43+
44+
.Differences .Skipped {
45+
background: #F7F7F7;
46+
display: block;
47+
}
48+
49+
/*
50+
* HTML Side by Side Diff
51+
*/
52+
.DifferencesSideBySide .ChangeInsert td.Left {
53+
background: #DDFFDD;
54+
}
55+
56+
.DifferencesSideBySide .ChangeInsert td.Right {
57+
background: #CCFFCC;
58+
}
59+
60+
.DifferencesSideBySide .ChangeDelete td.Left {
61+
background: #FF8888;
62+
}
63+
64+
.DifferencesSideBySide .ChangeDelete td.Right {
65+
background: #FFAAAA;
66+
}
67+
68+
.DifferencesSideBySide .ChangeReplace .Left {
69+
background: #FFEE99;
70+
}
71+
72+
.DifferencesSideBySide .ChangeReplace .Right {
73+
background: #FFDD88;
74+
}
75+
76+
.DifferencesSideBySide .ChangeIgnore .Left,
77+
.DifferencesSideBySide .ChangeIgnore .Right {
78+
background: #FBF2BF;
79+
}
80+
81+
.DifferencesSideBySide .ChangeIgnore .Left.Ignore {
82+
background: #F7F7F7;
83+
}
84+
85+
.DifferencesSideBySide .ChangeIgnore .Right.Ignore {
86+
background: #F7F7F7;
87+
}
88+
89+
.Differences ins,
90+
.Differences del {
91+
text-decoration: none;
92+
}
93+
94+
.DifferencesSideBySide .ChangeReplace ins,
95+
.DifferencesSideBySide .ChangeReplace del {
96+
background: #FFCC00;
97+
}
98+
99+
/*
100+
* HTML Unified Diff
101+
*/
102+
.DifferencesUnified .ChangeReplace .Left,
103+
.DifferencesUnified .ChangeDelete .Left {
104+
background: #FFDDDD;
105+
}
106+
107+
.DifferencesUnified .ChangeReplace .Right,
108+
.DifferencesUnified .ChangeInsert .Right {
109+
background: #DDFFDD;
110+
}
111+
112+
.DifferencesUnified .ChangeReplace ins {
113+
background: #99EE99;
114+
}
115+
116+
.DifferencesUnified .ChangeReplace del {
117+
background: #EE9999;
118+
}
119+
120+
.DifferencesUnified .ChangeIgnore .Left,
121+
.DifferencesUnified .ChangeIgnore .Right {
122+
background: #FBF2BF;
123+
}
124+
125+
.DifferencesUnified .ChangeIgnore .Left.Ignore {
126+
background: #F7F7F7;
127+
}
128+
129+
.DifferencesUnified .ChangeIgnore .Right.Ignore {
130+
background: #F7F7F7;
131+
}
132+
133+
/*
134+
* HTML Merged Diff
135+
*/
136+
.DifferencesMerged td.ChangeReplace {
137+
background: #FFDD88;
138+
}
139+
140+
.DifferencesMerged .ChangeDelete {
141+
background: #FFDDDD;
142+
}
143+
144+
.DifferencesMerged .ChangeInsert {
145+
background: #DDFFDD;
146+
}
147+
148+
.DifferencesMerged .ChangeReplace ins {
149+
background: #99EE99;
150+
}
151+
152+
.DifferencesMerged .ChangeReplace del {
153+
background: #EE9999;
154+
}
155+
156+
.DifferencesMerged th.ChangeDelete {
157+
background-image: linear-gradient(-45deg, #CCCCCC 0%, #EE9999 100%);
158+
}
159+
160+
.DifferencesMerged th.ChangeReplace {
161+
background-image: linear-gradient(-45deg, #CCCCCC 0%, #FFDD88 100%);
162+
}
163+
</style>
164+
{% elseif this.diff is same as('') %}
165+
<div class="alert alert-success" role="alert">
166+
比較結果無差異。
167+
</div>
168+
{% else %}
169+
<div class="alert alert-warning" role="alert">
170+
沒有可以比較的項目。你可能是還沒有進行答案提交,或者是你提交的答案在語法上是錯的。
171+
</div>
172+
{% endif %}
173+
</div>

0 commit comments

Comments
 (0)