categoryCache = $categoryCache; $this->config = $config; $this->dbProvider = $dbProvider; $this->linkRenderer = $linkRenderer; $this->titleFormatter = $titleFormatter; } /** * @param Parser $parser */ public function onParserFirstCallInit( $parser ) { if ( !$this->config->get( 'CategoryTreeAllowTag' ) ) { return; } $parser->setHook( 'categorytree', [ $this, 'parserHook' ] ); $parser->setFunctionHook( 'categorytree', [ $this, 'parserFunction' ] ); } /** * Entry point for the {{#categorytree}} tag parser function. * This is a wrapper around Hooks::parserHook * @param Parser $parser * @param string ...$params * @return array|string */ public function parserFunction( Parser $parser, ...$params ) { // first user-supplied parameter must be category name if ( !$params ) { // no category specified, return nothing return ''; } $cat = array_shift( $params ); // build associative arguments from flat parameter list $argv = []; foreach ( $params as $p ) { if ( preg_match( '/^\s*(\S.*?)\s*=\s*(.*?)\s*$/', $p, $m ) ) { $k = $m[1]; // strip any quotes enclosing the value $v = preg_replace( '/^"\s*(.*?)\s*"$/', '$1', $m[2] ); } else { $k = trim( $p ); $v = true; } $argv[$k] = $v; } if ( $parser->getOutputType() === Parser::OT_PREPROCESS ) { return Html::rawElement( 'categorytree', $argv, $cat ); } else { // now handle just like a tag $html = $this->parserHook( $cat, $argv, $parser ); return [ $html, 'noparse' => true, 'isHTML' => true ]; } } /** * Obtain a category sidebar link based on config * @return bool|string of link */ private function getCategorySidebarBox() { if ( !$this->config->get( 'CategoryTreeSidebarRoot' ) ) { return false; } return $this->parserHook( $this->config->get( 'CategoryTreeSidebarRoot' ), $this->config->get( 'CategoryTreeSidebarOptions' ) ); } /** * Hook implementation for injecting a category tree into the sidebar. * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name. * @param Skin $skin * @param array &$sidebar */ public function onSkinBuildSidebar( $skin, &$sidebar ) { $html = $this->getCategorySidebarBox(); if ( $html ) { $sidebar['categorytree-portlet'] = []; CategoryTree::setHeaders( $skin->getOutput() ); } } /** * Hook implementation for injecting a category tree link into the sidebar. * Only does anything if $wgCategoryTreeSidebarRoot is set to a category name. * @param Skin $skin * @param string $portlet * @param string &$html */ public function onSkinAfterPortlet( $skin, $portlet, &$html ) { if ( $portlet === 'categorytree-portlet' ) { $box = $this->getCategorySidebarBox(); if ( $box ) { $html .= $box; } } } /** * Entry point for the tag parser hook. * This loads CategoryTree and calls CategoryTree::getTag() * @param string|null $cat * @param array $argv * @param Parser|null $parser * @return bool|string */ public function parserHook( ?string $cat, array $argv, ?Parser $parser = null ) { if ( $parser ) { $parserOutput = $parser->getOutput(); $parserOutput->addModuleStyles( [ 'ext.categoryTree.styles' ] ); $parserOutput->addModules( [ 'ext.categoryTree' ] ); $disableCache = $this->config->get( 'CategoryTreeDisableCache' ); if ( $disableCache === true ) { $parserOutput->updateCacheExpiry( 0 ); } elseif ( is_int( $disableCache ) ) { $parserOutput->updateCacheExpiry( $disableCache ); } } $ct = new CategoryTree( $argv, $this->config, $this->dbProvider, $this->linkRenderer ); $attr = Sanitizer::validateTagAttributes( $argv, 'div' ); $hideroot = isset( $argv['hideroot'] ) ? OptionManager::decodeBoolean( $argv['hideroot'] ) : false; $onlyroot = isset( $argv['onlyroot'] ) ? OptionManager::decodeBoolean( $argv['onlyroot'] ) : false; $depthArg = isset( $argv['depth'] ) ? (int)$argv['depth'] : 1; $depth = $ct->optionManager->capDepth( $depthArg ); if ( $onlyroot ) { $depth = 0; $message = '' . wfMessage( 'categorytree-onlyroot-message' )->inContentLanguage()->parse() . ''; if ( $parser ) { $parser->getOutput()->addWarningMsg( 'categorytree-deprecation-warning' ); $parser->addTrackingCategory( 'categorytree-deprecation-category' ); } } else { $message = ''; } return $message . $ct->getTag( $cat ?? '', $hideroot, $attr, $depth ); } /** * OutputPageRenderCategoryLink hook * @param OutputPage $out * @param ProperPageIdentity $categoryTitle * @param string $text * @param ?string &$link * @return void */ public function onOutputPageRenderCategoryLink( OutputPage $out, ProperPageIdentity $categoryTitle, string $text, ?string &$link ): void { if ( !$this->config->get( 'CategoryTreeHijackPageCategories' ) ) { // Not enabled, don't do anything return; } if ( !$categoryTitle->exists() ) { // Category doesn't exist. Let the normal LinkRenderer generate the link. return; } CategoryTree::setHeaders( $out ); $options = $this->config->get( 'CategoryTreePageCategoryOptions' ); $link = $this->parserHook( $this->titleFormatter->getPrefixedText( $categoryTitle ), $options ); } /** * Get exported data for the "ext.categoryTree" ResourceLoader module. * * @internal For use in extension.json only. * @param RL\Context $context * @param Config $config * @return array Data to be serialised as data.json */ public static function getDataForJs( RL\Context $context, Config $config ) { // Look, this is pretty bad but CategoryTree is just whacky, it needs to be rewritten $optionManager = new OptionManager( $config->get( 'CategoryTreeCategoryPageOptions' ), $config ); return [ 'defaultCtOptions' => $optionManager->getOptionsAsJsStructure(), ]; } /** * Hook handler for the SpecialTrackingCategories::preprocess hook * @param SpecialPage $specialPage SpecialTrackingCategories object * @param array $trackingCategories [ 'msg' => LinkTarget, 'cats' => LinkTarget[] ] * @phan-param array $trackingCategories */ public function onSpecialTrackingCategories__preprocess( $specialPage, $trackingCategories ) { $categoryTargets = []; foreach ( $trackingCategories as $data ) { foreach ( $data['cats'] as $catTitle ) { $categoryTargets[] = $catTitle; } } $this->categoryCache->doQuery( $categoryTargets ); } /** * Hook handler for the SpecialTrackingCategories::generateCatLink hook * @param SpecialPage $specialPage SpecialTrackingCategories object * @param LinkTarget $catTitle LinkTarget object of the linked category * @param string &$html Result html */ public function onSpecialTrackingCategories__generateCatLink( $specialPage, $catTitle, &$html ) { $cat = $this->categoryCache->getCategory( $catTitle ); $html .= CategoryTree::createCountString( $specialPage->getContext(), $cat, 0 ); } /** * @param string $type * @param IResultWrapper $res */ public function onCategoryViewer__doCategoryQuery( $type, $res ) { if ( $type === 'subcat' && $res ) { $this->categoryCache->fillFromQuery( $res ); } } /** * @param string $type * @param Title $title * @param string $html * @param string &$link * @return bool */ public function onCategoryViewer__generateLink( $type, $title, $html, &$link ) { if ( $type !== 'subcat' || $link !== null ) { return true; } $request = RequestContext::getMain()->getRequest(); if ( $request->getCheck( 'notree' ) ) { return true; } $options = $this->config->get( 'CategoryTreeCategoryPageOptions' ); $mode = $request->getRawVal( 'mode' ); if ( $mode !== null ) { $options['mode'] = $mode; } $tree = new CategoryTree( $options, $this->config, $this->dbProvider, $this->linkRenderer ); $cat = $this->categoryCache->getCategory( $title ); $link = $tree->renderNodeInfo( $title, $cat ); CategoryTree::setHeaders( RequestContext::getMain()->getOutput() ); return false; } }