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