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 Content;
8use Diff\DiffOp\AtomicDiffOp;
9use Diff\DiffOp\Diff\Diff;
10use Diff\DiffOp\DiffOp;
11use Diff\DiffOp\DiffOpAdd;
12use Diff\DiffOp\DiffOpChange;
13use Diff\DiffOp\DiffOpRemove;
14use EntitySchema\Services\Converter\EntitySchemaConverter;
15use EntitySchema\Services\Diff\EntitySchemaDiffer;
16use 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    public function localizeDiff( string $diff, array $options = [] ) {
64        return $this->textSlotDiffRenderer->localizeDiff( $diff, $options );
65    }
66
67    public function renderSchemaDiffRows( Diff $diff ): string {
68        // split $diff into labels/descriptions/aliases (renderDiffOp())
69        // and schema (renderTextDiff())
70        $nameBadgeDiffOps = [];
71        if ( isset( $diff['labels'] ) ) {
72            $nameBadgeDiffOps[
73                $this->msgLocalizer->msg( 'entityschema-diff-label' )->text()
74                ] = $diff['labels'];
75        }
76        if ( isset( $diff['descriptions'] ) ) {
77            $nameBadgeDiffOps[
78                $this->msgLocalizer->msg( 'entityschema-diff-description' )->text()
79            ] = $diff['descriptions'];
80        }
81        if ( isset( $diff['aliases'] ) ) {
82            $nameBadgeDiffOps[
83                $this->msgLocalizer->msg( 'entityschema-diff-aliases' )->text()
84            ] = $diff['aliases'];
85        }
86        $nameBadgeDiff = $this->renderDiffOp( [], new Diff( $nameBadgeDiffOps, true ) );
87
88        if ( isset( $diff['schemaText'] ) ) {
89            $schemaDiff = $this->renderTextDiff(
90                $this->msgLocalizer->msg( 'entityschema-diff-schema' )->text(),
91                $diff['schemaText']
92            );
93        } else {
94            $schemaDiff = '';
95        }
96
97        return $nameBadgeDiff . $schemaDiff;
98    }
99
100    private function renderDiffOp( array $keys, DiffOp $diffOp ): string {
101        if ( $diffOp instanceof Diff ) {
102            $output = '';
103            foreach ( $diffOp->getOperations() as $key => $op ) {
104                $moreKeys = $keys;
105                $moreKeys[] = $key;
106                $output .= $this->renderDiffOp( $moreKeys, $op );
107            }
108            return $output;
109        }
110
111        if ( $diffOp instanceof DiffOpRemove || $diffOp instanceof DiffOpChange ) {
112            $leftContext = $this->diffContext( implode( ' / ', $keys ) );
113            $leftTds = $this->diffRemovedLine( $diffOp->getOldValue() );
114        } else {
115            $leftContext = $this->diffContext( '' );
116            $leftTds = $this->diffBlankLine();
117        }
118
119        if ( $diffOp instanceof DiffOpAdd || $diffOp instanceof DiffOpChange ) {
120            $rightContext = $this->diffContext( implode( ' / ', $keys ) );
121            $rightTds = $this->diffAddedLine( $diffOp->getNewValue() );
122        } else {
123            $rightContext = $this->diffContext( '' );
124            $rightTds = $this->diffBlankLine();
125        }
126
127        $context = $this->diffRow( $leftContext . $rightContext );
128        $changes = $this->diffRow( $leftTds . $rightTds );
129
130        return $context . $changes;
131    }
132
133    /**
134     * @suppress PhanUndeclaredMethod
135     */
136    private function renderTextDiff( string $key, AtomicDiffOp $diffOp ): string {
137        if ( $diffOp instanceof DiffOpAdd || $diffOp instanceof DiffOpRemove ) {
138            return $this->renderDiffOp( [ $key ], $diffOp );
139        }
140
141        /** @var DiffOpChange $diffOp */
142        // Line 1 → schema / Line 1
143        return preg_replace(
144            '/<td[^>]* class="diff-lineno"[^>]*>/',
145            '$0' . htmlspecialchars( $key ) . ' / ',
146            $this->textSlotDiffRenderer->getTextDiff(
147                trim( $diffOp->getOldValue() ),
148                trim( $diffOp->getNewValue() )
149            )
150        );
151    }
152
153    private function diffRow( string $content ): string {
154        return Html::rawElement(
155            'tr',
156            [],
157            $content
158        );
159    }
160
161    private function diffContext( string $context ): string {
162        return Html::element(
163            'td',
164            [ 'colspan' => '2', 'class' => 'diff-lineno' ],
165            $context
166        );
167    }
168
169    private function diffBlankLine(): string {
170        return Html::element( 'td', [ 'colspan' => '2' ] );
171    }
172
173    private function diffMarker( string $marker ): string {
174        return Html::element(
175            'td',
176            [ 'class' => 'diff-marker', 'data-marker' => $marker ]
177        );
178    }
179
180    private function diffAddedLine( string $line ): string {
181        return $this->diffMarker( '+' ) . Html::element(
182            'td',
183            [ 'class' => 'diff-addedline' ],
184            $line
185        );
186    }
187
188    private function diffRemovedLine( string $line ): string {
189        return $this->diffMarker( '−' ) . Html::element(
190            'td',
191            [ 'class' => 'diff-deletedline' ],
192            $line
193        );
194    }
195
196}