From 698752e38aa0dd36fbae7362a97c2a4e795d8972 Mon Sep 17 00:00:00 2001 From: jdlrobson Date: Fri, 14 Feb 2020 12:33:54 -0800 Subject: [PATCH] [Dev] processTemplate used in one place Moving all the templateParser calls to one function so its easier to see how the template is composed. The diff of changes to the stories folder highlight the internal changes which are: * html-portals replaced with html-sidebar in main template * new Sidebar template added which outputs to html-sidebar * Mention of "MainMenu" replaced with better understood "Sidebar" This is precursory work to adopt templatePartials Change-Id: I6b2196e39087f818e774d04b2d1b9ab8cb8816a1 --- includes/VectorTemplate.php | 101 ++++++++++-------- includes/templates/Navigation.mustache | 10 +- includes/templates/Portal.mustache | 5 +- includes/templates/Sidebar.mustache | 13 +++ stories/navigation.stories.js | 21 +--- ...rtal.stories.js => portal.stories.data.js} | 40 +++---- stories/portals.stories.js | 11 ++ stories/sidebar.stories.js | 51 +++++++++ .../integration/VectorTemplateTest.php | 26 ++--- 9 files changed, 166 insertions(+), 112 deletions(-) create mode 100644 includes/templates/Sidebar.mustache rename stories/{portal.stories.js => portal.stories.data.js} (95%) create mode 100644 stories/portals.stories.js create mode 100644 stories/sidebar.stories.js diff --git a/includes/VectorTemplate.php b/includes/VectorTemplate.php index 51d8f46..c5b3f6d 100644 --- a/includes/VectorTemplate.php +++ b/includes/VectorTemplate.php @@ -87,6 +87,7 @@ class VectorTemplate extends BaseTemplate { $htmlHookVectorBeforeFooter = ob_get_contents(); ob_end_clean(); + $tp = $this->getTemplateParser(); // Naming conventions for Mustache parameters: // - Prefix "is" for boolean values. // - Prefix "msg-" for interface messages. @@ -134,37 +135,43 @@ class VectorTemplate extends BaseTemplate { 'html-debuglog' => $this->get( 'debughtml', '' ), // From BaseTemplate::getTrail (handles bottom JavaScript) 'html-printtail' => $this->getTrail() . '', - 'html-footer' => $this->getTemplateParser()->processTemplate( 'Footer', [ + 'html-footer' => $tp->processTemplate( 'Footer', [ 'html-userlangattributes' => $this->get( 'userlangattributes', '' ), 'html-hook-vector-before-footer' => $htmlHookVectorBeforeFooter, 'array-footer-rows' => $this->getTemplateFooterRows(), ] ), - 'html-navigation' => $this->getTemplateParser()->processTemplate( 'Navigation', [ + 'html-navigation' => $tp->processTemplate( 'Navigation', [ 'html-navigation-heading' => $this->getMsg( 'navigation-heading' ), - 'html-personal-menu' => $this->renderPersonalComponent(), - 'html-navigation-left-tabs' => $this->renderNamespacesComponent() . - $this->renderVariantsComponent(), - 'html-navigation-right-tabs' => $this->renderViewsComponent() - . $this->renderActionsComponent() . $this->renderSearchComponent(), - 'html-logo-attributes' => Xml::expandAttributes( - Linker::tooltipAndAccesskeyAttribs( 'p-logo' ) + [ - 'class' => 'mw-wiki-logo', - 'href' => Skin::makeMainPageUrl(), - ] + 'html-personal-menu' => $tp->processTemplate( 'PersonalMenu', $this->buildPersonalProps() ), + 'html-navigation-left-tabs' => + $tp->processTemplate( 'VectorTabs', $this->buildNamespacesProps() ) + . $tp->processTemplate( 'VectorMenu', $this->buildVariantsProps() ), + 'html-navigation-right-tabs' => + $tp->processTemplate( 'VectorTabs', $this->buildViewsProps() ) + . $tp->processTemplate( 'VectorMenu', $this->buildActionsProps() ) + . $tp->processTemplate( 'SearchBox', $this->buildSearchProps() ), + 'html-sidebar' => $tp->processTemplate( 'Sidebar', + [ + 'html-logo-attributes' => Xml::expandAttributes( + Linker::tooltipAndAccesskeyAttribs( 'p-logo' ) + [ + 'class' => 'mw-wiki-logo', + 'href' => Skin::makeMainPageUrl(), + ] + ), + ] + $this->buildSidebarProps( $this->data['sidebar'] ) ), - 'html-portals' => $this->renderPortals( $this->data['sidebar'] ), ] ), ]; // Prepare and output the HTML response - echo $this->getTemplateParser()->processTemplate( 'index', $params ); + echo $tp->processTemplate( 'index', $params ); } /** * Get rows that make up the footer * @return array for use in Mustache template describing the footer elements. */ - private function getTemplateFooterRows() { + private function getTemplateFooterRows() : array { $footerRows = []; foreach ( $this->getFooterLinks() as $category => $links ) { $items = []; @@ -213,10 +220,10 @@ class VectorTemplate extends BaseTemplate { * Render a series of portals * * @param array $portals - * @return string + * @return array */ - protected function renderPortals( array $portals ) { - $html = ''; + private function buildSidebarProps( array $portals ) : array { + $props = []; // Force the rendering of the following portals if ( !isset( $portals['TOOLBOX'] ) ) { $portals['TOOLBOX'] = true; @@ -237,24 +244,29 @@ class VectorTemplate extends BaseTemplate { case 'SEARCH': break; case 'TOOLBOX': - $html .= $this->renderPortal( 'tb', $this->getToolbox(), 'toolbox', 'SkinTemplateToolboxEnd' ); + $portal = $this->buildPortalProps( 'tb', $this->getToolbox(), 'toolbox', + 'SkinTemplateToolboxEnd' ); ob_start(); Hooks::run( 'VectorAfterToolbox', [], '1.35' ); - $html .= ob_get_clean(); + $props[] = $portal + [ + 'html-hook-vector-after-toolbox' => ob_get_clean(), + ]; break; case 'LANGUAGES': if ( $this->data['language_urls'] !== false ) { - $html .= $this->renderPortal( + $props[] = $this->buildPortalProps( 'lang', $this->data['language_urls'], 'otherlanguages' ); } break; default: - $html .= $this->renderPortal( $name, $content ); + $props[] = $this->buildPortalProps( $name, $content ); break; } } - return $html; + return [ + 'array-portals' => $props, + ]; } /** @@ -262,9 +274,9 @@ class VectorTemplate extends BaseTemplate { * @param array|string $content * @param null|string $msg * @param null|string|array $hook - * @return string + * @return array */ - protected function renderPortal( $name, $content, $msg = null, $hook = null ) { + private function buildPortalProps( $name, $content, $msg = null, $hook = null ) : array { if ( $msg === null ) { $msg = $name; } @@ -300,7 +312,7 @@ class VectorTemplate extends BaseTemplate { $props['html-portal-content'] = $content; } - return $this->getTemplateParser()->processTemplate( 'Portal', $props ); + return $props; } /** @@ -332,9 +344,9 @@ class VectorTemplate extends BaseTemplate { } /** - * @return string + * @return array */ - private function renderNamespacesComponent() { + private function buildNamespacesProps() : array { $props = [ 'tabs-id' => 'p-namespaces', 'empty-portlet' => ( count( $this->data['namespace_urls'] ) == 0 ) ? 'emptyPortlet' : '', @@ -348,13 +360,13 @@ class VectorTemplate extends BaseTemplate { $props[ 'html-items' ] .= $this->makeListItem( $key, $item ); } - return $this->getTemplateParser()->processTemplate( 'VectorTabs', $props ); + return $props; } /** - * @return string + * @return array */ - private function renderVariantsComponent() { + private function buildVariantsProps() : array { $props = [ 'empty-portlet' => ( count( $this->data['variant_urls'] ) == 0 ) ? 'emptyPortlet' : '', 'menu-id' => 'p-variants', @@ -375,13 +387,13 @@ class VectorTemplate extends BaseTemplate { $props['html-items'] .= $this->makeListItem( $key, $item ); } - return $this->getTemplateParser()->processTemplate( 'VectorMenu', $props ); + return $props; } /** - * @return string + * @return array */ - private function renderViewsComponent() { + private function buildViewsProps() : array { $props = [ 'tabs-id' => 'p-views', 'empty-portlet' => ( count( $this->data['view_urls'] ) == 0 ) ? 'emptyPortlet' : '', @@ -397,13 +409,13 @@ class VectorTemplate extends BaseTemplate { ] ); } - return $this->getTemplateParser()->processTemplate( 'VectorTabs', $props ); + return $props; } /** - * @return string + * @return array */ - private function renderActionsComponent() { + private function buildActionsProps() : array { $props = [ 'empty-portlet' => ( count( $this->data['action_urls'] ) == 0 ) ? 'emptyPortlet' : '', 'msg-label' => $this->getMsg( 'vector-more-actions' )->text(), @@ -417,13 +429,13 @@ class VectorTemplate extends BaseTemplate { $props['html-items'] .= $this->makeListItem( $key, $item ); } - return $this->getTemplateParser()->processTemplate( 'VectorMenu', $props ); + return $props; } /** - * @return string + * @return array */ - private function renderPersonalComponent() { + private function buildPersonalProps() : array { $personalTools = $this->getPersonalTools(); $props = [ 'empty-portlet' => ( count( $this->data['personal_urls'] ) == 0 ) ? 'emptyPortlet' : '', @@ -452,10 +464,13 @@ class VectorTemplate extends BaseTemplate { $props['html-personal-tools'] .= $this->makeListItem( $key, $item ); } - return $this->getTemplateParser()->processTemplate( 'PersonalMenu', $props ); + return $props; } - private function renderSearchComponent() { + /** + * @return array + */ + private function buildSearchProps() : array { $props = [ 'searchHeaderAttrsHTML' => $this->data[ 'userlangattributes' ] ?? '', 'searchActionURL' => $this->data[ 'wgScript' ] ?? '', @@ -472,6 +487,6 @@ class VectorTemplate extends BaseTemplate { ), 'searchInputLabel' => $this->getMsg( 'search' ) ]; - return $this->getTemplateParser()->processTemplate( 'SearchBox', $props ); + return $props; } } diff --git a/includes/templates/Navigation.mustache b/includes/templates/Navigation.mustache index d2e1492..8338679 100644 --- a/includes/templates/Navigation.mustache +++ b/includes/templates/Navigation.mustache @@ -4,8 +4,7 @@ string html-personal-menu content that appears as the first child of mw-head string html-navigation-left-tabs that appears inside #left-navigation (namespaces and variants) string html-navigation-right-tabs that appears inside #right-navigation (page actions and search) - string html-logo-attributes for site logo. Must be used inside tag e.g. `class="logo" lang="en-gb"` - string html-portals for portal(s) that appear in the main menu for Vector + string html-sidebar sidebar HTML }}

{{{html-navigation-heading}}}

@@ -18,10 +17,5 @@ {{{html-navigation-right-tabs}}}
-
- - {{{html-portals}}} -
+ {{{html-sidebar}}} diff --git a/includes/templates/Portal.mustache b/includes/templates/Portal.mustache index 3d7cbc2..62a25e8 100644 --- a/includes/templates/Portal.mustache +++ b/includes/templates/Portal.mustache @@ -1,4 +1,5 @@ {{! +Each portal has the following composition: string portal-id string html-tooltip string msg-label-id @@ -6,8 +7,8 @@ string msg-label} string html-portal-content string|null html-after-portal + string|null html-hook-vector-after-toolbox is deprecated and used by the toolbox portal. }} - +{{! Note: html-hook-vector-after-toolbox is deprecated. }} +{{{html-hook-vector-after-toolbox}}} diff --git a/includes/templates/Sidebar.mustache b/includes/templates/Sidebar.mustache new file mode 100644 index 0000000..0f9a3ac --- /dev/null +++ b/includes/templates/Sidebar.mustache @@ -0,0 +1,13 @@ +{{! + string html-logo-attributes for site logo. Must be used inside tag e.g. `class="logo" lang="en-gb"` + array array-portals contains options for Portal template +}} + +
+ + {{#array-portals}} + {{>Portal}} + {{/array-portals}} +
diff --git a/stories/navigation.stories.js b/stories/navigation.stories.js index 6392354..b8e045a 100644 --- a/stories/navigation.stories.js +++ b/stories/navigation.stories.js @@ -7,13 +7,10 @@ import { loggedOut, loggedInWithEcho } from './personalNavigation.stories'; import { viewTabs, namespaceTabs } from './tabs.stories'; import { more, variants } from './menu.stories'; import { simpleSearch } from './searchBox.stories'; -import { navigationPortal, otherProjects, toolbox, langlinks } from './portal.stories'; -import { placeholder } from './utils'; - -const HOOKINFO = 'Portals can be added, removed or reordered using SidebarBeforeOutput hook'; +import { sidebarWithPortals } from './sidebar.stories'; export default { - title: 'Navigation (Header + Main Menu)' + title: 'Navigation (Header + Sidebar)' }; export const navigationLoggedOutWithVariants = () => mustache.render( navTemplate, @@ -21,12 +18,7 @@ export const navigationLoggedOutWithVariants = () => mustache.render( navTemplat 'html-personalmenu': loggedOut(), 'html-navigation-left-tabs': namespaceTabs() + variants(), 'html-navigation-right-tabs': `${viewTabs()} ${simpleSearch()}`, - 'html-portals': `${navigationPortal().innerHTML} - ${toolbox().innerHTML} - ${otherProjects().innerHTML} - ${langlinks().innerHTML} - ${placeholder( HOOKINFO, 60 )} -`, + 'html-sidebar': sidebarWithPortals(), 'html-navigation-heading': 'Navigation menu', 'html-logo-attributes': `class="mw-wiki-logo" href="/wiki/Main_Page" title="Visit the main page"` } @@ -37,12 +29,7 @@ export const navigationLoggedInWithMore = () => mustache.render( navTemplate, 'html-personalmenu': loggedInWithEcho(), 'html-navigation-left-tabs': namespaceTabs(), 'html-navigation-right-tabs': `${viewTabs()} ${more()} ${simpleSearch()}`, - 'html-portals': `${navigationPortal().innerHTML} - ${toolbox().innerHTML} - ${otherProjects().innerHTML} - ${langlinks().innerHTML} - ${placeholder( HOOKINFO, 60 )} -`, + 'html-sidebar': sidebarWithPortals(), 'html-navigation-heading': 'Navigation menu', 'html-logo-attributes': `class="mw-wiki-logo" href="/wiki/Main_Page" title="Visit the main page"` } diff --git a/stories/portal.stories.js b/stories/portal.stories.data.js similarity index 95% rename from stories/portal.stories.js rename to stories/portal.stories.data.js index 3bcf165..d5a92d7 100644 --- a/stories/portal.stories.js +++ b/stories/portal.stories.data.js @@ -4,11 +4,7 @@ import '../resources/skins.vector.styles/navigation.less'; import '../.storybook/common.less'; import { placeholder, htmluserlangattributes } from './utils'; -export default { - title: 'Portals' -}; - -const wrapPortlet = ( data ) => { +export const wrapPortlet = ( data ) => { const node = document.createElement( 'div' ); node.setAttribute( 'id', 'mw-panel' ); node.innerHTML = mustache.render( portalTemplate, data ); @@ -19,8 +15,8 @@ const portletAfter = ( html ) => { return `
${html}
`; }; -export const portal = () => wrapPortlet( - { +export const PORTALS = { + example: { 'portal-id': 'p-example', 'html-tooltip': 'Message tooltip-p-example acts as tooltip', 'msg-label': 'Portal title', @@ -34,11 +30,8 @@ export const portal = () => wrapPortlet( 'html-after-portal': portletAfter( placeholder( `

Beware: The BaseTemplateAfterPortlet hook can be used to inject arbitary HTML here for any portlet.

`, 60 ) ) - } -); - -export const navigationPortal = () => wrapPortlet( - { + }, + navigation: { 'portal-id': 'p-navigation', 'msg-label': 'Navigation', 'msg-label-id': 'p-navigation-label', @@ -47,11 +40,8 @@ export const navigationPortal = () => wrapPortlet(
  • Main page
  • Contents
  • Featured content
  • Current events
  • Random page
  • Donate
  • `, 'html-after-portal': portletAfter( placeholder( 'Possible hook output (navigation)', 50 ) ) - } -); - -export const toolbox = () => wrapPortlet( - { + }, + toolbox: { 'portal-id': 'p-tb', 'html-tooltip': 'A message tooltip-p-tb must exist for this to appear', 'msg-label': 'Tools', @@ -61,11 +51,8 @@ export const toolbox = () => wrapPortlet(
  • What links here
  • Related changes
  • Upload file
  • Special pages
  • Page information
  • Wikidata item
  • Cite this page
  • `, 'html-after-portal': portletAfter( placeholder( 'Possible hook output (tb)', 50 ) ) - } -); - -export const langlinks = () => wrapPortlet( - { + }, + langlinks: { 'portal-id': 'p-lang', 'html-tooltip': 'A message tooltip-p-lang must exist for this to appear', 'msg-label': 'In other languages', @@ -81,11 +68,8 @@ export const langlinks = () => wrapPortlet( `Edit links ${placeholder( `

    Further hook output possible (lang)

    `, 60 )}` ) - } -); - -export const otherProjects = () => wrapPortlet( - { + }, + otherProjects: { 'portal-id': 'p-wikibase-otherprojects', 'html-tooltip': 'A message tooltip-p-lang must exist for this to appear', 'msg-label': 'In other projects', @@ -95,4 +79,4 @@ export const otherProjects = () => wrapPortlet( `, 'html-after-portal': '' } -); +}; diff --git a/stories/portals.stories.js b/stories/portals.stories.js new file mode 100644 index 0000000..b43bf5b --- /dev/null +++ b/stories/portals.stories.js @@ -0,0 +1,11 @@ +import { PORTALS, wrapPortlet } from './portal.stories.data'; + +export default { + title: 'Portals' +}; + +export const portal = () => wrapPortlet( PORTALS.example ); +export const navigationPortal = () => wrapPortlet( PORTALS.navigation ); +export const toolbox = () => wrapPortlet( PORTALS.toolbox ); +export const langlinks = () => wrapPortlet( PORTALS.langlinks ); +export const otherProjects = () => wrapPortlet( PORTALS.otherProjects ); diff --git a/stories/sidebar.stories.js b/stories/sidebar.stories.js new file mode 100644 index 0000000..08888a7 --- /dev/null +++ b/stories/sidebar.stories.js @@ -0,0 +1,51 @@ +import mustache from 'mustache'; +import sidebarTemplate from '!!raw-loader!../includes/templates/Sidebar.mustache'; +import portalTemplate from '!!raw-loader!../includes/templates/Portal.mustache'; +import { PORTALS } from './portal.stories.data'; +import '../.storybook/common.less'; +import '../resources/skins.vector.styles/navigation.less'; +const HTML_LOGO_ATTRIBUTES = `class="mw-wiki-logo" href="/wiki/Main_Page" title="Visit the main page"`; +const SIDEBAR_BEFORE_OUTPUT_HOOKINFO = `Beware: Portals can be added, removed or reordered using +SidebarBeforeOutput hook as in this example.`; + +export default { + title: 'Sidebar' +}; + +export const sidebarWithNoPortals = () => mustache.render( sidebarTemplate, + { + 'array-portals': [], + 'html-logo-attributes': HTML_LOGO_ATTRIBUTES + } +); + +export const sidebarWithPortals = () => mustache.render( sidebarTemplate, + { + 'array-portals': [ + PORTALS.navigation, + PORTALS.toolbox, + PORTALS.otherProjects, + PORTALS.langlinks + ], + 'html-logo-attributes': HTML_LOGO_ATTRIBUTES + }, + { + Portal: portalTemplate + } +); + +export const sidebarThirdParty = () => mustache.render( sidebarTemplate, + { + 'array-portals': [ + PORTALS.toolbox, + PORTALS.navigation, + { + 'html-portal-content': SIDEBAR_BEFORE_OUTPUT_HOOKINFO + } + ], + 'html-logo-attributes': HTML_LOGO_ATTRIBUTES + }, + { + Portal: portalTemplate + } +); diff --git a/tests/phpunit/integration/VectorTemplateTest.php b/tests/phpunit/integration/VectorTemplateTest.php index 936918e..0706b02 100644 --- a/tests/phpunit/integration/VectorTemplateTest.php +++ b/tests/phpunit/integration/VectorTemplateTest.php @@ -124,29 +124,25 @@ class VectorTemplateTest extends \MediaWikiTestCase { } /** - * @covers ::renderViewsComponent + * @covers ::buildViewsProps */ - public function testRenderViewsComponent() { + public function testbuildViewsProps() { $langAttrs = 'LANG_ATTRIBUTES'; - $templateParserMock = $this->createMock( \TemplateParser::class ); - $templateParserMock->expects( $this->once() ) - ->method( 'processTemplate' ) - ->with( 'VectorTabs', $this->callback( function ( $data ) use ( $langAttrs ){ - if ( !array_key_exists( 'empty-portlet', $data ) ) { - return false; - } - return $data['empty-portlet'] == 'emptyPortlet' && - $data['html-userlangattributes'] == $langAttrs; - } ) ); - $vectorTemplate = new \VectorTemplate( \GlobalVarConfig::newInstance() ); - $vectorTemplate->setTemplateParser( $templateParserMock ); $vectorTemplate->set( 'view_urls', [] ); $vectorTemplate->set( 'skin', new \SkinVector() ); $vectorTemplate->set( 'userlangattributes', $langAttrs ); $openVectorTemplate = TestingAccessWrapper::newFromObject( $vectorTemplate ); - $openVectorTemplate->renderViewsComponent(); + $props = $openVectorTemplate->buildViewsProps(); + $this->assertSame( $props, [ + 'tabs-id' => 'p-views', + 'empty-portlet' => 'emptyPortlet', + 'label-id' => 'p-views-label', + 'msg-label' => 'Views', + 'html-userlangattributes' => $langAttrs, + 'html-items' => '', + ] ); } }