Implement page status indicators
authorBartosz Dziewoński <matma.rex@gmail.com>
Wed, 24 Sep 2014 10:44:16 +0000 (12:44 +0200)
committerJames D. Forrester <jforrester@wikimedia.org>
Fri, 17 Oct 2014 22:49:15 +0000 (15:49 -0700)
Page status indicators are icons (or short text snippets) usually
displayed in the top-right corner of the page, outside of the main
content. Basically, <indicator name="foo">[[File:Foo.svg|20px]]</indicator>
may be used on a page to place the icon in the indicator area. They
are also known as top icons, page icons, heading icons or title icons.

I found the discussion on bug 23796 highly illuminating. I suggest
that everyone read it before suggesting different design choices.

I spent some time with a thesaurus pondering the name. "Emblems" and
"badges" were also considered, but the former has a much more limited
meaning and the latter is already taken by Wikidata, with a similar
but subtly different feature set. I am not aware of any naming
conflicts ;) besides new talk page message "indicator" (used by core
and Echo in some documents) and OOjs UI indicators (tiny icons like
the arrow on a dropdown form element), which shouldn't be confusing.

Potential use cases include:
* "Lock" indicators for page protection levels
* Featured/good article indicators
* Redirect shortcuts display ("WP:VPT")
* Links to help/manual for special pages
* Coordinates?… or globe icon for inline pop-up maps

Design features:
* Skin-customizable. Skins can fully control where and how indicators
  are shown, or may just do <?php echo $this->getIndicators(); ?> to
  output the default structure. By default they are not shown at all.
* Extension-customizable. Extensions can call ParserOutput::addIndicator()
  to insert an indicator from one of the numerous parser hooks.
* Wiki-customizable. In addition to just using the parser functions,
  on-wiki styles and scripts can use the provided classes and ids
  (.mw-indicator, #mw-indicator-<name>) to customize their display.

Design limitations:
* Every indicator must have a unique identifier (name). It's not
  possible to create arrays, or to have several indicators with the
  same name. In case of duplicates, the latest occurrence of the
  parser function wins.
* Indicators are displayed ordered by their names (and not occurrence
  order). This ensures consistency across pages and provides a simple
  means of ordering or grouping them.
* Indicators are not stored, tracked or accessible outside of
  ParserOutput (in particular they're not in the page_props table).
  They are intended to merely reflect the content or metadata that is
  already present on the page, and not be data themselves. If you ever
  think you need to list pages with a given status indicator, instead
  figure out what it means and use the appropriate tracking category,
  special page report, already existing page_prop, or other means.

Corresponding patch in Vector: I90a8ae15ac8275d084ea5f47b6b2684d5e6c7412.
I'll implement support in the other three skins included in the tarball
and document it on mediawiki.org after this is merged.

Bug: 23796
Change-Id: I2389ff9a5332a2b1d033eb75f0946e5241cfaaf4

RELEASE-NOTES-1.25
includes/OutputPage.php
includes/api/ApiParse.php
includes/parser/CoreTagHooks.php
includes/parser/ParserOutput.php
includes/skins/BaseTemplate.php
includes/skins/SkinTemplate.php
languages/i18n/en.json
languages/i18n/qqq.json

index 2d5e7f4..2180022 100644 (file)
@@ -25,6 +25,14 @@ production.
   in CSS. This results in slight size increase before gzip compression (due to
   percent-encoding), but up to 20% decrease after it.
 * Upgrade jStorage to v0.4.12.
+* MediaWiki now natively supports page status indicators: icons (or short text
+  snippets) usually displayed in the top-right corner of the page. They have
+  been in use on Wikipedia for a long time, implemented using templates and CSS
+  absolute positioning.
+  - Basic wikitext syntax: <indicator name="foo">[[File:Foo.svg|20px]]</indicator>
+  - Usage instructions: https://www.mediawiki.org/wiki/Help:Page_status_indicators
+  - Adjusting custom skins to support indicators:
+    https://www.mediawiki.org/wiki/Manual:Skinning#Page_status_indicators
 
 === Bug fixes in 1.25 ===
 * (bug 71003) No additional code will be generated to try to load CSS-embedded
index 084f714..3bb2175 100644 (file)
@@ -122,6 +122,9 @@ class OutputPage extends ContextSource {
        /** @var array */
        protected $mCategories = array();
 
+       /** @var array */
+       protected $mIndicators = array();
+
        /** @var array Array of Interwiki Prefixed (non DB key) Titles (e.g. 'fr:Test page') */
        private $mLanguageLinks = array();
 
@@ -1329,6 +1332,32 @@ class OutputPage extends ContextSource {
                return $this->mCategories;
        }
 
+       /**
+        * Add an array of indicators, with their identifiers as array keys and HTML contents as values.
+        *
+        * In case of duplicate keys, existing values are overwritten.
+        *
+        * @param array $indicators
+        * @since 1.25
+        */
+       public function setIndicators( array $indicators ) {
+               $this->mIndicators = $indicators + $this->mIndicators;
+               // Keep ordered by key
+               ksort( $this->mIndicators );
+       }
+
+       /**
+        * Get the indicators associated with this page.
+        *
+        * The array will be internally ordered by item keys.
+        *
+        * @return array Keys: identifiers, values: HTML contents
+        * @since 1.25
+        */
+       public function getIndicators() {
+               return $this->mIndicators;
+       }
+
        /**
         * Restrict the page to loading modules bundled the software.
         *
@@ -1641,6 +1670,7 @@ class OutputPage extends ContextSource {
        public function addParserOutputMetadata( $parserOutput ) {
                $this->mLanguageLinks += $parserOutput->getLanguageLinks();
                $this->addCategoryLinks( $parserOutput->getCategories() );
+               $this->setIndicators( $parserOutput->getIndicators() );
                $this->mNewSectionLink = $parserOutput->getNewSection();
                $this->mHideNewSectionLink = $parserOutput->getHideNewSection();
 
index 0b1f4db..c2ec8d4 100644 (file)
@@ -337,6 +337,14 @@ class ApiParse extends ApiBase {
                        $result_array['modulemessages'] = array_values( array_unique( $p_result->getModuleMessages() ) );
                }
 
+               if ( isset( $prop['indicators'] ) ) {
+                       foreach ( $p_result->getIndicators() as $name => $content ) {
+                               $indicator = array( 'name' => $name );
+                               ApiResult::setContent( $indicator, $content );
+                               $result_array['indicators'][] = $indicator;
+                       }
+               }
+
                if ( isset( $prop['iwlinks'] ) ) {
                        $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() );
                }
@@ -391,6 +399,7 @@ class ApiParse extends ApiBase {
                        'sections' => 's',
                        'headitems' => 'hi',
                        'modules' => 'm',
+                       'indicators' => 'ind',
                        'modulescripts' => 'm',
                        'modulestyles' => 'm',
                        'modulemessages' => 'm',
@@ -680,6 +689,7 @@ class ApiParse extends ApiBase {
                                        'headitems',
                                        'headhtml',
                                        'modules',
+                                       'indicators',
                                        'iwlinks',
                                        'wikitext',
                                        'properties',
@@ -735,6 +745,7 @@ class ApiParse extends ApiBase {
                                ' headitems      - Gives items to put in the <head> of the page',
                                ' headhtml       - Gives parsed <head> of the page',
                                ' modules        - Gives the ResourceLoader modules used on the page',
+                               ' indicators     - Gives the HTML of page status indicators used on the page',
                                ' iwlinks        - Gives interwiki links in the parsed wikitext',
                                ' wikitext       - Gives the original wikitext that was parsed',
                                ' properties     - Gives various properties defined in the parsed wikitext',
index 85920cc..3ffa16c 100644 (file)
@@ -35,6 +35,7 @@ class CoreTagHooks {
                $parser->setHook( 'pre', array( __CLASS__, 'pre' ) );
                $parser->setHook( 'nowiki', array( __CLASS__, 'nowiki' ) );
                $parser->setHook( 'gallery', array( __CLASS__, 'gallery' ) );
+               $parser->setHook( 'indicator', array( __CLASS__, 'indicator' ) );
                if ( $wgRawHtml ) {
                        $parser->setHook( 'html', array( __CLASS__, 'html' ) );
                }
@@ -119,4 +120,30 @@ class CoreTagHooks {
        public static function gallery( $content, $attributes, $parser ) {
                return $parser->renderImageGallery( $content, $attributes );
        }
+
+       /**
+        * XML-style tag for page status indicators: icons (or short text snippets) usually displayed in
+        * the top-right corner of the page, outside of the main content.
+        *
+        * @param string $content
+        * @param array $attributes
+        * @param Parser $parser
+        * @param PPFrame $frame
+        * @return string
+        * @since 1.25
+        */
+       public static function indicator( $content, array $attributes, Parser $parser, PPFrame $frame ) {
+               if ( !isset( $attributes['name'] ) || trim( $attributes['name'] ) === '' ) {
+                       return '<span class="error">' .
+                               wfMessage( 'invalid-indicator-name' )->inContentLanguage()->text() .
+                               '</span>';
+               }
+
+               $parser->getOutput()->setIndicator(
+                       trim( $attributes['name'] ),
+                       $parser->recursiveTagParse( $content, $frame )
+               );
+
+               return '';
+       }
 }
index 43e8d0b..428e7b2 100644 (file)
@@ -25,6 +25,7 @@ class ParserOutput extends CacheTime {
        public $mText,                       # The output text
                $mLanguageLinks,              # List of the full text of language links, in the order they appear
                $mCategories,                 # Map of category names to sort keys
+               $mIndicators = array(),       # Page status indicators, usually displayed in top-right corner
                $mTitleText,                  # title text of the chosen language variant
                $mLinks = array(),            # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
                $mTemplates = array(),        # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
@@ -130,6 +131,13 @@ class ParserOutput extends CacheTime {
                return $this->mCategories;
        }
 
+       /**
+        * @since 1.25
+        */
+       public function getIndicators() {
+               return $this->mIndicators;
+       }
+
        public function getTitleText() {
                return $this->mTitleText;
        }
@@ -267,6 +275,13 @@ class ParserOutput extends CacheTime {
                $this->mCategories[$c] = $sort;
        }
 
+       /**
+        * @since 1.25
+        */
+       public function setIndicator( $id, $content ) {
+               $this->mIndicators[$id] = $content;
+       }
+
        public function addLanguageLink( $t ) {
                $this->mLanguageLinks[] = $t;
        }
index 6dc33ac..7217000 100644 (file)
@@ -600,6 +600,37 @@ abstract class BaseTemplate extends QuickTemplate {
                return $footericons;
        }
 
+       /**
+        * Get the suggested HTML for page status indicators: icons (or short text snippets) usually
+        * displayed in the top-right corner of the page, outside of the main content.
+        *
+        * Your skin may implement this differently, for example by handling some indicator names
+        * specially with a different UI. However, it is recommended to use a `<div class="mw-indicator"
+        * id="mw-indicator-<id>" />` as a wrapper element for each indicator, for better compatibility
+        * with extensions and user scripts.
+        *
+        * The raw data is available in `$this->data['indicators']` as an associative array (keys:
+        * identifiers, values: contents) internally ordered by keys.
+        *
+        * @return string HTML
+        * @since 1.25
+        */
+       public function getIndicators() {
+               $out = "<div class=\"mw-indicators\">\n";
+               foreach ( $this->data['indicators'] as $id => $content ) {
+                       $out .= Html::rawElement(
+                               'div',
+                               array(
+                                       'id' => Sanitizer::escapeId( "mw-indicator-$id" ),
+                                       'class' => 'mw-indicator',
+                               ),
+                               $content
+                       ) . "\n";
+               }
+               $out .= "</div>\n";
+               return $out;
+       }
+
        /**
         * Output the basic end-page trail including bottomscripts, reporttime, and
         * debug stuff. This should be called right before outputting the closing
index c1db302..1b99bc3 100644 (file)
@@ -438,6 +438,8 @@ class SkinTemplate extends Skin {
                        }
                }
 
+               $tpl->set( 'indicators', $out->getIndicators() );
+
                $tpl->set( 'sitenotice', $this->getSiteNotice() );
                $tpl->set( 'bottomscripts', $this->bottomScripts() );
                $tpl->set( 'printfooter', $this->printSource() );
index 9aa05f3..77f707a 100644 (file)
        "unknown_extension_tag": "Unknown extension tag \"$1\"",
        "duplicate-defaultsort": "<strong>Warning:</strong> Default sort key \"$2\" overrides earlier default sort key \"$1\".",
        "duplicate-displaytitle": "<strong>Warning:</strong> Display title \"$2\" overrides earlier display title \"$1\".",
+       "invalid-indicator-name": "<strong>Error:</strong> Page status indicators' <code>name</code> attribute must not be empty.",
        "version": "Version",
        "version-summary": "",
        "version-extensions": "Installed extensions",
index 55945e9..d938f6f 100644 (file)
        "unknown_extension_tag": "This is an error shown when you use an unknown extension tag name.\n\nThis feature allows tags like <code><nowiki><pre></nowiki></code> to be called with a parser like <code><nowiki>{{#tag:pre}}</nowiki></code>.\n\nParameters:\n* $1 - the unknown extension tag name",
        "duplicate-defaultsort": "See definition of [[w:Sorting|sort key]] on Wikipedia. Parameters:\n* $1 - old default sort key\n* $2 - new default sort key",
        "duplicate-displaytitle": "Warning shown when a page has its display title set multiple times. Parameters:\n* $1 - old display title\n* $2 - new display title",
+       "invalid-indicator-name": "Warning shown when the [https://www.mediawiki.org/wiki/Help:Page_status_indicators &lt;indicator name=\"''unique-identifier''\">''content''&lt;/indicator>] parser tag is used incorrectly.",
        "version": "{{doc-special|Version}}\n{{Identical|Version}}",
        "version-summary": "{{doc-specialpagesummary|version}}",
        "version-extensions": "Header on [[Special:Version]].",