MediaWiki extension: DynamicSkin
| DynamicSkin Release status: beta | |
|---|---|
| Type: | User interface |
| Description: | This extension allows the skin layout and content to be defined using normal wikitext articles instead of PHP script files, and to be modified dynamically depending on the contents of each page. |
| Author: | Eric Hartwell |
| Version: | 0.91 (August 24, 2007) |
| MediaWiki: | 1.10 |
| Download: | Code |
| Example: | DynamicSkin development |
[edit] What can this extension do?
|
This extension allows the skin layout to be defined using normal wikitext articles instead of PHP script files. The layout and content can change automatically depending on the page being displayed.
- Dynamic layout
- This extension allows the skin layout to be defined using normal wikitext articles.
- Controls the entire page layout, including the HTML header. you can add styles, scripts, meta tags and more without code changes.
- Prebuilt code to generate standard page components
- Select which skin components and classes are used.
- Dynamic content
- Menus, styles, navigation can change depnding on the contents of the page being rendered.
- Show, hide, or modify navigation and control menus for anonymous users
- Extended sidebar syntax for easier conditional menus
- Apply a different style to pages in a particular category or namespace
For a different approach to the same problem, see the WikiSkin extension (thanks to User:Nad for the idea).
Strictly speaking, this isn't an extension at all, but a skin: it's installed in the /skins directory, and does not use any extension hooks or global variables. However, unlike a regular skin, it doesn't define the layout or appearance. Instead it provides a framework for dynamically changing them.
While DynamicSkin makes it easy to customize skins without any code changes, it's written as a base class so you can add additional code in a derived class. For an example of a derived skin, and the site it's used on, see InfoDabble skin at ehartwell.com.
[edit] Usage
This extension allows the skin layout to be defined using normal wikitext articles instead of PHP script files. The layout and content can change automatically depending on the page being displayed.
[edit] Basics
| Default DynamicSkinSkin |
|---|
&DOCTYPE&
&HEAD_START& &STYLES& &SCRIPTS&
&BODY_START&
<div id=\"globalWrapper\">
<div id=\"column-content\">
<div id=\"content\">
<a name=\"top\" id=\"top\"></a>&TITLE&
<div id=\"bodyContent\">
&SITESUB& &SUBTITLE& &JUMPLINKS&
<!-- start content -->
&CONTENTS&
<!-- end content -->
<div class=\"visualClear\"></div>
</div>
</div>
</div>
<div id=\"column-one\">
&PAGETOOLS& &PERSONAL&
&LOGO& &SIDEBAR&
&SEARCH& &TOOLBOX&
&SITENOTICE& &USERMSG& &LANGUAGE& &UNDELETE&
&CATLINKS&
</div><!-- end of the left
(by default at least) column -->
<div class=\"visualClear\"></div>
&FOOTER&
</div>
&END&
|
DynamicSkin lets you define the skin layout using a normal wikitext article instead of PHP script files.
- The name of the layout article matches the name of the skin: MediaWiki:<name of the skin>Skin. For example, the default name for this skin is 'DynamicSkin', so the layout is in the page MediaWiki:DynamicSkinSkin.
- If the layout article does not exist, it is created the first time DynamicSkin is used. The default layout duplicates the Monobook skin.
- Note: DynamicSkin must be installed and configured by a user with Sysop privileges.
The default layout duplicates the Monobook skin and is shown at right.
The layout is specified by a combination of HTML and DynamicSkin tags. Wherever a tag of the for &TAG& is encountered, it is replaced by dynamically generated HTML corresponding to the MediaWiki installation.
The skin is built using the layout as follows:
- DynamicSkin pseudo-variables of the form &&VAR&& are evaluated.
- Templates and variables in the text are expanded. You can use parser functions like #if: if installed.
- Keyword tags of the form &TAG& are expanded according to the table below
- The resulting text is output to the browser. Raw HTML can be included in the skin layout, including normally forbidden tags such as SCRIPT.
[edit] Pseudo-variables
DynamicSkin adds a few pseudo-variables to the standard magic words for use in creating conditional output.
| Pseudo-variable | Value |
|---|---|
| &&CATEGORIES&& | Comma-delimited list of categories for the current page |
| &&TITLE&& | Full page title as displayed (includes 'Editing ' prefix etc.) |
| &&USERID&& | User ID, blank if anonymous |
| &&USERGROUPS&& | Comma-delimited list of groups this user belongs to |
[edit] Sidebar
MediaWikiWiki:MediaWiki:Sidebar defines the standard navigation bar, which provides links to the most important locations in the wiki and supplies site administrators with a place to add a persistent collection of links. For instance, most wikis will link to their community discussion page and some useful tools.
The default Monobook skin places the navigation bar on the top-left (top-right for right-to-left languages) along with the search bar and toolbox, but the placement may be different in other skins. Other skins, like GuMax, move the navigation bar to the top of the page instead of the Discussion/Edit/History tools.
- If the plain &SIDEBAR& tag is specified, DynamicSkin uses normal MediaWiki sidebar support using MediaWiki syntax.
- If an alternate sidebar is specified (e.g. &SIDEBAR=MediaWiki:DynamicSkinSidebar&), then the substitute sidebar is generated using DynamicSkin syntax:
- Uses '=' instead of '|' to separate the link from the target, which greatly simplifies the use of templates and conditionals.
- DynamicSkin pseudo-variables are available for conditional navigation
[edit] Conditional skin content
Here are some examples of how to hide some skin components depending on the environment. You'll need the ParserFunctions extension for the #if: function and StringFunctions extension for the #pos: function to work.
| Function | Skin page snippet |
|---|---|
| Hide the title on the main page | {{#ifeq:{{PAGENAME}}|Main Page||&TITLE&}}
|
| Use a different style subdirectory for pages in the 'Special' namespace | $STYLES={{#ifeq:{{NAMESPACE}}|Special|special|monobook}}&
|
| Use a different style subdirectory for pages in the 'Special' category | $STYLES={{#if:{{#pos:&&CATEGORIES&&|Special}}|Special|special|monobook}}&
|
| Hide the page tools from anonymous users | {{#if:&&USERID&&|&PAGETOOLS&}}
|
Extra sidebar functions for users in the "Company" and "Sysops" groups. Note the use of = as a delimiter instead of |.
| * navigation
** mainpage=mainpage
{{#if:{{#pos:&&USERGROUPS&&|Company}}|
** Category:Company=Company news
** Project:Support desk=Support desk
}}
** faqpage=FAQ
** Manual:Contents=Manual
{{#if:{{#pos:&&USERGROUPS&&|Sysops}}|
** Special:Allpages=Contents
}}
** help=help
|
[edit] Tags
The DynamicSkin tags generate HTML output according to the sample skin in Manual:Skinning.
| Tag | Function |
|---|---|
| &BODY_START& | End of head, start of body |
| &CATLINKS& | Category links |
| &COMMENT=& | Comment (contents are ignored) |
| &CONTENTS& | Page contents |
| &CUSTOMTOOLBAR=& | Create a custom toolbar based on the parameters. The parameters are a comma-delimited list.
|
| &DOCTYPE& | Compose XHTML output |
| &END& | Closing trail (scripts and debugging information) |
| &FOOTER& | Footer |
| &HEAD_START& | HTML header. Add <meta> tags after this. |
| &HIDE=xxx& | Hide individual command or option links. These include:
|
| &JUMPLINKS& | Jump-to links |
| &LANGUAGE& | Language links |
| &LOGO& &LOGO=xxx& | Logo image. If a parameter is specified, use that image file instead of the site default (the path is relative to the root of your wiki). |
| &PAGETOOLS& | Page toolbar |
| &PERSONAL& | User toolbar. Examples: Article, Discussion, Edit, History, Protect, Delete, Move, Watch |
| &SCRIPTS& | Various MediaWiki-related scripts and styles. Add custom styles and site-wide scripts after this tag. |
| &SEARCH& &SEARCH=xxx& | Search. Optional parameters:
Examples: &SEARCH=image& for image buttons, &SEARCH=search,image& for "Search" image button only |
| &SIDEBAR& &SIDEBAR=xxx& | Sidebar navigation. Default is normal MediaWiki sidebar support using MediaWiki syntax. If a parameter is specified, parse that page (e.g. &SIDEBAR=MediaWiki:DynamicSkinSidebar&) using DynamicSkin syntax. This is the same as MediaWiki's sidebar syntax except it uses = instead of | to separate the link from the target, which greatly simplifies the use of templates and conditionals.
|
| &SITENAME& | Site name. Not used in Monobook skin. |
| &SITENOTICE& | Site notice |
| &SITESUB& | Tagline (site subtitle) |
| &STYLES& &STYLES=xxx& | Style sheets. If a parameter is specified, use the style directory by that name instead of the default in the source file (normally monobook). For example,$STYLES={{#ifeq:{{NAMESPACE}}|Special|special|monobook}}&uses styles from the /styles/monobook subdirectory except for pages in the 'Special' namespace, where it uses /styles/special. |
| &SUBTITLE& | Page subtitle |
| &TITLE& | Page name |
| &TOOLBOX& | Toolbox |
| &UNDELETE& | Undelete notice |
| &USERMSG& | User-messages notification |
| &Anything else& | Ignored, unless used in derived skins. |
[edit] Installation
Copy DynamicSkin.php to the /skins directory.
If you want to change the name displayed on the skin preferences page, edit the line near the top of the source:
$this->initDynamicSkin( $out, 'DynamicSkin', 'monobook', 'DynamicSkinTemplate');
- The second parameter, 'DynamicSkin', is the name displayed in the list of skin preferences
- The third parameter, 'monobook', is the name of the skins/ subdirectory for style sheets and graphics.
[edit] Changes to LocalSettings.php
There are no changes to LocalSettings.php. Simply copy DynamicSkin.php to the /skins directory.
If you want to make DynamicSkin the default, add this to LocalSettings.php: $wgDefaultSkin = 'dynamicskin';. Note that the skin file name must be all in lower case and without the '.php' extension.
[edit] Code
<?php /*** * DynamicSkin skin * @version 0.91 * @author Eric Hartwell ( http://www.ehartwell.com) * @license [URL] [name] Based on the sample skin from MediaWiki Manual:Skinning (http://www.mediawiki.org/wiki/Manual:Skinning), then tweaked to produce the same result as the Monobook skin for MediaWiki 1.10. The skin builder code has been heavily refactored for consistency. License information: Contributions by Eric Hartwell are offered to the community for any use whatsoever with no restrictions other than that credit be given, at least in the source code (Creative Commons Attribution 3.0 License). However, parts of this module and the classes it uses are licensed under the GNU General Public License 2 which restricts your rights to use them. # 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 **/ // initialize if( !defined('MEDIAWIKI') ) { die("This Skin file is not a valid Entry Point."); } require_once('includes/SkinTemplate.php'); // inherit main code from SkinTemplate, set the CSS and template filter class SkinDynamicSkin extends SkinTemplate { function initPage(&$out) { $this->initDynamicSkin( $out, 'DynamicSkin', 'monobook', 'DynamicSkinTemplate'); } function initDynamicSkin( &$out, $skinname, $stylename, $template ) { SkinTemplate::initPage($out); $this->skinname = $skinname; // Name displayed on preferences page $this->stylename = $stylename; // Subdirectory for css and graphics $this->template = $template; // Name of implementation class // Create the template page if it doesn't already exist (MediaWiki:DynamicSkin) $skin = new Article( Title::newFromText($this->skinname.'Skin', NS_MEDIAWIKI) ); if ( ! $skin->exists() ) { $default = call_user_func(array($template, 'getDefaultSkinText')); $skin->doEdit( $default, 'Created by ' . $this->skinname, EDIT_NEW | EDIT_FORCE_BOT ); } } } /** * Template filter callback for this skin. * Takes an associative array of data set from a SkinTemplate-based * class, and a wrapper for MediaWiki's localization database, and * outputs a formatted page. */ class DynamicSkinTemplate extends QuickTemplate { var $hide = array(); // List of command/option links to hide function execute() { global $wgSitename, $wgUser; // retrieve site name $this->set('sitename', $wgSitename); // suppress warnings to prevent notices about missing indexes in $this->data wfSuppressWarnings(); // Load the template article for this skin, or use the default // Use the parser preprocessor to evaluate conditionals in the template $skin = $wgUser->getSkin(); $template = $this->loadTemplate ( 'MediaWiki:'.$skin->getSkinName().'Skin', $this->getDefaultSkinText() ); // Now replace all the fields and send to output echo preg_replace_callback('/&(.*?)&/', array(&$this, 'skin_callback'), $template ); // Restore warnings before exit wfRestoreWarnings(); } // end of execute() method # Callback function to translate tags in preg_replace_callback # $matches[0] is the complete match function skin_callback( $matches ) { return( $this->translate_tag( $matches[1] ) ); } # Returns HTML text for the specified tag function translate_tag( $tag ) { global $wgParser, $wgUser; $skin = $wgUser->getSkin(); $PATH = $this->otext('stylepath'); $STYLE = $PATH . '/' . $this->otext('stylename'); $VER = $GLOBALS['wgStyleVersion']; $JSMIME = $this->otext('jsmimetype'); $out = ''; // For convenience in loops $tagdata = explode( '=', $tag, 2 ); // Check for parameters $args = (count($tagdata)>1) ? $tagdata[1] : ''; // Parameter(s) switch ( strtoupper($tagdata[0]) ) { case 'BODY_START': // End of head, start of body $out .= '</head>'; $out .= '<!-- Body --><body '; if ($this->data['body_ondblclick']) $out .= 'ondblclick="' . $this->otext('body_ondblclick') . '"'; if ($this->data['body_onload']) $out .= 'onload="' . $this->otext('body_onload') . '"'; $out .= 'class="mediawiki ' . $this->otext('nsclass') . ' ' . $this->otext('dir') . ' ' . $this->otext('pageclass') . '">'; return( $out ); case 'CATLINKS': // Category links return( $this->makeDiv('catlinks', 'catlinks') ); case 'COMMENT': // Comments are ignored return( '' ); case 'CONTENTS': // Page contents return( $this->ohtml('bodytext') ); case 'CUSTOMTOOLBAR': // Custom menu $list = explode( ',', $args ); // Array of options $out .= $this->startPortletList( array_shift($list), array_shift($list) ); // Find out which menu the keyword comes from foreach ( $list as $key) { $key = trim( $key ); if ( array_key_exists($key, $this->data['content_actions'])) $out .= $this->makeDataLink( $key, 'content_actions' ); elseif ( array_key_exists($key, $this->data['personal_urls']) ) $out .= $this->makeDataLink( $key, 'personal_urls' ); elseif ( array_key_exists($key, $this->data['nav_urls']) ) $out .= $this->makeNavLink( $key ); elseif ( array_key_exists($key, $this->data) ) $out .= $this->makeListText( $key ); else $out .= '<li>' . $key . '</li>'; } return( $this->endPortletList( $out ) ); case 'DOCTYPE': // Compose XHTML output $out = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' . '<html xmlns="' . $this->otext('xhtmldefaultnamespace') . '" '; foreach ($this->data['xhtmlnamespaces'] as $id => $ns) { $out .= 'xmlns:' . "{$id}=\"{$ns}\" "; } $out .= 'xml:lang="' . $this->otext('lang') . '" lang="' . $this->otext('lang') . '" dir="' . $this->otext('dir') . '">'; return( $out ); case 'END': // Closing trail '<!-- scripts and debugging information -->' $out .= $this->ohtml('reporttime'); if ( $this->data['debug'] ) $out .= '<!-- Debug output: ' . $this->otext( 'debug' ) . ' -->'; return( $out . '</body></html>' ); case 'FOOTER': // Footer $out .= '<div id="footer">'; $out .= $this->makeDiv( 'poweredbyico', 'f-poweredbyico' ); $out .= $this->makeDiv( 'copyrightico', 'f-copyrightico' ); // generate additional footer links $out .= '<ul id="f-list">'; foreach( array( 'lastmod', 'viewcount', 'numberofwatchingusers', 'credits', 'copyright', 'privacy', 'about', 'disclaimer', 'tagline') as $key ) { $out .= $this->makeListText( $key ); } $out .= '</ul></div>'; // Closing trail '<!-- scripts and debugging information -->' $out .= $this->ohtml('bottomscripts'); return( $out ); case 'HEAD_START': // HTML header $out = '<head><meta http-equiv="Content-Type" content="' . $this->otext('mimetype') . '; charset=' . $this->otext('charset') . '" />' . $this->ohtml('headlinks') . '<title>' . $this->otext('pagetitle') . '</title>'; return( $out ); case 'HIDE': // Hide command/option links $this->hide = explode( ',', $args ); // Get list of links to hide return( '' ); case 'JUMPLINKS': // Jump-to links if ($this->data['showjumplinks']) { $out .= '<div id="jump-to-nav">' . $this->omsg('jumpto') . ' <a href="#column-one">' . $this->omsg('jumptonavigation') . '</a>, <a href="#searchInput">' . $this->omsg('jumptosearch') . '</a></div>'; } return( $out ); case 'LANGUAGE': // Language links if ( $this->data['language_urls'] ) { $out .= $this->startPortletList( 'p-lang', 'otherlanguages' ); foreach ($this->data['language_urls'] as $item) { $out .= $this->makeListLink( 'language', '', $item['class'], '', $item['href'], $item['text']); } $out .= $this->endPortletList(); } return( $out ); case 'LOGO': // Logo image $out .= '<div class="portlet" id="p-logo">'; $out .= '<a style="background-image: url('; $out .= !empty($args) ? $args : $this->otext('logopath'); $out .= ');" href="' . htmlspecialchars($this->data['nav_urls']['mainpage']['href']) . '"'; $out .= $skin->tooltipAndAccesskey('n-mainpage') . '></a></div>'; $out .= '<script type="'.$JSMIME.'"> if (window.isMSIE55) fixalpha(); </script>'; // <!-- IE alpha-transparency fix --> return( $out ); case 'PAGETOOLS': // Page toolbar $out .= $this->startPortletList( 'p-cactions', 'views' ); foreach ($this->data['content_actions'] as $key => $item) { $out .= $this->makeListLink( $key, 'ca-'.$key, $item['class'], '', $item['href'], $item['text']); } return( $this->endPortletList($out) ); case 'PERSONAL': // User toolbar $out .= $this->startPortletList( 'p-personal', 'personaltools' ); foreach ($this->data['personal_urls'] as $key => $item) { $out .= $this->makeListLink( $key, 'pt-'.$key, $item['active']? 'active' : '', $item['class'], $item['href'], $item['text']); } return( $this->endPortletList( $out ) ); case 'SCRIPTS': // various MediaWiki-related scripts and styles $out = Skin::makeGlobalVariablesScript($this->data) . '<script type="'.$JSMIME.'" src="'.$PATH.'/common/wikibits.js?'.$VER.'"><!-- wikibits js --></script>'; if ($this->data['jsvarurl']) $out.= '<script type="'.$JSMIME.'" src="' . $this->otext('jsvarurl') . '"><!-- site js --></script>'; if ($this->data['pagecss']) $out.= '<style type="text/css">' . $this->ohtml('pagecss') . '</style>'; if ($this->data['usercss']) $out.= '<style type="text/css">' . $this->ohtml('usercss') . '</style>'; if ($this->data['userjs']) $out.= '<script type="'.$JSMIME.'" src="' . $this->otext('userjs' ) . '"></script>'; if ($this->data['userjsprev']) $out.= '<script type="'.$JSMIME.'">' . $this->ohtml('userjsprev') . '</script>'; if ($this->data['trackbackhtml']) $out.= $this->data['trackbackhtml']; $out .= '<!-- Head Scripts -->' . $this->ohtml('headscripts'); return( $out ); case 'SEARCH': // Search button(s) $out .= $this->startPortlet( 'p-search', 'search', 'searchBody', 'searchInput' ) . '<form action="' . $this->otext('searchaction') . '" id="searchform"><div>' . '<input id="searchInput" name="search" type="text"' . $skin->tooltipAndAccesskey('search') . ( (isset($this->data['search'])) ? ' value="' . $this->otext('search') . '"' : '' ) . ' />'; if ( !stristr($args,'go') && !stristr($args,'se')) $args .= ',go,search'; // Default if only 'image' was specified if ( stristr($args,'go') ) $out .= ' <input ' . (stristr($args,'im') ? 'type="image" src="'. $STYLE.'/go.gif"' : 'type="submit"') . ' name="go" class="searchButton" id="searchGoButton" value="' . $this->omsg('searcharticle') . '" />'; if ( stristr($args,'se') ) $out .= ' <input ' . (stristr($args,'im') ? 'type="image" src="'. $STYLE.'/search.gif"' : 'type="submit"') . ' name="fulltext" class="searchButton" id="searchGoButton" value="' . $this->omsg('searchbutton') . '" />'; $out .= '</div></form>'; return( $this->endPortlet( $out ) ); case 'SIDEBAR': // Normal Sidebar navigation $data = $this->buildDynamicSidebar($args); foreach ($data as $bar => $cont) { $out .= $this->startPortletList( 'p-'.$bar, wfEmptyMsg($bar, wfMsg($bar)) ? $bar : wfMsg($bar) ); foreach ($cont as $key => $item) { $out .= $this->makeListLink( $item['id'], $item['id'], $item['active'] ? 'active' : '', '', $item['href'], $item['text']); } $out .= $this->endPortletList(); } return( $out ); case 'SITENAME': // Site name return( $this->otext('sitename') ); case 'SITENOTICE': // Site notice return( ($this->data['sitenotice']) ? '<div id="siteNotice">' . $this->ohtml('sitenotice') . '</div>' : '' ); case 'SITESUB': // Tagline (site subtitle) return( '<h3 id="siteSub">' . $this->omsg('tagline') . '</h3>' ); case 'STYLES': // Style sheets if ( !empty($args) ) { $this->stylename = $args; $STYLE = $PATH . '/' . $this->otext('stylename'); } $out = '<style type="text/css" media="screen,projection">/*<![CDATA[*/ ' /*** general style sheets ***/ . '@import "'.$STYLE.'/main.css?'.$VER.'"; @import "'.$STYLE.'/contents.css?'.$VER.'";/*]]>*/</style>' . '<link rel="stylesheet" type="text/css"' /*** media-specific style sheets ***/ . (empty($this->data['printable']) ? ' media="print" ' : '') . 'href="'.$PATH.'/common/commonPrint.css?'.$VER.'" />' . '<link rel="stylesheet" type="text/css" media="handheld" href="'.$STYLE.'/handheld.css?'.$VER.'" />' /*** browser-specific style sheets ***/ . '<!--[if lt IE 5.5000]><style type="text/css">@import "'.$STYLE.'/IE50Fixes.css?'.$VER.'";</style><![endif]-->' . '<!--[if IE 5.5000]><style type="text/css">@import "'.$STYLE.'/IE55Fixes.css?'.$VER.'";</style><![endif]-->' . '<!--[if IE 6]><style type="text/css">@import "'.$STYLE.'/IE60Fixes.css?'.$VER.'";</style><![endif]-->' . '<!--[if IE 7]><style type="text/css">@import "'.$STYLE.'/IE70Fixes.css?'.$VER.'";</style><![endif]-->' /*** general IE fixes ***/ . '<!--[if lt IE 7]><script type="'.$JSMIME.'" src="'.$PATH.'/common/IEFixes.js?'.$VER.'"></script>' . '<meta http-equiv="imagetoolbar" content="no" /><![endif]-->'; return( $out ); case 'SUBTITLE': // Page subtitle return( $this->makeDiv('subtitle', 'contentSub') ); case 'TITLE': // Page name return( '<h1 class="firstHeading">' . $this->translate_variable( 'TITLE' ) . '</h1>' ); case 'TOOLBOX': // Toolbox $out .= $this->startPortletList( 'p-tb', 'toolbox' ); if ($this->data['notspecialpage']) { $out .= $this->makeNavLink( "whatlinkshere" ); $out .= $this->makeNavLink( "recentchangeslinked" ); } $out .= $this->makeNavLink( "trackbacklink" ); if ( $this->canShow('feeds') ) { $out .= '<li id="feedlinks">'; foreach ($this->data['feeds'] as $key => $feed) { $out .= '<span id="feed-' . Sanitizer::escapeId($key) . '">' . '<a href="' . htmlspecialchars($feed['href']) . '"' . $skin->tooltipAndAccesskey('feed-'.$key) . '>' . htmlspecialchars($feed['text']) . '</a> </span>'; } $out .= '</li>'; } foreach( array('contributions', 'blockip', 'emailuser', 'upload', 'specialpages') as $special ) { $out .= $this->makeNavLink( $special ); } $out .= $this->makeNavLink( "print", $this->omsg("printableversion") ); if (!empty($this->data['nav_urls']['permalink']['href'])) { $out .= $this->makeNavLink( "permalink" ); } elseif ($this->data['nav_urls']['permalink']['href'] === '') { $out .= '<li id="t-ispermalink"' . $skin->tooltip('t-ispermalink') . '>' . $this->omsg('permalink') . '</li>'; } // Buffer any output from monobook hook(s) ob_start(); wfRunHooks( 'MonoBookTemplateToolboxEnd', array( &$this ) ); $out .= ob_get_contents(); ob_end_clean(); return( $this->endPortletList( $out ) ); case 'UNDELETE': // Undelete notice return( $this->makeDiv('undelete', 'contentSub2') ); case 'USERMSG': // User-messages notification return( $this->makeDiv('newtalk', '', 'usermessage' ) ); default: return( '@@@ Unknown tag: ' . __FILE__ . ' (' . __LINE__ . '): ' . $tag . '@@@' ); } } # Returns HTML text for the specified pseudo-variable function translate_variable( $tag ) { global $wgParser, $wgUser; switch ( strtoupper($tag) ) { case 'CATEGORIES': // &&CATEGORIES&& pseudo-variable: List of categories for this screen preg_match_all( '`title="Category:(.*?)"`', $skin->getCategories(), $matches, PREG_PATTERN_ORDER ); return( implode( ",", $matches[1] ) ); case 'TITLE': // &&TITLE&& pseudo-variable Page title return( $this->data['displaytitle']!="" ? $this->ohtml('title') : $this->otext('title') ); case 'USERGROUPS': // &&USERGROUPS&& pseudo-variable: Groups this user belongs to $wgParser->disableCache(); // Mark this content as uncacheable return( implode( ",", $wgUser->getGroups() ) ); case 'USERID': // &&USERID&& pseudo-variable: User Name, blank if anonymous $wgParser->disableCache(); // Mark this content as uncacheable // getName() returns IP for anonymous users, so check if logged in first return( $wgUser->isLoggedIn() ? $wgUser->getName() : '' ); } } // Loads and preprocesses the template page function loadTemplate( $title, $default=null ) { global $wgUser, $wgParser, $wgTitle; // Load the template article for this skin, or use the default $template = null; $article = new Article( Title::newFromText( $title ) ); if ( $article ) $template = $article->fetchContent(0,false,false); if ( !$template ) $template = $default; if ( $template ) { // Drop leading and trailing blanks before parsing $template = preg_replace('/(^\s+|\s+$)/m', '', $template ); // Substitute a few skin-related variables before parsing $template = preg_replace_callback('/&&(.*?)&&/', array(&$this, 'var_callback'), $template ); // Use the parser preprocessor to evaluate conditionals in the template // Copy the parser to make sure we don't trash the current page $lparse = clone $wgParser; $opt = ParserOptions::newFromUser($wgUser); $template = $lparse->preprocess( $template, $wgTitle, $opt ); } return( $template ); } # Callback function to translate pseudo-variables in preg_replace_callback # $matches[0] is the complete match function var_callback( $matches ) { return( $this->translate_variable( $matches[1] ) ); } // Format a link function makeNavLink( $key, $msg=null ) { if ( $this->canShow($key, $this->data['nav_urls'][$key]['href']) ) return( $this->makeListLink( $key, 't-'.$key, '', '', $this->data['nav_urls'][$key]['href'], $msg ? $msg : $this->omsg($key)) ); return( '' ); } // Format a data link function makeDataLink( $key, $menu ) { $item = $this->data[$menu][$key]; return( $this->makeListLink( $key, 'pt-'.$key, $item['active'] ? 'active' : '', $item['class'], $item['href'], $item['text'] ) ); } // Format a list element link function makeListLink( $key, $id, $class, $hclass, $href, $text ) { if ( !$this->canShow($key, $text) ) return( '' ); global $wgUser; $skin = $wgUser->getSkin(); $out = '<li'; if ( !empty($id) ) $out .= ' id="' . Sanitizer::escapeId($id) . '"'; if ( !empty($class) ) $out .= ' class="' . htmlspecialchars($class) . '"'; $out .= '>'; if ( !empty($href) ) { $out .= '<a href="' . htmlspecialchars($href) . '"'; if ( !empty($hclass) ) $out .= ' class="' . htmlspecialchars($hclass) . '"'; if ( !empty($id) ) $out .= $skin->tooltipAndAccesskey($id); $out .= '>'; } $out .= htmlspecialchars($text); if ( !empty($href) ) $out .= '</a>'; return ( $out . '</li>' ); } // Format a list element with no link function makeListText( $key ) { if ( $this->canShow($key, $this->data[$key]) ) return( '<li id="' . $key . '">' . $this->ohtml($key) . '</li>' ); return( '' ); } // Output within a <DIV> function makeDiv( $key, $id=null, $class=null ) { if ( empty($this->data[$key]) ) return( '' ); return( '<div' . ( !empty($id) ? (' id="' . $id . '"') : '') . ( !empty($class) ? (' class="' . $class . '"') : '') . '>' . $this->ohtml($key) . '</div>' ); } // Common HTML for portlets function startPortlet( $id, $msg, $divid=null, $label=null ) { $out = '<div class="portlet" id="'.$id.'"><h5>'; if ( $label ) $out .= '<label for="' . $label . '">'; $out .= $this->omsg($msg); if ( $label ) $out .= '</label>'; $out .= '</h5><div class="pBody"'; if ( $divid ) $out .= ' id="' . $divid . '"'; return ( $out . '>' ); } function endPortlet( $html=null ) { return( ($html ? $html : '') . '</div></div>' ); } function startPortletList( $id, $msg, $divid=null, $label=null ) { return( $this->startPortlet( $id, $msg, $divid, $label ) . '<ul>' ); } function endPortletList( $html=null ) { return( ($html ? $html : '') . '</ul>' . $this->endPortlet() ); } // See if the command or link for this key can be displayed function canShow( $key, $data=null ) { if ( in_array( $key, $this->hide ) ) return( false ); if ( $data == null ) { if ( !isset($this->data[$key]) ) return( false ); $data = $this->data[$key]; // Default value } return ( !empty($data) ); } // Process messages (but don't echo as in QuickTemplate base class) function omsg( $str ) { return ( htmlspecialchars( $this->translator->translate( $str ) ) ); } function otext( $str ) { return ( htmlspecialchars( $this->data[$str] ) ); } function ohtml( $str ) { return ( $this->data[$str] ); } // Category List Fix // hijack category functions to create a proper list function getCategories() { $catlinks=$this->getCategoryLinks(); if(!empty($catlinks)) { return "<ul id='catlinks'>{$catlinks}</ul>"; } } // This is a fix Note for returning category links as a proper <code>UL</code> element // (instead of returning a mostly unordered string, which is the default behavior). function getCategoryLinks() { global $wgOut, $wgUser, $wgTitle, $wgUseCategoryBrowser; global $wgContLang; if(count($wgOut->mCategoryLinks) == 0) return ''; $skin = $wgUser->getSkin(); # separator $sep = ""; // use Unicode bidi embedding override characters, // to make sure links don't smash each other up in ugly ways $dir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; $embed = "<li dir='$dir'>"; $pop = '</li>'; $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $wgOut->mCategoryLinks ) . $pop; $msg = wfMsgExt('pagecategories', array('parsemag', 'escape'), count($wgOut->mCategoryLinks)); $s = $skin->makeLinkObj(Title::newFromText(wfMsgForContent('pagecategorieslink')), $msg) . $t; # optional 'dmoz-like' category browser - will be shown under the list # of categories an article belongs to if($wgUseCategoryBrowser) { $s .= '<br /><hr />'; # get a big array of the parents tree $parenttree = $wgTitle->getParentCategoryTree(); # Skin object passed by reference because it can not be # accessed under the method subfunction drawCategoryBrowser $tempout = explode("\n", Skin::drawCategoryBrowser($parenttree, $this)); # clean out bogus first entry and sort them unset($tempout[0]); asort($tempout); # output one per line $s .= implode("<br />\n", $tempout); } return $s; } // Load and parse the dynamic sidebar // Similar to default processing in SKin.php function buildSidebar() // except it uses the '!' character as a delimiter instead of '|' // which simplifies using conditional templates. function buildDynamicSidebar( $name ) { // If a name was specified, try to load the sidebar from that page if ( !empty($name) ) $sidebar = $this->loadTemplate( $name ); // Default is standard MediaWiki sidebar if ( empty($name) || empty($sidebar) ) return( $this->data['sidebar'] ); // The navigation sidebar is formatted as a two-level list. // Parsing code is copied from SKin.php function buildSidebar() $bar = array(); $lines = explode( "\n", $sidebar ); foreach ( $lines as $line ) { if (strpos($line, '*') !== 0) // Ignore lines that aren't part of a list continue; if (strpos($line, '**') !== 0) { // Top level lines are start of a new heading $line = trim($line, '* '); $heading = $line; } else { if (strpos($line, '=') !== false) { // sanity check $line = explode( '=' , trim($line, '* '), 2 ); $link = wfMsgForContent( $line[0] ); if ($link == '-') continue; if (wfEmptyMsg($line[1], $text = wfMsg($line[1]))) $text = $line[1]; if (wfEmptyMsg($line[0], $link)) $link = $line[0]; if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $link ) ) { $href = $link; } else { $title = Title::newFromText( $link ); if ( $title ) { $title = $title->fixSpecialName(); $href = $title->getLocalURL(); } else { $href = 'INVALID-TITLE'; } } $bar[$heading][] = array( 'text' => $text, 'href' => $href, 'id' => 'n-' . strtr($line[1], ' ', '-'), 'active' => false ); } else { continue; } } } return( $bar ); } static function getDefaultSkinText( ) { return( /**** Monobook skin (MediaWiki 1.10) ****/ "&DOCTYPE&\n" . "&HEAD_START& &STYLES& &SCRIPTS&\n" . "&BODY_START&\n" . "<div id=\"globalWrapper\">\n" . " <div id=\"column-content\">\n" . " <div id=\"content\">\n" . " <a name=\"top\" id=\"top\"></a>&TITLE&\n" . " <div id=\"bodyContent\">\n" . " &SITESUB& &SUBTITLE& &JUMPLINKS&\n" . " <!-- start content -->&CONTENTS&<!-- end content --><div class=\"visualClear\"></div>\n" . " </div>\n" . " </div>\n" . " </div>\n" . " <div id=\"column-one\">\n" . " &PAGETOOLS& &PERSONAL&\n" . " &LOGO& &SIDEBAR&\n" . " &SEARCH& &TOOLBOX&\n" . " &SITENOTICE& &USERMSG& &LANGUAGE& &UNDELETE&\n" . " &CATLINKS&\n" . " </div><!-- end of the left (by default at least) column -->\n" . " <div class=\"visualClear\"></div>\n" . " &FOOTER&\n" . "</div>\n" . "&END&\n" // No &SITENAME& /**** Default layout according to MediaWiki Manual:Skinning "&DOCTYPE&\n" . "&HEAD_START& &STYLES& &SCRIPTS&\n" . "&BODY_START& &SITENAME& &LOGO& &SITESUB& &SITENOTICE&\n" . "&USERMSG& &PERSONAL& &JUMPLINKS& &SEARCH&\n" . "&SIDEBAR& &TOOLBOX& &LANGUAGE&\n" . "&TITLE& &SUBTITLE& &UNDELETE&\n" . "&CONTENTS&\n" . "&CATLINKS& &PAGETOOLS&\n" . "&FOOTER&\n" . "&END&\n" ****/ ); } } ?>
[edit] Implementation
As an example, the &LOGO& tag invokes the following code:
case 'LOGO': // Logo image $out .= '<div class="portlet" id="p-logo">'; $out .= '<a style="background-image: url(' . $this->otext('logopath') . ');" '; $out .= 'href="' . htmlspecialchars($this->data['nav_urls']['mainpage']['href']) . '"'; $out .= $skin->tooltipAndAccesskey('n-mainpage') . '></a></div>'; $out .= '<script type="'.$JSMIME.'"> if (window.isMSIE55) fixalpha(); </script>'; return( $out );
Which generates the following output, where the values in red will be replaced according to the wiki configuration.
<div class="portlet" id="p-logo"> <a style="background-image: url(logopath);" href="navmainpage" maintooltip></a> </div> <script type="mimetype"> if (window.isMSIE55) fixalpha(); </script>
[edit] Derived classes
While DynamicSkin makes it easy to customize skins without any code changes, it's written as a base class so you can add additional code in a derived class. For an example of a derived skin, and the site it's used on, see InfoDabble skin at ehartwell.com.
At a minimum, a derived class must override SkinDynamicSkin::initPage:
class SkinDynamicSkin extends SkinTemplate { function initPage(&$out) { $this->initDynamicSkin( $out, 'DynamicSkin', 'monobook', 'DynamicSkinTemplate'); }
- MediaWiki builds the list of skin names from the list of .php files in the skins/ subdirectory. For a class to be recognized as a MediaWiki skin, its name must be Skin<name of source file>. For example, if your derived skin is called MySkin, the source file must be skins/MySkin.php and the class name must be SkinMySkin, or the skin won't be recognized.
class SkinMySkin extends SkinDynamicSkin { function initPage(&$out) { $this->initDynamicSkin( $out, 'MySkin', 'MySkin', 'MySkinTemplate'); }
- The second parameter, MySkin, is the name displayed in the list of skin preferences
- The third parameter, MySkin, is the name of the skins/ subdirectory for style sheets and graphics.
- The fourth parameter, MySkinTemplate, is the name of your overriden template class.
If you're using a derived class and you don't want DynamicSkin to appear on the Preferences page, rename DynamicSkin.php to DynamicSkin.php.dep and change the reference in the derived class.
[edit] Tips, tricks, and hoops
- This version embeds some of the class information (
<div>and<span>) in the code and some in the template page. There's a tradeoff between providing flexibility and ensuring that the standard MediaWiki style classes are present for derived skins. The default generates the Monobook skin with code derived from the MediaWiki Skinning sample. - Depending on what's dynamic in the skin, you may want to disable caching. Caching is automatically disabled if the &&USERID&& pseudo-variable is used.
[edit] Version history
- 2007.8.24 v1.91: Added image file parameter to &LOGO& tag (thanks to Patricia Barden for the idea). Also added description for the &CUSTOMTOOLBAR& tag.
- 2007.8.22 v1.9: Code cleanup, document, first published version
[edit] See also
- Extension:WikiSkin (MediaWiki.org), Wikiskin.php (Organic Design)
- Meta MediaWiki: Layout architecture of MediaWiki pages, Skins
- MediaWiki manual: Skins, Skinning