Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.00% covered (warning)
76.00%
152 / 200
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateDataHtmlFormatter
76.00% covered (warning)
76.00%
152 / 200
50.00% covered (danger)
50.00%
2 / 4
35.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getHtml
81.25% covered (warning)
81.25%
65 / 80
0.00% covered (danger)
0.00%
0 / 1
12.95
 replaceEditLink
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
6
 formatParameterTableRow
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2
3namespace MediaWiki\Extension\TemplateData;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Html\Html;
7use MediaWiki\Logger\LoggerFactory;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10use MessageLocalizer;
11use stdClass;
12
13/**
14 * @license GPL-2.0-or-later
15 */
16class TemplateDataHtmlFormatter {
17
18    private Config $config;
19    private MessageLocalizer $localizer;
20    private string $languageCode;
21
22    public function __construct( Config $config, MessageLocalizer $localizer, string $languageCode = 'en' ) {
23        $this->config = $config;
24        $this->localizer = $localizer;
25        $this->languageCode = $languageCode;
26    }
27
28    /**
29     * @param TemplateDataBlob $templateData
30     * @param Title $frameTitle
31     * @param bool $showEditLink
32     *
33     * @return string HTML
34     */
35    public function getHtml( TemplateDataBlob $templateData, Title $frameTitle, bool $showEditLink = true ): string {
36        $data = $templateData->getDataInLanguage( $this->languageCode );
37
38        $icon = null;
39        $formatMsg = null;
40        if ( isset( $data->format ) && is_string( $data->format ) ) {
41            $format = $data->format;
42            '@phan-var string $format';
43            if ( isset( TemplateDataValidator::PREDEFINED_FORMATS[$format] ) ) {
44                // The following icon names are used here:
45                // * template-format-block
46                // * template-format-inline
47                $icon = 'template-format-' . $format;
48                // Messages that can be used here:
49                // * templatedata-doc-format-block
50                // * templatedata-doc-format-inline
51                $formatMsg = $this->localizer->msg( 'templatedata-doc-format-' . $format );
52            }
53            if ( !$formatMsg || $formatMsg->isDisabled() ) {
54                $icon = 'settings';
55                $formatMsg = $this->localizer->msg( 'templatedata-doc-format-custom' );
56            }
57        }
58
59        $editorNamespaces = $this->config->get( 'TemplateDataEditorNamespaces' );
60        $sorting = count( (array)$data->params ) > 1 ? ' sortable' : '';
61        $html = '<header>'
62            . Html::element( 'p',
63                [
64                    'class' => [
65                        'mw-templatedata-doc-desc',
66                        'mw-templatedata-doc-muted' => $data->description === null,
67                    ]
68                ],
69                $data->description ??
70                    $this->localizer->msg( 'templatedata-doc-desc-empty' )->text()
71            )
72            . '</header>'
73            . '<table class="wikitable mw-templatedata-doc-params' . $sorting . '">'
74            . Html::rawElement( 'caption', [],
75                Html::rawElement( 'p',
76                    [ 'class' => 'mw-templatedata-caption' ],
77                    $this->localizer->msg( 'templatedata-doc-params' )->escaped() .
78                    // Edit interface is only loaded in the template namespace (see Hooks::onEditPage)
79                    ( $showEditLink && $frameTitle->inNamespaces( $editorNamespaces ) ?
80                        Html::element( 'mw:edittemplatedata', [
81                            'page' => $frameTitle->getPrefixedText()
82                        ] ) :
83                        ''
84                    )
85                )
86                . ( $formatMsg ?
87                    Html::rawElement( 'p', [],
88                        new \OOUI\IconWidget( [ 'icon' => $icon ] )
89                        . Html::element(
90                            'span',
91                            [ 'class' => 'mw-templatedata-format' ],
92                            $formatMsg->text()
93                        )
94                    ) :
95                    ''
96                )
97            )
98            . '<thead><tr>'
99            . Html::element( 'th', [ 'colspan' => 2 ],
100                $this->localizer->msg( 'templatedata-doc-param-name' )->text()
101            )
102            . Html::element( 'th', [],
103                $this->localizer->msg( 'templatedata-doc-param-desc' )->text()
104            )
105            . Html::element( 'th', [],
106                $this->localizer->msg( 'templatedata-doc-param-type' )->text()
107            )
108            . Html::element( 'th', [],
109                $this->localizer->msg( 'templatedata-doc-param-status' )->text()
110            )
111            . '</tr></thead>'
112            . '<tbody>';
113
114        $paramNames = $data->paramOrder ?? array_keys( (array)$data->params );
115        if ( !$paramNames ) {
116            // Display no parameters message
117            $html .= '<tr>'
118            . Html::element( 'td',
119                [
120                    'class' => 'mw-templatedata-doc-muted',
121                    'colspan' => 7
122                ],
123                $this->localizer->msg( 'templatedata-doc-no-params-set' )->text()
124            )
125            . '</tr>';
126        }
127
128        foreach ( $paramNames as $paramName ) {
129            $html .= $this->formatParameterTableRow( $paramName, $data->params->$paramName );
130        }
131        $html .= '</tbody></table>';
132
133        return Html::rawElement( 'section', [ 'class' => 'mw-templatedata-doc-wrap' ], $html );
134    }
135
136    /**
137     * Replace <mw:edittemplatedata> markers with links
138     *
139     * @param string &$text
140     */
141    public function replaceEditLink( string &$text ): void {
142        $localizer = $this->localizer;
143        $text = preg_replace_callback(
144            // Based on EDITSECTION_REGEX in ParserOutput
145            '#<mw:edittemplatedata page="(.*?)"></mw:edittemplatedata>#s',
146            static function ( array $m ) use ( $localizer ): string {
147                $editsectionPage = Title::newFromText( htmlspecialchars_decode( $m[1] ) );
148
149                if ( !is_object( $editsectionPage ) ) {
150                    LoggerFactory::getInstance( 'Parser' )
151                        ->error(
152                            'TemplateDataHtmlFormatter::replaceEditLink(): bad title in edittemplatedata placeholder',
153                            [
154                                'placeholder' => $m[0],
155                                'editsectionPage' => $m[1],
156                            ]
157                        );
158                    return '';
159                }
160
161                $result = Html::openElement( 'span', [ 'class' => 'mw-editsection-like' ] );
162                $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], '[' );
163
164                $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
165                $result .= $linkRenderer->makeKnownLink(
166                    $editsectionPage,
167                    $localizer->msg( 'templatedata-editbutton' )->text(),
168                    [],
169                    [
170                        'action' => 'edit',
171                        'templatedata' => 'edit',
172                    ]
173                );
174
175                $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], ']' );
176                $result .= Html::closeElement( 'span' );
177
178                return $result;
179            },
180            $text
181        );
182    }
183
184    /**
185     * @param int|string $paramName
186     * @param stdClass $param
187     *
188     * @return string HTML
189     */
190    private function formatParameterTableRow( $paramName, stdClass $param ): string {
191        '@phan-var object $param';
192
193        $allParamNames = [ Html::element( 'code', [], $paramName ) ];
194        foreach ( $param->aliases as $alias ) {
195            $allParamNames[] = Html::element( 'code', [ 'class' => 'mw-templatedata-doc-param-alias' ],
196                $alias
197            );
198        }
199
200        $suggestedValues = [];
201        foreach ( $param->suggestedvalues as $suggestedValue ) {
202            $suggestedValues[] = Html::element( 'code', [], $suggestedValue );
203        }
204
205        if ( $param->deprecated ) {
206            $status = 'deprecated';
207        } elseif ( $param->required ) {
208            $status = 'required';
209        } elseif ( $param->suggested ) {
210            $status = 'suggested';
211        } else {
212            $status = 'optional';
213        }
214
215        return '<tr>'
216            // Label
217            . Html::element( 'th', [], $param->label ?? $paramName )
218            // Parameters and aliases
219            . Html::rawElement( 'td', [ 'class' => 'mw-templatedata-doc-param-name' ],
220                implode( ' ', $allParamNames )
221            )
222            // Description
223            . Html::rawElement( 'td', [],
224                Html::element( 'p',
225                    [
226                        'class' => $param->description ? null : 'mw-templatedata-doc-muted',
227                    ],
228                    $param->description ??
229                        $this->localizer->msg( 'templatedata-doc-param-desc-empty' )->text()
230                )
231                . Html::rawElement( 'dl', [],
232                    // Suggested Values
233                    ( $suggestedValues ? ( Html::element( 'dt', [],
234                        $this->localizer->msg( 'templatedata-doc-param-suggestedvalues' )->text()
235                    )
236                    . Html::rawElement( 'dd', [],
237                        implode( ' ', $suggestedValues )
238                    ) ) : '' ) .
239                    // Default
240                    ( $param->default !== null ? ( Html::element( 'dt', [],
241                        $this->localizer->msg( 'templatedata-doc-param-default' )->text()
242                    )
243                    . Html::element( 'dd', [],
244                        $param->default
245                    ) ) : '' )
246                    // Example
247                    . ( $param->example !== null ? ( Html::element( 'dt', [],
248                        $this->localizer->msg( 'templatedata-doc-param-example' )->text()
249                    )
250                    . Html::element( 'dd', [],
251                        $param->example
252                    ) ) : '' )
253                    // Auto value
254                    . ( $param->autovalue !== null ? ( Html::element( 'dt', [],
255                        $this->localizer->msg( 'templatedata-doc-param-autovalue' )->text()
256                    )
257                    . Html::rawElement( 'dd', [],
258                        Html::element( 'code', [], $param->autovalue )
259                    ) ) : '' )
260                )
261            )
262            // Type
263            . Html::element( 'td',
264                [
265                    'class' => [
266                        'mw-templatedata-doc-param-type',
267                        'mw-templatedata-doc-muted' => $param->type === 'unknown'
268                    ]
269                ],
270                // Known messages, for grepping:
271                // templatedata-doc-param-type-boolean, templatedata-doc-param-type-content,
272                // templatedata-doc-param-type-date, templatedata-doc-param-type-line,
273                // templatedata-doc-param-type-number, templatedata-doc-param-type-string,
274                // templatedata-doc-param-type-unbalanced-wikitext, templatedata-doc-param-type-unknown,
275                // templatedata-doc-param-type-url, templatedata-doc-param-type-wiki-file-name,
276                // templatedata-doc-param-type-wiki-page-name, templatedata-doc-param-type-wiki-template-name,
277                // templatedata-doc-param-type-wiki-user-name
278                $this->localizer->msg( 'templatedata-doc-param-type-' . $param->type )->text()
279            )
280            // Status
281            . Html::element( 'td',
282                [
283                    // CSS class names that can be used here:
284                    // mw-templatedata-doc-param-status-deprecated
285                    // mw-templatedata-doc-param-status-optional
286                    // mw-templatedata-doc-param-status-required
287                    // mw-templatedata-doc-param-status-suggested
288                    'class' => "mw-templatedata-doc-param-status-$status",
289                    'data-sort-value' => [
290                        'deprecated' => -1,
291                        'suggested' => 1,
292                        'required' => 2,
293                    ][$status] ?? 0,
294                ],
295                // Messages that can be used here:
296                // templatedata-doc-param-status-deprecated
297                // templatedata-doc-param-status-optional
298                // templatedata-doc-param-status-required
299                // templatedata-doc-param-status-suggested
300                $this->localizer->msg( "templatedata-doc-param-status-$status" )->text()
301            )
302            . '</tr>';
303    }
304
305}