Merge SkinVector and VectorTemplate (step 1/2)

Please note I7e06a4cc226f3434c0f655212a464b8b98bcc7f4 should be
merged at the same time as this patch.

== The background ==
All extensions have been weaned of BaseTemplate hooks in
Wikimedia projects.

This change now means that Vector will no longer run
any BaseTemplate hooks. See the epic T253809 for the
implementation details.

== The change ==
BaseTemplate will now have nothing to do with the rendering of
Vector. The skin version is added to express the significance of
breaking compatibility with 3rd party extensions.

We TEMPORARILY remove SkinVector to retain git blame. SkinTemplateVector will
be renamed SkinVector in the follow up (see 2/2)
Update skin.json to use SkinTemplateVector for the skin (this will be fixed
in a follow up).

The isLegacy method is moved to SkinTemplateVector.

Changes of note:
* html-debuglog is no longer needed. SkinMustache includes this information on
the skins behalf
* html-printtail and html-headelement are now not needed in the master template
and added by SkinMustache
* Skin::getAfterPortlet does not provide the `after-portlet` wrapping element provided
by BaseTemplate::getAfterPortlet so this is added
* SkinTemplate::getFooterIcons does not support the options that BaseTemplate::getFooterIcons
does so any icons which do not have an image must be manually checked for and unset

Known changes to HTML output as a result of intentionally
delegating their output to the core SkinMustache class:
* A new line is removed between the body element and #mw-page-base
* #mw-html-debug-log now appears at the end of the body element
* #printfooter is now a child of #mw-content-text rather than sibling.

Bug: T251212
Change-Id: I4e89beb96f6401ed7e51bafdf0aac408f5a2c42f
This commit is contained in:
jdlrobson 2020-04-28 14:16:21 -07:00
parent ad6c4ea6f3
commit ee6974ad35
8 changed files with 127 additions and 284 deletions

View File

@ -9,7 +9,7 @@ use OutputPage;
use RequestContext;
use Skin;
use SkinTemplate;
use SkinVector;
use SkinTemplateVector;
use User;
/**
@ -29,7 +29,7 @@ class Hooks {
* @param SkinTemplate $sk
*/
public static function onBeforePageDisplay( OutputPage $out, $sk ) {
if ( !$sk instanceof SkinVector ) {
if ( !$sk instanceof SkinTemplateVector ) {
return;
}
@ -236,7 +236,7 @@ class Hooks {
* @param OutputPage $out OutputPage instance calling the hook
*/
public static function onMakeGlobalVariablesScript( &$vars, OutputPage $out ) {
if ( $out->getSkin() instanceof SkinVector ) {
if ( $out->getSkin() instanceof SkinTemplateVector ) {
$skinVersionLookup = new SkinVersionLookup(
$out->getRequest(),
$out->getUser(),

View File

@ -22,15 +22,15 @@
* @ingroup Skins
*/
use MediaWiki\MediaWikiServices;
use Vector\Constants;
/**
* QuickTemplate subclass for Vector
* Skin subclass for Vector
* @ingroup Skins
* @deprecated Since 1.35, duplicate class locally if its functionality is needed.
* Extensions or skins should extend it under no circumstances.
*/
class VectorTemplate extends BaseTemplate {
class SkinTemplateVector extends SkinMustache {
/** @var array of alternate message keys for menu labels */
private const MENU_LABEL_KEYS = [
'cactions' => 'vector-more-actions',
@ -58,61 +58,46 @@ class VectorTemplate extends BaseTemplate {
*/
private const OPT_OUT_LINK_TRACKING_CODE = 'vctw1';
/** @var TemplateParser */
private $templateParser;
/** @var string File name of the root (master) template without folder path and extension */
private $templateRoot;
/** @var bool */
private $isLegacy;
/**
* @param Config $config
* @param TemplateParser $templateParser
* @param bool $isLegacy
*/
public function __construct(
Config $config,
TemplateParser $templateParser,
bool $isLegacy
) {
parent::__construct( $config );
$this->templateParser = $templateParser;
$this->isLegacy = $isLegacy;
$this->templateRoot = $isLegacy ? 'skin-legacy' : 'skin';
}
/**
* @return Config
*/
private function getConfig() {
return $this->config;
}
/**
* The template parser might be undefined. This function will check if it set first
* Whether or not the legacy version of the skin is being used.
*
* @return TemplateParser
* @return bool
*/
protected function getTemplateParser() {
if ( $this->templateParser === null ) {
throw new \LogicException(
'TemplateParser has to be set first via setTemplateParser method'
);
}
return $this->templateParser;
private function isLegacy() : bool {
$isLatestSkinFeatureEnabled = MediaWikiServices::getInstance()
->getService( Constants::SERVICE_FEATURE_MANAGER )
->isFeatureEnabled( Constants::FEATURE_LATEST_SKIN );
return !$isLatestSkinFeatureEnabled;
}
/**
* @deprecated Please use Skin::getTemplateData instead
* @return array Returns an array of data shared between Vector and legacy
* Vector.
* Overrides template, styles and scripts module when skin operates
* in legacy mode.
*
* @inheritDoc
*/
private function getSkinData() : array {
// @phan-suppress-next-line PhanUndeclaredMethod
$contentNavigation = $this->getSkin()->getMenuProps();
$skin = $this->getSkin();
public function __construct( $options = [] ) {
// Legacy overrides
$this->isLegacy = $this->isLegacy();
if ( $this->isLegacy ) {
$options['scripts'] = [ 'skins.vector.legacy.js' ];
$options['styles'] = [ 'skins.vector.styles.legacy' ];
$options['template'] = 'skin-legacy';
}
$options['templateDirectory'] = __DIR__ . '/templates';
parent::__construct( $options );
}
/**
* @inheritDoc
*/
public function getTemplateData() : array {
$contentNavigation = $this->buildContentNavigationUrls();
$skin = $this;
$out = $skin->getOutput();
$title = $out->getTitle();
@ -136,9 +121,7 @@ class VectorTemplate extends BaseTemplate {
// From Skin::getNewtalks(). Always returns string, cast to null if empty.
$newTalksHtml = $skin->getNewtalks() ?: null;
// @phan-suppress-next-line PhanUndeclaredMethod
$commonSkinData = $skin->getTemplateData() + [
'html-headelement' => $out->headElement( $skin ),
$commonSkinData = parent::getTemplateData() + [
'page-langcode' => $title->getPageViewLanguage()->getHtmlCode(),
'page-isarticle' => (bool)$out->isArticle(),
@ -156,7 +139,6 @@ class VectorTemplate extends BaseTemplate {
'html-categories' => $skin->getCategories(),
'data-footer' => $this->getFooterData(),
'html-navigation-heading' => $skin->msg( 'navigation-heading' ),
'data-search-box' => $this->buildSearchProps(),
// Header
'data-logos' => ResourceLoaderSkinModule::getAvailableLogos( $this->getConfig() ),
@ -186,30 +168,25 @@ class VectorTemplate extends BaseTemplate {
return $commonSkinData;
}
/**
* Renders the entire contents of the HTML page.
*/
public function execute() {
$tp = $this->getTemplateParser();
echo $tp->processTemplate( $this->templateRoot, $this->getSkinData() );
}
/**
* Get rows that make up the footer
* @return array for use in Mustache template describing the footer elements.
*/
private function getFooterData() : array {
$skin = $this->getSkin();
$skin = $this;
$footerRows = [];
foreach ( $this->getFooterLinks() as $category => $links ) {
$items = [];
$rowId = "footer-$category";
foreach ( $links as $link ) {
$items[] = [
'id' => "$rowId-$link",
'html' => $this->get( $link, '' ),
];
foreach ( $links as $key => $link ) {
// Link may be null. If so don't include it.
if ( $link ) {
$items[] = [
'id' => "$rowId-$key",
'html' => $link,
];
}
}
$footerRows[] = [
@ -220,25 +197,39 @@ class VectorTemplate extends BaseTemplate {
}
// If footer icons are enabled append to the end of the rows
$footerIcons = $this->getFooterIcons( 'icononly' );
$footerIcons = $this->getFooterIcons();
if ( count( $footerIcons ) > 0 ) {
$items = [];
foreach ( $footerIcons as $blockName => $blockIcons ) {
$html = '';
foreach ( $blockIcons as $icon ) {
$html .= $skin->makeFooterIcon( $icon );
// Only output icons which have an image.
// For historic reasons this mimics the `icononly` option
// for BaseTemplate::getFooterIcons.
if ( is_string( $icon ) || isset( $icon['src'] ) ) {
$html .= $skin->makeFooterIcon( $icon );
}
}
// For historic reasons this mimics the `icononly` option
// for BaseTemplate::getFooterIcons. Empty rows should not be output.
if ( $html ) {
$items[] = [
'id' => 'footer-' . htmlspecialchars( $blockName ) . 'ico',
'html' => $html,
];
}
$items[] = [
'id' => 'footer-' . htmlspecialchars( $blockName ) . 'ico',
'html' => $html,
];
}
$footerRows[] = [
'id' => 'footer-icons',
'className' => 'noprint',
'array-items' => $items,
];
// Empty rows should not be output.
// This is how Vector has behaved historically but we can revisit.
if ( count( $items ) > 0 ) {
$footerRows[] = [
'id' => 'footer-icons',
'className' => 'noprint',
'array-items' => $items,
];
}
}
ob_start();
@ -286,9 +277,9 @@ class VectorTemplate extends BaseTemplate {
*
* @return array
*/
private function buildSidebar() : array {
$skin = $this->getSkin();
$portals = $skin->buildSidebar();
public function buildSidebar() {
$skin = $this;
$portals = parent::buildSidebar();
$props = [];
$languages = null;
@ -433,7 +424,23 @@ class VectorTemplate extends BaseTemplate {
}
}
$props['html-after-portal'] = $isPortal ? $this->getAfterPortlet( $label ) : '';
$afterPortal = '';
if ( $isPortal ) {
// The BaseTemplate::getAfterPortlet method ran the SkinAfterPortlet
// hook and if content is added appends it to the html-after-portal method.
// This replicates that historic behaviour.
// This code should eventually be upstreamed to SkinMustache in core.
// Currently in production this supports the Wikibase 'edit' link.
$content = $this->getAfterPortlet( $label );
if ( $content !== '' ) {
$afterPortal = Html::rawElement(
'div',
[ 'class' => [ 'after-portlet', 'after-portlet-' . $label ] ],
$content
);
}
}
$props['html-after-portal'] = $afterPortal;
// Mark the portal as empty if it has no content
$class = ( count( $urls ) == 0 && !$props['html-after-portal'] )
@ -446,10 +453,11 @@ class VectorTemplate extends BaseTemplate {
* @return array
*/
private function getMenuProps() : array {
// @phan-suppress-next-line PhanUndeclaredMethod
$contentNavigation = $this->getSkin()->getMenuProps();
$personalTools = $this->getPersonalTools();
$skin = $this->getSkin();
$contentNavigation = $this->buildContentNavigationUrls();
$personalTools = self::getPersonalToolsForMakeListItem(
$this->buildPersonalUrls()
);
$skin = $this;
// For logged out users Vector shows a "Not logged in message"
// This should be upstreamed to core, with instructions for how to hide it for skins
@ -506,27 +514,4 @@ class VectorTemplate extends BaseTemplate {
),
];
}
/**
* @return array
*/
private function buildSearchProps() : array {
$config = $this->getConfig();
$skin = $this->getSkin();
$props = [
'form-action' => $config->get( 'Script' ),
'html-button-search-fallback' => $this->makeSearchButton(
'fulltext',
[ 'id' => 'mw-searchButton', 'class' => 'searchButton mw-fallbackSearchButton' ]
),
'html-button-search' => $this->makeSearchButton(
'go',
[ 'id' => 'searchButton', 'class' => 'searchButton' ]
),
'html-input' => $this->makeSearchInput( [ 'id' => 'searchInput' ] ),
'msg-search' => $skin->msg( 'search' ),
'page-title' => SpecialPage::getTitleFor( 'Search' )->getPrefixedDBkey(),
];
return $props;
}
}

View File

@ -1,140 +0,0 @@
<?php
/**
* Vector - Modern version of MonoBook with fresh look and many usability
* improvements.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Skins
*/
use MediaWiki\MediaWikiServices;
use Vector\Constants;
use Wikimedia\WrappedString;
/**
* Skin subclass for Vector
* @ingroup Skins
* @final skins extending SkinVector are not supported
* @unstable
*/
class SkinVector extends SkinTemplate {
public $skinname = Constants::SKIN_NAME;
public $stylename = 'Vector';
public $template = 'VectorTemplate';
/**
* @inheritDoc
* @return array
*/
public function getDefaultModules() {
$modules = parent::getDefaultModules();
if ( $this->isLegacy() ) {
$modules['styles']['skin'][] = 'skins.vector.styles.legacy';
$modules[Constants::SKIN_NAME] = 'skins.vector.legacy.js';
} else {
$modules['styles'] = array_merge(
$modules['styles'],
[ 'skins.vector.styles', 'mediawiki.ui.icon', 'skins.vector.icons' ]
);
$modules[Constants::SKIN_NAME][] = 'skins.vector.js';
}
return $modules;
}
/**
* Set up the VectorTemplate. Overrides the default behaviour of SkinTemplate allowing
* the safe calling of constructor with additional arguments. If dropping this method
* please ensure that VectorTemplate constructor arguments match those in SkinTemplate.
*
* @internal
* @param string $classname
* @return VectorTemplate
*/
protected function setupTemplate( $classname ) {
$tp = new TemplateParser( __DIR__ . '/templates' );
return new VectorTemplate( $this->getConfig(), $tp, $this->isLegacy() );
}
/**
* Whether or not the legacy version of the skin is being used.
*
* @return bool
*/
private function isLegacy() : bool {
$isLatestSkinFeatureEnabled = MediaWikiServices::getInstance()
->getService( Constants::SERVICE_FEATURE_MANAGER )
->isFeatureEnabled( Constants::FEATURE_LATEST_SKIN );
return !$isLatestSkinFeatureEnabled;
}
/**
* @internal only for use inside VectorTemplate
* @return array of data for a Mustache template
*/
public function getTemplateData() {
$out = $this->getOutput();
$title = $out->getTitle();
$indicators = [];
foreach ( $out->getIndicators() as $id => $content ) {
$indicators[] = [
'id' => Sanitizer::escapeIdForAttribute( "mw-indicator-$id" ),
'class' => 'mw-indicator',
'html' => $content,
];
}
$printFooter = Html::rawElement(
'div',
[ 'class' => 'printfooter' ],
$this->printSource()
);
return [
// Data objects:
'array-indicators' => $indicators,
// HTML strings:
'html-printtail' => WrappedString::join( "\n", [
MWDebug::getHTMLDebugLog(),
MWDebug::getDebugHTML( $this->getContext() ),
$this->bottomScripts(),
wfReportTime( $out->getCSP()->getNonce() )
] ) . '</body></html>',
'html-site-notice' => $this->getSiteNotice(),
'html-user-language-attributes' => $this->prepareUserLanguageAttributes(),
'html-subtitle' => $this->prepareSubtitle(),
// Always returns string, cast to null if empty.
'html-undelete-link' => $this->prepareUndeleteLink() ?: null,
// Result of OutputPage::addHTML calls
'html-body-content' => $this->wrapHTML( $title, $out->mBodytext )
. $printFooter,
'html-after-content' => $this->afterContentHook(),
];
}
/**
* @internal only for use inside VectorTemplate
* @return array
*/
public function getMenuProps() {
return $this->buildContentNavigationUrls();
}
}

View File

@ -1,6 +1,4 @@
{{!
string html-headelement a string of attribute HTML that begins with `<html>` and ends with
`</head>` and contains `meta` tags and ResourceLoader internals.
string|null html-site-notice the contents of a banner defined in MediaWiki:Sitenotice.
Also used by CentralNotice to inject banners into Vector.
Indicator[] array-indicators wiki-defined badges such as "good article",
@ -27,11 +25,7 @@
object data-search-box. See SearchBox.mustache for documentation.
object data-sidebar. See Sidebar.mustache for documentation.
object data-footer for footer template partial. see Footer.mustache for documentation.
string html-printtail HTML to render at the end of the page contained necessary script tags for
ResourceLoader terminated with `</body></html>`.
}}
{{{html-headelement}}}
<div id="mw-page-base" class="noprint"></div>
<div id="mw-head-base" class="noprint"></div>
<div id="content" class="mw-body" role="main">
@ -73,4 +67,3 @@
{{#data-sidebar}}{{>legacy/Sidebar}}{{/data-sidebar}}
</div>
{{#data-footer}}{{>Footer}}{{/data-footer}}
{{{html-printtail}}}

View File

@ -1,6 +1,4 @@
{{!
string html-headelement a string of attribute HTML that begins with `<html>` and ends with
`</head>` and contains `meta` tags and ResourceLoader internals.
string|null html-site-notice the contents of a banner defined in MediaWiki:Sitenotice.
Also used by CentralNotice to inject banners into Vector.
Indicator[] array-indicators wiki-defined badges such as "good article",
@ -31,12 +29,7 @@
string msg-vector-action-toggle-sidebar The label used by the sidebar button.
object data-sidebar. See Sidebar.mustache for documentation.
object data-footer for footer template partial. see Footer.mustache for documentation.
string html-printtail HTML to render at the end of the page contained necessary script tags for
ResourceLoader terminated with `</body></html>`.
}}
{{{html-headelement}}}
<div class="mw-page-container">
<div class="mw-page-container-inner">
@ -119,4 +112,3 @@
</div>
</div> {{! END mw-page-container-inner }}
</div> {{! END mw-page-container }}
{{{html-printtail}}}

View File

@ -1,5 +1,6 @@
{
"name": "Vector",
"version": "1.0.0",
"author": [
"Trevor Parscal",
"Roan Kattouw",
@ -14,7 +15,23 @@
"MediaWiki": ">= 1.35.0"
},
"ValidSkinNames": {
"vector": "Vector"
"vector": {
"class": "SkinTemplateVector",
"@args": "See SkinTemplateVector::__construct for more detail.",
"args": [
{
"name": "vector",
"scripts": [
"skins.vector.js"
],
"styles": [
"skins.vector.styles",
"mediawiki.ui.icon",
"skins.vector.icons"
]
}
]
}
},
"MessagesDirs": {
"Vector": [
@ -22,8 +39,7 @@
]
},
"AutoloadClasses": {
"SkinVector": "includes/SkinVector.php",
"VectorTemplate": "includes/VectorTemplate.php"
"SkinTemplateVector": "includes/SkinTemplateVector.php"
},
"AutoloadNamespaces": {
"Vector\\": "includes/"

View File

@ -1,12 +1,10 @@
<?php
namespace MediaWiki\Skins\Vector\Tests\Integration;
use GlobalVarConfig;
use MediaWikiIntegrationTestCase;
use RequestContext;
use TemplateParser;
use SkinTemplateVector;
use Title;
use VectorTemplate;
use Wikimedia\TestingAccessWrapper;
/**
@ -15,20 +13,15 @@ use Wikimedia\TestingAccessWrapper;
* @group Vector
* @group Skins
*
* @coversDefaultClass \VectorTemplate
* @coversDefaultClass \SkinTemplateVector
*/
class VectorTemplateTest extends MediaWikiIntegrationTestCase {
class SkinTemplateVectorTest extends MediaWikiIntegrationTestCase {
/**
* @return \VectorTemplate
*/
private function provideVectorTemplateObject() {
$template = new VectorTemplate(
GlobalVarConfig::newInstance(),
new TemplateParser(),
true
);
$template->set( 'skin', new \SkinVector() );
$template = new SkinTemplateVector();
return $template;
}
@ -102,9 +95,13 @@ class VectorTemplateTest extends MediaWikiIntegrationTestCase {
$context->setTitle( $title );
$context->setLanguage( 'fr' );
$vectorTemplate = $this->provideVectorTemplateObject();
// used internally by getPersonalTools
$vectorTemplate->set( 'personal_urls', [] );
$this->setMwGlobals( 'wgHooks', [
'PersonalUrls' => [
function ( &$personal_urls, &$title, $skin ) {
$personal_urls = [];
}
],
'SkinTemplateNavigation' => [
function ( &$skinTemplate, &$content_navigation ) {
$content_navigation = [

View File

@ -294,7 +294,7 @@ class VectorHooksTest extends \MediaWikiTestCase {
$this->setMwGlobals( [
'wgVectorUseIconWatch' => true
] );
$skin = new SkinVector();
$skin = new SkinTemplateVector( [ 'name' => 'vector' ] );
$contentNavWatch = [
'actions' => [
'watch' => [ 'class' => 'watch' ],