diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5f6404..44eceae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,8 +10,8 @@ jobs: build: strategy: matrix: - php_version: ['8.0'] - mw: ['REL1_39'] + php_version: ['8.1'] + mw: ['REL1_43'] runs-on: ubuntu-latest steps: diff --git a/.phan/config.php b/.phan/config.php index f08d37a..72de48d 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -2,6 +2,6 @@ $config = require __DIR__ . '/../vendor/mediawiki/mediawiki-phan-config/src/config.php'; -$config['minimum_target_php_version'] = '8.0.28'; +$config['minimum_target_php_version'] = '8.1.0'; return $config; diff --git a/.phan/issue-baseline.php b/.phan/issue-baseline.php index c39cc1e..9ca7c57 100644 --- a/.phan/issue-baseline.php +++ b/.phan/issue-baseline.php @@ -19,8 +19,8 @@ // Currently, file_suppressions and directory_suppressions are the only supported suppressions 'file_suppressions' => [ - 'src/UsingDataHooks.php' => [ 'PhanCoalescingNeverNull', 'PhanTypeMismatchArgumentNullable', 'PhanUndeclaredProperty', 'SecurityCheck-XSS' ], - 'src/UsingDataPPFrameDOM.php' => [ 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredMethod', 'PhanUndeclaredProperty' ], +// 'src/UsingDataHooks.php' => [ 'PhanCoalescingNeverNull', 'PhanTypeMismatchArgumentNullable', 'PhanUndeclaredProperty', 'SecurityCheck-XSS' ], +// 'src/UsingDataPPFrameDOM.php' => [ 'PhanTypeMismatchReturn', 'PhanTypeMismatchReturnProbablyReal', 'PhanUndeclaredMethod', 'PhanUndeclaredProperty' ], ], // 'directory_suppressions' => ['src/directory_name' => ['PhanIssueName1', 'PhanIssueName2']] can be manually added if needed. // (directory_suppressions will currently be ignored by subsequent calls to --save-baseline, but may be preserved in future Phan releases) diff --git a/extension.json b/extension.json index 71ef04d..37c5b2e 100644 --- a/extension.json +++ b/extension.json @@ -6,7 +6,7 @@ "license-name": "GPL-2.0-or-later", "type": "parserhook", "requires": { - "MediaWiki": ">= 1.39.0" + "MediaWiki": ">= 1.43.0" }, "MessagesDirs": { "UsingData": [ @@ -16,15 +16,20 @@ "ExtensionMessagesFiles": { "UsingDataMagic": "UsingData.i18n.magic.php" }, - "AutoloadClasses": { - "UsingDataHooks": "src/UsingDataHooks.php", - "UsingDataPPFrameDOM": "src/UsingDataPPFrameDOM.php" + "AutoloadNamespaces": { + "Fandom\\UsingData\\": "src" }, "Hooks": { - "BeforeParserFetchTemplateRevisionRecord": "UsingDataHooks::onBeforeParserFetchTemplateRevisionRecord", - "GetMagicVariableIDs": "UsingDataHooks::onGetMagicVariableIDs", - "ParserFirstCallInit": "UsingDataHooks::onParserFirstCallInit", - "ParserGetVariableValueSwitch": "UsingDataHooks::ancestorNameVar" + "BeforeParserFetchTemplateRevisionRecord": "main", + "GetMagicVariableIDs": "main", + "ParserFirstCallInit": "main", + "ParserGetVariableValueSwitch": "main" + }, + "HookHandlers": { + "main": { + "class": "Fandom\\UsingData\\UsingDataHooks", + "services": ["TitleFactory", "NamespaceInfo"] + } }, "manifest_version": 1 } diff --git a/src/UsingDataHooks.php b/src/UsingDataHooks.php index 9b7eb24..f9c0690 100644 --- a/src/UsingDataHooks.php +++ b/src/UsingDataHooks.php @@ -1,197 +1,196 @@ setFunctionHook( 'using', [ self::$instance, 'usingParserFunction' ], SFH_OBJECT_ARGS ); - $parser->setFunctionHook( 'usingarg', [ self::$instance, 'usingArgParserFunction' ], SFH_OBJECT_ARGS ); - $parser->setFunctionHook( 'data', [ self::$instance, 'dataParserFunction' ], SFH_OBJECT_ARGS ); - $parser->setFunctionHook( - 'ancestorname', - [ __CLASS__, 'ancestorNameFunction' ], - SFH_OBJECT_ARGS | SFH_NO_HASH - ); - $parser->setHook( 'using', [ self::$instance, 'usingTag' ] ); +class UsingDataHooks implements + ParserFirstCallInitHook, + GetMagicVariableIDsHook, + ParserGetVariableValueSwitchHook, + BeforeParserFetchTemplateRevisionRecordHook +{ + /** @var UsingDataPPFrameDOM[] Data frames for each page */ + private array $dataFrames = []; + + /** @var bool Whether we are currently searching for data */ + private bool $isInDataSearchMode = false; + + public function __construct( + private readonly TitleFactory $titleFactory, + private readonly NamespaceInfo $namespaceInfo, + ) { + } - return true; + public function onParserFirstCallInit( $parser ): void { + $parser->setFunctionHook( 'using', $this->renderFunctionUsing( ... ), SFH_OBJECT_ARGS ); + $parser->setFunctionHook( 'usingarg', $this->renderFunctionUsingArg( ... ), SFH_OBJECT_ARGS ); + $parser->setFunctionHook( 'data', $this->renderFunctionData( ... ), SFH_OBJECT_ARGS ); + $parser->setFunctionHook( 'ancestorname', $this->renderFunctionAcenstorName( ... ), + SFH_OBJECT_ARGS | SFH_NO_HASH ); + $parser->setHook( 'using', $this->renderTagUsing( ... ) ); } /** * Tells MediaWiki that one or more magic word IDs should be treated as variables. - * - * @return void */ - public static function onGetMagicVariableIDs( &$magicWords ) { - $magicWords[] = 'parentname'; - $magicWords[] = 'selfname'; + public function onGetMagicVariableIDs( &$variableIDs ): void { + $variableIDs[] = 'parentname'; + $variableIDs[] = 'selfname'; } /** - * Returns a UsingData frame for a given page + * Handles {{PARENTNAME}}, {{SELFNAME}} */ - private function getDataFrame( $sourcePage, $title, &$parser, $frame ) { - global $wgHooks; - if ( !isset( $this->dataFrames[$sourcePage] ) ) { - $this->dataFrames[$sourcePage] = new UsingDataPPFrameDOM( $frame, $sourcePage ); - if ( $sourcePage != '' - && ( $sourcePage != $parser->getTitle()->getPrefixedText() ) - || $parser->getOptions()->getIsSectionPreview() ) { - [ $text, $fTitle ] = $parser->fetchTemplateAndTitle( $title ); - if ( is_object( $fTitle ) && $fTitle->getPrefixedText() != $sourcePage ) { - $this->dataFrames[$fTitle->getPrefixedText()] = $this->dataFrames[$sourcePage]; - } - if ( is_string( $text ) && $text != '' ) { - $this->searchingForData = true; - $clearStateHooks = $wgHooks['ParserClearState']; - // Other extensions tend to assume the hook is only called by wgParser and reset internal state - $wgHooks['ParserClearState'] = []; - $subParser = clone $parser; - $subParser->preprocess( $text, $fTitle, clone $parser->getOptions() ); - // We might've blocked access to templates while preprocessing; should not be cached - $subParser->clearState(); - if ( $parser->getOutput()->hasText() ) { - $subParser->getOutput()->setText( $parser->getOutput()->getText() ); - } - $wgHooks['ParserClearState'] = empty( $wgHooks['ParserClearState'] ) - ? $clearStateHooks - : array_merge( $clearStateHooks, $wgHooks['ParserClearState'] ); - $parser->mPPNodeCount += $subParser->mPPNodeCount; - $this->searchingForData = false; - } - } + public function onParserGetVariableValueSwitch( + $parser, &$variableCache, $magicWordId, &$ret, $frame + ): void { + if ( $magicWordId == 'parentname' ) { + $ret = $this->getAncestorName( $frame, 1 ); + } + if ( $magicWordId == 'selfname' ) { + $ret = $this->getAncestorName( $frame, 0 ); } - return $this->dataFrames[$sourcePage]; } - /** - * Returns the page title of the $depth ancestor of $frame; empty string if invalid - */ - private static function ancestorNameHandler( $frame, $depth ) { - while ( $depth-- && $frame != null ) { - $frame = $frame->parent ?? null; + public function onBeforeParserFetchTemplateRevisionRecord( + ?LinkTarget $contextTitle, LinkTarget $title, + bool &$skip, ?RevisionRecord &$revRecord + ): bool { + if ( $this->isInDataSearchMode ) { + $skip = true; + return false; } - return is_object( $frame ) && isset( $frame->title ) && is_object( $frame->title ) - ? wfEscapeWikiText( $frame->title->getPrefixedText() ) : ''; + + return true; } /** * Handles {{ANCESTORNAME:depth}} */ - public static function ancestorNameFunction( &$parser, $frame, $args ) { + public function renderFunctionAcenstorName( Parser $parser, PPFrame $frame, array $args ): array { $arg = $frame->expand( $args[0] ); return [ - self::ancestorNameHandler( $frame, max( 0, is_numeric( $arg ) ? intval( $arg ) : 1 ) ), + $this->getAncestorName( $frame, max( 0, is_numeric( $arg ) ? intval( $arg ) : 1 ) ), 'noparse' => true ]; } /** - * Handles {{PARENTNAME}}, {{SELFNAME}}, {{ANCESTORNAME}} + * Returns the page title of the $depth ancestor of $frame; empty string if invalid */ - public static function ancestorNameVar( &$parser, &$varCache, &$index, &$ret, &$frame ) { - if ( $index == 'parentname' ) { - $ret = self::ancestorNameHandler( $frame, 1 ); - } - if ( $index == 'selfname' ) { - $ret = self::ancestorNameHandler( $frame, 0 ); + private function getAncestorName( PPFrame $frame, int $depth ): string { + while ( $depth-- && $frame != null ) { + $frame = $frame->parent ?? null; } - return true; + /** @phan-suppress-next-line PhanUndeclaredProperty PPFrame->title */ + return is_object( $frame ) && isset( $frame->title ) && is_object( $frame->title ) + /** @phan-suppress-next-line PhanUndeclaredProperty PPFrame->title */ + ? wfEscapeWikiText( $frame->title->getPrefixedText() ) : ''; } /** - * Parses common elements of #using syntax. + * Returns a UsingData frame for a given page */ - private function usingParse( &$parser, $frame, $args ) { - if ( $this->searchingForData ) { - return ''; + private function getDataFrame( + string $sourcePage, ?Title $title, Parser $parser, PPFrame $frame + ): UsingDataPPFrameDOM { + if ( !isset( $this->dataFrames[$sourcePage] ) ) { + $this->dataFrames[$sourcePage] = new UsingDataPPFrameDOM( $frame, $sourcePage ); + $parsingTitle = $this->titleFactory->castFromPageReference( $parser->getPage() ); + if ( ( $sourcePage != '' && $sourcePage != $parsingTitle?->getPrefixedText() ) + || $parser->getOptions()->getIsSectionPreview() + ) { + $text = null; + if ( $title ) { + [ $text, $title ] = $parser->fetchTemplateAndTitle( $title ); + } + if ( $title && $title->getPrefixedText() != $sourcePage ) { + $this->dataFrames[$title->getPrefixedText()] = $this->dataFrames[$sourcePage]; + } + if ( $text !== null ) { + $this->makeDataParserAndRun( $parser, + static function ( Parser $dataParser ) use ( $text, $title, $parser ) { + $dataParser->preprocess( $text, $title, clone $parser->getOptions() ); + $parser->mPPNodeCount += $dataParser->mPPNodeCount; + } + ); + } + } } + return $this->dataFrames[$sourcePage]; + } - $titleArg = trim( $frame->expand( $args[0] ) ); - if ( strpos( $titleArg, '%' ) !== false ) { - $titleArg = str_replace( [ '<', '>' ], [ '<', '>' ], urldecode( $titleArg ) ); - } - $title = \Title::newFromText( $titleArg, NS_MAIN ); - $sourcePage = is_object( $title ) ? $title->getPrefixedText() : ''; - $sourceHash = is_object( $title ) ? $title->getFragment() : ''; - $namedArgs = []; + private function makeDataParserAndRun( Parser $parser, callable $callback ): void { + $hookRunnerProperty = new ReflectionProperty( $parser, 'hookRunner' ); + $originalHookRunner = $hookRunnerProperty->getValue( $parser ); - $one = null; - $two = null; - foreach ( $args as $key => $val ) { - if ( $key === 0 ) { - continue; - } - $bits = $val->splitArg(); - // It looks like indexes are now integers, it is a change from legacy implementation - if ( $bits['index'] === 1 ) { - $one = $frame->expand( $bits['value'] ); - } elseif ( $bits['index'] === 2 ) { - $two = $bits['value']; - } elseif ( $bits['index'] === '' ) { - $namedArgs[trim( $frame->expand( $bits['name'] ) )] = $bits['value']; + $hookContainerProperty = new ReflectionProperty( $originalHookRunner, 'container' ); + $hookContainer = $hookContainerProperty->getValue( $originalHookRunner ); + + $newHookRunner = new class ( $hookContainer ) extends HookRunner { + public function onParserClearState( $parser ): bool { + return true; } + }; + + try { + $dataParser = clone $parser; + $hookRunnerProperty->setValue( $dataParser, $newHookRunner ); + $callback( $dataParser ); + } finally { + $this->isInDataSearchMode = false; } - return [ $this->getDataFrame( $sourcePage, $title, $parser, $frame ), $sourceHash, $namedArgs, $one, $two ]; } /** * {{#using:Page#Hash|Template|Default|...}} parses Template using #data from Page's Hash fragment; or Default * if no data from Page can be found. Named arguments override those in the #data tag. */ - public function usingParserFunction( &$parser, $frame, $args ) { - $parse = $this->usingParse( $parser, $frame, $args ); + public function renderFunctionUsing( Parser $parser, PPFrame $frame, array $args ): string { + $parse = $this->parseUsingCommons( $parser, $frame, $args ); if ( !is_array( $parse ) ) { return ''; } - [ $dframe, $fragment, $namedArgs, $templateTitle, $defaultValue ] = $parse; - if ( !$dframe->hasFragment( $fragment ) && $defaultValue !== null ) { + /** @var UsingDataPPFrameDOM $dataFrame */ + [ $dataFrame, $fragment, $namedArgs, $templateTitle, $defaultValue ] = $parse; + if ( !$dataFrame->hasFragment( $fragment ) && $defaultValue !== null ) { return $frame->expand( $defaultValue ); } [ $dom, $title ] = $this->fetchTemplate( $parser, $templateTitle ); - return $dframe->expandUsing( $frame, $title, $dom, $namedArgs, $fragment ); + return $dataFrame->expandOn( $frame, $title, $dom, $namedArgs, $fragment ); } /** * {{#usingarg:Page#Hash|Arg|Default}} returns the value of Arg data field on Page's Hash fragment, Default if * undefined. */ - public function usingArgParserFunction( &$parser, $frame, $args ) { - $parse = $this->usingParse( $parser, $frame, $args ); + public function renderFunctionUsingArg( Parser $parser, PPFrame $frame, array $args ) { + $parse = $this->parseUsingCommons( $parser, $frame, $args ); if ( !is_array( $parse ) ) { return ''; } - [ $dframe, $fragment, $namedArgs, $argName, $defaultValue ] = $parse; - $ret = $dframe->getArgumentForParser( + /** @var UsingDataPPFrameDOM $dataFrame */ + [ $dataFrame, $fragment, $namedArgs, $argName, $defaultValue ] = $parse; + $ret = $dataFrame->getArgumentForParser( $parser, UsingDataPPFrameDOM::normalizeFragment( $fragment ), $argName, @@ -200,6 +199,42 @@ public function usingArgParserFunction( &$parser, $frame, $args ) { return $ret !== false ? $ret : $frame->expand( $defaultValue ); } + /** + * Parses common elements of #using syntax. + */ + private function parseUsingCommons( Parser $parser, PPFrame $frame, array $args ): ?array { + if ( $this->isInDataSearchMode ) { + return null; + } + + $source = trim( $frame->expand( $args[0] ) ); + if ( str_contains( $source, '%' ) ) { + $source = str_replace( [ '<', '>' ], [ '<', '>' ], urldecode( $source ) ); + } + $title = $this->titleFactory->newFromText( $source ); + $sourcePage = is_object( $title ) ? $title->getPrefixedText() : ''; + $sourceFragment = is_object( $title ) ? $title->getFragment() : ''; + $namedArgs = []; + + $one = null; + $two = null; + foreach ( $args as $key => $val ) { + if ( $key === 0 ) { + continue; + } + $bits = $val->splitArg(); + // It looks like indexes are now integers, it is a change from legacy implementation + if ( $bits['index'] === 1 ) { + $one = $frame->expand( $bits['value'] ); + } elseif ( $bits['index'] === 2 ) { + $two = $bits['value']; + } elseif ( $bits['index'] === '' ) { + $namedArgs[trim( $frame->expand( $bits['name'] ) )] = $bits['value']; + } + } + return [ $this->getDataFrame( $sourcePage, $title, $parser, $frame ), $sourceFragment, $namedArgs, $one, $two ]; + } + /** * ... * expands ... using the data from Page's Hash fragment; Default if undefined. @@ -208,27 +243,31 @@ public function usingArgParserFunction( &$parser, $frame, $args ) { * or using insertStripItem directly, is a viable short-term alternative -- but one that call certain hooks * prematurely, potentially causing other extensions to misbehave slightly. */ - public function usingTag( $text, array $args, Parser $parser, PPFrame $frame ) { - if ( $this->searchingForData ) { + public function renderTagUsing( + string $text, array $args, Parser $parser, PPFrame $frame + ): array { + if ( $this->isInDataSearchMode ) { return [ '', 'markerType' => 'none' ]; } $source = isset( $args['page'] ) ? $parser->replaceVariables( $args['page'], $frame ) : ''; unset( $args['page'] ); - if ( strpos( $source, '%' ) !== false ) { + if ( str_contains( $source, '%' ) ) { $source = str_replace( [ '<', '>' ], [ '<', '>' ], urldecode( $source ) ); } - $title = \Title::newFromText( $source, NS_MAIN ); + $title = $this->titleFactory->newFromText( $source ); + if ( is_object( $title ) ) { - $dframe = $this->getDataFrame( $title->getPrefixedText(), $title, $parser, $frame ); - if ( is_object( $dframe ) && $dframe->hasFragment( $title->getFragment() ) ) { + $dataFrame = $this->getDataFrame( $title->getPrefixedText(), $title, $parser, $frame ); + if ( $dataFrame->hasFragment( $title->getFragment() ) ) { $ovr = []; unset( $args['default'] ); foreach ( $args as $key => $val ) { $ovr[$key] = $parser->replaceVariables( $val, $frame ); } return [ - $dframe->expandUsing( $frame, $frame->title, $text, $ovr, $title->getFragment(), true ), + /** @phan-suppress-next-line PhanUndeclaredProperty PPFrame->title */ + $dataFrame->expandOn( $frame, $frame->title, $text, $ovr, $title->getFragment(), true ), 'markerType' => 'none' ]; } @@ -243,66 +282,63 @@ public function usingTag( $text, array $args, Parser $parser, PPFrame $frame ) { /** * {{#data:Template#Hash|...}} specifies data-transcludable arguments for the page; may not be transcluded. */ - public function dataParserFunction( Parser &$parser, PPFrame $frame, $args ) { - $templateTitle = trim( $frame->expand( $args[0] ) ); + public function renderFunctionData( Parser $parser, PPFrame $frame, array $args ): string { + /** @phan-suppress-next-line PhanUndeclaredProperty PPFrame->title */ $hostPage = $frame->title->getPrefixedText(); - unset( $args[0] ); - $fragment = ''; - if ( strpos( $templateTitle, '%' ) !== false ) { - $templateTitle = str_replace( [ '<', '>' ], [ '<', '>' ], urldecode( $templateTitle ) ); + $templateName = trim( $frame->expand( $args[0] ) ); + unset( $args[0] ); + if ( str_contains( $templateName, '%' ) ) { + $templateName = str_replace( [ '<', '>' ], [ '<', '>' ], urldecode( $templateName ) ); } + $templateTitle = $this->titleFactory->newFromText( $templateName, NS_TEMPLATE ); - $templateTitleObj = \Title::newFromText( $templateTitle, NS_TEMPLATE ); - if ( is_object( $templateTitleObj ) ) { - $fragment = $templateTitleObj->getFragment(); - } elseif ( $templateTitle != '' && $templateTitle[0] == '#' ) { - $fragment = substr( $templateTitle, 1 ); + $fragment = ''; + if ( is_object( $templateTitle ) ) { + $fragment = $templateTitle->getFragment(); + } elseif ( $templateName != '' && $templateName[0] == '#' ) { + $fragment = substr( $templateName, 1 ); } - if ( $frame->depth == 0 || $this->searchingForData ) { - if ( !isset( $this->dataFrames[$hostPage] ) ) { - $this->dataFrames[$hostPage] = new UsingDataPPFrameDOM( $frame, $hostPage ); - } - $df =& $this->dataFrames[$hostPage]; - $df->addArgs( $frame, $args, $fragment ); - if ( $this->searchingForData ) { + if ( $frame->depth == 0 || $this->isInDataSearchMode ) { + $this->dataFrames[$hostPage] ??= new UsingDataPPFrameDOM( $frame, $hostPage ); + $this->dataFrames[$hostPage]->addArgs( $frame, $args, $fragment ); + if ( $this->isInDataSearchMode ) { return ''; } } - if ( !is_object( $templateTitleObj ) ) { + if ( !is_object( $templateTitle ) ) { return ''; } - [ $dom, $tTitle ] = $this->fetchTemplate( $parser, $templateTitleObj ); - foreach ( $args as $k => $v ) { - // Line below breaks #data processing, but it exists in old implementation - // $args[$k] = $v->node; - } - $cframe = $frame->newChild( $args, $tTitle ); - $nargs =& $cframe->namedArgs; - $nargs['data-found'] = $frame->depth == 0 ? '3' : '2'; - $nargs['data-source'] = $hostPage; - $nargs['data-sourcee'] = wfEscapeWikiText( $hostPage ); - $nargs['data-fragment'] = $fragment; - $nargs['data-source-fragment'] = $hostPage . ( empty( $fragment ) ? '' : ( '#' . $fragment ) ); - return $cframe->expand( $dom ); + [ $dom, $templateTitle ] = $this->fetchTemplate( $parser, $templateTitle ); + $childFrame = $frame->newChild( $args, $templateTitle ); + /** @phan-suppress-next-line PhanUndeclaredProperty PPFrame->title */ + $childFrame->namedArgs = array_merge( $childFrame->namedArgs ?? [], [ + 'data-found' => $frame->depth == 0 ? '3' : '2', + 'data-source' => $hostPage, + 'data-sourcee' => wfEscapeWikiText( $hostPage ), + 'data-fragment' => $fragment, + 'data-source-fragment' => $hostPage . ( empty( $fragment ) ? '' : ( '#' . $fragment ) ), + ] ); + return $childFrame->expand( $dom ); } /** * Returns template text for transclusion. + * @return array{0:PPNode|string|null,1:Title|null} */ - private function fetchTemplate( $parser, $template ) { - global $wgNonincludableNamespaces; - - if ( $template == '' || ( !is_string( $template ) && !is_object( $template ) ) ) { - return ''; + private function fetchTemplate( Parser $parser, Title|string $template ): array { + if ( $template === '' ) { + return [ null, null ]; } - $title = is_object( $template ) ? $template : \Title::newFromText( $template, NS_TEMPLATE ); + $title = is_object( $template ) ? $template + : $this->titleFactory->newFromText( $template, NS_TEMPLATE ); if ( !is_object( $title ) || $title->getNamespace() == NS_SPECIAL - || ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) ) { + || $this->namespaceInfo->isNonincludable( $title->getNamespace() ) + ) { return is_object( $title ) ? [ '[[:' . $title->getPrefixedText() . ']]', $title ] : [ '[[:' . $template . ']]', null ]; @@ -310,19 +346,4 @@ private function fetchTemplate( $parser, $template ) { [ $dom, $title ] = $parser->getTemplateDom( $title ); return [ $dom ?: ( '[[:' . $title->getPrefixedText() . ']]' ), $title ]; } - - public static function onBeforeParserFetchTemplateRevisionRecord( ?LinkTarget $contextTitle, LinkTarget $title, - bool &$skip, ?RevisionRecord &$revRecord ): bool { - if ( !self::$instance->searchingForData ) { - return true; - } - if ( self::$phTitle === null ) { - self::$phTitle = \Title::newFromText( 'UsingDataPlaceholderTitle', NS_MEDIAWIKI ); - } - - $title = self::$phTitle; - $skip = true; - - return false; - } } diff --git a/src/UsingDataPPFrameDOM.php b/src/UsingDataPPFrameDOM.php index 9926dae..36c5b01 100644 --- a/src/UsingDataPPFrameDOM.php +++ b/src/UsingDataPPFrameDOM.php @@ -1,37 +1,44 @@ preprocessor ); - $this->args = []; $this->parent = $inner; $this->depth = $inner->depth + 1; $this->title = $inner->title; $this->sourcePage = $pageName; } - public static function normalizeFragment( $fragment ) { + public static function normalizeFragment( string $fragment ): string { return str_replace( '#', '# ', strtolower( $fragment ) ) . '##'; } - public function addArgs( $frame, $args, $fragment ) { - if ( $this->pendingArgs === null ) { - $this->pendingArgs = []; - } - + public function addArgs( PPFrame $frame, array $args, string $fragment ): void { $namedArgs = []; $prefix = self::normalizeFragment( $fragment ); foreach ( $args as $k => $arg ) { @@ -48,67 +55,73 @@ public function addArgs( $frame, $args, $fragment ) { $this->knownFragments[$prefix] = true; } - public function expandUsing( PPFrame $frame, $templateTitle, $text, $moreArgs, $fragment, $useRTP = false ) { - $oldParser = $this->expansionForParser; - $oldExpanded = $this->expandedArgs; + public function expandOn( + PPFrame $frame, ?Title $templateTitle, PPNode|string|null $text, + array $moreArgs, string $fragment, bool $useRTP = false + ): string { + if ( !$frame instanceof PPFrame_Hash ) { + throw new InvalidArgumentException( __CLASS__ . ' expects an instance of PPFrame_Hash' ); + } + $oldParser = $this->expansionParser; $oldArgs = $this->overrideArgs; $oldFrame = $this->overrideFrame; $oldFragment = $this->expansionFragment; - $oldTitle =& $this->title; + $oldTitle = $this->title; + $oldExpanded = $this->expandedArgs; - $this->expansionForParser = $frame->parser; - $this->expansionFragment = $fragment; + $this->expansionParser = $frame->parser; $this->overrideArgs = $moreArgs; $this->overrideFrame = $frame; - $this->expansionFragmentN = self::normalizeFragment( $this->expansionFragment ); + $this->expansionFragment = $fragment; + $this->expansionNormalizedFragment = self::normalizeFragment( $this->expansionFragment ); $this->title = is_object( $templateTitle ) ? $templateTitle : $frame->title; if ( $oldParser != null && $oldParser !== $frame->parser && !empty( $this->expandedArgs ) ) { $this->expandedArgs = []; } $ret = is_string( $text ) && $useRTP ? - $this->expansionForParser->replaceVariables( $text, $this ) : + $this->expansionParser->replaceVariables( $text, $this ) : $this->expand( $text === null ? '' : $text ); $this->overrideArgs = $oldArgs; - $this->expansionFragment = $oldFragment; $this->overrideFrame = $oldFrame; - $this->expansionFragmentN = self::normalizeFragment( $this->expansionFragment ); - $this->title =& $oldTitle; + $this->expansionFragment = $oldFragment; + $this->expansionNormalizedFragment = self::normalizeFragment( $this->expansionFragment ); + $this->title = $oldTitle; if ( $oldParser != null ) { - $this->expansionForParser = $oldParser; + $this->expansionParser = $oldParser; $this->expandedArgs = $oldExpanded; } return $ret; } - public function hasFragment( $fragment ) { + public function hasFragment( string $fragment ): bool { return isset( $this->knownFragments[self::normalizeFragment( $fragment )] ); } - public function isEmpty() { - return !isset( $this->knownFragments[$this->expansionFragmentN] ); + public function isEmpty(): bool { + return !isset( $this->knownFragments[$this->expansionNormalizedFragment] ); } - public function getArgumentForParser( $parser, $normalizedFragment, $arg, $default = false ) { - $arg = $normalizedFragment . strval( $arg ); - if ( isset( $this->expandedArgs[$arg] ) && $this->expansionForParser === $parser ) { + public function getArgumentForParser( + Parser $parser, string $normalizedFragment, ?string $arg, string|false $default = false + ): string|false { + $arg = $normalizedFragment . $arg; + if ( isset( $this->expandedArgs[$arg] ) && $this->expansionParser === $parser ) { return $this->expandedArgs[$arg]; } if ( !isset( $this->serializedArgs[$arg] ) ) { - if ( $this->pendingArgs === null ) { + if ( !$this->pendingArgs ) { return $default; } foreach ( $this->pendingArgs as &$aar ) { if ( isset( $aar[1][$arg] ) ) { $text = $aar[1][$arg]; unset( $aar[1][$arg] ); - $text = $aar[0]->expand( $text ); - if ( str_contains( $text, Parser::MARKER_PREFIX ) ) { - $text = $aar[0]->parser->serialiseHalfParsedText( ' ' . $text ); // MW bug 26731 - } - $this->serializedArgs[$arg] = $text; + /** @var PPFrame $frame */ + $frame = $aar[0]; + $this->serializedArgs[$arg] = $frame->expand( $text ); break; } } @@ -118,18 +131,20 @@ public function getArgumentForParser( $parser, $normalizedFragment, $arg, $defau return $default; } - $ret = $this->serializedArgs[$arg]; - $ret = trim( is_array( $ret ) ? $parser->unserialiseHalfParsedText( $ret ) : $ret ); - if ( $parser === $this->expansionForParser ) { + $ret = trim( $this->serializedArgs[$arg] ); + if ( $parser === $this->expansionParser ) { $this->expandedArgs[$arg] = $ret; } return $ret; } - public function getArgument( $index ) { - switch ( $index ) { + /** + * @suppress PhanTypeMismatchReturn + */ + public function getArgument( $name ): string|false { + switch ( $name ) { case 'data-found': - return $this->isEmpty() ? null : '1'; + return $this->isEmpty() ? '' : '1'; case 'data-source': return $this->sourcePage; case 'data-sourcee': @@ -143,14 +158,15 @@ public function getArgument( $index ) { ( '#' . $this->expansionFragment ) ); default: - if ( is_array( $this->overrideArgs ) && isset( $this->overrideArgs[$index] ) ) { - if ( is_object( $this->overrideArgs[$index] ) ) { - $this->overrideArgs[$index] = $this->overrideFrame->expand( $this->overrideArgs[$index] ); + if ( isset( $this->overrideArgs[$name] ) ) { + if ( is_object( $this->overrideArgs[$name] ) ) { + $this->overrideArgs[$name] = $this->overrideFrame->expand( + $this->overrideArgs[$name] ); } - return $this->overrideArgs[$index]; + return $this->overrideArgs[$name]; } - $p = $this->expansionForParser === null ? $this->parent->parser : $this->expansionForParser; - return $this->getArgumentForParser( $p, $this->expansionFragmentN, $index, false ); + $parser = $this->expansionParser ?? $this->parent->parser; + return $this->getArgumentForParser( $parser, $this->expansionNormalizedFragment, $name ); } } }