Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 406 |
|
0.00% |
0 / 90 |
CRAP | |
0.00% |
0 / 1 |
ParserFunctions | |
0.00% |
0 / 406 |
|
0.00% |
0 / 90 |
29756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
rejoinKV | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
56 | |||
expandV | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
expandKV | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
90 | |||
pf_if | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
trimRes | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
noTrimRes | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
switchLookupFallback | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
306 | |||
pf_switch | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
pf_ifeq | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
ifeq_worker | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
pf_expr | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
pf_ifexpr | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
pf_iferror | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
pf_lc | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_uc | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_ucfirst | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
pf_lcfirst | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
pf_padleft | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
pf_padright | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
pf_tag | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
tag_worker | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
pf_currentyear | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localyear | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentmonth | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localmonth | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentmonthname | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localmonthname | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentmonthabbrev | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localmonthabbrev | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentweek | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_localweek | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentday | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localday | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentday2 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localday2 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentdow | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localdow | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentdayname | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localdayname | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currenttime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localtime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currenthour | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localhour | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currenttimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_localtimestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentmonthnamegen | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_localmonthnamegen | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_time | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_timel | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pfTime_tokens | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pfTimel_tokens | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pfTime | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
pf_localurl | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
pf_formatnum | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_currentpage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_pagenamee | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_fullpagename | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
pf_fullpagenamee | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
pf_pagelanguage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
pf_directionmark | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
pf_dirmark | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_fullurl | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
pf_urlencode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_ifexist | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_pagesize | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_sitename | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
encodeCharEntity | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
pf_anchorencode | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
pf_protectionlevel | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_ns | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
pf_subjectspace | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_talkspace | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_numberofarticles | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_language | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_contentlanguage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
pf_contentlang | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
pf_numberoffiles | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_namespace | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
pf_namespacee | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
pf_namespacenumber | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
pf_pagename | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_pagenamebase | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_scriptpath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
pf_server | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
pf_servername | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_talkpagename | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
pf_defaultsort | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
pf_displaytitle | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
pf_equal | |
0.00% |
0 / 1 |
|
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 | |
8 | namespace Wikimedia\Parsoid\Wt2Html\TT; |
9 | |
10 | use DateTime; |
11 | use DateTimeZone; |
12 | use Wikimedia\Parsoid\Config\Env; |
13 | use Wikimedia\Parsoid\Core\Sanitizer; |
14 | use Wikimedia\Parsoid\NodeData\DataParsoid; |
15 | use Wikimedia\Parsoid\Tokens\EndTagTk; |
16 | use Wikimedia\Parsoid\Tokens\KV; |
17 | use Wikimedia\Parsoid\Tokens\SelfclosingTagTk; |
18 | use Wikimedia\Parsoid\Tokens\TagTk; |
19 | use Wikimedia\Parsoid\Utils\PHPUtils; |
20 | use Wikimedia\Parsoid\Utils\TokenUtils; |
21 | use Wikimedia\Parsoid\Utils\Utils; |
22 | use Wikimedia\Parsoid\Wt2Html\Frame; |
23 | use 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 | */ |
46 | class 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' ? '‏' : '‎'; |
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 | } |