Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 407
0.00% covered (danger)
0.00%
0 / 91
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserFunctions
0.00% covered (danger)
0.00%
0 / 407
0.00% covered (danger)
0.00%
0 / 91
30102
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rejoinKV
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
56
 expandV
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expandKV
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
90
 prefixedTitleText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_if
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 trimRes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 noTrimRes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 switchLookupFallback
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
306
 pf_switch
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 pf_ifeq
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 ifeq_worker
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pf_expr
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 pf_ifexpr
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 pf_iferror
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 pf_lc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_uc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_ucfirst
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 pf_lcfirst
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 pf_padleft
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 pf_padright
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 pf_tag
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 tag_worker
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 pf_currentyear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localyear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentmonth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localmonth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentmonthname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localmonthname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentmonthabbrev
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localmonthabbrev
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentweek
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localweek
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentday
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localday
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentday2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localday2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentdow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localdow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentdayname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localdayname
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currenttime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localtime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currenthour
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localhour
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currenttimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localtimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentmonthnamegen
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_localmonthnamegen
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_time
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_timel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pfTime_tokens
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pfTimel_tokens
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pfTime
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 pf_localurl
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 pf_formatnum
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_currentpage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_pagenamee
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_fullpagename
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pf_fullpagenamee
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pf_pagelanguage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 pf_directionmark
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pf_dirmark
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_fullurl
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 pf_urlencode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_ifexist
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_pagesize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_sitename
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 encodeCharEntity
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 pf_anchorencode
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 pf_protectionlevel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_ns
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 pf_subjectspace
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_talkspace
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_numberofarticles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_language
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_contentlanguage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 pf_contentlang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pf_numberoffiles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_namespace
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 pf_namespacee
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 pf_namespacenumber
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 pf_pagename
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_pagenamebase
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_scriptpath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 pf_server
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 pf_servername
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_talkpagename
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 pf_defaultsort
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 pf_displaytitle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 pf_equal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic
4// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
5// phpcs:disable MediaWiki.Commenting.FunctionComment.WrongStyle
6// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPrivate
7
8namespace Wikimedia\Parsoid\Wt2Html\TT;
9
10use DateTime;
11use DateTimeZone;
12use Wikimedia\Parsoid\Config\Env;
13use Wikimedia\Parsoid\Core\Sanitizer;
14use Wikimedia\Parsoid\NodeData\DataParsoid;
15use Wikimedia\Parsoid\Tokens\EndTagTk;
16use Wikimedia\Parsoid\Tokens\KV;
17use Wikimedia\Parsoid\Tokens\SelfclosingTagTk;
18use Wikimedia\Parsoid\Tokens\TagTk;
19use Wikimedia\Parsoid\Utils\PHPUtils;
20use Wikimedia\Parsoid\Utils\TokenUtils;
21use Wikimedia\Parsoid\Utils\Utils;
22use Wikimedia\Parsoid\Wt2Html\Frame;
23use Wikimedia\Parsoid\Wt2Html\Params;
24
25/**
26 * Some parser functions, and quite a bunch of stubs of parser functions.
27 *
28 * IMPORTANT NOTE: These parser functions are only used by the Parsoid-native
29 * template expansion pipeline, which is *not* the default or used in
30 * production. Normally core provides us SiteConfig and DataAccess objects
31 * that provide parser functions and other preprocessor functionality.
32 *
33 * There are still quite a few missing, see
34 * {@link http://www.mediawiki.org/wiki/Help:Magic_words} and
35 * {@link http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions}.
36 * Instantiated and called by the {@link TemplateHandler} extension.
37 * Any `pf_<prefix>`
38 * matching a lower-cased template name prefix up to the first colon will
39 * override that template.
40 *
41 * The only use of this code is currently in parserTests and offline tests.
42 * But, eventually as the two parsers are integrated, the core parser tests
43 * implementation from $mw/includes/parser/CoreParserFunctions.php might
44 * move over here.
45 */
46class ParserFunctions {
47    /** @var Env */
48    private $env;
49
50    public function __construct( Env $env ) {
51        $this->env = $env;
52    }
53
54    // Temporary helper.
55    private function rejoinKV( bool $trim, $k, $v ) {
56        if ( is_string( $k ) && strlen( $k ) > 0 ) {
57            return array_merge( [ $k, '=' ], $v );
58        } elseif ( is_array( $k ) && count( $k ) > 0 ) {
59            $k[] = '=';
60            return array_merge( $k, $v );
61        } else {
62            return $trim ? ( is_string( $v ) ? trim( $v ) : TokenUtils::tokenTrim( $v ) ) : $v;
63        }
64    }
65
66    private function expandV( $v, Frame $frame ) {
67        // FIXME: This hasn't been implemented on the JS side
68        return $v;
69    }
70
71    // XXX: move to frame?
72    private function expandKV(
73        $kv, Frame $frame, $defaultValue = null, ?string $type = null, bool $trim = false
74    ): array {
75        if ( $type === null ) {
76            $type = 'expanded-tokens-to-fragment';
77        }
78
79        if ( $kv === null ) {
80            return [ $defaultValue ?: '' ];
81        } elseif ( is_string( $kv ) ) {
82            return [ $kv ];
83        } elseif ( is_string( $kv->k ) && is_string( $kv->v ) ) {
84            if ( $kv->k ) {
85                return [ $kv->k . '=' . $kv->v ];
86            } else {
87                return [ $trim ? trim( $kv->v ) : $kv->v ];
88            }
89        } else {
90            $v = $this->expandV( $kv->v, $frame );
91            return $this->rejoinKV( $trim, $kv->k, $v );
92        }
93    }
94
95    private function prefixedTitleText(): string {
96        return $this->env->getContextTitle()->getPrefixedText();
97    }
98
99    public function pf_if( $token, Frame $frame, Params $params ): array {
100        $args = $params->args;
101        if ( trim( $args[0]->k ) !== '' ) {
102            return $this->expandKV( $args[1] ?? null, $frame );
103        } else {
104            return $this->expandKV( $args[2] ?? null, $frame );
105        }
106    }
107
108    private function trimRes( $res ) {
109        if ( is_string( $res ) ) {
110            return [ trim( $res ) ];
111        } elseif ( is_array( $res ) ) {
112            return TokenUtils::tokenTrim( $res );
113        } else {
114            return $res;
115        }
116    }
117
118    private function noTrimRes( $res ): array {
119        if ( is_string( $res ) ) {
120            return [ $res ];
121        } elseif ( is_array( $res ) ) {
122            return $res;
123        } else {
124            $this->env->log( 'error', 'Unprocessable res in ParserFunctions:noTrimRes', $res );
125            return [];
126        }
127    }
128
129    private function switchLookupFallback(
130        Frame $frame, array $kvs, string $key, array $dict, $v = null
131    ): array {
132        $kv = null;
133        $l = count( $kvs );
134        $this->env->log( 'debug', 'switchLookupFallback', $key, $v );
135        // 'v' need not be a string in cases where it is the last fall-through case
136        $vStr = $v ? TokenUtils::tokensToString( $v ) : null;
137        if ( $vStr && $key === trim( $vStr ) ) {
138            // This handles fall-through switch cases:
139            //
140            //   {{#switch:<key>
141            //     | c1 | c2 | c3 = <res>
142            //     ...
143            //   }}
144            //
145            // So if <key> matched c1, we want to return <res>.
146            // Hence, we are looking for the next entry with a non-empty key.
147            $this->env->log( 'debug', 'switch found' );
148            foreach ( $kvs as $kv ) {
149                // XXX: make sure the key is always one of these!
150                if ( count( $kv->k ) > 0 ) {
151                    return $this->trimRes( $this->expandV( $kv->v, $frame ) );
152                }
153            }
154
155            // No value found, return empty string? XXX: check this
156            return [];
157        } elseif ( count( $kvs ) > 0 ) {
158            // search for value-only entry which matches
159            $i = 0;
160            if ( $v ) {
161                $i = 1;
162            }
163            for ( ;  $i < $l;  $i++ ) {
164                $kv = $kvs[$i];
165                if ( count( $kv->k ) || !count( $kv->v ) ) {
166                    // skip entries with keys or empty values
167                    continue;
168                } else {
169                    // We found a value-only entry.  However, we have to verify
170                    // if we have any fall-through cases that this matches.
171                    //
172                    //   {{#switch:<key>
173                    //     | c1 | c2 | c3 = <res>
174                    //     ...
175                    //   }}
176                    //
177                    // In the switch example above, if we found 'c1', that is
178                    // not the fallback value -- we have to check for fall-through
179                    // cases.  Hence the recursive callback to switchLookupFallback.
180                    //
181                    //   {{#switch:<key>
182                    //     | c1 = <..>
183                    //     | c2 = <..>
184                    //     | [[Foo]]</div>
185                    //   }}
186                    //
187                    // 'val' may be an array of tokens rather than a string as in the
188                    // example above where 'val' is indeed the final return value.
189                    // Hence 'expanded-tokens-to-fragment' type below.
190                    $v = $this->expandV( $kv->v, $frame );
191                    return $this->switchLookupFallback( $frame, array_slice( $kvs, $i + 1 ), $key, $dict, $v );
192                }
193            }
194
195            // value not found!
196            if ( isset( $dict['#default'] ) ) {
197                return $this->trimRes( $this->expandV( $dict['#default'], $frame ) );
198            } elseif ( count( $kvs ) ) {
199                $lastKV = $kvs[count( $kvs ) - 1];
200                if ( $lastKV && !count( $lastKV->k ) ) {
201                    return $this->noTrimRes( $this->expandV( $lastKV->v, $frame ) );
202                } else {
203                    return [];
204                }
205            } else {
206                // nothing found at all.
207                return [];
208            }
209        } elseif ( $v ) {
210            return is_array( $v ) ? $v : [ $v ];
211        } else {
212            // nothing found at all.
213            return [];
214        }
215    }
216
217    public function pf_switch( $token, Frame $frame, Params $params ): array {
218        // TODO: Implement http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#Grouping_results
219        $args = $params->args;
220        $target = trim( $args[0]->k );
221        $this->env->log( 'debug', 'switch enter', $target, $token );
222        // create a dict from the remaining args
223        array_shift( $args );
224        $dict = $params->dict();
225        if ( $target && $dict[$target] !== null ) {
226            $this->env->log( 'debug', 'switch found: ', $target, $dict, ' res=', $dict[$target] );
227            $v = $this->expandV( $dict[$target], $frame );
228            return $this->trimRes( $v );
229        } else {
230            return $this->switchLookupFallback( $frame, $args, $target, $dict );
231        }
232    }
233
234    public function pf_ifeq( $token, Frame $frame, Params $params ): array {
235        $args = $params->args;
236        if ( count( $args ) < 3 ) {
237            return [];
238        } else {
239            $v = $this->expandV( $args[1]->v, $frame );
240            return $this->ifeq_worker( $frame, $args, $v );
241        }
242    }
243
244    private function ifeq_worker( Frame $frame, array $args, $b ): array {
245        if ( trim( $args[0]->k ) === trim( $b ) ) {
246            return $this->expandKV( $args[2], $frame );
247        } else {
248            return $this->expandKV( $args[3], $frame );
249        }
250    }
251
252    public function pf_expr( $token, Frame $frame, Params $params ): array {
253        $args = $params->args;
254        $target = $args[0]->k;
255        if ( $target ) {
256            try {
257                // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
258                $res = eval( $target );
259            } catch ( \Exception $e ) {
260                $res = null;
261            }
262        } else {
263            $res = '';
264        }
265
266        // Avoid crashes
267        if ( $res === null ) {
268            return [ 'class="error" in expression ' . $target ];
269        }
270
271        return [ (string)$res ];
272    }
273
274    public function pf_ifexpr( $token, Frame $frame, Params $params ): array {
275        $args = $params->args;
276        $this->env->log( 'debug', '#ifexp: ', $args );
277        $target = $args[0]->k;
278        $res = null;
279        if ( $target ) {
280            try {
281                // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
282                $res = eval( $target );
283            } catch ( \Exception $e ) {
284                return [ 'class="error" in expression ' . $target ];
285            }
286        }
287        if ( $res ) {
288            return $this->expandKV( $args[1], $frame );
289        } else {
290            return $this->expandKV( $args[2], $frame );
291        }
292    }
293
294    public function pf_iferror( $token, Frame $frame, Params $params ): array {
295        $args = $params->args;
296        $target = $args[0]->k;
297        if ( in_array( 'class="error"', $target, true ) ) {
298            return $this->expandKV( $args[1], $frame );
299        } else {
300            return $this->expandKV( $args[1], $frame, $target );
301        }
302    }
303
304    public function pf_lc( $token, Frame $frame, Params $params ): array {
305        $args = $params->args;
306        return [ mb_strtolower( $args[0]->k ) ];
307    }
308
309    public function pf_uc( $token, Frame $frame, Params $params ): array {
310        $args = $params->args;
311        return [ mb_strtoupper( $args[0]->k ) ];
312    }
313
314    public function pf_ucfirst( $token, Frame $frame, Params $params ): array {
315        $args = $params->args;
316        $target = $args[0]->k;
317        '@phan-var string $target';
318        if ( $target ) {
319            return [ mb_strtoupper( mb_substr( $target, 0, 1 ) ) . mb_substr( $target, 1 ) ];
320        } else {
321            return [];
322        }
323    }
324
325    public function pf_lcfirst( $token, Frame $frame, Params $params ): array {
326        $args = $params->args;
327        $target = $args[0]->k;
328        '@phan-var string $target';
329        if ( $target ) {
330            return [ mb_strtolower( mb_substr( $target, 0, 1 ) ) . mb_substr( $target, 1 ) ];
331        } else {
332            return [];
333        }
334    }
335
336    public function pf_padleft( $token, Frame $frame, Params $params ): array {
337        $args = $params->args;
338        $target = $args[0]->k;
339        $env = $this->env;
340        if ( !$args[1] ) {
341            return [];
342        }
343        // expand parameters 1 and 2
344        $args = $params->getSlice( 1, 3 );
345        $n = +( $args[0]->v );
346        if ( $n > 0 ) {
347            $pad = '0';
348            if ( isset( $args[1] ) && $args[1]->v !== '' ) {
349                $pad = $args[1]->v;
350            }
351            $padLength = mb_strlen( $pad );
352            $extra = '';
353            while ( ( mb_strlen( $target ) + mb_strlen( $extra ) + $padLength ) < $n ) {
354                $extra .= $pad;
355            }
356            if ( mb_strlen( $target ) + mb_strlen( $extra ) < $n ) {
357                $extra .= mb_substr( $pad, 0, $n - mb_strlen( $target ) - mb_strlen( $extra ) );
358            }
359            return [ $extra . $target ];
360        } else {
361            $env->log( 'debug', 'padleft no pad width', $args );
362            return [];
363        }
364    }
365
366    public function pf_padright( $token, Frame $frame, Params $params ): array {
367        $args = $params->args;
368        $target = $args[0]->k;
369        $env = $this->env;
370        if ( !$args[1] ) {
371            return [];
372        }
373
374        // expand parameters 1 and 2
375        $args = $params->getSlice( 1, 3 );
376        $n = +( $args[0]->v );
377        if ( $n > 0 ) {
378            $pad = '0';
379            if ( isset( $args[1] ) && $args[1]->v !== '' ) {
380                $pad = $args[1]->v;
381            }
382            $padLength = mb_strlen( $pad );
383            while ( ( mb_strlen( $target ) + $padLength ) < $n ) {
384                $target .= $pad;
385            }
386            if ( mb_strlen( $target ) < $n ) {
387                $target .= mb_substr( $pad, 0, $n - mb_strlen( $target ) );
388            }
389            return [ $target ];
390        } else {
391            $env->log( 'debug', 'padright no pad width', $args );
392            return [];
393        }
394    }
395
396    public function pf_tag( $token, Frame $frame, Params $params ): array {
397        $args = $params->args;
398        // Check http://www.mediawiki.org/wiki/Extension:TagParser for more info
399        // about the #tag parser function.
400        $target = $args[0]->k;
401        if ( !$target ) {
402            return [];
403        } else {
404            // remove tag-name
405            array_shift( $args );
406            $ret = $this->tag_worker( $target, $args );
407            return $ret;
408        }
409    }
410
411    private function tag_worker( $target, array $kvs ) {
412        $tagTk = new TagTk( $target );
413        $toks = [ $tagTk ];
414        $tagAttribs = [];
415        foreach ( $kvs as $kv ) {
416            if ( $kv->k === '' ) {
417                if ( is_array( $kv->v ) ) {
418                    PHPUtils::pushArray( $toks, $kv->v );
419                } else {
420                    $toks[] = $kv->v;
421                }
422            } else {
423                $tagAttribs[] = $kv;
424            }
425        }
426
427        $tagTk->attribs = $tagAttribs;
428        $toks[] = new EndTagTk( $target );
429        return $toks;
430    }
431
432    public function pf_currentyear( $token, Frame $frame, Params $params ): array {
433        return $this->pfTime_tokens( 'Y', [] );
434    }
435
436    public function pf_localyear( $token, Frame $frame, Params $params ): array {
437        return $this->pfTimel_tokens( 'Y', [] );
438    }
439
440    public function pf_currentmonth( $token, Frame $frame, Params $params ): array {
441        return $this->pfTime_tokens( 'm', [] );
442    }
443
444    public function pf_localmonth( $token, Frame $frame, Params $params ): array {
445        return $this->pfTimel_tokens( 'm', [] );
446    }
447
448    public function pf_currentmonthname( $token, Frame $frame, Params $params ): array {
449        return $this->pfTime_tokens( 'F', [] );
450    }
451
452    public function pf_localmonthname( $token, Frame $frame, Params $params ): array {
453        return $this->pfTimel_tokens( 'F', [] );
454    }
455
456    public function pf_currentmonthabbrev( $token, Frame $frame, Params $params ): array {
457        return $this->pfTime_tokens( 'M', [] );
458    }
459
460    public function pf_localmonthabbrev( $token, Frame $frame, Params $params ): array {
461        return $this->pfTimel_tokens( 'M', [] );
462    }
463
464    public function pf_currentweek( $token, Frame $frame, Params $params ): array {
465        $toks = $this->pfTime_tokens( 'W', [] );
466        // Cast to int to remove padding, as in core
467        $toks[0] = (string)(int)$toks[0];
468        return $toks;
469    }
470
471    public function pf_localweek( $token, Frame $frame, Params $params ): array {
472        $toks = $this->pfTimel_tokens( 'W', [] );
473        // Cast to int to remove padding, as in core
474        $toks[0] = (string)(int)$toks[0];
475        return $toks;
476    }
477
478    public function pf_currentday( $token, Frame $frame, Params $params ): array {
479        return $this->pfTime_tokens( 'j', [] );
480    }
481
482    public function pf_localday( $token, Frame $frame, Params $params ): array {
483        return $this->pfTimel_tokens( 'j', [] );
484    }
485
486    public function pf_currentday2( $token, Frame $frame, Params $params ): array {
487        return $this->pfTime_tokens( 'd', [] );
488    }
489
490    public function pf_localday2( $token, Frame $frame, Params $params ): array {
491        return $this->pfTimel_tokens( 'd', [] );
492    }
493
494    public function pf_currentdow( $token, Frame $frame, Params $params ): array {
495        return $this->pfTime_tokens( 'w', [] );
496    }
497
498    public function pf_localdow( $token, Frame $frame, Params $params ): array {
499        return $this->pfTimel_tokens( 'w', [] );
500    }
501
502    public function pf_currentdayname( $token, Frame $frame, Params $params ): array {
503        return $this->pfTime_tokens( 'l', [] );
504    }
505
506    public function pf_localdayname( $token, Frame $frame, Params $params ): array {
507        return $this->pfTimel_tokens( 'l', [] );
508    }
509
510    public function pf_currenttime( $token, Frame $frame, Params $params ): array {
511        return $this->pfTime_tokens( 'H:i', [] );
512    }
513
514    public function pf_localtime( $token, Frame $frame, Params $params ): array {
515        return $this->pfTimel_tokens( 'H:i', [] );
516    }
517
518    public function pf_currenthour( $token, Frame $frame, Params $params ): array {
519        return $this->pfTime_tokens( 'H', [] );
520    }
521
522    public function pf_localhour( $token, Frame $frame, Params $params ): array {
523        return $this->pfTimel_tokens( 'H', [] );
524    }
525
526    public function pf_currenttimestamp( $token, Frame $frame, Params $params ): array {
527        return $this->pfTime_tokens( 'YmdHis', [] );
528    }
529
530    public function pf_localtimestamp( $token, Frame $frame, Params $params ): array {
531        return $this->pfTimel_tokens( 'YmdHis', [] );
532    }
533
534    public function pf_currentmonthnamegen( $token, Frame $frame, Params $params ): array {
535        // XXX Actually use genitive form!
536        $args = $params->args;
537        return $this->pfTime_tokens( 'F', [] );
538    }
539
540    public function pf_localmonthnamegen( $token, Frame $frame, Params $params ): array {
541        $args = $params->args;
542        return $this->pfTimel_tokens( 'F', [] );
543    }
544
545    /*
546     * A first approximation of time stuff.
547     * TODO: Implement time spec (+ 1 day etc), check if formats are complete etc.
548     * See http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
549     * for the full list of requirements!
550     *
551     * First (very rough) approximation below based on
552     * http://jacwright.com/projects/javascript/date_format/, MIT licensed.
553     */
554    public function pf_time( $token, Frame $frame, Params $params ): array {
555        $args = $params->args;
556        return $this->pfTime( $args[0]->k, array_slice( $args, 1 ) );
557    }
558
559    public function pf_timel( $token, Frame $frame, Params $params ): array {
560        $args = $params->args;
561        return $this->pfTime( $args[0]->k, array_slice( $args, 1 ), true );
562    }
563
564    private function pfTime_tokens( $target, $args ) {
565        return $this->pfTime( $target, $args );
566    }
567
568    private function pfTimel_tokens( $target, $args ) {
569        return $this->pfTime( $target, $args, true );
570    }
571
572    private function pfTime( $target, $args, $isLocal = false ) {
573        $date = new DateTime( "now", new DateTimeZone( "UTC" ) );
574
575        $timestamp = $this->env->getSiteConfig()->fakeTimestamp();
576        if ( $timestamp ) {
577            $date->setTimestamp( $timestamp );
578        }
579        if ( $isLocal ) {
580            $date->setTimezone( new DateTimeZone( "-" . $this->env->getSiteConfig()->timezoneOffset() ) );
581        }
582
583        try {
584            return [ $date->format( trim( $target ) ) ];
585        } catch ( \Exception $e2 ) {
586            $this->env->log( 'error', '#time ' . $e2 );
587            return [ $date->format( 'D, d M Y H:i:s O' ) ];
588        }
589    }
590
591    public function pf_localurl( $token, Frame $frame, Params $params ): array {
592        $args = $params->args;
593        $target = $args[0]->k;
594        $env = $this->env;
595        $args = array_slice( $args, 1 );
596        $accum = [];
597        foreach ( $args as $item ) {
598            // FIXME: we are swallowing all errors
599            $res = $this->expandKV( $item, $frame, '', 'expanded-tokens-to-fragment', false );
600            PHPUtils::pushArray( $accum, $res );
601        }
602
603        return [
604            $env->getSiteConfig()->script() . '?title=' .
605            $env->normalizedTitleKey( $target ) . '&' .
606            implode( '&', $accum )
607        ];
608    }
609
610    /* Stub section: Pick any of these and actually implement them!  */
611
612    public function pf_formatnum( $token, Frame $frame, Params $params ): array {
613        $args = $params->args;
614        $target = $args[0]->k;
615        return [ $target ];
616    }
617
618    public function pf_currentpage( $token, Frame $frame, Params $params ): array {
619        $args = $params->args;
620        $target = $args[0]->k;
621        return [ $target ];
622    }
623
624    public function pf_pagenamee( $token, Frame $frame, Params $params ): array {
625        $args = $params->args;
626        $target = $args[0]->k;
627        return [ explode( ':', $target, 2 )[1] ?? '' ];
628    }
629
630    public function pf_fullpagename( $token, Frame $frame, Params $params ): array {
631        $args = $params->args;
632        $target = $args[0]->k;
633        return [ $target ?: ( $this->prefixedTitleText() ) ];
634    }
635
636    public function pf_fullpagenamee( $token, Frame $frame, Params $params ): array {
637        $args = $params->args;
638        $target = $args[0]->k;
639        return [ $target ?: ( $this->prefixedTitleText() ) ];
640    }
641
642    public function pf_pagelanguage( $token, Frame $frame, Params $params ): array {
643        $args = $params->args;
644        // The language (code) of the current page.
645        // Note: this is exposed as a mediawiki-internal code.
646        $code = Utils::bcp47ToMwCode(
647            $this->env->getPageConfig()->getPageLanguageBcp47()
648        );
649        return [ $code ];
650    }
651
652    public function pf_directionmark( $token, Frame $frame, Params $args ): array {
653        // The directionality of the current page.
654        $dir = $this->env->getPageConfig()->getPageLanguageDir();
655        $mark = $dir === 'rtl' ? '&rlm;' : '&lrm;';
656        // See Parser.php::getVariableValue()
657        return [ Utils::decodeWtEntities( $mark ) ];
658    }
659
660    public function pf_dirmark( $token, Frame $frame, Params $args ): array {
661        return $this->pf_directionmark( $token, $frame, $args );
662    }
663
664    public function pf_fullurl( $token, Frame $frame, Params $params ): array {
665        $args = $params->args;
666        $target = $args[0]->k;
667        $target = str_replace( ' ', '_', $target ?: ( $this->prefixedTitleText() ) );
668        $wikiConf = $this->env->getSiteConfig();
669        $url = null;
670        if ( $args[1] ) {
671            $url = $wikiConf->server() .
672                $wikiConf->script() .
673                '?title=' . PHPUtils::encodeURIComponent( $target ) .
674                '&' . $args[1]->k . '=' . $args[1]->v;
675        } else {
676            $url = $wikiConf->baseURI() .
677                implode( '/',
678                    array_map( [ PHPUtils::class, 'encodeURIComponent' ],
679                        explode( '/', str_replace( ' ', '_', $target ) ) )
680                );
681        }
682
683        return [ $url ];
684    }
685
686    public function pf_urlencode( $token, Frame $frame, Params $params ): array {
687        $args = $params->args;
688        $target = $args[0]->k;
689        return [ PHPUtils::encodeURIComponent( trim( $target ) ) ];
690    }
691
692    /*
693     * The following items all depends on information from the Wiki, so are hard
694     * to implement independently. Some might require using action=parse in the
695     * API to get the value. See
696     * http://www.mediawiki.org/wiki/Parsoid#Token_stream_transforms,
697     * http://etherpad.wikimedia.org/ParserNotesExtensions and
698     * http://www.mediawiki.org/wiki/Wikitext_parser/Environment.
699     * There might be better solutions for some of these.
700     */
701    public function pf_ifexist( $token, Frame $frame, Params $params ): array {
702        $args = $params->args;
703        return $this->expandKV( $args[1], $frame );
704    }
705
706    public function pf_pagesize( $token, Frame $frame, Params $params ): array {
707        $args = $params->args;
708        return [ '100' ];
709    }
710
711    public function pf_sitename( $token, Frame $frame, Params $params ): array {
712        $args = $params->args;
713        return [ 'MediaWiki' ];
714    }
715
716    private function encodeCharEntity( string $c, array &$tokens ) {
717        $enc = Utils::entityEncodeAll( $c );
718        $dp = new DataParsoid;
719        $dp->src = $enc;
720        $dp->srcContent = $c;
721        $tokens[] = new TagTk( 'span',
722            [ new KV( 'typeof', 'mw:Entity' ) ],
723            $dp
724        );
725        $tokens[] = $c;
726        $tokens[] = new EndTagTk( 'span', [], new DataParsoid );
727    }
728
729    public function pf_anchorencode( $token, Frame $frame, Params $params ): array {
730        $args = $params->args;
731        $target = $args[0]->k;
732
733        // Parser::guessSectionNameFromWikiText, which invokes
734        // Sanitizer::normalizeSectionNameWhitespace and
735        // Sanitizer::escapeIdForLink, then calls
736        // Sanitizer::safeEncodeAttribute on the result. See: T179544
737        $target = trim( preg_replace( '/[ _]+/', ' ', $target ) );
738        $target = Sanitizer::decodeCharReferences( $target );
739        $target = Sanitizer::escapeIdForLink( $target );
740        $pieces = preg_split(
741            "/([\\{\\}\\[\\]|]|''|ISBN|RFC|PMID|__)/", $target, -1, PREG_SPLIT_DELIM_CAPTURE );
742
743        $tokens = [];
744        foreach ( $pieces as $i => $p ) {
745            if ( ( $i % 2 ) === 0 ) {
746                $tokens[] = $p;
747            } elseif ( $p === "''" ) {
748                $this->encodeCharEntity( $p[0], $tokens );
749                $this->encodeCharEntity( $p[1], $tokens );
750            } else {
751                $this->encodeCharEntity( $p[0], $tokens );
752                $tokens[] = substr( $p, 1 );
753            }
754        }
755
756        return $tokens;
757    }
758
759    public function pf_protectionlevel( $token, Frame $frame, Params $params ): array {
760        $args = $params->args;
761        return [ '' ];
762    }
763
764    public function pf_ns( $token, Frame $frame, Params $params ): array {
765        $args = $params->args;
766        $nsid = null;
767        $target = $args[0]->k;
768        $env = $this->env;
769        $normalizedTarget = str_replace( ' ', '_', mb_strtolower( $target ) );
770
771        $siteConfig = $this->env->getSiteConfig();
772        if ( $siteConfig->namespaceId( $normalizedTarget ) !== null ) {
773            $nsid = $siteConfig->namespaceId( $normalizedTarget );
774        } elseif ( $siteConfig->canonicalNamespaceId( $normalizedTarget ) ) {
775            $nsid = $siteConfig->canonicalNamespaceId( $normalizedTarget );
776        }
777
778        if ( $nsid !== null && $siteConfig->namespaceName( $nsid ) ) {
779            $target = $siteConfig->namespaceName( $nsid );
780        }
781        // FIXME: What happens in the else case above?
782        return [ $target ];
783    }
784
785    public function pf_subjectspace( $token, Frame $frame, Params $params ): array {
786        $args = $params->args;
787        return [ 'Main' ];
788    }
789
790    public function pf_talkspace( $token, Frame $frame, Params $params ): array {
791        $args = $params->args;
792        return [ 'Talk' ];
793    }
794
795    public function pf_numberofarticles( $token, Frame $frame, Params $params ): array {
796        $args = $params->args;
797        return [ '1' ];
798    }
799
800    public function pf_language( $token, Frame $frame, Params $params ): array {
801        $args = $params->args;
802        return [ $args[0]->k ];
803    }
804
805    public function pf_contentlanguage( $token, Frame $frame, Params $params ): array {
806        $args = $params->args;
807        // Despite the name, this returns the wiki's default interface language
808        // ($wgLanguageCode), *not* the language of the current page content.
809        // Note: this is exposed as a mediawiki-internal code.
810        $code = Utils::bcp47ToMwCode(
811            $this->env->getSiteConfig()->langBcp47()
812        );
813        return [ $code ];
814    }
815
816    public function pf_contentlang( $token, Frame $frame, Params $params ): array {
817        return $this->pf_contentlanguage( $token, $frame, $params );
818    }
819
820    public function pf_numberoffiles( $token, Frame $frame, Params $params ): array {
821        $args = $params->args;
822        return [ '2' ];
823    }
824
825    public function pf_namespace( $token, Frame $frame, Params $params ): array {
826        $args = $params->args;
827        $target = $args[0]->k;
828        // The JS implementation is broken
829        $pieces = explode( ':', $target );
830        return [ count( $pieces ) > 1 ? $pieces[0] : 'Main' ];
831    }
832
833    public function pf_namespacee( $token, Frame $frame, Params $params ): array {
834        $args = $params->args;
835        $target = $args[0]->k;
836        // The JS implementation is broken
837        $pieces = explode( ':', $target );
838        return [ count( $pieces ) > 1 ? $pieces[0] : 'Main' ];
839    }
840
841    public function pf_namespacenumber( $token, Frame $frame, Params $params ): array {
842        $args = $params->args;
843        $a = explode( ':', $args[0]->k );
844        $target = array_pop( $a );
845        return [ (string)$this->env->getSiteConfig()->namespaceId( $target ) ];
846    }
847
848    public function pf_pagename( $token, Frame $frame, Params $params ): array {
849        $args = $params->args;
850        return [ $this->prefixedTitleText() ];
851    }
852
853    public function pf_pagenamebase( $token, Frame $frame, Params $params ): array {
854        $args = $params->args;
855        return [ $this->prefixedTitleText() ];
856    }
857
858    public function pf_scriptpath( $token, Frame $frame, Params $params ): array {
859        $args = $params->args;
860        return [ $this->env->getSiteConfig()->scriptpath() ];
861    }
862
863    public function pf_server( $token, Frame $frame, Params $params ): array {
864        $args = $params->args;
865        $dataParsoid = clone $token->dataParsoid;
866        return [
867            new TagTk( 'a', [
868                    new KV( 'rel', 'nofollow' ),
869                    new KV( 'class', 'external free' ),
870                    new KV( 'href', $this->env->getSiteConfig()->server() ),
871                    new KV( 'typeof', 'mw:ExtLink/URL' )
872                ], $dataParsoid
873            ),
874            $this->env->getSiteConfig()->server(),
875            new EndTagTk( 'a' )
876        ];
877    }
878
879    public function pf_servername( $token, Frame $frame, Params $params ): array {
880        $args = $params->args;
881        $server = $this->env->getSiteConfig()->server();
882        return [ preg_replace( '#^https?://#', '', $server, 1 ) ];
883    }
884
885    public function pf_talkpagename( $token, Frame $frame, Params $params ): array {
886        $args = $params->args;
887        $title = $this->prefixedTitleText();
888        return [ preg_replace( '/^[^:]:/', 'Talk:', $title, 1 ) ];
889    }
890
891    public function pf_defaultsort( $token, Frame $frame, Params $params ): array {
892        $args = $params->args;
893        $key = $args[0]->k;
894        return [
895            new SelfclosingTagTk( 'meta', [
896                    new KV( 'property', 'mw:PageProp/categorydefaultsort' ),
897                    new KV( 'content', trim( $key ) )
898                ]
899            )
900        ];
901    }
902
903    public function pf_displaytitle( $token, Frame $frame, Params $params ): array {
904        $args = $params->args;
905        $key = $args[0]->k;
906        return [
907            new SelfclosingTagTk( 'meta', [
908                    new KV( 'property', 'mw:PageProp/displaytitle' ),
909                    new KV( 'content', trim( $key ) )
910                ]
911            )
912        ];
913    }
914
915    public function pf_equal( $token, Frame $frame, Params $params ): array {
916        return [ '=' ];
917    }
918
919    // TODO: #titleparts, SUBJECTPAGENAME, BASEPAGENAME. SUBPAGENAME, DEFAULTSORT
920}