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