` * 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: // | c1 | c2 | c3 = // ... // }} // // So if matched c1, we want to return . // 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: // | c1 | c2 | c3 = // ... // }} // // 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: // | c1 = <..> // | c2 = <..> // | [[Foo]] // }} // // '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 ( array_search( 'class="error"', $target, true ) !== false ) { 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 || $target === '' ) { return []; } else { // remove tag-name array_shift( $args ); $ret = $this->tag_worker( $target, $args ); return $ret; } } private function tag_worker( $target, array $kvs ) { $contentToks = []; $tagAttribs = []; foreach ( $kvs as $kv ) { if ( $kv->k === '' ) { if ( is_array( $kv->v ) ) { $contentToks = array_merge( $contentToks, $kv->v ); } else { $contentToks[] = $kv->v; } } else { $tagAttribs[] = $kv; } } return array_merge( [ new TagTk( $target, $tagAttribs ) ], $contentToks, [ new EndTagTk( $target ) ] ); } 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 ); $accum = array_merge( $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 ); $tokens[] = new TagTk( 'span', [ new KV( 'typeof', 'mw:Entity' ) ], (object)[ 'src' => $enc, 'srcContent' => $c ] ); $tokens[] = $c; $tokens[] = new EndTagTk( 'span', [], new stdClass ); } 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 = Utils::clone( $token->dataAttribs ); 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 }