Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 89 |
CRAP | |
0.00% |
0 / 386 |
ParserFunctions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 89 |
29412 | |
0.00% |
0 / 386 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
rejoinKV | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 6 |
|||
expandV | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
expandKV | |
0.00% |
0 / 1 |
90 | |
0.00% |
0 / 12 |
|||
pf_if | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
trimRes | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 5 |
|||
noTrimRes | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 6 |
|||
switchLookupFallback | |
0.00% |
0 / 1 |
306 | |
0.00% |
0 / 31 |
|||
pf_switch | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 10 |
|||
pf_ifeq | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
ifeq_worker | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
pf_expr | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 10 |
|||
pf_ifexpr | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 11 |
|||
pf_iferror | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
pf_lc | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_uc | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_ucfirst | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
pf_lcfirst | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 5 |
|||
pf_padleft | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 20 |
|||
pf_padright | |
0.00% |
0 / 1 |
56 | |
0.00% |
0 / 19 |
|||
pf_tag | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
|||
tag_worker | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 12 |
|||
pf_currentyear | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localyear | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentmonth | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localmonth | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentmonthname | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localmonthname | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentmonthabbrev | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localmonthabbrev | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentweek | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_localweek | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_currentday | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localday | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentday2 | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localday2 | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentdow | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localdow | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentdayname | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localdayname | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currenttime | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localtime | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currenthour | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localhour | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currenttimestamp | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_localtimestamp | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_currentmonthnamegen | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_localmonthnamegen | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_time | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_timel | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pfTime_tokens | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pfTimel_tokens | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pfTime | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 10 |
|||
pf_localurl | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 11 |
|||
pf_formatnum | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_currentpage | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_pagenamee | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_fullpagename | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
pf_fullpagenamee | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
pf_pagelanguage | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_directionmark | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
pf_dirmark | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_fullurl | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 15 |
|||
pf_urlencode | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_ifexist | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_pagesize | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_sitename | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
encodeCharEntity | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 10 |
|||
pf_anchorencode | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 17 |
|||
pf_protectionlevel | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_ns | |
0.00% |
0 / 1 |
30 | |
0.00% |
0 / 13 |
|||
pf_subjectspace | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_talkspace | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_numberofarticles | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_language | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_contentlanguage | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_contentlang | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
pf_numberoffiles | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_namespace | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
pf_namespacee | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 4 |
|||
pf_namespacenumber | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 4 |
|||
pf_pagename | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_pagenamebase | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_scriptpath | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
pf_server | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 10 |
|||
pf_servername | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_talkpagename | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 3 |
|||
pf_defaultsort | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
|||
pf_displaytitle | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 5 |
<?php | |
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic | |
// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName | |
// phpcs:disable MediaWiki.Commenting.FunctionComment.WrongStyle | |
// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPrivate | |
namespace Wikimedia\Parsoid\Wt2Html\TT; | |
use DateTime; | |
use DateTimeZone; | |
use Wikimedia\Parsoid\Config\Env; | |
use Wikimedia\Parsoid\Core\Sanitizer; | |
use Wikimedia\Parsoid\NodeData\DataParsoid; | |
use Wikimedia\Parsoid\Tokens\EndTagTk; | |
use Wikimedia\Parsoid\Tokens\KV; | |
use Wikimedia\Parsoid\Tokens\SelfclosingTagTk; | |
use Wikimedia\Parsoid\Tokens\TagTk; | |
use Wikimedia\Parsoid\Utils\PHPUtils; | |
use Wikimedia\Parsoid\Utils\TokenUtils; | |
use Wikimedia\Parsoid\Utils\Utils; | |
use Wikimedia\Parsoid\Wt2Html\Frame; | |
use Wikimedia\Parsoid\Wt2Html\Params; | |
/** | |
* Some parser functions, and quite a bunch of stubs of parser functions. | |
* | |
* IMPORTANT NOTE: These parser functions are only used by the Parsoid-native | |
* template expansion pipeline, which is *not* the default or used in | |
* production. Normally core provides us SiteConfig and DataAccess objects | |
* that provide parser functions and other preprocessor functionality. | |
* | |
* There are still quite a few missing, see | |
* {@link http://www.mediawiki.org/wiki/Help:Magic_words} and | |
* {@link http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions}. | |
* Instantiated and called by the {@link TemplateHandler} extension. | |
* Any `pf_<prefix>` | |
* matching a lower-cased template name prefix up to the first colon will | |
* override that template. | |
* | |
* The only use of this code is currently in parserTests and offline tests. | |
* But, eventually as the two parsers are integrated, the core parser tests | |
* implementation from $mw/includes/parser/CoreParserFunctions.php might | |
* move over here. | |
*/ | |
class ParserFunctions { | |
/** @var Env */ | |
private $env; | |
/** | |
* @param Env $env | |
*/ | |
public function __construct( Env $env ) { | |
$this->env = $env; | |
} | |
// Temporary helper. | |
private function rejoinKV( bool $trim, $k, $v ) { | |
if ( is_string( $k ) && strlen( $k ) > 0 ) { | |
return array_merge( [ $k, '=' ], $v ); | |
} elseif ( is_array( $k ) && count( $k ) > 0 ) { | |
$k[] = '='; | |
return array_merge( $k, $v ); | |
} else { | |
return $trim ? ( is_string( $v ) ? trim( $v ) : TokenUtils::tokenTrim( $v ) ) : $v; | |
} | |
} | |
private function expandV( $v, Frame $frame ) { | |
// FIXME: This hasn't been implemented on the JS side | |
return $v; | |
} | |
// XXX: move to frame? | |
private function expandKV( | |
$kv, Frame $frame, $defaultValue = null, string $type = null, bool $trim = false | |
): array { | |
if ( $type === null ) { | |
$type = 'tokens/x-mediawiki/expanded'; | |
} | |
if ( $kv === null ) { | |
return [ $defaultValue ?: '' ]; | |
} elseif ( is_string( $kv ) ) { | |
return [ $kv ]; | |
} elseif ( is_string( $kv->k ) && is_string( $kv->v ) ) { | |
if ( $kv->k ) { | |
return [ $kv->k . '=' . $kv->v ]; | |
} else { | |
return [ $trim ? trim( $kv->v ) : $kv->v ]; | |
} | |
} else { | |
$v = $this->expandV( $kv->v, $frame ); | |
return $this->rejoinKV( $trim, $kv->k, $v ); | |
} | |
} | |
public function pf_if( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
if ( trim( $args[0]->k ) !== '' ) { | |
return $this->expandKV( $args[1] ?? null, $frame ); | |
} else { | |
return $this->expandKV( $args[2] ?? null, $frame ); | |
} | |
} | |
private function trimRes( $res ) { | |
if ( is_string( $res ) ) { | |
return [ trim( $res ) ]; | |
} elseif ( is_array( $res ) ) { | |
return TokenUtils::tokenTrim( $res ); | |
} else { | |
return $res; | |
} | |
} | |
private function noTrimRes( $res ): array { | |
if ( is_string( $res ) ) { | |
return [ $res ]; | |
} elseif ( is_array( $res ) ) { | |
return $res; | |
} else { | |
$this->env->log( 'error', 'Unprocessable res in ParserFunctions:noTrimRes', $res ); | |
return []; | |
} | |
} | |
private function switchLookupFallback( | |
Frame $frame, array $kvs, string $key, array $dict, $v = null | |
): array { | |
$kv = null; | |
$l = count( $kvs ); | |
$this->env->log( 'debug', 'switchLookupFallback', $key, $v ); | |
// 'v' need not be a string in cases where it is the last fall-through case | |
$vStr = $v ? TokenUtils::tokensToString( $v ) : null; | |
if ( $vStr && $key === trim( $vStr ) ) { | |
// This handles fall-through switch cases: | |
// | |
// {{#switch:<key> | |
// | c1 | c2 | c3 = <res> | |
// ... | |
// }} | |
// | |
// So if <key> matched c1, we want to return <res>. | |
// Hence, we are looking for the next entry with a non-empty key. | |
$this->env->log( 'debug', 'switch found' ); | |
foreach ( $kvs as $kv ) { | |
// XXX: make sure the key is always one of these! | |
if ( count( $kv->k ) > 0 ) { | |
return $this->trimRes( $this->expandV( $kv->v, $frame ) ); | |
} | |
} | |
// No value found, return empty string? XXX: check this | |
return []; | |
} elseif ( count( $kvs ) > 0 ) { | |
// search for value-only entry which matches | |
$i = 0; | |
if ( $v ) { | |
$i = 1; | |
} | |
for ( ; $i < $l; $i++ ) { | |
$kv = $kvs[$i]; | |
if ( count( $kv->k ) || !count( $kv->v ) ) { | |
// skip entries with keys or empty values | |
continue; | |
} else { | |
// We found a value-only entry. However, we have to verify | |
// if we have any fall-through cases that this matches. | |
// | |
// {{#switch:<key> | |
// | c1 | c2 | c3 = <res> | |
// ... | |
// }} | |
// | |
// In the switch example above, if we found 'c1', that is | |
// not the fallback value -- we have to check for fall-through | |
// cases. Hence the recursive callback to switchLookupFallback. | |
// | |
// {{#switch:<key> | |
// | c1 = <..> | |
// | c2 = <..> | |
// | [[Foo]]</div> | |
// }} | |
// | |
// 'val' may be an array of tokens rather than a string as in the | |
// example above where 'val' is indeed the final return value. | |
// Hence 'tokens/x-mediawiki/expanded' type below. | |
$v = $this->expandV( $kv->v, $frame ); | |
return $this->switchLookupFallback( $frame, array_slice( $kvs, $i + 1 ), $key, $dict, $v ); | |
} | |
} | |
// value not found! | |
if ( isset( $dict['#default'] ) ) { | |
return $this->trimRes( $this->expandV( $dict['#default'], $frame ) ); | |
} elseif ( count( $kvs ) ) { | |
$lastKV = $kvs[count( $kvs ) - 1]; | |
if ( $lastKV && !count( $lastKV->k ) ) { | |
return $this->noTrimRes( $this->expandV( $lastKV->v, $frame ) ); | |
} else { | |
return []; | |
} | |
} else { | |
// nothing found at all. | |
return []; | |
} | |
} elseif ( $v ) { | |
return is_array( $v ) ? $v : [ $v ]; | |
} else { | |
// nothing found at all. | |
return []; | |
} | |
} | |
public function pf_switch( $token, Frame $frame, Params $params ): array { | |
// TODO: Implement http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#Grouping_results | |
$args = $params->args; | |
$target = trim( $args[0]->k ); | |
$this->env->log( 'debug', 'switch enter', $target, $token ); | |
// create a dict from the remaining args | |
array_shift( $args ); | |
$dict = $params->dict(); | |
if ( $target && $dict[$target] !== null ) { | |
$this->env->log( 'debug', 'switch found: ', $target, $dict, ' res=', $dict[$target] ); | |
$v = $this->expandV( $dict[$target], $frame ); | |
return $this->trimRes( $v ); | |
} else { | |
return $this->switchLookupFallback( $frame, $args, $target, $dict ); | |
} | |
} | |
public function pf_ifeq( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
if ( count( $args ) < 3 ) { | |
return []; | |
} else { | |
$v = $this->expandV( $args[1]->v, $frame ); | |
return $this->ifeq_worker( $frame, $args, $v ); | |
} | |
} | |
private function ifeq_worker( Frame $frame, array $args, $b ): array { | |
if ( trim( $args[0]->k ) === trim( $b ) ) { | |
return $this->expandKV( $args[2], $frame ); | |
} else { | |
return $this->expandKV( $args[3], $frame ); | |
} | |
} | |
public function pf_expr( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
if ( $target ) { | |
try { | |
$res = eval( $target ); | |
} catch ( \Exception $e ) { | |
$res = null; | |
} | |
} else { | |
$res = ''; | |
} | |
// Avoid crashes | |
if ( $res === null ) { | |
return [ 'class="error" in expression ' . $target ]; | |
} | |
return [ (string)$res ]; | |
} | |
public function pf_ifexpr( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$this->env->log( 'debug', '#ifexp: ', $args ); | |
$target = $args[0]->k; | |
$res = null; | |
if ( $target ) { | |
try { | |
$res = eval( $target ); | |
} catch ( \Exception $e ) { | |
return [ 'class="error" in expression ' . $target ]; | |
} | |
} | |
if ( $res ) { | |
return $this->expandKV( $args[1], $frame ); | |
} else { | |
return $this->expandKV( $args[2], $frame ); | |
} | |
} | |
public function pf_iferror( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
if ( in_array( 'class="error"', $target, true ) ) { | |
return $this->expandKV( $args[1], $frame ); | |
} else { | |
return $this->expandKV( $args[1], $frame, $target ); | |
} | |
} | |
public function pf_lc( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ mb_strtolower( $args[0]->k ) ]; | |
} | |
public function pf_uc( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ mb_strtoupper( $args[0]->k ) ]; | |
} | |
public function pf_ucfirst( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
'@phan-var string $target'; | |
if ( $target ) { | |
return [ mb_strtoupper( mb_substr( $target, 0, 1 ) ) . mb_substr( $target, 1 ) ]; | |
} else { | |
return []; | |
} | |
} | |
public function pf_lcfirst( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
'@phan-var string $target'; | |
if ( $target ) { | |
return [ mb_strtolower( mb_substr( $target, 0, 1 ) ) . mb_substr( $target, 1 ) ]; | |
} else { | |
return []; | |
} | |
} | |
public function pf_padleft( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
$env = $this->env; | |
if ( !$args[1] ) { | |
return []; | |
} | |
// expand parameters 1 and 2 | |
$args = $params->getSlice( 1, 3 ); | |
$n = +( $args[0]->v ); | |
if ( $n > 0 ) { | |
$pad = '0'; | |
if ( isset( $args[1] ) && $args[1]->v !== '' ) { | |
$pad = $args[1]->v; | |
} | |
$padLength = mb_strlen( $pad ); | |
$extra = ''; | |
while ( ( mb_strlen( $target ) + mb_strlen( $extra ) + $padLength ) < $n ) { | |
$extra .= $pad; | |
} | |
if ( mb_strlen( $target ) + mb_strlen( $extra ) < $n ) { | |
$extra .= mb_substr( $pad, 0, $n - mb_strlen( $target ) - mb_strlen( $extra ) ); | |
} | |
return [ $extra . $target ]; | |
} else { | |
$env->log( 'debug', 'padleft no pad width', $args ); | |
return []; | |
} | |
} | |
public function pf_padright( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
$env = $this->env; | |
if ( !$args[1] ) { | |
return []; | |
} | |
// expand parameters 1 and 2 | |
$args = $params->getSlice( 1, 3 ); | |
$n = +( $args[0]->v ); | |
if ( $n > 0 ) { | |
$pad = '0'; | |
if ( isset( $args[1] ) && $args[1]->v !== '' ) { | |
$pad = $args[1]->v; | |
} | |
$padLength = mb_strlen( $pad ); | |
while ( ( mb_strlen( $target ) + $padLength ) < $n ) { | |
$target .= $pad; | |
} | |
if ( mb_strlen( $target ) < $n ) { | |
$target .= mb_substr( $pad, 0, $n - mb_strlen( $target ) ); | |
} | |
return [ $target ]; | |
} else { | |
$env->log( 'debug', 'padright no pad width', $args ); | |
return []; | |
} | |
} | |
public function pf_tag( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
// Check http://www.mediawiki.org/wiki/Extension:TagParser for more info | |
// about the #tag parser function. | |
$target = $args[0]->k; | |
if ( !$target ) { | |
return []; | |
} else { | |
// remove tag-name | |
array_shift( $args ); | |
$ret = $this->tag_worker( $target, $args ); | |
return $ret; | |
} | |
} | |
private function tag_worker( $target, array $kvs ) { | |
$tagTk = new TagTk( $target ); | |
$toks = [ $tagTk ]; | |
$tagAttribs = []; | |
foreach ( $kvs as $kv ) { | |
if ( $kv->k === '' ) { | |
if ( is_array( $kv->v ) ) { | |
PHPUtils::pushArray( $toks, $kv->v ); | |
} else { | |
$toks[] = $kv->v; | |
} | |
} else { | |
$tagAttribs[] = $kv; | |
} | |
} | |
$tagTk->attribs = $tagAttribs; | |
$toks[] = new EndTagTk( $target ); | |
return $toks; | |
} | |
public function pf_currentyear( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'Y', [] ); | |
} | |
public function pf_localyear( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'Y', [] ); | |
} | |
public function pf_currentmonth( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'm', [] ); | |
} | |
public function pf_localmonth( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'm', [] ); | |
} | |
public function pf_currentmonthname( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'F', [] ); | |
} | |
public function pf_localmonthname( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'F', [] ); | |
} | |
public function pf_currentmonthabbrev( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'M', [] ); | |
} | |
public function pf_localmonthabbrev( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'M', [] ); | |
} | |
public function pf_currentweek( $token, Frame $frame, Params $params ): array { | |
$toks = $this->pfTime_tokens( 'W', [] ); | |
// Cast to int to remove padding, as in core | |
$toks[0] = (string)(int)$toks[0]; | |
return $toks; | |
} | |
public function pf_localweek( $token, Frame $frame, Params $params ): array { | |
$toks = $this->pfTimel_tokens( 'W', [] ); | |
// Cast to int to remove padding, as in core | |
$toks[0] = (string)(int)$toks[0]; | |
return $toks; | |
} | |
public function pf_currentday( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'j', [] ); | |
} | |
public function pf_localday( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'j', [] ); | |
} | |
public function pf_currentday2( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'd', [] ); | |
} | |
public function pf_localday2( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'd', [] ); | |
} | |
public function pf_currentdow( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'w', [] ); | |
} | |
public function pf_localdow( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'w', [] ); | |
} | |
public function pf_currentdayname( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'l', [] ); | |
} | |
public function pf_localdayname( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'l', [] ); | |
} | |
public function pf_currenttime( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'H:i', [] ); | |
} | |
public function pf_localtime( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'H:i', [] ); | |
} | |
public function pf_currenthour( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'H', [] ); | |
} | |
public function pf_localhour( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'H', [] ); | |
} | |
public function pf_currenttimestamp( $token, Frame $frame, Params $params ): array { | |
return $this->pfTime_tokens( 'YmdHis', [] ); | |
} | |
public function pf_localtimestamp( $token, Frame $frame, Params $params ): array { | |
return $this->pfTimel_tokens( 'YmdHis', [] ); | |
} | |
public function pf_currentmonthnamegen( $token, Frame $frame, Params $params ): array { | |
// XXX Actually use genitive form! | |
$args = $params->args; | |
return $this->pfTime_tokens( 'F', [] ); | |
} | |
public function pf_localmonthnamegen( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return $this->pfTimel_tokens( 'F', [] ); | |
} | |
/* | |
* A first approximation of time stuff. | |
* TODO: Implement time spec (+ 1 day etc), check if formats are complete etc. | |
* See http://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time | |
* for the full list of requirements! | |
* | |
* First (very rough) approximation below based on | |
* http://jacwright.com/projects/javascript/date_format/, MIT licensed. | |
*/ | |
public function pf_time( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return $this->pfTime( $args[0]->k, array_slice( $args, 1 ) ); | |
} | |
public function pf_timel( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return $this->pfTime( $args[0]->k, array_slice( $args, 1 ), true ); | |
} | |
private function pfTime_tokens( $target, $args ) { | |
return $this->pfTime( $target, $args ); | |
} | |
private function pfTimel_tokens( $target, $args ) { | |
return $this->pfTime( $target, $args, true ); | |
} | |
private function pfTime( $target, $args, $isLocal = false ) { | |
$date = new DateTime( "now", new DateTimeZone( "UTC" ) ); | |
$timestamp = $this->env->getSiteConfig()->fakeTimestamp(); | |
if ( $timestamp ) { | |
$date->setTimestamp( $timestamp ); | |
} | |
if ( $isLocal ) { | |
$date->setTimezone( new DateTimeZone( "-" . $this->env->getSiteConfig()->timezoneOffset() ) ); | |
} | |
try { | |
return [ $date->format( trim( $target ) ) ]; | |
} catch ( \Exception $e2 ) { | |
$this->env->log( 'error', '#time ' . $e2 ); | |
return [ $date->format( 'D, d M Y H:i:s O' ) ]; | |
} | |
} | |
public function pf_localurl( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
$env = $this->env; | |
$args = array_slice( $args, 1 ); | |
$accum = []; | |
foreach ( $args as $item ) { | |
// FIXME: we are swallowing all errors | |
$res = $this->expandKV( $item, $frame, '', 'text/x-mediawiki/expanded', false ); | |
PHPUtils::pushArray( $accum, $res ); | |
} | |
return [ | |
$env->getSiteConfig()->script() . '?title=' . | |
$env->normalizedTitleKey( $target ) . '&' . | |
implode( '&', $accum ) | |
]; | |
} | |
/* Stub section: Pick any of these and actually implement them! */ | |
public function pf_formatnum( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ $target ]; | |
} | |
public function pf_currentpage( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ $target ]; | |
} | |
public function pf_pagenamee( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ explode( ':', $target, 2 )[1] ?? '' ]; | |
} | |
public function pf_fullpagename( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ $target ?: ( $this->env->getPageConfig()->getTitle() ) ]; | |
} | |
public function pf_fullpagenamee( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ $target ?: ( $this->env->getPageConfig()->getTitle() ) ]; | |
} | |
public function pf_pagelanguage( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
// The language (code) of the current page. | |
return [ $this->env->getPageConfig()->getPageLanguage() ]; | |
} | |
public function pf_directionmark( $token, Frame $frame, Params $args ): array { | |
// The directionality of the current page. | |
$dir = $this->env->getPageConfig()->getPageLanguageDir(); | |
$mark = $dir === 'rtl' ? '‏' : '‎'; | |
// See Parser.php::getVariableValue() | |
return [ Utils::decodeWtEntities( $mark ) ]; | |
} | |
public function pf_dirmark( $token, Frame $frame, Params $args ): array { | |
return $this->pf_directionmark( $token, $frame, $args ); | |
} | |
public function pf_fullurl( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
$target = str_replace( ' ', '_', $target ?: ( $this->env->getPageConfig()->getTitle() ) ); | |
$wikiConf = $this->env->getSiteConfig(); | |
$url = null; | |
if ( $args[1] ) { | |
$url = $wikiConf->server() . | |
$wikiConf->script() . | |
'?title=' . PHPUtils::encodeURIComponent( $target ) . | |
'&' . $args[1]->k . '=' . $args[1]->v; | |
} else { | |
$url = $wikiConf->baseURI() . | |
implode( '/', | |
array_map( [ PHPUtils::class, 'encodeURIComponent' ], | |
explode( '/', str_replace( ' ', '_', $target ) ) ) | |
); | |
} | |
return [ $url ]; | |
} | |
public function pf_urlencode( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
return [ PHPUtils::encodeURIComponent( trim( $target ) ) ]; | |
} | |
/* | |
* The following items all depends on information from the Wiki, so are hard | |
* to implement independently. Some might require using action=parse in the | |
* API to get the value. See | |
* http://www.mediawiki.org/wiki/Parsoid#Token_stream_transforms, | |
* http://etherpad.wikimedia.org/ParserNotesExtensions and | |
* http://www.mediawiki.org/wiki/Wikitext_parser/Environment. | |
* There might be better solutions for some of these. | |
*/ | |
public function pf_ifexist( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return $this->expandKV( $args[1], $frame ); | |
} | |
public function pf_pagesize( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ '100' ]; | |
} | |
public function pf_sitename( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ 'MediaWiki' ]; | |
} | |
private function encodeCharEntity( string $c, array &$tokens ) { | |
$enc = Utils::entityEncodeAll( $c ); | |
$dp = new DataParsoid; | |
$dp->src = $enc; | |
$dp->srcContent = $c; | |
$tokens[] = new TagTk( 'span', | |
[ new KV( 'typeof', 'mw:Entity' ) ], | |
$dp | |
); | |
$tokens[] = $c; | |
$tokens[] = new EndTagTk( 'span', [], new DataParsoid ); | |
} | |
public function pf_anchorencode( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
// Parser::guessSectionNameFromWikiText, which invokes | |
// Sanitizer::normalizeSectionNameWhitespace and | |
// Sanitizer::escapeIdForLink, then calls | |
// Sanitizer::safeEncodeAttribute on the result. See: T179544 | |
$target = trim( preg_replace( '/[ _]+/', ' ', $target ) ); | |
$target = Sanitizer::decodeCharReferences( $target ); | |
$target = Sanitizer::escapeIdForLink( $target ); | |
$pieces = preg_split( | |
"/([\\{\\}\\[\\]|]|''|ISBN|RFC|PMID|__)/", $target, -1, PREG_SPLIT_DELIM_CAPTURE ); | |
$tokens = []; | |
foreach ( $pieces as $i => $p ) { | |
if ( ( $i % 2 ) === 0 ) { | |
$tokens[] = $p; | |
} elseif ( $p === "''" ) { | |
$this->encodeCharEntity( $p[0], $tokens ); | |
$this->encodeCharEntity( $p[1], $tokens ); | |
} else { | |
$this->encodeCharEntity( $p[0], $tokens ); | |
$tokens[] = substr( $p, 1 ); | |
} | |
} | |
return $tokens; | |
} | |
public function pf_protectionlevel( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ '' ]; | |
} | |
public function pf_ns( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$nsid = null; | |
$target = $args[0]->k; | |
$env = $this->env; | |
$normalizedTarget = str_replace( ' ', '_', mb_strtolower( $target ) ); | |
$siteConfig = $this->env->getSiteConfig(); | |
if ( $siteConfig->namespaceId( $normalizedTarget ) !== null ) { | |
$nsid = $siteConfig->namespaceId( $normalizedTarget ); | |
} elseif ( $siteConfig->canonicalNamespaceId( $normalizedTarget ) ) { | |
$nsid = $siteConfig->canonicalNamespaceId( $normalizedTarget ); | |
} | |
if ( $nsid !== null && $siteConfig->namespaceName( $nsid ) ) { | |
$target = $siteConfig->namespaceName( $nsid ); | |
} | |
// FIXME: What happens in the else case above? | |
return [ $target ]; | |
} | |
public function pf_subjectspace( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ 'Main' ]; | |
} | |
public function pf_talkspace( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ 'Talk' ]; | |
} | |
public function pf_numberofarticles( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ '1' ]; | |
} | |
public function pf_language( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ $args[0]->k ]; | |
} | |
public function pf_contentlanguage( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
// Despite the name, this returns the wiki's default interface language | |
// ($wgLanguageCode), *not* the language of the current page content. | |
return [ $this->env->getSiteConfig()->lang() ]; | |
} | |
public function pf_contentlang( $token, Frame $frame, Params $params ): array { | |
return $this->pf_contentlanguage( $token, $frame, $params ); | |
} | |
public function pf_numberoffiles( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ '2' ]; | |
} | |
public function pf_namespace( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
// The JS implementation is broken | |
$pieces = explode( ':', $target ); | |
return [ count( $pieces ) > 1 ? $pieces[0] : 'Main' ]; | |
} | |
public function pf_namespacee( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$target = $args[0]->k; | |
// The JS implementation is broken | |
$pieces = explode( ':', $target ); | |
return [ count( $pieces ) > 1 ? $pieces[0] : 'Main' ]; | |
} | |
public function pf_namespacenumber( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$a = explode( ':', $args[0]->k ); | |
$target = array_pop( $a ); | |
return [ (string)$this->env->getSiteConfig()->namespaceId( $target ) ]; | |
} | |
public function pf_pagename( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ $this->env->getPageConfig()->getTitle() ]; | |
} | |
public function pf_pagenamebase( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ $this->env->getPageConfig()->getTitle() ]; | |
} | |
public function pf_scriptpath( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
return [ $this->env->getSiteConfig()->scriptpath() ]; | |
} | |
public function pf_server( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$dataAttribs = $token->dataAttribs->clone(); | |
return [ | |
new TagTk( 'a', [ | |
new KV( 'rel', 'nofollow' ), | |
new KV( 'class', 'external free' ), | |
new KV( 'href', $this->env->getSiteConfig()->server() ), | |
new KV( 'typeof', 'mw:ExtLink/URL' ) | |
], $dataAttribs | |
), | |
$this->env->getSiteConfig()->server(), | |
new EndTagTk( 'a' ) | |
]; | |
} | |
public function pf_servername( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$server = $this->env->getSiteConfig()->server(); | |
return [ preg_replace( '#^https?://#', '', $server, 1 ) ]; | |
} | |
public function pf_talkpagename( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$title = $this->env->getPageConfig()->getTitle(); | |
return [ preg_replace( '/^[^:]:/', 'Talk:', $title, 1 ) ]; | |
} | |
public function pf_defaultsort( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$key = $args[0]->k; | |
return [ | |
new SelfclosingTagTk( 'meta', [ | |
new KV( 'property', 'mw:PageProp/categorydefaultsort' ), | |
new KV( 'content', trim( $key ) ) | |
] | |
) | |
]; | |
} | |
public function pf_displaytitle( $token, Frame $frame, Params $params ): array { | |
$args = $params->args; | |
$key = $args[0]->k; | |
return [ | |
new SelfclosingTagTk( 'meta', [ | |
new KV( 'property', 'mw:PageProp/displaytitle' ), | |
new KV( 'content', trim( $key ) ) | |
] | |
) | |
]; | |
} | |
// TODO: #titleparts, SUBJECTPAGENAME, BASEPAGENAME. SUBPAGENAME, DEFAULTSORT | |
} |