Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.76% covered (warning)
75.76%
150 / 198
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateDataHtmlFormatter
75.76% covered (warning)
75.76%
150 / 198
50.00% covered (danger)
50.00%
2 / 4
35.63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHtml
81.01% covered (warning)
81.01%
64 / 79
0.00% covered (danger)
0.00%
0 / 1
12.99
 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\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    private MessageLocalizer $localizer;
18    private string $languageCode;
19
20    public function __construct( MessageLocalizer $localizer, string $languageCode = 'en' ) {
21        $this->localizer = $localizer;
22        $this->languageCode = $languageCode;
23    }
24
25    /**
26     * @param TemplateDataBlob $templateData
27     * @param Title $frameTitle
28     * @param bool $showEditLink
29     *
30     * @return string HTML
31     */
32    public function getHtml( TemplateDataBlob $templateData, Title $frameTitle, bool $showEditLink = true ): string {
33        $data = $templateData->getDataInLanguage( $this->languageCode );
34
35        $icon = null;
36        $formatMsg = null;
37        if ( isset( $data->format ) && is_string( $data->format ) ) {
38            $format = $data->format;
39            '@phan-var string $format';
40            if ( isset( TemplateDataValidator::PREDEFINED_FORMATS[$format] ) ) {
41                // The following icon names are used here:
42                // * template-format-block
43                // * template-format-inline
44                $icon = 'template-format-' . $format;
45                // Messages that can be used here:
46                // * templatedata-doc-format-block
47                // * templatedata-doc-format-inline
48                $formatMsg = $this->localizer->msg( 'templatedata-doc-format-' . $format );
49            }
50            if ( !$formatMsg || $formatMsg->isDisabled() ) {
51                $icon = 'settings';
52                $formatMsg = $this->localizer->msg( 'templatedata-doc-format-custom' );
53            }
54        }
55
56        $sorting = count( (array)$data->params ) > 1 ? ' sortable' : '';
57        $html = '<header>'
58            . Html::element( 'p',
59                [
60                    'class' => [
61                        'mw-templatedata-doc-desc',
62                        'mw-templatedata-doc-muted' => $data->description === null,
63                    ]
64                ],
65                $data->description ??
66                    $this->localizer->msg( 'templatedata-doc-desc-empty' )->text()
67            )
68            . '</header>'
69            . '<table class="wikitable mw-templatedata-doc-params' . $sorting . '">'
70            . Html::rawElement( 'caption', [],
71                Html::rawElement( 'p',
72                    [ 'class' => 'mw-templatedata-caption' ],
73                    $this->localizer->msg( 'templatedata-doc-params' )->escaped() .
74                    // Edit interface is only loaded in the template namespace (see Hooks::onEditPage)
75                    ( $showEditLink && $frameTitle->inNamespace( NS_TEMPLATE ) ?
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     * @param string &$text
136     */
137    public function replaceEditLink( string &$text ): void {
138        $localizer = $this->localizer;
139        $text = preg_replace_callback(
140            // Based on EDITSECTION_REGEX in ParserOutput
141            '#<mw:edittemplatedata page="(.*?)"></mw:edittemplatedata>#s',
142            static function ( array $m ) use ( $localizer ): string {
143                $editsectionPage = Title::newFromText( htmlspecialchars_decode( $m[1] ) );
144
145                if ( !is_object( $editsectionPage ) ) {
146                    LoggerFactory::getInstance( 'Parser' )
147                        ->error(
148                            'TemplateDataHtmlFormatter::replaceEditLink(): bad title in edittemplatedata placeholder',
149                            [
150                                'placeholder' => $m[0],
151                                'editsectionPage' => $m[1],
152                            ]
153                        );
154                    return '';
155                }
156
157                $result = Html::openElement( 'span', [ 'class' => 'mw-editsection-like' ] );
158                $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], '[' );
159
160                $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
161                $result .= $linkRenderer->makeKnownLink(
162                    $editsectionPage,
163                    $localizer->msg( 'templatedata-editbutton' )->text(),
164                    [],
165                    [
166                        'action' => 'edit',
167                        'templatedata' => 'edit',
168                    ]
169                );
170
171                $result .= Html::rawElement( 'span', [ 'class' => 'mw-editsection-bracket' ], ']' );
172                $result .= Html::closeElement( 'span' );
173
174                return $result;
175            },
176            $text
177        );
178    }
179
180    /**
181     * @param int|string $paramName
182     * @param stdClass $param
183     *
184     * @return string HTML
185     */
186    private function formatParameterTableRow( $paramName, stdClass $param ): string {
187        '@phan-var object $param';
188
189        $allParamNames = [ Html::element( 'code', [], $paramName ) ];
190        foreach ( $param->aliases as $alias ) {
191            $allParamNames[] = Html::element( 'code', [ 'class' => 'mw-templatedata-doc-param-alias' ],
192                $alias
193            );
194        }
195
196        $suggestedValues = [];
197        foreach ( $param->suggestedvalues as $suggestedValue ) {
198            $suggestedValues[] = Html::element( 'code', [], $suggestedValue );
199        }
200
201        if ( $param->deprecated ) {
202            $status = 'deprecated';
203        } elseif ( $param->required ) {
204            $status = 'required';
205        } elseif ( $param->suggested ) {
206            $status = 'suggested';
207        } else {
208            $status = 'optional';
209        }
210
211        return '<tr>'
212            // Label
213            . Html::element( 'th', [], $param->label ?? $paramName )
214            // Parameters and aliases
215            . Html::rawElement( 'td', [ 'class' => 'mw-templatedata-doc-param-name' ],
216                implode( ' ', $allParamNames )
217            )
218            // Description
219            . Html::rawElement( 'td', [],
220                Html::element( 'p',
221                    [
222                        'class' => $param->description ? null : 'mw-templatedata-doc-muted',
223                    ],
224                    $param->description ??
225                        $this->localizer->msg( 'templatedata-doc-param-desc-empty' )->text()
226                )
227                . Html::rawElement( 'dl', [],
228                    // Suggested Values
229                    ( $suggestedValues ? ( Html::element( 'dt', [],
230                        $this->localizer->msg( 'templatedata-doc-param-suggestedvalues' )->text()
231                    )
232                    . Html::rawElement( 'dd', [],
233                        implode( ' ', $suggestedValues )
234                    ) ) : '' ) .
235                    // Default
236                    ( $param->default !== null ? ( Html::element( 'dt', [],
237                        $this->localizer->msg( 'templatedata-doc-param-default' )->text()
238                    )
239                    . Html::element( 'dd', [],
240                        $param->default
241                    ) ) : '' )
242                    // Example
243                    . ( $param->example !== null ? ( Html::element( 'dt', [],
244                        $this->localizer->msg( 'templatedata-doc-param-example' )->text()
245                    )
246                    . Html::element( 'dd', [],
247                        $param->example
248                    ) ) : '' )
249                    // Auto value
250                    . ( $param->autovalue !== null ? ( Html::element( 'dt', [],
251                        $this->localizer->msg( 'templatedata-doc-param-autovalue' )->text()
252                    )
253                    . Html::rawElement( 'dd', [],
254                        Html::element( 'code', [], $param->autovalue )
255                    ) ) : '' )
256                )
257            )
258            // Type
259            . Html::element( 'td',
260                [
261                    'class' => [
262                        'mw-templatedata-doc-param-type',
263                        'mw-templatedata-doc-muted' => $param->type === 'unknown'
264                    ]
265                ],
266                // Known messages, for grepping:
267                // templatedata-doc-param-type-boolean, templatedata-doc-param-type-content,
268                // templatedata-doc-param-type-date, templatedata-doc-param-type-line,
269                // templatedata-doc-param-type-number, templatedata-doc-param-type-string,
270                // templatedata-doc-param-type-unbalanced-wikitext, templatedata-doc-param-type-unknown,
271                // templatedata-doc-param-type-url, templatedata-doc-param-type-wiki-file-name,
272                // templatedata-doc-param-type-wiki-page-name, templatedata-doc-param-type-wiki-template-name,
273                // templatedata-doc-param-type-wiki-user-name
274                $this->localizer->msg( 'templatedata-doc-param-type-' . $param->type )->text()
275            )
276            // Status
277            . Html::element( 'td',
278                [
279                    // CSS class names that can be used here:
280                    // mw-templatedata-doc-param-status-deprecated
281                    // mw-templatedata-doc-param-status-optional
282                    // mw-templatedata-doc-param-status-required
283                    // mw-templatedata-doc-param-status-suggested
284                    'class' => "mw-templatedata-doc-param-status-$status",
285                    'data-sort-value' => [
286                        'deprecated' => -1,
287                        'suggested' => 1,
288                        'required' => 2,
289                    ][$status] ?? 0,
290                ],
291                // Messages that can be used here:
292                // templatedata-doc-param-status-deprecated
293                // templatedata-doc-param-status-optional
294                // templatedata-doc-param-status-required
295                // templatedata-doc-param-status-suggested
296                $this->localizer->msg( "templatedata-doc-param-status-$status" )->text()
297            )
298            . '</tr>';
299    }
300
301}