MediaWiki  REL1_24
Html.php
Go to the documentation of this file.
00001 <?php
00050 class Html {
00051     // List of void elements from HTML5, section 8.1.2 as of 2011-08-12
00052     private static $voidElements = array(
00053         'area',
00054         'base',
00055         'br',
00056         'col',
00057         'command',
00058         'embed',
00059         'hr',
00060         'img',
00061         'input',
00062         'keygen',
00063         'link',
00064         'meta',
00065         'param',
00066         'source',
00067         'track',
00068         'wbr',
00069     );
00070 
00071     // Boolean attributes, which may have the value omitted entirely.  Manually
00072     // collected from the HTML5 spec as of 2011-08-12.
00073     private static $boolAttribs = array(
00074         'async',
00075         'autofocus',
00076         'autoplay',
00077         'checked',
00078         'controls',
00079         'default',
00080         'defer',
00081         'disabled',
00082         'formnovalidate',
00083         'hidden',
00084         'ismap',
00085         'itemscope',
00086         'loop',
00087         'multiple',
00088         'muted',
00089         'novalidate',
00090         'open',
00091         'pubdate',
00092         'readonly',
00093         'required',
00094         'reversed',
00095         'scoped',
00096         'seamless',
00097         'selected',
00098         'truespeed',
00099         'typemustmatch',
00100         // HTML5 Microdata
00101         'itemscope',
00102     );
00103 
00111     public static function getTextInputAttributes( $attrs ) {
00112         global $wgUseMediaWikiUIEverywhere;
00113         if ( !$attrs ) {
00114             $attrs = array();
00115         }
00116         if ( isset( $attrs['class'] ) ) {
00117             if ( is_array( $attrs['class'] ) ) {
00118                 $attrs['class'][] = 'mw-ui-input';
00119             } else {
00120                 $attrs['class'] .= ' mw-ui-input';
00121             }
00122         } else {
00123             $attrs['class'] = 'mw-ui-input';
00124         }
00125         if ( $wgUseMediaWikiUIEverywhere ) {
00126             // Note that size can effect the desired width rendering of mw-ui-input elements
00127             // so it is removed. Left intact when mediawiki ui not enabled.
00128             unset( $attrs['size'] );
00129         }
00130         return $attrs;
00131     }
00132 
00153     public static function rawElement( $element, $attribs = array(), $contents = '' ) {
00154         global $wgWellFormedXml;
00155         $start = self::openElement( $element, $attribs );
00156         if ( in_array( $element, self::$voidElements ) ) {
00157             if ( $wgWellFormedXml ) {
00158                 // Silly XML.
00159                 return substr( $start, 0, -1 ) . ' />';
00160             }
00161             return $start;
00162         } else {
00163             return "$start$contents" . self::closeElement( $element );
00164         }
00165     }
00166 
00177     public static function element( $element, $attribs = array(), $contents = '' ) {
00178         return self::rawElement( $element, $attribs, strtr( $contents, array(
00179             // There's no point in escaping quotes, >, etc. in the contents of
00180             // elements.
00181             '&' => '&amp;',
00182             '<' => '&lt;'
00183         ) ) );
00184     }
00185 
00195     public static function openElement( $element, $attribs = array() ) {
00196         global $wgWellFormedXml;
00197         $attribs = (array)$attribs;
00198         // This is not required in HTML5, but let's do it anyway, for
00199         // consistency and better compression.
00200         $element = strtolower( $element );
00201 
00202         // In text/html, initial <html> and <head> tags can be omitted under
00203         // pretty much any sane circumstances, if they have no attributes.  See:
00204         // <http://www.whatwg.org/html/syntax.html#optional-tags>
00205         if ( !$wgWellFormedXml && !$attribs
00206         && in_array( $element, array( 'html', 'head' ) ) ) {
00207             return '';
00208         }
00209 
00210         // Remove invalid input types
00211         if ( $element == 'input' ) {
00212             $validTypes = array(
00213                 'hidden',
00214                 'text',
00215                 'password',
00216                 'checkbox',
00217                 'radio',
00218                 'file',
00219                 'submit',
00220                 'image',
00221                 'reset',
00222                 'button',
00223 
00224                 // HTML input types
00225                 'datetime',
00226                 'datetime-local',
00227                 'date',
00228                 'month',
00229                 'time',
00230                 'week',
00231                 'number',
00232                 'range',
00233                 'email',
00234                 'url',
00235                 'search',
00236                 'tel',
00237                 'color',
00238             );
00239             if ( isset( $attribs['type'] )
00240             && !in_array( $attribs['type'], $validTypes ) ) {
00241                 unset( $attribs['type'] );
00242             }
00243         }
00244 
00245         // According to standard the default type for <button> elements is "submit".
00246         // Depending on compatibility mode IE might use "button", instead.
00247         // We enforce the standard "submit".
00248         if ( $element == 'button' && !isset( $attribs['type'] ) ) {
00249             $attribs['type'] = 'submit';
00250         }
00251 
00252         return "<$element" . self::expandAttributes(
00253             self::dropDefaults( $element, $attribs ) ) . '>';
00254     }
00255 
00263     public static function closeElement( $element ) {
00264         $element = strtolower( $element );
00265 
00266         return "</$element>";
00267     }
00268 
00286     private static function dropDefaults( $element, $attribs ) {
00287 
00288         // Whenever altering this array, please provide a covering test case
00289         // in HtmlTest::provideElementsWithAttributesHavingDefaultValues
00290         static $attribDefaults = array(
00291             'area' => array( 'shape' => 'rect' ),
00292             'button' => array(
00293                 'formaction' => 'GET',
00294                 'formenctype' => 'application/x-www-form-urlencoded',
00295             ),
00296             'canvas' => array(
00297                 'height' => '150',
00298                 'width' => '300',
00299             ),
00300             'command' => array( 'type' => 'command' ),
00301             'form' => array(
00302                 'action' => 'GET',
00303                 'autocomplete' => 'on',
00304                 'enctype' => 'application/x-www-form-urlencoded',
00305             ),
00306             'input' => array(
00307                 'formaction' => 'GET',
00308                 'type' => 'text',
00309             ),
00310             'keygen' => array( 'keytype' => 'rsa' ),
00311             'link' => array( 'media' => 'all' ),
00312             'menu' => array( 'type' => 'list' ),
00313             // Note: the use of text/javascript here instead of other JavaScript
00314             // MIME types follows the HTML5 spec.
00315             'script' => array( 'type' => 'text/javascript' ),
00316             'style' => array(
00317                 'media' => 'all',
00318                 'type' => 'text/css',
00319             ),
00320             'textarea' => array( 'wrap' => 'soft' ),
00321         );
00322 
00323         $element = strtolower( $element );
00324 
00325         foreach ( $attribs as $attrib => $value ) {
00326             $lcattrib = strtolower( $attrib );
00327             if ( is_array( $value ) ) {
00328                 $value = implode( ' ', $value );
00329             } else {
00330                 $value = strval( $value );
00331             }
00332 
00333             // Simple checks using $attribDefaults
00334             if ( isset( $attribDefaults[$element][$lcattrib] ) &&
00335             $attribDefaults[$element][$lcattrib] == $value ) {
00336                 unset( $attribs[$attrib] );
00337             }
00338 
00339             if ( $lcattrib == 'class' && $value == '' ) {
00340                 unset( $attribs[$attrib] );
00341             }
00342         }
00343 
00344         // More subtle checks
00345         if ( $element === 'link' && isset( $attribs['type'] )
00346         && strval( $attribs['type'] ) == 'text/css' ) {
00347             unset( $attribs['type'] );
00348         }
00349         if ( $element === 'input' ) {
00350             $type = isset( $attribs['type'] ) ? $attribs['type'] : null;
00351             $value = isset( $attribs['value'] ) ? $attribs['value'] : null;
00352             if ( $type === 'checkbox' || $type === 'radio' ) {
00353                 // The default value for checkboxes and radio buttons is 'on'
00354                 // not ''. By stripping value="" we break radio boxes that
00355                 // actually wants empty values.
00356                 if ( $value === 'on' ) {
00357                     unset( $attribs['value'] );
00358                 }
00359             } elseif ( $type === 'submit' ) {
00360                 // The default value for submit appears to be "Submit" but
00361                 // let's not bother stripping out localized text that matches
00362                 // that.
00363             } else {
00364                 // The default value for nearly every other field type is ''
00365                 // The 'range' and 'color' types use different defaults but
00366                 // stripping a value="" does not hurt them.
00367                 if ( $value === '' ) {
00368                     unset( $attribs['value'] );
00369                 }
00370             }
00371         }
00372         if ( $element === 'select' && isset( $attribs['size'] ) ) {
00373             if ( in_array( 'multiple', $attribs )
00374                 || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
00375             ) {
00376                 // A multi-select
00377                 if ( strval( $attribs['size'] ) == '4' ) {
00378                     unset( $attribs['size'] );
00379                 }
00380             } else {
00381                 // Single select
00382                 if ( strval( $attribs['size'] ) == '1' ) {
00383                     unset( $attribs['size'] );
00384                 }
00385             }
00386         }
00387 
00388         return $attribs;
00389     }
00390 
00432     public static function expandAttributes( $attribs ) {
00433         global $wgWellFormedXml;
00434 
00435         $ret = '';
00436         $attribs = (array)$attribs;
00437         foreach ( $attribs as $key => $value ) {
00438             // Support intuitive array( 'checked' => true/false ) form
00439             if ( $value === false || is_null( $value ) ) {
00440                 continue;
00441             }
00442 
00443             // For boolean attributes, support array( 'foo' ) instead of
00444             // requiring array( 'foo' => 'meaningless' ).
00445             if ( is_int( $key )
00446             && in_array( strtolower( $value ), self::$boolAttribs ) ) {
00447                 $key = $value;
00448             }
00449 
00450             // Not technically required in HTML5 but we'd like consistency
00451             // and better compression anyway.
00452             $key = strtolower( $key );
00453 
00454             // Bug 23769: Blacklist all form validation attributes for now.  Current
00455             // (June 2010) WebKit has no UI, so the form just refuses to submit
00456             // without telling the user why, which is much worse than failing
00457             // server-side validation.  Opera is the only other implementation at
00458             // this time, and has ugly UI, so just kill the feature entirely until
00459             // we have at least one good implementation.
00460 
00461             // As the default value of "1" for "step" rejects decimal
00462             // numbers to be entered in 'type="number"' fields, allow
00463             // the special case 'step="any"'.
00464 
00465             if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) )
00466                 || $key === 'step' && $value !== 'any' ) {
00467                 continue;
00468             }
00469 
00470             // http://www.w3.org/TR/html401/index/attributes.html ("space-separated")
00471             // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated")
00472             $spaceSeparatedListAttributes = array(
00473                 'class', // html4, html5
00474                 'accesskey', // as of html5, multiple space-separated values allowed
00475                 // html4-spec doesn't document rel= as space-separated
00476                 // but has been used like that and is now documented as such
00477                 // in the html5-spec.
00478                 'rel',
00479             );
00480 
00481             // Specific features for attributes that allow a list of space-separated values
00482             if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
00483                 // Apply some normalization and remove duplicates
00484 
00485                 // Convert into correct array. Array can contain space-separated
00486                 // values. Implode/explode to get those into the main array as well.
00487                 if ( is_array( $value ) ) {
00488                     // If input wasn't an array, we can skip this step
00489                     $newValue = array();
00490                     foreach ( $value as $k => $v ) {
00491                         if ( is_string( $v ) ) {
00492                             // String values should be normal `array( 'foo' )`
00493                             // Just append them
00494                             if ( !isset( $value[$v] ) ) {
00495                                 // As a special case don't set 'foo' if a
00496                                 // separate 'foo' => true/false exists in the array
00497                                 // keys should be authoritative
00498                                 $newValue[] = $v;
00499                             }
00500                         } elseif ( $v ) {
00501                             // If the value is truthy but not a string this is likely
00502                             // an array( 'foo' => true ), falsy values don't add strings
00503                             $newValue[] = $k;
00504                         }
00505                     }
00506                     $value = implode( ' ', $newValue );
00507                 }
00508                 $value = explode( ' ', $value );
00509 
00510                 // Normalize spacing by fixing up cases where people used
00511                 // more than 1 space and/or a trailing/leading space
00512                 $value = array_diff( $value, array( '', ' ' ) );
00513 
00514                 // Remove duplicates and create the string
00515                 $value = implode( ' ', array_unique( $value ) );
00516             } elseif ( is_array( $value ) ) {
00517                 throw new MWException( "HTML attribute $key can not contain a list of values" );
00518             }
00519 
00520             // See the "Attributes" section in the HTML syntax part of HTML5,
00521             // 9.1.2.3 as of 2009-08-10.  Most attributes can have quotation
00522             // marks omitted, but not all.  (Although a literal " is not
00523             // permitted, we don't check for that, since it will be escaped
00524             // anyway.)
00525             #
00526             // See also research done on further characters that need to be
00527             // escaped: http://code.google.com/p/html5lib/issues/detail?id=93
00528             $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
00529                 . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
00530                 . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
00531             if ( $wgWellFormedXml || $value === ''
00532             || preg_match( "![$badChars]!u", $value ) ) {
00533                 $quote = '"';
00534             } else {
00535                 $quote = '';
00536             }
00537 
00538             if ( in_array( $key, self::$boolAttribs ) ) {
00539                 // In HTML5, we can leave the value empty. If we don't need
00540                 // well-formed XML, we can omit the = entirely.
00541                 if ( !$wgWellFormedXml ) {
00542                     $ret .= " $key";
00543                 } else {
00544                     $ret .= " $key=\"\"";
00545                 }
00546             } else {
00547                 // Apparently we need to entity-encode \n, \r, \t, although the
00548                 // spec doesn't mention that.  Since we're doing strtr() anyway,
00549                 // and we don't need <> escaped here, we may as well not call
00550                 // htmlspecialchars().
00551                 // @todo FIXME: Verify that we actually need to
00552                 // escape \n\r\t here, and explain why, exactly.
00553                 #
00554                 // We could call Sanitizer::encodeAttribute() for this, but we
00555                 // don't because we're stubborn and like our marginal savings on
00556                 // byte size from not having to encode unnecessary quotes.
00557                 $map = array(
00558                     '&' => '&amp;',
00559                     '"' => '&quot;',
00560                     "\n" => '&#10;',
00561                     "\r" => '&#13;',
00562                     "\t" => '&#9;'
00563                 );
00564                 if ( $wgWellFormedXml ) {
00565                     // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
00566                     // But reportedly it breaks some XML tools?
00567                     // @todo FIXME: Is this really true?
00568                     $map['<'] = '&lt;';
00569                 }
00570                 $ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
00571             }
00572         }
00573         return $ret;
00574     }
00575 
00585     public static function inlineScript( $contents ) {
00586         global $wgWellFormedXml;
00587 
00588         $attrs = array();
00589 
00590         if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
00591             $contents = "/*<![CDATA[*/$contents/*]]>*/";
00592         }
00593 
00594         return self::rawElement( 'script', $attrs, $contents );
00595     }
00596 
00604     public static function linkedScript( $url ) {
00605         $attrs = array( 'src' => $url );
00606 
00607         return self::element( 'script', $attrs );
00608     }
00609 
00619     public static function inlineStyle( $contents, $media = 'all' ) {
00620         global $wgWellFormedXml;
00621 
00622         if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
00623             $contents = "/*<![CDATA[*/$contents/*]]>*/";
00624         }
00625 
00626         return self::rawElement( 'style', array(
00627             'type' => 'text/css',
00628             'media' => $media,
00629         ), $contents );
00630     }
00631 
00640     public static function linkedStyle( $url, $media = 'all' ) {
00641         return self::element( 'link', array(
00642             'rel' => 'stylesheet',
00643             'href' => $url,
00644             'type' => 'text/css',
00645             'media' => $media,
00646         ) );
00647     }
00648 
00660     public static function input( $name, $value = '', $type = 'text', $attribs = array() ) {
00661         $attribs['type'] = $type;
00662         $attribs['value'] = $value;
00663         $attribs['name'] = $name;
00664         if ( in_array( $type, array( 'text', 'search', 'email', 'password', 'number' ) ) ) {
00665             $attribs = Html::getTextInputAttributes( $attribs );
00666         }
00667         return self::element( 'input', $attribs );
00668     }
00669 
00678     public static function check( $name, $checked = false, array $attribs = array() ) {
00679         if ( isset( $attribs['value'] ) ) {
00680             $value = $attribs['value'];
00681             unset( $attribs['value'] );
00682         } else {
00683             $value = 1;
00684         }
00685 
00686         if ( $checked ) {
00687             $attribs[] = 'checked';
00688         }
00689 
00690         return self::input( $name, $value, 'checkbox', $attribs );
00691     }
00692 
00701     public static function radio( $name, $checked = false, array $attribs = array() ) {
00702         if ( isset( $attribs['value'] ) ) {
00703             $value = $attribs['value'];
00704             unset( $attribs['value'] );
00705         } else {
00706             $value = 1;
00707         }
00708 
00709         if ( $checked ) {
00710             $attribs[] = 'checked';
00711         }
00712 
00713         return self::input( $name, $value, 'radio', $attribs );
00714     }
00715 
00724     public static function label( $label, $id, array $attribs = array() ) {
00725         $attribs += array(
00726             'for' => $id
00727         );
00728         return self::element( 'label', $attribs, $label );
00729     }
00730 
00740     public static function hidden( $name, $value, $attribs = array() ) {
00741         return self::input( $name, $value, 'hidden', $attribs );
00742     }
00743 
00756     public static function textarea( $name, $value = '', $attribs = array() ) {
00757         $attribs['name'] = $name;
00758 
00759         if ( substr( $value, 0, 1 ) == "\n" ) {
00760             // Workaround for bug 12130: browsers eat the initial newline
00761             // assuming that it's just for show, but they do keep the later
00762             // newlines, which we may want to preserve during editing.
00763             // Prepending a single newline
00764             $spacedValue = "\n" . $value;
00765         } else {
00766             $spacedValue = $value;
00767         }
00768         return self::element( 'textarea', Html::getTextInputAttributes( $attribs ), $spacedValue );
00769     }
00770 
00787     public static function namespaceSelector( array $params = array(),
00788         array $selectAttribs = array()
00789     ) {
00790         global $wgContLang;
00791 
00792         ksort( $selectAttribs );
00793 
00794         // Is a namespace selected?
00795         if ( isset( $params['selected'] ) ) {
00796             // If string only contains digits, convert to clean int. Selected could also
00797             // be "all" or "" etc. which needs to be left untouched.
00798             // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues
00799             // and returns false for already clean ints. Use regex instead..
00800             if ( preg_match( '/^\d+$/', $params['selected'] ) ) {
00801                 $params['selected'] = intval( $params['selected'] );
00802             }
00803             // else: leaves it untouched for later processing
00804         } else {
00805             $params['selected'] = '';
00806         }
00807 
00808         if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) {
00809             $params['exclude'] = array();
00810         }
00811         if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) {
00812             $params['disable'] = array();
00813         }
00814 
00815         // Associative array between option-values and option-labels
00816         $options = array();
00817 
00818         if ( isset( $params['all'] ) ) {
00819             // add an option that would let the user select all namespaces.
00820             // Value is provided by user, the name shown is localized for the user.
00821             $options[$params['all']] = wfMessage( 'namespacesall' )->text();
00822         }
00823         // Add all namespaces as options (in the content language)
00824         $options += $wgContLang->getFormattedNamespaces();
00825 
00826         // Convert $options to HTML and filter out namespaces below 0
00827         $optionsHtml = array();
00828         foreach ( $options as $nsId => $nsName ) {
00829             if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
00830                 continue;
00831             }
00832             if ( $nsId === NS_MAIN ) {
00833                 // For other namespaces use use the namespace prefix as label, but for
00834                 // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)")
00835                 $nsName = wfMessage( 'blanknamespace' )->text();
00836             } elseif ( is_int( $nsId ) ) {
00837                 $nsName = $wgContLang->convertNamespace( $nsId );
00838             }
00839             $optionsHtml[] = Html::element(
00840                 'option', array(
00841                     'disabled' => in_array( $nsId, $params['disable'] ),
00842                     'value' => $nsId,
00843                     'selected' => $nsId === $params['selected'],
00844                 ), $nsName
00845             );
00846         }
00847 
00848         if ( !array_key_exists( 'id', $selectAttribs ) ) {
00849             $selectAttribs['id'] = 'namespace';
00850         }
00851 
00852         if ( !array_key_exists( 'name', $selectAttribs ) ) {
00853             $selectAttribs['name'] = 'namespace';
00854         }
00855 
00856         $ret = '';
00857         if ( isset( $params['label'] ) ) {
00858             $ret .= Html::element(
00859                 'label', array(
00860                     'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null,
00861                 ), $params['label']
00862             ) . '&#160;';
00863         }
00864 
00865         // Wrap options in a <select>
00866         $ret .= Html::openElement( 'select', $selectAttribs )
00867             . "\n"
00868             . implode( "\n", $optionsHtml )
00869             . "\n"
00870             . Html::closeElement( 'select' );
00871 
00872         return $ret;
00873     }
00874 
00883     public static function htmlHeader( $attribs = array() ) {
00884         $ret = '';
00885 
00886         global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces;
00887 
00888         $isXHTML = self::isXmlMimeType( $wgMimeType );
00889 
00890         if ( $isXHTML ) { // XHTML5
00891             // XML MIME-typed markup should have an xml header.
00892             // However a DOCTYPE is not needed.
00893             $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
00894 
00895             // Add the standard xmlns
00896             $attribs['xmlns'] = 'http://www.w3.org/1999/xhtml';
00897 
00898             // And support custom namespaces
00899             foreach ( $wgXhtmlNamespaces as $tag => $ns ) {
00900                 $attribs["xmlns:$tag"] = $ns;
00901             }
00902         } else { // HTML5
00903             // DOCTYPE
00904             $ret .= "<!DOCTYPE html>\n";
00905         }
00906 
00907         if ( $wgHtml5Version ) {
00908             $attribs['version'] = $wgHtml5Version;
00909         }
00910 
00911         $html = Html::openElement( 'html', $attribs );
00912 
00913         if ( $html ) {
00914             $html .= "\n";
00915         }
00916 
00917         $ret .= $html;
00918 
00919         return $ret;
00920     }
00921 
00928     public static function isXmlMimeType( $mimetype ) {
00929         # http://www.whatwg.org/html/infrastructure.html#xml-mime-type
00930         # * text/xml
00931         # * application/xml
00932         # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
00933         return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
00934     }
00935 
00946     static function infoBox( $text, $icon, $alt, $class = false ) {
00947         $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class" ) );
00948 
00949         $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ) .
00950                 Html::element( 'img',
00951                     array(
00952                         'src' => $icon,
00953                         'alt' => $alt,
00954                     )
00955                 ) .
00956                 Html::closeElement( 'div' );
00957 
00958         $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ) .
00959                 $text .
00960                 Html::closeElement( 'div' );
00961         $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' );
00962 
00963         $s .= Html::closeElement( 'div' );
00964 
00965         $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' );
00966 
00967         return $s;
00968     }
00969 
00978     static function srcSet( $urls ) {
00979         $candidates = array();
00980         foreach ( $urls as $density => $url ) {
00981             // Image candidate syntax per current whatwg live spec, 2012-09-23:
00982             // http://www.whatwg.org/html/embedded-content-1.html#attr-img-srcset
00983             $candidates[] = "{$url} {$density}x";
00984         }
00985         return implode( ", ", $candidates );
00986     }
00987 }