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