]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/Xml.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / Xml.php
index 970444fa882b683b9fc8556c17883578ea4ffb15..0091513125ca68ee601dac526b532271fbba76cd 100644 (file)
@@ -1,9 +1,28 @@
 <?php
+/**
+ * Methods to generate XML.
+ *
+ * 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
+ */
 
 /**
  * Module of static functions for generating XML
  */
-
 class Xml {
        /**
         * Format an XML element with given attributes and, optionally, text content.
@@ -11,21 +30,23 @@ class Xml {
         * Strings are assumed to not contain XML-illegal characters; special
         * characters (<, >, &) are escaped but illegals are not touched.
         *
-        * @param $element String: element name
-        * @param $attribs Array: Name=>value pairs. Values will be escaped.
-        * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
-        * @param $allowShortTag Bool: whether '' in $contents will result in a contentless closed tag
+        * @param string $element Element name
+        * @param array $attribs Name=>value pairs. Values will be escaped.
+        * @param string $contents Null to make an open tag only; '' for a contentless closed tag (default)
+        * @param bool $allowShortTag Whether '' in $contents will result in a contentless closed tag
         * @return string
         */
-       public static function element( $element, $attribs = null, $contents = '', $allowShortTag = true ) {
+       public static function element( $element, $attribs = null, $contents = '',
+               $allowShortTag = true
+       ) {
                $out = '<' . $element;
-               if( !is_null( $attribs ) ) {
-                       $out .=  self::expandAttributes( $attribs );
+               if ( !is_null( $attribs ) ) {
+                       $out .= self::expandAttributes( $attribs );
                }
-               if( is_null( $contents ) ) {
+               if ( is_null( $contents ) ) {
                        $out .= '>';
                } else {
-                       if( $allowShortTag && $contents === '' ) {
+                       if ( $allowShortTag && $contents === '' ) {
                                $out .= ' />';
                        } else {
                                $out .= '>' . htmlspecialchars( $contents ) . "</$element>";
@@ -38,15 +59,17 @@ class Xml {
         * Given an array of ('attributename' => 'value'), it generates the code
         * to set the XML attributes : attributename="value".
         * The values are passed to Sanitizer::encodeAttribute.
-        * Return null if no attributes given.
-        * @param $attribs Array of attributes for an XML element
+        * Returns null or empty string if no attributes given.
+        * @param array|null $attribs Array of attributes for an XML element
+        * @throws MWException
+        * @return null|string
         */
        public static function expandAttributes( $attribs ) {
                $out = '';
-               if( is_null( $attribs ) ) {
+               if ( is_null( $attribs ) ) {
                        return null;
-               } elseif( is_array( $attribs ) ) {
-                       foreach( $attribs as $name => $val ) {
+               } elseif ( is_array( $attribs ) ) {
+                       foreach ( $attribs as $name => $val ) {
                                $out .= " {$name}=\"" . Sanitizer::encodeAttribute( $val ) . '"';
                        }
                        return $out;
@@ -60,20 +83,18 @@ class Xml {
         * $wgContLang->normalize() validator first to ensure that no invalid UTF-8
         * is passed.
         *
-        * @param $element String:
-        * @param $attribs Array: Name=>value pairs. Values will be escaped.
-        * @param $contents String: NULL to make an open tag only; '' for a contentless closed tag (default)
+        * @param string $element
+        * @param array $attribs Name=>value pairs. Values will be escaped.
+        * @param string $contents Null to make an open tag only; '' for a contentless closed tag (default)
         * @return string
         */
-       public static function elementClean( $element, $attribs = array(), $contents = '') {
+       public static function elementClean( $element, $attribs = [], $contents = '' ) {
                global $wgContLang;
-               if( $attribs ) {
-                       $attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
+               if ( $attribs ) {
+                       $attribs = array_map( [ 'UtfNormal\Validator', 'cleanUp' ], $attribs );
                }
-               if( $contents ) {
-                       wfProfileIn( __METHOD__ . '-norm' );
+               if ( $contents ) {
                        $contents = $wgContLang->normalize( $contents );
-                       wfProfileOut( __METHOD__ . '-norm' );
                }
                return self::element( $element, $attribs, $contents );
        }
@@ -81,8 +102,8 @@ class Xml {
        /**
         * This opens an XML element
         *
-        * @param $element String name of the element
-        * @param $attribs array of attributes, see Xml::expandAttributes()
+        * @param string $element Name of the element
+        * @param array $attribs Array of attributes, see Xml::expandAttributes()
         * @return string
         */
        public static function openElement( $element, $attribs = null ) {
@@ -91,429 +112,510 @@ class Xml {
 
        /**
         * Shortcut to close an XML element
-        * @param $element String element name
+        * @param string $element Element name
         * @return string
         */
-       public static function closeElement( $element ) { return "</$element>"; }
+       public static function closeElement( $element ) {
+               return "</$element>";
+       }
 
        /**
         * Same as Xml::element(), but does not escape contents. Handy when the
         * content you have is already valid xml.
         *
-        * @param $element String element name
-        * @param $attribs array of attributes
-        * @param $contents String content of the element
+        * @param string $element Element name
+        * @param array $attribs Array of attributes
+        * @param string $contents Content of the element
         * @return string
         */
        public static function tags( $element, $attribs = null, $contents ) {
                return self::openElement( $element, $attribs ) . $contents . "</$element>";
        }
 
-       /**
-        * Build a drop-down box for selecting a namespace
-        *
-        * @param $selected Mixed: Namespace which should be pre-selected
-        * @param $all Mixed: Value of an item denoting all namespaces, or null to omit
-        * @param $element_name String: value of the "name" attribute of the select tag
-        * @param $label String: optional label to add to the field
-        * @return string
-        */
-       public static function namespaceSelector( $selected = '', $all = null, $element_name = 'namespace', $label = null ) {
-               global $wgContLang;
-               $namespaces = $wgContLang->getFormattedNamespaces();
-               $options = array();
-
-               // Godawful hack... we'll be frequently passed selected namespaces
-               // as strings since PHP is such a shithole.
-               // But we also don't want blanks and nulls and "all"s matching 0,
-               // so let's convert *just* string ints to clean ints.
-               if( preg_match( '/^\d+$/', $selected ) ) {
-                       $selected = intval( $selected );
-               }
-
-               if( !is_null( $all ) )
-                       $namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces;
-               foreach( $namespaces as $index => $name ) {
-                       if( $index < NS_MAIN ) {
-                               continue;
-                       }
-                       if( $index === 0 ) {
-                               $name = wfMsg( 'blanknamespace' );
-                       }
-                       $options[] = self::option( $name, $index, $index === $selected );
-               }
-
-               $ret = Xml::openElement( 'select', array( 'id' => 'namespace', 'name' => $element_name,
-                       'class' => 'namespaceselector' ) )
-                       . "\n"
-                       . implode( "\n", $options )
-                       . "\n"
-                       . Xml::closeElement( 'select' );
-               if ( !is_null( $label ) ) {
-                       $ret = Xml::label( $label, $element_name ) . '&#160;' . $ret;
-               }
-               return $ret;
-       }
-
        /**
         * Create a date selector
         *
-        * @param $selected Mixed: the month which should be selected, default ''
-        * @param $allmonths String: value of a special item denoting all month. Null to not include (default)
-        * @param $id String: Element identifier
-        * @return String: Html string containing the month selector
+        * @param string $selected The month which should be selected, default ''.
+        * @param string $allmonths Value of a special item denoting all month.
+        *   Null to not include (default).
+        * @param string $id Element identifier
+        * @return string Html string containing the month selector
         */
        public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
                global $wgLang;
-               $options = array();
-               if( is_null( $selected ) )
+               $options = [];
+               $data = new XmlSelect( 'month', $id, $selected );
+               if ( is_null( $selected ) ) {
                        $selected = '';
-               if( !is_null( $allmonths ) )
-                       $options[] = self::option( wfMsg( 'monthsall' ), $allmonths, $selected === $allmonths );
-               for( $i = 1; $i < 13; $i++ )
-                       $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i );
-               return self::openElement( 'select', array( 'id' => $id, 'name' => 'month', 'class' => 'mw-month-selector' ) )
-                       . implode( "\n", $options )
-                       . self::closeElement( 'select' );
+               }
+               if ( !is_null( $allmonths ) ) {
+                       $options[wfMessage( 'monthsall' )->text()] = $allmonths;
+               }
+               for ( $i = 1; $i < 13; $i++ ) {
+                       $options[$wgLang->getMonthName( $i )] = $i;
+               }
+               $data->addOptions( $options );
+               $data->setAttribute( 'class', 'mw-month-selector' );
+               return $data->getHTML();
        }
 
        /**
-        * @param $year Integer
-        * @param $month Integer
+        * @param int $year
+        * @param int $month
         * @return string Formatted HTML
         */
        public static function dateMenu( $year, $month ) {
                # Offset overrides year/month selection
-               if( $month && $month !== -1 ) {
+               if ( $month && $month !== -1 ) {
                        $encMonth = intval( $month );
                } else {
                        $encMonth = '';
                }
-               if( $year ) {
+               if ( $year ) {
                        $encYear = intval( $year );
-               } else if( $encMonth ) {
-                       $thisMonth = intval( gmdate( 'n' ) );
-                       $thisYear = intval( gmdate( 'Y' ) );
-                       if( intval($encMonth) > $thisMonth ) {
+               } elseif ( $encMonth ) {
+                       $timestamp = MWTimestamp::getInstance();
+                       $thisMonth = intval( $timestamp->format( 'n' ) );
+                       $thisYear = intval( $timestamp->format( 'Y' ) );
+                       if ( intval( $encMonth ) > $thisMonth ) {
                                $thisYear--;
                        }
                        $encYear = $thisYear;
                } else {
                        $encYear = '';
                }
-               return Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
-                       Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '.
-                       Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
-                       Xml::monthSelector( $encMonth, -1 );
+               $inputAttribs = [ 'id' => 'year', 'maxlength' => 4, 'size' => 7 ];
+               return self::label( wfMessage( 'year' )->text(), 'year' ) . ' ' .
+                       Html::input( 'year', $encYear, 'number', $inputAttribs ) . ' ' .
+                       self::label( wfMessage( 'month' )->text(), 'month' ) . ' ' .
+                       self::monthSelector( $encMonth, -1 );
        }
 
        /**
+        * Construct a language selector appropriate for use in a form or preferences
         *
-        * @param $selected The language code of the selected language
-        * @param $customisedOnly If true only languages which have some content are listed
-        * @return array of label and select
-        */
-       public static function languageSelector( $selected, $customisedOnly = true ) {
+        * @param string $selected The language code of the selected language
+        * @param bool $customisedOnly If true only languages which have some content are listed
+        * @param string $inLanguage The ISO code of the language to display the select list in (optional)
+        * @param array $overrideAttrs Override the attributes of the select tag (since 1.20)
+        * @param Message|null $msg Label message key (since 1.20)
+        * @return array Array containing 2 items: label HTML and select list HTML
+        */
+       public static function languageSelector( $selected, $customisedOnly = true,
+               $inLanguage = null, $overrideAttrs = [], Message $msg = null
+       ) {
                global $wgLanguageCode;
-               /**
-                * Make sure the site language is in the list; a custom language code
-                * might not have a defined name...
-                */
-               $languages = Language::getLanguageNames( $customisedOnly );
-               if( !array_key_exists( $wgLanguageCode, $languages ) ) {
+
+               $include = $customisedOnly ? 'mwfile' : 'mw';
+               $languages = Language::fetchLanguageNames( $inLanguage, $include );
+
+               // Make sure the site language is in the list;
+               // a custom language code might not have a defined name...
+               if ( !array_key_exists( $wgLanguageCode, $languages ) ) {
                        $languages[$wgLanguageCode] = $wgLanguageCode;
                }
+
                ksort( $languages );
 
                /**
                 * If a bogus value is set, default to the content language.
                 * Otherwise, no default is selected and the user ends up
-                * with an Afrikaans interface since it's first in the list.
+                * with Afrikaans since it's first in the list.
                 */
                $selected = isset( $languages[$selected] ) ? $selected : $wgLanguageCode;
                $options = "\n";
-               foreach( $languages as $code => $name ) {
-                       $options .= Xml::option( "$code - $name", $code, ($code == $selected) ) . "\n";
+               foreach ( $languages as $code => $name ) {
+                       $options .= self::option( "$code - $name", $code, $code == $selected ) . "\n";
                }
 
-               return array(
-                       Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ),
-                       Xml::tags( 'select',
-                               array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ),
-                               $options
-                       )
-               );
+               $attrs = [ 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ];
+               $attrs = array_merge( $attrs, $overrideAttrs );
 
+               if ( $msg === null ) {
+                       $msg = wfMessage( 'yourlanguage' );
+               }
+               return [
+                       self::label( $msg->text(), $attrs['id'] ),
+                       self::tags( 'select', $attrs, $options )
+               ];
        }
 
        /**
         * Shortcut to make a span element
-        * @param $text String content of the element, will be escaped
-        * @param $class String class name of the span element
-        * @param $attribs other attributes
+        * @param string $text Content of the element, will be escaped
+        * @param string $class Class name of the span element
+        * @param array $attribs Other attributes
         * @return string
         */
-       public static function span( $text, $class, $attribs=array() ) {
-               return self::element( 'span', array( 'class' => $class ) + $attribs, $text );
+       public static function span( $text, $class, $attribs = [] ) {
+               return self::element( 'span', [ 'class' => $class ] + $attribs, $text );
        }
 
        /**
         * Shortcut to make a specific element with a class attribute
-        * @param $text content of the element, will be escaped
-        * @param $class class name of the span element
-        * @param $tag element name
-        * @param $attribs other attributes
+        * @param string $text Content of the element, will be escaped
+        * @param string $class Class name of the span element
+        * @param string $tag Element name
+        * @param array $attribs Other attributes
         * @return string
         */
-       public static function wrapClass( $text, $class, $tag='span', $attribs=array() ) {
-               return self::tags( $tag, array( 'class' => $class ) + $attribs, $text );
+       public static function wrapClass( $text, $class, $tag = 'span', $attribs = [] ) {
+               return self::tags( $tag, [ 'class' => $class ] + $attribs, $text );
        }
 
        /**
         * Convenience function to build an HTML text input field
-        * @param $name String value of the name attribute
-        * @param $size value of the size attribute
-        * @param $value value of the value attribute
-        * @param $attribs other attributes
+        * @param string $name Value of the name attribute
+        * @param int $size Value of the size attribute
+        * @param mixed $value Value of the value attribute
+        * @param array $attribs Other attributes
         * @return string HTML
         */
-       public static function input( $name, $size=false, $value=false, $attribs=array() ) {
-               $attributes = array( 'name' => $name );
+       public static function input( $name, $size = false, $value = false, $attribs = [] ) {
+               $attributes = [ 'name' => $name ];
 
-               if( $size ) {
+               if ( $size ) {
                        $attributes['size'] = $size;
                }
 
-               if( $value !== false ) { // maybe 0
+               if ( $value !== false ) { // maybe 0
                        $attributes['value'] = $value;
                }
 
-               return self::element( 'input', $attributes + $attribs );
+               return self::element( 'input',
+                       Html::getTextInputAttributes( $attributes + $attribs ) );
        }
 
        /**
         * Convenience function to build an HTML password input field
-        * @param $name value of the name attribute
-        * @param $size value of the size attribute
-        * @param $value value of the value attribute
-        * @param $attribs other attributes
+        * @param string $name Value of the name attribute
+        * @param int $size Value of the size attribute
+        * @param mixed $value Value of the value attribute
+        * @param array $attribs Other attributes
         * @return string HTML
         */
-       public static function password( $name, $size=false, $value=false, $attribs=array() ) {
-               return self::input( $name, $size, $value, array_merge($attribs, array('type' => 'password')));
+       public static function password( $name, $size = false, $value = false,
+               $attribs = []
+       ) {
+               return self::input( $name, $size, $value,
+                       array_merge( $attribs, [ 'type' => 'password' ] ) );
        }
 
        /**
         * Internal function for use in checkboxes and radio buttons and such.
+        *
+        * @param string $name
+        * @param bool $present
+        *
         * @return array
         */
        public static function attrib( $name, $present = true ) {
-               return $present ? array( $name => $name ) : array();
+               return $present ? [ $name => $name ] : [];
        }
 
        /**
         * Convenience function to build an HTML checkbox
-        * @param $name String value of the name attribute
-        * @param $checked Bool Whether the checkbox is checked or not
-        * @param $attribs Array other attributes
+        * @param string $name Value of the name attribute
+        * @param bool $checked Whether the checkbox is checked or not
+        * @param array $attribs Array other attributes
         * @return string HTML
         */
-       public static function check( $name, $checked=false, $attribs=array() ) {
+       public static function check( $name, $checked = false, $attribs = [] ) {
                return self::element( 'input', array_merge(
-                       array(
+                       [
                                'name' => $name,
                                'type' => 'checkbox',
-                               'value' => 1 ),
+                               'value' => 1 ],
                        self::attrib( 'checked', $checked ),
                        $attribs ) );
        }
 
        /**
         * Convenience function to build an HTML radio button
-        * @param $name value of the name attribute
-        * @param $value value of the value attribute
-        * @param $checked Whether the checkbox is checked or not
-        * @param $attribs other attributes
+        * @param string $name Value of the name attribute
+        * @param string $value Value of the value attribute
+        * @param bool $checked Whether the checkbox is checked or not
+        * @param array $attribs Other attributes
         * @return string HTML
         */
-       public static function radio( $name, $value, $checked=false, $attribs=array() ) {
-               return self::element( 'input', array(
+       public static function radio( $name, $value, $checked = false, $attribs = [] ) {
+               return self::element( 'input', [
                        'name' => $name,
                        'type' => 'radio',
-                       'value' => $value ) + self::attrib( 'checked', $checked ) + $attribs );
+                       'value' => $value ] + self::attrib( 'checked', $checked ) + $attribs );
        }
 
        /**
         * Convenience function to build an HTML form label
-        * @param $label String text of the label
-        * @param $id
-        * @param $attribs Array an attribute array.  This will usuall be 
+        * @param string $label Text of the label
+        * @param string $id
+        * @param array $attribs An attribute array.  This will usually be
         *     the same array as is passed to the corresponding input element,
-        *     so this function will cherry-pick appropriate attributes to 
-        *     apply to the label as well; currently only class is applied.
+        *     so this function will cherry-pick appropriate attributes to
+        *     apply to the label as well; only class and title are applied.
         * @return string HTML
         */
-       public static function label( $label, $id, $attribs=array() ) {
-               $a = array( 'for' => $id );
-               if( isset( $attribs['class'] ) ){
-                               $a['class'] = $attribs['class'];
+       public static function label( $label, $id, $attribs = [] ) {
+               $a = [ 'for' => $id ];
+
+               foreach ( [ 'class', 'title' ] as $attr ) {
+                       if ( isset( $attribs[$attr] ) ) {
+                               $a[$attr] = $attribs[$attr];
+                       }
                }
+
                return self::element( 'label', $a, $label );
        }
 
        /**
         * Convenience function to build an HTML text input field with a label
-        * @param $label String text of the label
-        * @param $name String value of the name attribute
-        * @param $id String id of the input
-        * @param $size value of the size attribute
-        * @param $value value of the value attribute
-        * @param $attribs other attributes
+        * @param string $label Text of the label
+        * @param string $name Value of the name attribute
+        * @param string $id Id of the input
+        * @param int|bool $size Value of the size attribute
+        * @param string|bool $value Value of the value attribute
+        * @param array $attribs Other attributes
         * @return string HTML
         */
-       public static function inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+       public static function inputLabel( $label, $name, $id, $size = false,
+               $value = false, $attribs = []
+       ) {
                list( $label, $input ) = self::inputLabelSep( $label, $name, $id, $size, $value, $attribs );
                return $label . '&#160;' . $input;
        }
 
        /**
         * Same as Xml::inputLabel() but return input and label in an array
+        *
+        * @param string $label
+        * @param string $name
+        * @param string $id
+        * @param int|bool $size
+        * @param string|bool $value
+        * @param array $attribs
+        *
+        * @return array
         */
-       public static function inputLabelSep( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
-               return array(
-                       Xml::label( $label, $id, $attribs ),
-                       self::input( $name, $size, $value, array( 'id' => $id ) + $attribs )
-               );
+       public static function inputLabelSep( $label, $name, $id, $size = false,
+               $value = false, $attribs = []
+       ) {
+               return [
+                       self::label( $label, $id, $attribs ),
+                       self::input( $name, $size, $value, [ 'id' => $id ] + $attribs )
+               ];
        }
 
        /**
         * Convenience function to build an HTML checkbox with a label
+        *
+        * @param string $label
+        * @param string $name
+        * @param string $id
+        * @param bool $checked
+        * @param array $attribs
+        *
         * @return string HTML
         */
-       public static function checkLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
-               return self::check( $name, $checked, array( 'id' => $id ) + $attribs ) .
+       public static function checkLabel( $label, $name, $id, $checked = false, $attribs = [] ) {
+               global $wgUseMediaWikiUIEverywhere;
+               $chkLabel = self::check( $name, $checked, [ 'id' => $id ] + $attribs ) .
                        '&#160;' .
                        self::label( $label, $id, $attribs );
+
+               if ( $wgUseMediaWikiUIEverywhere ) {
+                       $chkLabel = self::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
+                               $chkLabel . self::closeElement( 'div' );
+               }
+               return $chkLabel;
        }
 
        /**
         * Convenience function to build an HTML radio button with a label
+        *
+        * @param string $label
+        * @param string $name
+        * @param string $value
+        * @param string $id
+        * @param bool $checked
+        * @param array $attribs
+        *
         * @return string HTML
         */
-       public static function radioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
-               return self::radio( $name, $value, $checked, array( 'id' => $id ) + $attribs ) .
+       public static function radioLabel( $label, $name, $value, $id,
+               $checked = false, $attribs = []
+       ) {
+               return self::radio( $name, $value, $checked, [ 'id' => $id ] + $attribs ) .
                        '&#160;' .
                        self::label( $label, $id, $attribs );
        }
 
        /**
         * Convenience function to build an HTML submit button
-        * @param $value String: label text for the button
-        * @param $attribs Array: optional custom attributes
+        * When $wgUseMediaWikiUIEverywhere is true it will default to a progressive button
+        * @param string $value Label text for the button
+        * @param array $attribs Optional custom attributes
         * @return string HTML
         */
-       public static function submitButton( $value, $attribs=array() ) {
-               return Html::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
-       }
-
-       /**
-        * @deprecated Synonymous to Html::hidden()
-        */
-       public static function hidden( $name, $value, $attribs = array() ) {
-               return Html::hidden( $name, $value, $attribs );
+       public static function submitButton( $value, $attribs = [] ) {
+               global $wgUseMediaWikiUIEverywhere;
+               $baseAttrs = [
+                       'type' => 'submit',
+                       'value' => $value,
+               ];
+               // Done conditionally for time being as it is possible
+               // some submit forms
+               // might need to be mw-ui-destructive (e.g. delete a page)
+               if ( $wgUseMediaWikiUIEverywhere ) {
+                       $baseAttrs['class'] = 'mw-ui-button mw-ui-progressive';
+               }
+               // Any custom attributes will take precendence of anything in baseAttrs e.g. override the class
+               $attribs = $attribs + $baseAttrs;
+               return Html::element( 'input', $attribs );
        }
 
        /**
         * Convenience function to build an HTML drop-down list item.
-        * @param $text String: text for this item
-        * @param $value String: form submission value; if empty, use text
-        * @param $selected boolean: if true, will be the default selected item
-        * @param $attribs array: optional additional HTML attributes
+        * @param string $text Text for this item. Will be HTML escaped
+        * @param string $value Form submission value; if empty, use text
+        * @param bool $selected If true, will be the default selected item
+        * @param array $attribs Optional additional HTML attributes
         * @return string HTML
         */
-       public static function option( $text, $value=null, $selected=false,
-                       $attribs=array() ) {
-               if( !is_null( $value ) ) {
+       public static function option( $text, $value = null, $selected = false,
+                       $attribs = [] ) {
+               if ( !is_null( $value ) ) {
                        $attribs['value'] = $value;
                }
-               if( $selected ) {
+               if ( $selected ) {
                        $attribs['selected'] = 'selected';
                }
-               return self::element( 'option', $attribs, $text );
+               return Html::element( 'option', $attribs, $text );
        }
 
        /**
-        * Build a drop-down box from a textual list.
+        * Build a drop-down box from a textual list. This is a wrapper
+        * for Xml::listDropDownOptions() plus the XmlSelect class.
         *
-        * @param $name Mixed: Name and id for the drop-down
-        * @param $class Mixed: CSS classes for the drop-down
-        * @param $other Mixed: Text for the "Other reasons" option
-        * @param $list Mixed: Correctly formatted text to be used to generate the options
-        * @param $selected Mixed: Option which should be pre-selected
-        * @param $tabindex Mixed: Value of the tabindex attribute
+        * @param string $name Name and id for the drop-down
+        * @param string $list Correctly formatted text (newline delimited) to be
+        *   used to generate the options.
+        * @param string $other Text for the "Other reasons" option
+        * @param string $selected Option which should be pre-selected
+        * @param string $class CSS classes for the drop-down
+        * @param int $tabindex Value of the tabindex attribute
         * @return string
         */
-       public static function listDropDown( $name= '', $list = '', $other = '', $selected = '', $class = '', $tabindex = Null ) {
-               $optgroup = false;
+       public static function listDropDown( $name = '', $list = '', $other = '',
+               $selected = '', $class = '', $tabindex = null
+       ) {
+               $options = self::listDropDownOptions( $list, [ 'other' => $other ] );
+
+               $xmlSelect = new XmlSelect( $name, $name, $selected );
+               $xmlSelect->addOptions( $options );
+
+               if ( $class ) {
+                       $xmlSelect->setAttribute( 'class', $class );
+               }
+               if ( $tabindex ) {
+                       $xmlSelect->setAttribute( 'tabindex', $tabindex );
+               }
+
+               return $xmlSelect->getHTML();
+       }
+
+       /**
+        * Build options for a drop-down box from a textual list.
+        *
+        * The result of this function can be passed to XmlSelect::addOptions()
+        * (to render a plain `<select>` dropdown box) or to Xml::listDropDownOptionsOoui()
+        * and then OOUI\DropdownInputWidget() (to render a pretty one).
+        *
+        * @param string $list Correctly formatted text (newline delimited) to be
+        *   used to generate the options.
+        * @param array $params Extra parameters
+        * @param string $params['other'] If set, add an option with this as text and a value of 'other'
+        * @return array Array keys are textual labels, values are internal values
+        */
+       public static function listDropDownOptions( $list, $params = [] ) {
+               $options = [];
+
+               if ( isset( $params['other'] ) ) {
+                       $options[ $params['other'] ] = 'other';
+               }
 
-               $options = self::option( $other, 'other', $selected === 'other' );
-
-               foreach ( explode( "\n", $list ) as $option) {
-                               $value = trim( $option );
-                               if ( $value == '' ) {
-                                       continue;
-                               } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
-                                       // A new group is starting ...
-                                       $value = trim( substr( $value, 1 ) );
-                                       if( $optgroup ) $options .= self::closeElement('optgroup');
-                                       $options .= self::openElement( 'optgroup', array( 'label' => $value ) );
-                                       $optgroup = true;
-                               } elseif ( substr( $value, 0, 2) == '**' ) {
-                                       // groupmember
-                                       $value = trim( substr( $value, 2 ) );
-                                       $options .= self::option( $value, $value, $selected === $value );
+               $optgroup = false;
+               foreach ( explode( "\n", $list ) as $option ) {
+                       $value = trim( $option );
+                       if ( $value == '' ) {
+                               continue;
+                       } elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
+                               # A new group is starting...
+                               $value = trim( substr( $value, 1 ) );
+                               $optgroup = $value;
+                       } elseif ( substr( $value, 0, 2 ) == '**' ) {
+                               # groupmember
+                               $opt = trim( substr( $value, 2 ) );
+                               if ( $optgroup === false ) {
+                                       $options[$opt] = $opt;
                                } else {
-                                       // groupless reason list
-                                       if( $optgroup ) $options .= self::closeElement('optgroup');
-                                       $options .= self::option( $value, $value, $selected === $value );
-                                       $optgroup = false;
+                                       $options[$optgroup][$opt] = $opt;
                                }
+                       } else {
+                               # groupless reason list
+                               $optgroup = false;
+                               $options[$option] = $option;
                        }
-                       if( $optgroup ) $options .= self::closeElement('optgroup');
-
-               $attribs = array();
-               if( $name ) {
-                       $attribs['id'] = $name;
-                       $attribs['name'] = $name;
                }
-               if( $class ) {
-                       $attribs['class'] = $class;
-               }
-               if( $tabindex ) {
-                       $attribs['tabindex'] = $tabindex;
+
+               return $options;
+       }
+
+       /**
+        * Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
+        *
+        * TODO Find a better home for this function.
+        *
+        * @param array $options Options, as returned e.g. by Xml::listDropDownOptions()
+        * @return array
+        */
+       public static function listDropDownOptionsOoui( $options ) {
+               $optionsOoui = [];
+
+               foreach ( $options as $text => $value ) {
+                       if ( is_array( $value ) ) {
+                               $optionsOoui[] = [ 'optgroup' => (string)$text ];
+                               foreach ( $value as $text2 => $value2 ) {
+                                       $optionsOoui[] = [ 'data' => (string)$value2, 'label' => (string)$text2 ];
+                               }
+                       } else {
+                               $optionsOoui[] = [ 'data' => (string)$value, 'label' => (string)$text ];
+                       }
                }
-               return Xml::openElement( 'select', $attribs )
-                       . "\n"
-                       . $options
-                       . "\n"
-                       . Xml::closeElement( 'select' );
+
+               return $optionsOoui;
        }
 
        /**
         * Shortcut for creating fieldsets.
         *
-        * @param $legend Legend of the fieldset. If evaluates to false, legend is not added.
-        * @param $content Pre-escaped content for the fieldset. If false, only open fieldset is returned.
-        * @param $attribs Any attributes to fieldset-element.
+        * @param string|bool $legend Legend of the fieldset. If evaluates to false,
+        *   legend is not added.
+        * @param string $content Pre-escaped content for the fieldset. If false,
+        *   only open fieldset is returned.
+        * @param array $attribs Any attributes to fieldset-element.
+        *
+        * @return string
         */
-       public static function fieldset( $legend = false, $content = false, $attribs = array() ) {
-               $s = Xml::openElement( 'fieldset', $attribs ) . "\n";
+       public static function fieldset( $legend = false, $content = false, $attribs = [] ) {
+               $s = self::openElement( 'fieldset', $attribs ) . "\n";
+
                if ( $legend ) {
-                       $s .= Xml::element( 'legend', null, $legend ) . "\n";
+                       $s .= self::element( 'legend', null, $legend ) . "\n";
                }
+
                if ( $content !== false ) {
                        $s .= $content . "\n";
-                       $s .= Xml::closeElement( 'fieldset' ) . "\n";
+                       $s .= self::closeElement( 'fieldset' ) . "\n";
                }
 
                return $s;
@@ -522,149 +624,98 @@ class Xml {
        /**
         * Shortcut for creating textareas.
         *
-        * @param $name The 'name' for the textarea
-        * @param $content Content for the textarea
-        * @param $cols The number of columns for the textarea
-        * @param $rows The number of rows for the textarea
-        * @param $attribs Any other attributes for the textarea
+        * @param string $name The 'name' for the textarea
+        * @param string $content Content for the textarea
+        * @param int $cols The number of columns for the textarea
+        * @param int $rows The number of rows for the textarea
+        * @param array $attribs Any other attributes for the textarea
+        *
+        * @return string
         */
-       public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = array() ) {
+       public static function textarea( $name, $content, $cols = 40, $rows = 5, $attribs = [] ) {
                return self::element( 'textarea',
-                                       array(  'name' => $name,
-                                               'id' => $name,
-                                               'cols' => $cols,
-                                               'rows' => $rows
-                                       ) + $attribs,
+                                       Html::getTextInputAttributes(
+                                               [
+                                                       'name' => $name,
+                                                       'id' => $name,
+                                                       'cols' => $cols,
+                                                       'rows' => $rows
+                                               ] + $attribs
+                                       ),
                                        $content, false );
        }
 
        /**
-        * Returns an escaped string suitable for inclusion in a string literal
-        * for JavaScript source code.
-        * Illegal control characters are assumed not to be present.
+        * Encode a variable of arbitrary type to JavaScript.
+        * If the value is an XmlJsCode object, pass through the object's value verbatim.
         *
-        * @param $string String to escape
-        * @return String
-        */
-       public static function escapeJsString( $string ) {
-               // See ECMA 262 section 7.8.4 for string literal format
-               $pairs = array(
-                       "\\" => "\\\\",
-                       "\"" => "\\\"",
-                       '\'' => '\\\'',
-                       "\n" => "\\n",
-                       "\r" => "\\r",
-
-                       # To avoid closing the element or CDATA section
-                       "<" => "\\x3c",
-                       ">" => "\\x3e",
-
-                       # To avoid any complaints about bad entity refs
-                       "&" => "\\x26",
-
-                       # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152
-                       # Encode certain Unicode formatting chars so affected
-                       # versions of Gecko don't misinterpret our strings;
-                       # this is a common problem with Farsi text.
-                       "\xe2\x80\x8c" => "\\u200c", // ZERO WIDTH NON-JOINER
-                       "\xe2\x80\x8d" => "\\u200d", // ZERO WIDTH JOINER
-               );
-               return strtr( $string, $pairs );
-       }
-
-       /**
-        * Encode a variable of unknown type to JavaScript.
-        * Arrays are converted to JS arrays, objects are converted to JS associative
-        * arrays (objects). So cast your PHP associative arrays to objects before
-        * passing them to here.
-        */
-       public static function encodeJsVar( $value ) {
-               if ( is_bool( $value ) ) {
-                       $s = $value ? 'true' : 'false';
-               } elseif ( is_null( $value ) ) {
-                       $s = 'null';
-               } elseif ( is_int( $value ) || is_float( $value ) ) {
-                       $s = strval($value);
-               } elseif ( is_array( $value ) && // Make sure it's not associative.
-                                       array_keys($value) === range( 0, count($value) - 1 ) ||
-                                       count($value) == 0
-                               ) {
-                       $s = '[';
-                       foreach ( $value as $elt ) {
-                               if ( $s != '[' ) {
-                                       $s .= ', ';
-                               }
-                               $s .= self::encodeJsVar( $elt );
-                       }
-                       $s .= ']';
-               } elseif ( $value instanceof XmlJsCode ) {
-                       $s = $value->value;
-               } elseif ( is_object( $value ) || is_array( $value ) ) {
-                       // Objects and associative arrays
-                       $s = '{';
-                       foreach ( (array)$value as $name => $elt ) {
-                               if ( $s != '{' ) {
-                                       $s .= ', ';
-                               }
-                               $s .= '"' . self::escapeJsString( $name ) . '": ' .
-                                       self::encodeJsVar( $elt );
-                       }
-                       $s .= '}';
-               } else {
-                       $s = '"' . self::escapeJsString( $value ) . '"';
+        * @note Only use this function for generating JavaScript code. If generating output
+        *       for a proper JSON parser, just call FormatJson::encode() directly.
+        *
+        * @param mixed $value The value being encoded. Can be any type except a resource.
+        * @param bool $pretty If true, add non-significant whitespace to improve readability.
+        * @return string|bool String if successful; false upon failure
+        */
+       public static function encodeJsVar( $value, $pretty = false ) {
+               if ( $value instanceof XmlJsCode ) {
+                       return $value->value;
                }
-               return $s;
+               return FormatJson::encode( $value, $pretty, FormatJson::UTF8_OK );
        }
 
        /**
-        * Create a call to a JavaScript function. The supplied arguments will be 
-        * encoded using Xml::encodeJsVar(). 
+        * Create a call to a JavaScript function. The supplied arguments will be
+        * encoded using Xml::encodeJsVar().
         *
-        * @param $name The name of the function to call, or a JavaScript expression
-        *    which evaluates to a function object which is called.
-        * @param $args Array of arguments to pass to the function.
         * @since 1.17
-        */
-       public static function encodeJsCall( $name, $args ) {
-               $s = "$name(";
-               $first = true;
-               foreach ( $args as $arg ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $s .= ', ';
+        * @param string $name The name of the function to call, or a JavaScript expression
+        *    which evaluates to a function object which is called.
+        * @param array $args The arguments to pass to the function.
+        * @param bool $pretty If true, add non-significant whitespace to improve readability.
+        * @return string|bool String if successful; false upon failure
+        */
+       public static function encodeJsCall( $name, $args, $pretty = false ) {
+               foreach ( $args as &$arg ) {
+                       $arg = self::encodeJsVar( $arg, $pretty );
+                       if ( $arg === false ) {
+                               return false;
                        }
-                       $s .= Xml::encodeJsVar( $arg );
                }
-               $s .= ");\n";
-               return $s;
-       }
 
+               return "$name(" . ( $pretty
+                       ? ( ' ' . implode( ', ', $args ) . ' ' )
+                       : implode( ',', $args )
+               ) . ");";
+       }
 
        /**
         * Check if a string is well-formed XML.
         * Must include the surrounding tag.
+        * This function is a DoS vector if an attacker can define
+        * entities in $text.
         *
-        * @param $text String: string to test.
+        * @param string $text String to test.
         * @return bool
         *
         * @todo Error position reporting return
         */
-       public static function isWellFormed( $text ) {
+       private static function isWellFormed( $text ) {
                $parser = xml_parser_create( "UTF-8" );
 
                # case folding violates XML standard, turn it off
                xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
 
-               if( !xml_parse( $parser, $text, true ) ) {
-                       //$err = xml_error_string( xml_get_error_code( $parser ) );
-                       //$position = xml_get_current_byte_index( $parser );
-                       //$fragment = $this->extractFragment( $html, $position );
-                       //$this->mXmlError = "$err at byte $position:\n$fragment";
+               if ( !xml_parse( $parser, $text, true ) ) {
+                       // $err = xml_error_string( xml_get_error_code( $parser ) );
+                       // $position = xml_get_current_byte_index( $parser );
+                       // $fragment = $this->extractFragment( $html, $position );
+                       // $this->mXmlError = "$err at byte $position:\n$fragment";
                        xml_parser_free( $parser );
                        return false;
                }
+
                xml_parser_free( $parser );
+
                return true;
        }
 
@@ -673,7 +724,7 @@ class Xml {
         * Wraps fragment in an \<html\> bit and doctype, so it can be a fragment
         * and can use HTML named entities.
         *
-        * @param $text String:
+        * @param string $text
         * @return bool
         */
        public static function isWellFormedXmlFragment( $text ) {
@@ -682,183 +733,128 @@ class Xml {
                        '<html>' .
                        $text .
                        '</html>';
-               return Xml::isWellFormed( $html );
+
+               return self::isWellFormed( $html );
        }
 
        /**
         * Replace " > and < with their respective HTML entities ( &quot;,
         * &gt;, &lt;)
         *
-        * @param $in String: text that might contain HTML tags.
+        * @param string $in Text that might contain HTML tags.
         * @return string Escaped string
         */
        public static function escapeTagsOnly( $in ) {
                return str_replace(
-                       array( '"', '>', '<' ),
-                       array( '&quot;', '&gt;', '&lt;' ),
+                       [ '"', '>', '<' ],
+                       [ '&quot;', '&gt;', '&lt;' ],
                        $in );
        }
 
        /**
-       * Generate a form (without the opening form element).
-       * Output optionally includes a submit button.
-       * @param $fields Array Associative array, key is message corresponding to a description for the field (colon is in the message), value is appropriate input.
-       * @param $submitLabel String A message containing a label for the submit button.
-       * @return string HTML form.
-       */
-       public static function buildForm( $fields, $submitLabel = null ) {
+        * Generate a form (without the opening form element).
+        * Output optionally includes a submit button.
+        * @param array $fields Associative array, key is the name of a message that
+        *   contains a description for the field, value is an HTML string
+        *   containing the appropriate input.
+        * @param string $submitLabel The name of a message containing a label for
+        *   the submit button.
+        * @param array $submitAttribs The attributes to add to the submit button
+        * @return string HTML form.
+        */
+       public static function buildForm( $fields, $submitLabel = null, $submitAttribs = [] ) {
                $form = '';
                $form .= "<table><tbody>";
 
-               foreach( $fields as $labelmsg => $input ) {
+               foreach ( $fields as $labelmsg => $input ) {
                        $id = "mw-$labelmsg";
-                       $form .= Xml::openElement( 'tr', array( 'id' => $id ) );
-                       $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) );
-                       $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' );
-                       $form .= Xml::closeElement( 'tr' );
+                       $form .= self::openElement( 'tr', [ 'id' => $id ] );
+
+                       // TODO use a <label> here for accessibility purposes - will need
+                       // to either not use a table to build the form, or find the ID of
+                       // the input somehow.
+
+                       $form .= self::tags( 'td', [ 'class' => 'mw-label' ], wfMessage( $labelmsg )->parse() );
+                       $form .= self::openElement( 'td', [ 'class' => 'mw-input' ] )
+                               . $input . self::closeElement( 'td' );
+                       $form .= self::closeElement( 'tr' );
                }
 
-               if( $submitLabel ) {
-                       $form .= Xml::openElement( 'tr' );
-                       $form .= Xml::tags( 'td', array(), '' );
-                       $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' );
-                       $form .= Xml::closeElement( 'tr' );
+               if ( $submitLabel ) {
+                       $form .= self::openElement( 'tr' );
+                       $form .= self::tags( 'td', [], '' );
+                       $form .= self::openElement( 'td', [ 'class' => 'mw-submit' ] )
+                               . self::submitButton( wfMessage( $submitLabel )->text(), $submitAttribs )
+                               . self::closeElement( 'td' );
+                       $form .= self::closeElement( 'tr' );
                }
 
                $form .= "</tbody></table>";
 
-
                return $form;
        }
 
        /**
         * Build a table of data
-        * @param $rows An array of arrays of strings, each to be a row in a table
-        * @param $attribs An array of attributes to apply to the table tag [optional]
-        * @param $headers An array of strings to use as table headers [optional]
+        * @param array $rows An array of arrays of strings, each to be a row in a table
+        * @param array $attribs An array of attributes to apply to the table tag [optional]
+        * @param array $headers An array of strings to use as table headers [optional]
         * @return string
         */
-       public static function buildTable( $rows, $attribs = array(), $headers = null ) {
-               $s = Xml::openElement( 'table', $attribs );
+       public static function buildTable( $rows, $attribs = [], $headers = null ) {
+               $s = self::openElement( 'table', $attribs );
+
                if ( is_array( $headers ) ) {
-                       foreach( $headers as $id => $header ) {
-                               $attribs = array();
-                               if ( is_string( $id ) ) $attribs['id'] = $id;
-                               $s .= Xml::element( 'th', $attribs, $header );
-                       }
-               }
-               foreach( $rows as $id => $row ) {
-                       $attribs = array();
-                       if ( is_string( $id ) ) $attribs['id'] = $id;
-                       $s .= Xml::buildTableRow( $attribs, $row );
-               }
-               $s .= Xml::closeElement( 'table' );
-               return $s;
-       }
+                       $s .= self::openElement( 'thead', $attribs );
 
-       /**
-        * Build a row for a table
-        * @param $attribs An array of attributes to apply to the tr tag
-        * @param $cells An array of strings to put in <td>
-        * @return string
-        */
-       public static function buildTableRow( $attribs, $cells ) {
-               $s = Xml::openElement( 'tr', $attribs );
-               foreach( $cells as $id => $cell ) {
-                       $attribs = array();
-                       if ( is_string( $id ) ) $attribs['id'] = $id;
-                       $s .= Xml::element( 'td', $attribs, $cell );
-               }
-               $s .= Xml::closeElement( 'tr' );
-               return $s;
-       }
-}
+                       foreach ( $headers as $id => $header ) {
+                               $attribs = [];
 
-class XmlSelect {
-       protected $options = array();
-       protected $default = false;
-       protected $attributes = array();
+                               if ( is_string( $id ) ) {
+                                       $attribs['id'] = $id;
+                               }
 
-       public function __construct( $name = false, $id = false, $default = false ) {
-               if ( $name ) {
-                       $this->setAttribute( 'name', $name );
-               }
-               if ( $id ) {
-                       $this->setAttribute( 'id', $id );
-               }
-               if ( $default !== false ) {
-                       $this->default = $default;
+                               $s .= self::element( 'th', $attribs, $header );
+                       }
+                       $s .= self::closeElement( 'thead' );
                }
-       }
 
-       public function setDefault( $default ) {
-               $this->default = $default;
-       }
+               foreach ( $rows as $id => $row ) {
+                       $attribs = [];
 
-       public function setAttribute( $name, $value ) {
-               $this->attributes[$name] = $value;
-       }
+                       if ( is_string( $id ) ) {
+                               $attribs['id'] = $id;
+                       }
 
-       public function getAttribute( $name ) {
-               if ( isset($this->attributes[$name]) ) {
-                       return $this->attributes[$name];
-               } else {
-                       return null;
+                       $s .= self::buildTableRow( $attribs, $row );
                }
-       }
 
-       public function addOption( $name, $value = false ) {
-               // Stab stab stab
-               $value = ($value !== false) ? $value : $name;
-               $this->options[] = Xml::option( $name, $value, $value === $this->default );
-       }
+               $s .= self::closeElement( 'table' );
 
-       // This accepts an array of form
-       // label => value
-       // label => ( label => value, label => value )
-       public function addOptions( $options ) {
-               $this->options[] = trim(self::formatOptions( $options, $this->default ));
+               return $s;
        }
 
-       // This accepts an array of form
-       // label => value
-       // label => ( label => value, label => value )
-       static function formatOptions( $options, $default = false ) {
-               $data = '';
-               foreach( $options as $label => $value ) {
-                       if ( is_array( $value ) ) {
-                               $contents = self::formatOptions( $value, $default );
-                               $data .= Xml::tags( 'optgroup', array( 'label' => $label ), $contents ) . "\n";
-                       } else {
-                               $data .= Xml::option( $label, $value, $value === $default ) . "\n";
-                       }
-               }
+       /**
+        * Build a row for a table
+        * @param array $attribs An array of attributes to apply to the tr tag
+        * @param array $cells An array of strings to put in <td>
+        * @return string
+        */
+       public static function buildTableRow( $attribs, $cells ) {
+               $s = self::openElement( 'tr', $attribs );
 
-               return $data;
-       }
+               foreach ( $cells as $id => $cell ) {
+                       $attribs = [];
 
-       public function getHTML() {
-               return Xml::tags( 'select', $this->attributes, implode( "\n", $this->options ) );
-       }
+                       if ( is_string( $id ) ) {
+                               $attribs['id'] = $id;
+                       }
 
-}
+                       $s .= self::element( 'td', $attribs, $cell );
+               }
 
-/**
- * A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to 
- * interpret a given string as being a JavaScript expression, instead of string 
- * data.
- *
- * Example:
- *
- *    Xml::encodeJsVar( new XmlJsCode( 'a + b' ) );
- *
- * Returns "a + b".
- * @since 1.17
- */
-class XmlJsCode {
-       public $value;
+               $s .= self::closeElement( 'tr' );
 
-       function __construct( $value ) {
-               $this->value = $value;
+               return $s;
        }
 }