Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 407 |
|
0.00% |
0 / 91 |
CRAP | |
0.00% |
0 / 1 |
ParserFunctions | |
0.00% |
0 / 407 |
|
0.00% |
0 / 91 |
30102 | |
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 | |||
prefixedTitleText | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
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 | 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' ? '‏' : '‎'; |
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 | } |