Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.69% covered (success)
95.69%
222 / 232
80.00% covered (warning)
80.00%
12 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntitySchemaSlotViewRenderer
95.69% covered (success)
95.69%
222 / 232
80.00% covered (warning)
80.00%
12 / 15
29
0.00% covered (danger)
0.00%
0 / 1
 __construct
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
6.10
 msg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fillParserOutput
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 renderNameBadges
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
3
 renderNameBadgeHeader
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 renderNameBadge
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
1
 renderNameBadgeEditLink
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 renderSchemaSection
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 renderSchemaText
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
3.63
 renderSchemaTextLinks
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 renderSchemaCheckLink
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 makeExternalLink
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 renderSchemaAddTextLink
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 renderSchemaEditLink
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 renderHeadingToHtmlAndText
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare( strict_types = 1 );
4
5namespace EntitySchema\MediaWiki\Content;
6
7use EntitySchema\DataAccess\LabelLookup;
8use EntitySchema\MediaWiki\SpecificLanguageMessageLocalizer;
9use EntitySchema\Services\Converter\FullViewEntitySchemaData;
10use EntitySchema\Services\Converter\NameBadge;
11use MediaWiki\Config\Config;
12use MediaWiki\Html\Html;
13use MediaWiki\Language\LanguageCode;
14use MediaWiki\Linker\Linker;
15use MediaWiki\Linker\LinkRenderer;
16use MediaWiki\Linker\LinkTarget;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Message\Message;
19use MediaWiki\Page\PageReference;
20use MediaWiki\Parser\ParserOutput;
21use MediaWiki\Registration\ExtensionRegistry;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\SyntaxHighlight\SyntaxHighlight;
24use MediaWiki\Title\TitleFormatter;
25use MessageLocalizer;
26use Wikibase\Lib\LanguageFallbackIndicator;
27use Wikibase\Lib\LanguageNameLookupFactory;
28
29/**
30 * @license GPL-2.0-or-later
31 */
32class EntitySchemaSlotViewRenderer {
33
34    private MessageLocalizer $messageLocalizer;
35
36    private LinkRenderer $linkRenderer;
37
38    private Config $config;
39
40    private TitleFormatter $titleFormatter;
41
42    private ?SyntaxHighlight $syntaxHighlight;
43
44    private string $dir;
45
46    private string $currentLangCode;
47
48    private LabelLookup $labelLookup;
49
50    private LanguageNameLookupFactory $languageNameLookupFactory;
51
52    /**
53     * @param string $languageCode The language in which to render the view.
54     */
55    public function __construct(
56        string $languageCode,
57        LabelLookup $labelLookup,
58        LanguageNameLookupFactory $languageNameLookupFactory,
59        ?LinkRenderer $linkRenderer = null,
60        ?Config $config = null,
61        ?TitleFormatter $titleFormatter = null,
62        ?bool $useSyntaxHighlight = null
63    ) {
64        $this->messageLocalizer = new SpecificLanguageMessageLocalizer( $languageCode );
65        $this->linkRenderer = $linkRenderer ?: MediaWikiServices::getInstance()->getLinkRenderer();
66        $this->config = $config ?: MediaWikiServices::getInstance()->getMainConfig();
67        $this->titleFormatter = $titleFormatter ?: MediaWikiServices::getInstance()->getTitleFormatter();
68        if ( $useSyntaxHighlight === null ) {
69            $useSyntaxHighlight = ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' );
70        }
71        $this->syntaxHighlight = $useSyntaxHighlight ?
72            MediaWikiServices::getInstance()->getService( 'SyntaxHighlight.SyntaxHighlight' ) :
73            null;
74        $this->dir = MediaWikiServices::getInstance()->getLanguageFactory()
75            ->getLanguage( $languageCode )->getDir();
76        $this->currentLangCode = $languageCode;
77        $this->labelLookup = $labelLookup;
78        $this->languageNameLookupFactory = $languageNameLookupFactory;
79    }
80
81    private function msg( string $key ): Message {
82        return $this->messageLocalizer->msg( $key );
83    }
84
85    public function fillParserOutput(
86        FullViewEntitySchemaData $schemaData,
87        PageReference $page,
88        ParserOutput $parserOutput
89    ): void {
90        $parserOutput->addModules( [ 'ext.EntitySchema.action.view.trackclicks' ] );
91        $parserOutput->addModuleStyles( [ 'ext.EntitySchema.view' ] );
92        if ( $this->syntaxHighlight ) {
93            $parserOutput->addModuleStyles( [ 'ext.pygments' ] );
94        }
95        $parserOutput->setContentHolderText(
96            $this->renderNameBadges( $page, $schemaData->nameBadges ) .
97            $this->renderSchemaSection( $page, $schemaData->schemaText )
98        );
99        [ $headingHtml, $headingText ] = $this->renderHeadingToHtmlAndText( $schemaData, $page );
100        $parserOutput->setExtensionData( 'entityschema-meta-tags', [ 'title' => $headingText ] );
101        $parserOutput->setDisplayTitle( $headingHtml );
102    }
103
104    private function renderNameBadges( PageReference $page, array $nameBadges ): string {
105        $html = Html::openElement( 'table', [ 'class' => 'wikitable' ] );
106        $html .= $this->renderNameBadgeHeader();
107        $html .= Html::openElement( 'tbody' );
108        if ( !array_key_exists( $this->currentLangCode, $nameBadges ) ) {
109            $html .= "\n";
110            $html .= $this->renderNameBadge(
111                new NameBadge( '', '', [] ),
112                $this->currentLangCode,
113                $page->getDBkey()
114            );
115        }
116        foreach ( $nameBadges as $langCode => $nameBadge ) {
117            $html .= "\n";
118            $html .= $this->renderNameBadge(
119                $nameBadge,
120                $langCode,
121                $page->getDBkey()
122            );
123        }
124        $html .= Html::closeElement( 'tbody' );
125        $html .= Html::closeElement( 'table' );
126        return $html;
127    }
128
129    private function renderNameBadgeHeader(): string {
130        $tableHeaders = '';
131        // message keys:
132        // entityschema-namebadge-header-language-code
133        // entityschema-namebadge-header-label
134        // entityschema-namebadge-header-description
135        // entityschema-namebadge-header-aliases
136        // entityschema-namebadge-header-edit
137        foreach ( [ 'language-code', 'label', 'description', 'aliases', 'edit' ] as $key ) {
138            $tableHeaders .= Html::rawElement(
139                'th',
140                [],
141                $this->msg( 'entityschema-namebadge-header-' . $key )
142                    ->parse()
143            );
144        }
145
146        return Html::rawElement( 'thead', [], Html::rawElement(
147            'tr',
148            [],
149            $tableHeaders
150        ) );
151    }
152
153    private function renderNameBadge( NameBadge $nameBadge, string $languageCode, string $schemaId ): string {
154        $language = Html::element(
155            'td',
156            [],
157            $languageCode
158        );
159        $bcp47 = LanguageCode::bcp47( $languageCode ); // 'simple' => 'en-simple' etc.
160        $label = Html::element(
161            'td',
162            [
163                'class' => 'entityschema-label',
164                'lang' => $bcp47,
165                'dir' => 'auto',
166            ],
167            $nameBadge->label
168        );
169        $description = Html::element(
170            'td',
171            [
172                'class' => 'entityschema-description',
173                'lang' => $bcp47,
174                'dir' => 'auto',
175            ],
176            $nameBadge->description
177        );
178        $aliases = Html::element(
179            'td',
180            [
181                'class' => 'entityschema-aliases',
182                'lang' => $bcp47,
183                'dir' => 'auto',
184            ],
185            implode( ' | ', $nameBadge->aliases )
186        );
187        $editLink = $this->renderNameBadgeEditLink( $schemaId, $languageCode );
188        return Html::rawElement(
189            'tr',
190            [],
191            $language . $label . $description . $aliases . $editLink
192        );
193    }
194
195    private function renderNameBadgeEditLink( string $schemaId, string $langCode ): string {
196        $specialPageTitleValue = SpecialPage::getTitleValueFor(
197            'SetEntitySchemaLabelDescriptionAliases',
198            $schemaId . '/' . $langCode
199        );
200
201        return Html::rawElement(
202            'td',
203            [
204                'class' => 'entityschema-edit-button',
205            ],
206            $this->linkRenderer->makeKnownLink(
207                $specialPageTitleValue,
208                $this->msg( 'entityschema-edit' )->text(),
209                [ 'class' => 'edit-icon' ]
210            )
211        );
212    }
213
214    private function renderSchemaSection( PageReference $page, string $schemaText ): string {
215        $schemaSectionContent = $schemaText
216            ? $this->renderSchemaTextLinks( $page ) . $this->renderSchemaText( $schemaText )
217            : $this->renderSchemaAddTextLink( $page );
218        return Html::rawElement( 'div', [
219            'id' => 'entityschema-schema-view-section',
220            'class' => 'entityschema-section',
221            'dir' => 'ltr',
222        ],
223            $schemaSectionContent
224        );
225    }
226
227    private function renderSchemaText( string $schemaText ): string {
228        $attribs = [
229            'id' => 'entityschema-schema-text',
230            'class' => 'entityschema-schema-text',
231        ];
232
233        if ( $this->syntaxHighlight ) {
234            $highlighted = $this->syntaxHighlight->syntaxHighlight( $schemaText, 'shex' );
235
236            if ( $highlighted->isOK() ) {
237                return Html::rawElement(
238                    'div',
239                    $attribs,
240                    $highlighted->getValue()
241                );
242            }
243        }
244
245        return Html::element(
246            'pre',
247            $attribs,
248            $schemaText
249        );
250    }
251
252    private function renderSchemaTextLinks( PageReference $page ): string {
253        return Html::rawElement(
254            'div',
255            [
256                'class' => 'entityschema-schema-text-links',
257                'dir' => $this->dir,
258            ],
259            $this->renderSchemaCheckLink( $page ) .
260            $this->renderSchemaEditLink( $page )
261        );
262    }
263
264    private function renderSchemaCheckLink( PageReference $page ): string {
265        $url = $this->config->get( 'EntitySchemaShExSimpleUrl' );
266        if ( !$url ) {
267            return '';
268        }
269
270        $schemaTextTitle = SpecialPage::getTitleFor( 'EntitySchemaText', $page->getDBkey() );
271        $url = wfAppendQuery( $url, [
272            'schemaURL' => $schemaTextTitle->getFullURL(),
273        ] );
274
275        return $this->makeExternalLink(
276            $url,
277            $this->msg( 'entityschema-check-entities' )->parse(),
278            false, // link text already escaped in ->parse()
279            '',
280            [ 'class' => 'entityschema-check-schema' ]
281        );
282    }
283
284    /**
285     * Wrapper around {@see Linker::makeExternalLink} ensuring that the external link style
286     * is applied even though our whole output does not have class="mw-parser-output"
287     *
288     * @param string $url
289     * @param string $text
290     * @param bool $escape
291     * @param string $linktype
292     * @param array $attribs
293     * @param LinkTarget|null $title
294     * @return string
295     */
296    private function makeExternalLink(
297        $url,
298        $text,
299        $escape = true,
300        $linktype = '',
301        $attribs = [],
302        $title = null
303    ): string {
304        return Html::rawElement(
305            'span',
306            [ 'class' => 'mw-parser-output' ],
307            Linker::makeExternalLink( $url, $text, $escape, $linktype, $attribs, $title )
308        );
309    }
310
311    private function renderSchemaAddTextLink( PageReference $page ): string {
312        return Html::rawElement(
313            'span',
314            [
315                'id' => 'entityschema-edit-schema-text',
316                'class' => 'entityschema-edit-button',
317            ],
318            $this->linkRenderer->makeKnownLink(
319                $page,
320                $this->msg( 'entityschema-add-schema-text' )->text(),
321                [ 'class' => 'add-icon' ],
322                [ 'action' => 'edit' ]
323            )
324        );
325    }
326
327    private function renderSchemaEditLink( PageReference $page ): string {
328        return Html::rawElement(
329            'span',
330            [
331                'id' => 'entityschema-edit-schema-text',
332                'class' => 'entityschema-edit-button',
333            ],
334            $this->linkRenderer->makeKnownLink(
335                $page,
336                $this->msg( 'entityschema-edit' )->text(),
337                [ 'class' => 'edit-icon' ],
338                [ 'action' => 'edit' ]
339            )
340        );
341    }
342
343    private function renderHeadingToHtmlAndText( FullViewEntitySchemaData $schemaData, PageReference $page ): array {
344        $label = $this->labelLookup->getLabelForSchemaData( $schemaData, $this->currentLangCode );
345        if ( $label !== null ) {
346            $labelElement = Html::element(
347                'span', [
348                    'class' => 'entityschema-title-label',
349                    'lang' => $label->getActualLanguageCode(),
350                    'dir' => MediaWikiServices::getInstance()->getLanguageFactory()
351                        ->getLanguage( $label->getActualLanguageCode() )->getDir(),
352                ],
353                $label->getText()
354            );
355            $labelText = $label->getText();
356            $languageFallbackIndicator = new LanguageFallbackIndicator(
357                $this->languageNameLookupFactory->getForLanguageCode( $this->currentLangCode )
358            );
359            $languageFallbackIndicatorElement = $languageFallbackIndicator->getHtml( $label );
360        } else {
361            $labelText = $this->msg( 'entityschema-label-empty' )->text();
362            $labelElement = Html::element(
363                'span',
364                [ 'class' => 'entityschema-title-label-empty' ],
365                $labelText
366            );
367            $languageFallbackIndicatorElement = '';
368        }
369
370        $idText = $this->msg( 'parentheses' )
371            ->plaintextParams( $this->titleFormatter->getText( $page ) )
372            ->text();
373        $idElement = Html::element(
374            'span',
375            [ 'class' => 'entityschema-title-id' ],
376            $idText
377        );
378
379        $htmlTitle = Html::rawElement(
380            'span',
381            [ 'class' => 'entityschema-title' ],
382            $labelElement . $languageFallbackIndicatorElement . ' ' . $idElement
383        );
384        $textTitle = $labelText . ' ' . $idText;
385        return [ $htmlTitle, $textTitle ];
386    }
387
388}