44 global $wgCategoryTreeDefaultOptions;
45 $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
49 foreach ( $wgCategoryTreeDefaultOptions as $option => $default ) {
50 if ( isset( $options[$option] ) ) {
51 $this->mOptions[$option] = $options[$option];
53 $this->mOptions[$option] = $default;
61 $this->mOptions[
'namespaces'] =
false;
68 if ( $this->mOptions[
'namespaces'] ) {
69 # automatically adjust mode to match namespace filter
70 if ( count( $this->mOptions[
'namespaces'] ) === 1
71 && $this->mOptions[
'namespaces'][0] ==
NS_CATEGORY ) {
73 } elseif ( !in_array(
NS_FILE, $this->mOptions[
'namespaces'] ) ) {
86 return $this->mOptions[$name];
101 if ( $nn ===
false || is_null( $nn ) ) {
105 if ( !is_array( $nn ) ) {
106 $nn = preg_split(
'![\s#:|]+!', $nn );
110 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
111 foreach ( $nn as $n ) {
112 if ( is_int( $n ) ) {
120 $lower = strtolower( $n );
122 if ( is_numeric( $n ) ) {
124 } elseif ( $n ==
'-' || $n ==
'_' || $n ==
'*' || $lower ==
'main' ) {
127 $ns = $contLang->getNsIndex( $n );
131 if ( is_int( $ns ) ) {
136 # get elements into canonical order
146 global $wgCategoryTreeDefaultOptions;
148 if ( is_null( $mode ) ) {
149 return $wgCategoryTreeDefaultOptions[
'mode'];
151 if ( is_int( $mode ) ) {
155 $mode = trim( strtolower( $mode ) );
157 if ( is_numeric( $mode ) ) {
161 if ( $mode ==
'all' ) {
163 } elseif ( $mode ==
'pages' ) {
165 } elseif ( $mode ==
'categories' || $mode ==
'sub' ) {
167 } elseif ( $mode ==
'parents' || $mode ==
'super' || $mode ==
'inverse' ) {
169 } elseif ( $mode ==
'default' ) {
170 $mode = $wgCategoryTreeDefaultOptions[
'mode'];
183 if ( is_null( $value ) ) {
186 if ( is_bool( $value ) ) {
189 if ( is_int( $value ) ) {
190 return ( $value > 0 );
193 $value = trim( strtolower( $value ) );
194 if ( is_numeric( $value ) ) {
195 return ( (
int)$value > 0 );
198 if ( $value ==
'yes' || $value ==
'y'
199 || $value ==
'true' || $value ==
't' || $value ==
'on'
202 } elseif ( $value ==
'no' || $value ==
'n'
203 || $value ==
'false' || $value ==
'f' || $value ==
'off'
206 } elseif ( $value ==
'null' || $value ==
'default' || $value ==
'none' || $value ==
'x' ) {
218 global $wgCategoryTreeDefaultOptions;
220 if ( is_null( $value ) ) {
221 return $wgCategoryTreeDefaultOptions[
'hideprefix'];
223 if ( is_int( $value ) ) {
226 if ( $value ===
true ) {
229 if ( $value ===
false ) {
233 $value = trim( strtolower( $value ) );
235 if ( $value ==
'yes' || $value ==
'y'
236 || $value ==
'true' || $value ==
't' || $value ==
'on'
239 } elseif ( $value ==
'no' || $value ==
'n'
240 || $value ==
'false' || $value ==
'f' || $value ==
'off'
243 } elseif ( $value ==
'always' ) {
245 } elseif ( $value ==
'never' ) {
247 } elseif ( $value ==
'auto' ) {
249 } elseif ( $value ==
'categories' || $value ==
'category' || $value ==
'smart' ) {
252 return $wgCategoryTreeDefaultOptions[
'hideprefix'];
260 public static function setHeaders( OutputPage $outputPage ) {
262 $outputPage->addModuleStyles(
'ext.categoryTree.styles' );
263 $outputPage->addModules(
'ext.categoryTree' );
273 if ( $enc ==
'mode' || $enc ==
'' ) {
274 $opt = $options[
'mode'];
275 } elseif ( $enc ==
'json' ) {
278 throw new Exception(
'Unknown encoding for CategoryTree options: ' . $enc );
291 foreach ( $this->mOptions as $k => $v ) {
292 if ( is_array( $v ) ) {
293 $v = implode(
'|', $v );
295 $key .= $k .
':' . $v .
';';
298 if ( !is_null( $depth ) ) {
299 $key .=
";depth=" . $depth;
309 if ( $depth !==
null ) {
311 $opt[
'depth'] = $depth;
332 public function getTag( Parser $parser =
null, $category, $hideroot =
false, array $attr = [],
333 $depth = 1, $allowMissing =
false
335 global $wgCategoryTreeDisableCache;
337 $category = trim( $category );
338 if ( $category ===
'' ) {
343 if ( $wgCategoryTreeDisableCache ===
true ) {
344 $parser->getOutput()->updateCacheExpiry( 0 );
345 } elseif ( is_int( $wgCategoryTreeDisableCache ) ) {
346 $parser->getOutput()->updateCacheExpiry( $wgCategoryTreeDisableCache );
356 if ( isset( $attr[
'class'] ) ) {
357 $attr[
'class'] .=
' CategoryTreeTag';
359 $attr[
'class'] =
' CategoryTreeTag';
362 $attr[
'data-ct-mode'] = $this->mOptions[
'mode'];
366 $html .= Html::openElement(
'div', $attr );
368 if ( !$allowMissing && !
$title->getArticleID() ) {
369 $html .= Html::openElement(
'span', [
'class' =>
'CategoryTreeNotice' ] );
371 $html .= $parser->recursiveTagParse(
372 wfMessage(
'categorytree-not-found', $category )->plain() );
374 $html .=
wfMessage(
'categorytree-not-found', $category )->parse();
376 $html .= Html::closeElement(
'span' );
397 global $wgCategoryTreeMaxChildren, $wgCategoryTreeUseCategoryTable;
408 $namespaces = $this->
getOption(
'namespaces' );
410 $tables = [
'page',
'categorylinks' ];
411 $fields = [
'page_id',
'page_namespace',
'page_title',
412 'page_is_redirect',
'page_len',
'page_latest',
'cl_to',
416 $options = [
'ORDER BY' =>
'cl_type, cl_sortkey',
'LIMIT' => $wgCategoryTreeMaxChildren ];
419 $joins[
'categorylinks'] = [
'RIGHT JOIN', [
420 'cl_to = page_title',
'page_namespace' =>
NS_CATEGORY
422 $where[
'cl_from'] =
$title->getArticleID();
424 $joins[
'categorylinks'] = [
'JOIN',
'cl_from = page_id' ];
425 $where[
'cl_to'] =
$title->getDBkey();
426 $options[
'USE INDEX'][
'categorylinks'] =
'cl_sortkey';
432 $where[
'page_namespace'] = $namespaces;
435 $where[
'cl_type'] = [
'page',
'subcat' ];
437 $where[
'cl_type'] =
'subcat';
442 # fetch member count if possible
443 $doCount = !$inverse && $wgCategoryTreeUseCategoryTable;
446 $tables = array_merge( $tables, [
'category' ] );
447 $fields = array_merge( $fields, [
448 'cat_id',
'cat_title',
'cat_subcats',
'cat_pages',
'cat_files'
450 $joins[
'category'] = [
'LEFT JOIN', [
451 'cat_title = page_title',
'page_namespace' =>
NS_CATEGORY ]
455 $res =
$dbr->select( $tables, $fields, $where, __METHOD__, $options, $joins );
457 # collect categories separately from other pages
461 foreach (
$res as $row ) {
462 # NOTE: in inverse mode, the page record may be null, because we use a right join.
463 # happens for categories with no category page (red cat links)
464 if ( $inverse && $row->page_title ===
null ) {
467 # TODO: translation support; ideally added to Title object
473 if ( $doCount && $row->page_namespace ==
NS_CATEGORY ) {
486 return $categories . $other;
495 global $wgCategoryTreeMaxChildren;
502 [
'cl_from' =>
$title->getArticleID() ],
505 'LIMIT' => $wgCategoryTreeMaxChildren,
506 'ORDER BY' =>
'cl_to'
514 foreach (
$res as $row ) {
522 $s .= $this->linkRenderer->makeLink(
525 [
'class' =>
'CategoryTreeLabel' ],
541 global $wgCategoryTreeUseCategoryTable;
565 $ns =
$title->getNamespace();
566 $key =
$title->getDBkey();
568 $hideprefix = $this->
getOption(
'hideprefix' );
584 $label =
$title->getText();
586 $label =
$title->getPrefixedText();
589 $link = $this->linkRenderer->makeLink(
$title, $label );
594 # NOTE: things in CategoryTree.js rely on the exact order of tags!
595 # Specifically, the CategoryTreeChildren div must be the first
596 # sibling with nodeName = DIV of the grandparent of the expland link.
601 $attr = [
'class' =>
'CategoryTreeBullet' ];
606 $count = intval( $cat->getSubcatCount() );
608 $count = intval( $cat->getPageCount() ) - intval( $cat->getFileCount() );
610 $count = intval( $cat->getPageCount() );
613 if ( $count === 0 ) {
614 $bullet =
wfMessage(
'categorytree-empty-bullet' )->escaped() .
' ';
615 $attr[
'class'] =
'CategoryTreeEmptyBullet';
619 $linkattr[
'class' ] =
"CategoryTreeToggle";
620 $linkattr[
'data-ct-title'] = $key;
622 if ( $children == 0 ) {
626 $txt =
wfMessage(
'categorytree-expand-bullet' )->plain();
627 $linkattr[
'data-ct-state' ] =
'collapsed';
629 $txt =
wfMessage(
'categorytree-collapse-bullet' )->plain();
630 $linkattr[
'data-ct-loaded' ] =
true;
631 $linkattr[
'data-ct-state' ] =
'expanded';
634 $bullet = Html::element(
'span', $linkattr, $txt ) .
' ';
637 $bullet =
wfMessage(
'categorytree-page-bullet' )->escaped();
643 if ( $count !==
false && $this->
getOption(
'showcount' ) ) {
651 'class' =>
'CategoryTreeChildren',
652 'style' => $children > 0 ?
"display:block" :
"display:none"
658 if ( $children ==
'' ) {
661 $s .=
wfMessage(
'categorytree-no-subcategories' )->escaped();
663 $s .=
wfMessage(
'categorytree-no-pages' )->escaped();
665 $s .=
wfMessage(
'categorytree-no-parent-categories' )->escaped();
667 $s .=
wfMessage(
'categorytree-nothing-found' )->escaped();
692 # Get counts, with conversion to integer so === works
693 # Note: $allCount is the total number of cat members,
694 # not the count of how many members are normal pages.
695 $allCount = $cat ? intval( $cat->getPageCount() ) : 0;
696 $subcatCount = $cat ? intval( $cat->getSubcatCount() ) : 0;
697 $fileCount = $cat ? intval( $cat->getFileCount() ) : 0;
698 $pages = $allCount - $subcatCount - $fileCount;
701 'title' =>
$context->msg(
'categorytree-member-counts' )
702 ->numParams( $subcatCount, $pages, $fileCount, $allCount, $countMode )->text(),
703 # numbers and commas get messed up in a mixed dir env
704 'dir' =>
$context->getLanguage()->getDir()
706 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
707 $s = $contLang->getDirMark() .
' ';
709 # Create a list of category members with only non-zero member counts
711 if ( $subcatCount ) {
712 $memberNums[] =
$context->msg(
'categorytree-num-categories' )
713 ->numParams( $subcatCount )->text();
716 $memberNums[] =
$context->msg(
'categorytree-num-pages' )->numParams( $pages )->text();
719 $memberNums[] =
$context->msg(
'categorytree-num-files' )
720 ->numParams( $fileCount )->text();
722 $memberNumsShort = $memberNums
723 ?
$context->getLanguage()->commaList( $memberNums )
724 :
$context->msg(
'categorytree-num-empty' )->text();
726 # Only $5 is actually used in the default message.
727 # Other arguments can be used in a customized message.
731 $context->msg(
'categorytree-member-num' )
733 ->params( $subcatCount, $pages, $fileCount, $allCount, $memberNumsShort )
748 if ( strval(
$title ) ===
'' ) {
752 # The title must be in the category namespace
753 # Ignore a leading Category: if there is one
758 $title =
"Category:$title";
772 global $wgCategoryTreeMaxDepth;
774 if ( is_numeric( $depth ) ) {
775 $depth = intval( $depth );
780 if ( is_array( $wgCategoryTreeMaxDepth ) ) {
781 $max = isset( $wgCategoryTreeMaxDepth[$mode] ) ? $wgCategoryTreeMaxDepth[$mode] : 1;
782 } elseif ( is_numeric( $wgCategoryTreeMaxDepth ) ) {
783 $max = $wgCategoryTreeMaxDepth;
785 wfDebug(
'CategoryTree::capDepth: $wgCategoryTreeMaxDepth is invalid.' );
789 return min( $depth, $max );