Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.85% covered (success)
98.85%
86 / 87
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySchemaSlotDiffRenderer
98.85% covered (success)
98.85%
86 / 87
91.67% covered (success)
91.67%
11 / 12
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getDiff
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 localizeDiff
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderSchemaDiffRows
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
5
 renderDiffOp
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 renderTextDiff
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 diffRow
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 diffContext
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 diffBlankLine
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 diffMarker
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 diffAddedLine
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 diffRemovedLine
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Content;
6
7use Diff\DiffOp\AtomicDiffOp;
8use Diff\DiffOp\Diff\Diff;
9use Diff\DiffOp\DiffOp;
10use Diff\DiffOp\DiffOpAdd;
11use Diff\DiffOp\DiffOpChange;
12use Diff\DiffOp\DiffOpRemove;
13use EntitySchema\Services\Converter\EntitySchemaConverter;
14use EntitySchema\Services\Diff\EntitySchemaDiffer;
15use MediaWiki\Content\Content;
16use MediaWiki\Context\IContextSource;
17use MediaWiki\Html\Html;
18use MessageLocalizer;
19use SlotDiffRenderer;
20use TextSlotDiffRenderer;
21
22/**
23 * @license GPL-2.0-or-later
24 */
25class EntitySchemaSlotDiffRenderer extends SlotDiffRenderer {
26
27    private EntitySchemaConverter $schemaConverter;
28
29    private EntitySchemaDiffer $schemaDiffer;
30
31    private TextSlotDiffRenderer $textSlotDiffRenderer;
32
33    private MessageLocalizer $msgLocalizer;
34
35    public function __construct(
36        IContextSource $context,
37        TextSlotDiffRenderer $textSlotDiffRenderer
38    ) {
39        $this->schemaDiffer = new EntitySchemaDiffer();
40        $this->schemaConverter = new EntitySchemaConverter();
41        $this->textSlotDiffRenderer = $textSlotDiffRenderer;
42        $this->msgLocalizer = $context;
43    }
44
45    /**
46     * @param EntitySchemaContent|null $oldContent
47     * @param EntitySchemaContent|null $newContent
48     *
49     * @return string
50     * @suppress PhanParamSignatureMismatch LSP violation
51     */
52    public function getDiff( ?Content $oldContent = null, ?Content $newContent = null ): string {
53        $this->normalizeContents( $oldContent, $newContent, EntitySchemaContent::class );
54
55        $diff = $this->schemaDiffer->diffSchemas(
56            $this->schemaConverter->getFullArraySchemaData( $oldContent->getText() ),
57            $this->schemaConverter->getFullArraySchemaData( $newContent->getText() )
58        );
59
60        return $this->renderSchemaDiffRows( $diff );
61    }
62
63    /** @inheritDoc */
64    public function localizeDiff( string $diff, array $options = [] ) {
65        return $this->textSlotDiffRenderer->localizeDiff( $diff, $options );
66    }
67
68    public function renderSchemaDiffRows( Diff $diff ): string {
69        // split $diff into labels/descriptions/aliases (renderDiffOp())
70        // and schema (renderTextDiff())
71        $nameBadgeDiffOps = [];
72        if ( isset( $diff['labels'] ) ) {
73            $nameBadgeDiffOps[
74                $this->msgLocalizer->msg( 'entityschema-diff-label' )->text()
75                ] = $diff['labels'];
76        }
77        if ( isset( $diff['descriptions'] ) ) {
78            $nameBadgeDiffOps[
79                $this->msgLocalizer->msg( 'entityschema-diff-description' )->text()
80            ] = $diff['descriptions'];
81        }
82        if ( isset( $diff['aliases'] ) ) {
83            $nameBadgeDiffOps[
84                $this->msgLocalizer->msg( 'entityschema-diff-aliases' )->text()
85            ] = $diff['aliases'];
86        }
87        $nameBadgeDiff = $this->renderDiffOp( [], new Diff( $nameBadgeDiffOps, true ) );
88
89        if ( isset( $diff['schemaText'] ) ) {
90            $schemaDiff = $this->renderTextDiff(
91                $this->msgLocalizer->msg( 'entityschema-diff-schema' )->text(),
92                $diff['schemaText']
93            );
94        } else {
95            $schemaDiff = '';
96        }
97
98        return $nameBadgeDiff . $schemaDiff;
99    }
100
101    private function renderDiffOp( array $keys, DiffOp $diffOp ): string {
102        if ( $diffOp instanceof Diff ) {
103            $output = '';
104            foreach ( $diffOp->getOperations() as $key => $op ) {
105                $moreKeys = $keys;
106                $moreKeys[] = $key;
107                $output .= $this->renderDiffOp( $moreKeys, $op );
108            }
109            return $output;
110        }
111
112        if ( $diffOp instanceof DiffOpRemove || $diffOp instanceof DiffOpChange ) {
113            $leftContext = $this->diffContext( implode( ' / ', $keys ) );
114            $leftTds = $this->diffRemovedLine( $diffOp->getOldValue() );
115        } else {
116            $leftContext = $this->diffContext( '' );
117            $leftTds = $this->diffBlankLine();
118        }
119
120        if ( $diffOp instanceof DiffOpAdd || $diffOp instanceof DiffOpChange ) {
121            $rightContext = $this->diffContext( implode( ' / ', $keys ) );
122            $rightTds = $this->diffAddedLine( $diffOp->getNewValue() );
123        } else {
124            $rightContext = $this->diffContext( '' );
125            $rightTds = $this->diffBlankLine();
126        }
127
128        $context = $this->diffRow( $leftContext . $rightContext );
129        $changes = $this->diffRow( $leftTds . $rightTds );
130
131        return $context . $changes;
132    }
133
134    /**
135     * @suppress PhanUndeclaredMethod
136     */
137    private function renderTextDiff( string $key, AtomicDiffOp $diffOp ): string {
138        if ( $diffOp instanceof DiffOpAdd || $diffOp instanceof DiffOpRemove ) {
139            return $this->renderDiffOp( [ $key ], $diffOp );
140        }
141
142        /** @var DiffOpChange $diffOp */
143        // Line 1 → schema / Line 1
144        return preg_replace(
145            '/<td[^>]* class="diff-lineno"[^>]*>/',
146            '$0' . htmlspecialchars( $key ) . ' / ',
147            $this->textSlotDiffRenderer->getTextDiff(
148                trim( $diffOp->getOldValue() ),
149                trim( $diffOp->getNewValue() )
150            )
151        );
152    }
153
154    private function diffRow( string $content ): string {
155        return Html::rawElement(
156            'tr',
157            [],
158            $content
159        );
160    }
161
162    private function diffContext( string $context ): string {
163        return Html::element(
164            'td',
165            [ 'colspan' => '2', 'class' => 'diff-lineno' ],
166            $context
167        );
168    }
169
170    private function diffBlankLine(): string {
171        return Html::element( 'td', [ 'colspan' => '2' ] );
172    }
173
174    private function diffMarker( string $marker ): string {
175        return Html::element(
176            'td',
177            [ 'class' => 'diff-marker', 'data-marker' => $marker ]
178        );
179    }
180
181    private function diffAddedLine( string $line ): string {
182        return $this->diffMarker( '+' ) . Html::element(
183            'td',
184            [ 'class' => 'diff-addedline' ],
185            $line
186        );
187    }
188
189    private function diffRemovedLine( string $line ): string {
190        return $this->diffMarker( '−' ) . Html::element(
191            'td',
192            [ 'class' => 'diff-deletedline' ],
193            $line
194        );
195    }
196
197}