MediaWiki
REL1_24
|
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 '&' => '&', 00182 '<' => '<' 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 '&' => '&', 00559 '"' => '"', 00560 "\n" => ' ', 00561 "\r" => ' ', 00562 "\t" => '	' 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['<'] = '<'; 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 ) . ' '; 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 }