diff --git a/includes/CitizenTemplate.php b/includes/CitizenTemplate.php index 4ef8212d..11eaffde 100644 --- a/includes/CitizenTemplate.php +++ b/includes/CitizenTemplate.php @@ -45,12 +45,15 @@ class CitizenTemplate extends BaseTemplate { 'msg-citizen-header-menu-toggle' => $this->getMsg( 'citizen-header-menu-toggle' )->text(), 'data-menu' => $this->buildMenu(), 'msg-citizen-header-search-toggle' => $this->getMsg( 'citizen-header-search-toggle' )->text(), - 'data-extratools' => $this->buildExtraTools(), + 'data-extratools' => $this->getExtraTools(), 'data-searchbox' => $this->buildSearchbox(), ], 'html-sitenotice' => $this->get( 'sitenotice', null ), 'html-indicators' => $this->getIndicators(), + + 'data-pagetools' => $this->buildPageTools(), + // From Skin::getNewtalks(). Always returns string, cast to null if empty 'html-newtalk' => $this->get( 'newtalk', '' ) ?: null, 'page-langcode' => $this->getSkin()->getTitle()->getPageViewLanguage()->getHtmlCode(), @@ -74,6 +77,9 @@ class CitizenTemplate extends BaseTemplate { 'html-bodycontent' => $this->get( 'bodycontent' ), 'html-printfooter' => $this->get( 'printfooter', null ), + + 'data-pagelinks' => $this->buildPageLinks(), + 'html-catlinks' => $this->get( 'catlinks', '' ), 'html-dataAfterContent' => $this->get( 'dataAfterContent', '' ), // From MWDebug::getHTMLDebugLog (when $wgShowDebug is enabled) @@ -95,23 +101,6 @@ class CitizenTemplate extends BaseTemplate { 'data-bottombar' => $this->buildBottombar(), ]; - // TODO: Convert to Mustache - ob_start(); - - $html = $this->getPageTools(); - - echo $html; - $params['html-unported-pagetools'] = ob_get_contents(); - ob_end_clean(); - - ob_start(); - - $html = $this->getPageLinks(); - - echo $html; - $params['html-unported-pagelinks'] = ob_get_contents(); - ob_end_clean(); - // Prepare and output the HTML response $templates = new TemplateParser( __DIR__ . '/templates' ); echo $templates->processTemplate( 'skin', $params ); @@ -233,10 +222,10 @@ class CitizenTemplate extends BaseTemplate { } /** - * Render notification badges and ULS button + * Echo notification badges and ULS button * @return array */ - private function buildExtratools() { + private function getExtratools() { $personalTools = $this->getPersonalTools(); // Create the Echo badges and ULS @@ -251,12 +240,12 @@ class CitizenTemplate extends BaseTemplate { $extraTools['uls'] = $personalTools['uls']; } - $extraToolsPortal = $this->getMenuData( 'personal-extra', $extraTools ); + $html = $this->getMenuData( 'personal-extra', $extraTools ); // Hide label for extra tools - $extraToolsPortal[ 'label-class' ] .= 'screen-reader-text'; + $html[ 'label-class' ] .= 'screen-reader-text'; - return $extraToolsPortal; + return $html; } /** @@ -281,6 +270,88 @@ class CitizenTemplate extends BaseTemplate { return $props; } + /** + * Render page-related tools + * Possible visibility conditions: + * * true: always visible (bool) + * * false: never visible (bool) + * * 'login': only visible if logged in (string) + * * 'permission-*': only visible if user has permission + * e.g. permission-edit = only visible if user can edit pages + * @return string html + */ + protected function buildPageTools() { + $config = $this->config; + $skin = $this->getSkin(); + $condition = $config->get( 'CitizenShowPageTools' ); + $contentNavigation = $this->data['content_navigation']; + $props = []; + + // Login-based condition, return true if condition is met + if ( $condition === 'login' ) { + $condition = $skin()->getUser()->isLoggedIn(); + } + + // Permission-based condition, return true if condition is met + if ( is_string( $condition ) && strpos( $condition, 'permission' ) === 0 ) { + $permission = substr( $condition, 11 ); + try { + $condition = MediaWikiServices::getInstance()->getPermissionManager()->userCan( + $permission, $skin->getUser(), $skin->getTitle() ); + } catch ( Exception $e ) { + $condition = false; + } + } + + if ( $condition === true ) { + + $actionhtml = $this->getMenuData( 'views', $contentNavigation[ 'views' ] ?? [] ); + $actionmorehtml = $this->getMenuData( 'actions', $contentNavigation[ 'actions' ] ?? [] ); + + if ( $actionhtml ) { + $actionhtml[ 'label-class' ] .= 'screen-reader-text'; + } + + if ( $actionmorehtml ) { + $actionmorehtml[ 'label-class' ] .= 'screen-reader-text'; + } + + $props = [ + 'data-page-actions' => $actionhtml, + 'data-page-actions-more' => $actionmorehtml, + ]; + } + + return $props; + } + + + /** + * Render page-related links at the bottom + * @return string html + */ + private function buildPageLinks() : array { + $contentNavigation = $this->data['content_navigation']; + + $namespaceshtml = $this->getMenuData( 'namespaces', $contentNavigation[ 'namespaces' ] ?? [] ); + $variantshtml = $this->getMenuData( 'variants', $contentNavigation[ 'variants' ] ?? [] ); + + if ( $namespaceshtml ) { + $namespaceshtml[ 'label-class' ] .= 'screen-reader-text'; + } + + if ( $variantshtml ) { + $variantshtml[ 'label-class' ] .= 'screen-reader-text'; + } + + $props = [ + 'data-namespaces' => $namespaceshtml, + 'data-variants' => $variantshtml, + ]; + + return $props; + } + /** * Render the bottom bar * TODO: Convert button text to i18n message. @@ -301,261 +372,6 @@ class CitizenTemplate extends BaseTemplate { return $props; } - /** - * Language variants. Displays list for converting between different scripts in the same language, - * if using a language where this is applicable (such as latin vs cyric display for serbian). - * - * @return string html - */ - protected function getVariants() { - $html = ''; - if ( count( $this->data['content_navigation']['variants'] ) > 0 ) { - $html .= $this->getPortlet( 'variants', $this->data['content_navigation']['variants'] ); - } - - return $html; - } - - /** - * Generates page-related tools - * Possible visibility conditions: - * * true: always visible (bool) - * * false: never visible (bool) - * * 'login': only visible if logged in (string) - * * 'permission-*': only visible if user has permission - * e.g. permission-edit = only visible if user can edit pages - * @return string html - */ - protected function getPageTools() { - $html = ''; - - try { - $condition = $this->config->get( 'CitizenShowPageTools' ); - } catch ( ConfigException $e ) { - $condition = false; - } - - if ( $condition === 'login' ) { - $condition = $this->getSkin()->getUser()->isLoggedIn(); - } - - if ( is_string( $condition ) && strpos( $condition, 'permission' ) === 0 ) { - $permission = substr( $condition, 11 ); - try { - $condition = MediaWikiServices::getInstance()->getPermissionManager()->userCan( - $permission, $this->getSkin()->getUser(), $this->getSkin()->getTitle() ); - } catch ( Exception $e ) { - $condition = false; - } - } - - // Only display if user is logged in - if ( $condition === true ) { - $html .= Html::openElement( 'div', [ 'class' => 'mw-side', 'id' => 'page-tools' ] ); - // 'View' actions for the page: view, edit, view history, etc - $html .= $this->getPortlet( 'views', $this->data['content_navigation']['views'] ); - // Other actions for the page: move, delete, protect, everything else - $html .= $this->getPortlet( 'actions', $this->data['content_navigation']['actions'] ); - $html .= Html::closeElement( 'div' ); - } - - return $html; - } - - /** - * Generates page-related links at the bottom - * @return string html - */ - protected function getPageLinks() { - // Namespaces: links for 'content' and 'talk' for namespaces with talkpages. - // Otherwise is just the content. - // Usually rendered as tabs on the top of the page. - $html = $this->getPortlet( 'namespaces', $this->data['content_navigation']['namespaces'] ); - - // Language variant options - - return $html . $this->getVariants(); - } - - /** - * Generates a block of navigation links with a header - * - * @param string $name - * @param array|string $content array of links for use with makeListItem, or a block of text - * @param null|string|array $msg - * @param array $setOptions random crap to rename/do/whatever - * - * @return string html - */ - protected function getPortlet( $name, $content, $msg = null, $setOptions = [] ) { - // random stuff to override with any provided options - $options = $setOptions + [ - // extra classes/ids - 'id' => 'p-' . $name, - 'class' => 'mw-portlet', - 'extra-classes' => '', - // what to wrap the body list in, if anything - 'body-wrapper' => 'nav', - 'body-id' => null, - 'body-class' => 'mw-portlet-body', - // makeListItem options - 'list-item' => [ 'text-wrapper' => [ 'tag' => 'span' ] ], - // option to stick arbitrary stuff at the beginning of the ul - 'list-prepend' => '', - // old toolbox hook support (use: [ 'SkinTemplateToolboxEnd' => [ &$skin, true ] ]) - 'hooks' => '', - ]; - - // Handle the different $msg possibilities - if ( $msg === null ) { - $msg = $name; - } elseif ( is_array( $msg ) ) { - $msgString = array_shift( $msg ); - $msgParams = $msg; - $msg = $msgString; - } - $msgObj = $this->getMsg( $msg ); - if ( $msgObj->exists() ) { - if ( isset( $msgParams ) && !empty( $msgParams ) ) { - $msgString = $this->getMsg( $msg, $msgParams )->parse(); - } else { - $msgString = $msgObj->parse(); - } - } else { - $msgString = htmlspecialchars( $msg ); - } - - $labelId = Sanitizer::escapeIdForAttribute( "p-$name-label" ); - - if ( is_array( $content ) ) { - $contentText = - Html::openElement( 'ul', - [ 'lang' => $this->get( 'userlang' ), 'dir' => $this->get( 'dir' ) ] ); - $contentText .= $options['list-prepend']; - foreach ( $content as $key => $item ) { - $contentText .= $this->makeListItem( $key, $item, $options['list-item'] ); - } - // Compatibility with extensions still using SkinTemplateToolboxEnd or similar - if ( is_array( $options['hooks'] ) ) { - foreach ( $options['hooks'] as $hook ) { - if ( is_string( $hook ) ) { - $hookOptions = []; - } else { - // it should only be an array otherwise - $hookOptions = array_values( $hook )[0]; - $hook = array_keys( $hook )[0]; - } - $contentText .= $this->deprecatedHookHack( $hook, $hookOptions ); - } - } - - $contentText .= Html::closeElement( 'ul' ); - } else { - $contentText = $content; - } - - // Special handling for role=search and other weird things - $divOptions = [ - 'role' => 'navigation', - 'id' => Sanitizer::escapeIdForAttribute( $options['id'] ), - 'title' => Linker::titleAttrib( $options['id'] ), - 'aria-labelledby' => $labelId, - ]; - if ( !is_array( $options['class'] ) ) { - $class = [ $options['class'] ]; - } - if ( !is_array( $options['extra-classes'] ) ) { - $extraClasses = [ $options['extra-classes'] ]; - } - $divOptions['class'] = - array_merge( $class ?? $options['class'], $extraClasses ?? $options['extra-classes'] ); - - $labelOptions = [ - 'id' => $labelId, - 'lang' => $this->get( 'userlang' ), - 'dir' => $this->get( 'dir' ), - ]; - - if ( $options['body-wrapper'] !== 'none' ) { - $bodyDivOptions = [ 'class' => $options['body-class'] ]; - if ( is_string( $options['body-id'] ) ) { - $bodyDivOptions['id'] = $options['body-id']; - } - $body = - Html::rawElement( $options['body-wrapper'], $bodyDivOptions, - $contentText . $this->getAfterPortlet( $name ) ); - } else { - $body = $contentText . $this->getAfterPortlet( $name ); - } - - return Html::rawElement( 'div', $divOptions, - Html::rawElement( 'h3', $labelOptions, $msgString ) . $body ); - } - - /** - * Wrapper to catch output of old hooks expecting to write directly to page - * We no longer do things that way. - * - * @param string $hook event - * @param array $hookOptions args - * - * @return string html - */ - protected function deprecatedHookHack( $hook, $hookOptions = [] ) { - ob_start(); - try { - Hooks::run( $hook, $hookOptions ); - } catch ( Exception $e ) { - // Do nothing - } - - $hookContents = ob_get_clean(); - if ( !trim( $hookContents ) ) { - $hookContents = ''; - } - - return $hookContents; - } - - /** - * @param string $label to be used to derive the id and human readable label of the menu - * If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the - * human readable text instead. - * @param array $urls to convert to list items stored as string in html-items key - * @param array $options (optional) to be passed to makeListItem - * @return array - */ - private function getMenuData( - string $label, - array $urls = [], - array $options = [] - ) : array { - // For some menu items, there is no language key corresponding with its menu key. - // These inconsitencies are captured in MENU_LABEL_KEYS - $msgObj = $this->getMsg( self::MENU_LABEL_KEYS[ $label ] ?? $label ); - $props = [ - 'id' => "p-$label", - 'label-class' => '', - 'label-id' => "p-{$label}-label", - // If no message exists fallback to plain text (T252727) - 'label' => $msgObj->exists() ? $msgObj->text() : $label, - 'html-items' => '', - 'html-tooltip' => Linker::tooltip( 'p-' . $label ), - ]; - - foreach ( $urls as $key => $item ) { - $props['html-items'] .= $this->makeListItem( $key, $item, $options ); - } - - $props['html-after-portal'] = $this->getAfterPortlet( $label ); - - // Mark the portal as empty if it has no content - $class = ( count( $urls ) == 0 && !$props['html-after-portal'] ) - ? 'mw-portal-empty' : ''; - $props['class'] = $class; - return $props; - } - /** * Get last modified message * @return string html @@ -659,4 +475,43 @@ class CitizenTemplate extends BaseTemplate { return $footerRows; } + + /** + * @param string $label to be used to derive the id and human readable label of the menu + * If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the + * human readable text instead. + * @param array $urls to convert to list items stored as string in html-items key + * @param array $options (optional) to be passed to makeListItem + * @return array + */ + private function getMenuData( + string $label, + array $urls = [], + array $options = [] + ) : array { + // For some menu items, there is no language key corresponding with its menu key. + // These inconsitencies are captured in MENU_LABEL_KEYS + $msgObj = $this->getMsg( self::MENU_LABEL_KEYS[ $label ] ?? $label ); + $props = [ + 'id' => "p-$label", + 'label-class' => null, + 'label-id' => "p-{$label}-label", + // If no message exists fallback to plain text (T252727) + 'label' => $msgObj->exists() ? $msgObj->text() : $label, + 'html-items' => '', + 'html-tooltip' => Linker::tooltip( 'p-' . $label ), + ]; + + foreach ( $urls as $key => $item ) { + $props['html-items'] .= $this->makeListItem( $key, $item, $options ); + } + + $props['html-after-portal'] = $this->getAfterPortlet( $label ); + + // Mark the portal as empty if it has no content + $class = ( count( $urls ) == 0 && !$props['html-after-portal'] ) + ? ' mw-portal-empty' : ''; + $props['class'] = $class; + return $props; + } } diff --git a/includes/templates/skin.mustache b/includes/templates/skin.mustache index e78d9ead..2d7471d7 100644 --- a/includes/templates/skin.mustache +++ b/includes/templates/skin.mustache @@ -36,7 +36,12 @@ {{/html-sitenotice}} {{#html-newtalk}}
{{/html-newtalk}} {{{html-indicators}}} - {{{html-unported-pagetools}}} + {{#data-pagetools}} +