MediaWiki
master
|
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 00104 private static $HTMLFiveOnlyAttribs = array( 00105 'autocomplete', 00106 'autofocus', 00107 'max', 00108 'min', 00109 'multiple', 00110 'pattern', 00111 'placeholder', 00112 'required', 00113 'step', 00114 'spellcheck', 00115 ); 00116 00137 public static function rawElement( $element, $attribs = array(), $contents = '' ) { 00138 global $wgWellFormedXml; 00139 $start = self::openElement( $element, $attribs ); 00140 if ( in_array( $element, self::$voidElements ) ) { 00141 if ( $wgWellFormedXml ) { 00142 // Silly XML. 00143 return substr( $start, 0, -1 ) . ' />'; 00144 } 00145 return $start; 00146 } else { 00147 return "$start$contents" . self::closeElement( $element ); 00148 } 00149 } 00150 00161 public static function element( $element, $attribs = array(), $contents = '' ) { 00162 return self::rawElement( $element, $attribs, strtr( $contents, array( 00163 // There's no point in escaping quotes, >, etc. in the contents of 00164 // elements. 00165 '&' => '&', 00166 '<' => '<' 00167 ) ) ); 00168 } 00169 00179 public static function openElement( $element, $attribs = array() ) { 00180 global $wgHtml5, $wgWellFormedXml; 00181 $attribs = (array)$attribs; 00182 // This is not required in HTML5, but let's do it anyway, for 00183 // consistency and better compression. 00184 $element = strtolower( $element ); 00185 00186 // In text/html, initial <html> and <head> tags can be omitted under 00187 // pretty much any sane circumstances, if they have no attributes. See: 00188 // <http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags> 00189 if ( !$wgWellFormedXml && !$attribs 00190 && in_array( $element, array( 'html', 'head' ) ) ) { 00191 return ''; 00192 } 00193 00194 // Remove invalid input types 00195 if ( $element == 'input' ) { 00196 $validTypes = array( 00197 'hidden', 00198 'text', 00199 'password', 00200 'checkbox', 00201 'radio', 00202 'file', 00203 'submit', 00204 'image', 00205 'reset', 00206 'button', 00207 ); 00208 00209 // Allow more input types in HTML5 mode 00210 if( $wgHtml5 ) { 00211 $validTypes = array_merge( $validTypes, array( 00212 'datetime', 00213 'datetime-local', 00214 'date', 00215 'month', 00216 'time', 00217 'week', 00218 'number', 00219 'range', 00220 'email', 00221 'url', 00222 'search', 00223 'tel', 00224 'color', 00225 ) ); 00226 } 00227 if ( isset( $attribs['type'] ) 00228 && !in_array( $attribs['type'], $validTypes ) ) { 00229 unset( $attribs['type'] ); 00230 } 00231 } 00232 00233 if ( !$wgHtml5 && $element == 'textarea' && isset( $attribs['maxlength'] ) ) { 00234 unset( $attribs['maxlength'] ); 00235 } 00236 00237 // According to standard the default type for <button> elements is "submit". 00238 // Depending on compatibility mode IE might use "button", instead. 00239 // We enforce the standard "submit". 00240 if ( $element == 'button' && !isset( $attribs['type'] ) ) { 00241 $attribs['type'] = 'submit'; 00242 } 00243 00244 return "<$element" . self::expandAttributes( 00245 self::dropDefaults( $element, $attribs ) ) . '>'; 00246 } 00247 00256 public static function closeElement( $element ) { 00257 global $wgWellFormedXml; 00258 00259 $element = strtolower( $element ); 00260 00261 // Reference: 00262 // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags 00263 if ( !$wgWellFormedXml && in_array( $element, array( 00264 'html', 00265 'head', 00266 'body', 00267 'li', 00268 'dt', 00269 'dd', 00270 'tr', 00271 'td', 00272 'th', 00273 ) ) ) { 00274 return ''; 00275 } 00276 return "</$element>"; 00277 } 00278 00296 private static function dropDefaults( $element, $attribs ) { 00297 // Don't bother doing anything if we aren't outputting HTML5; it's too 00298 // much of a pain to maintain two sets of defaults. 00299 global $wgHtml5; 00300 if ( !$wgHtml5 ) { 00301 return $attribs; 00302 } 00303 00304 // Whenever altering this array, please provide a covering test case 00305 // in HtmlTest::provideElementsWithAttributesHavingDefaultValues 00306 static $attribDefaults = array( 00307 'area' => array( 'shape' => 'rect' ), 00308 'button' => array( 00309 'formaction' => 'GET', 00310 'formenctype' => 'application/x-www-form-urlencoded', 00311 ), 00312 'canvas' => array( 00313 'height' => '150', 00314 'width' => '300', 00315 ), 00316 'command' => array( 'type' => 'command' ), 00317 'form' => array( 00318 'action' => 'GET', 00319 'autocomplete' => 'on', 00320 'enctype' => 'application/x-www-form-urlencoded', 00321 ), 00322 'input' => array( 00323 'formaction' => 'GET', 00324 'type' => 'text', 00325 ), 00326 'keygen' => array( 'keytype' => 'rsa' ), 00327 'link' => array( 'media' => 'all' ), 00328 'menu' => array( 'type' => 'list' ), 00329 // Note: the use of text/javascript here instead of other JavaScript 00330 // MIME types follows the HTML5 spec. 00331 'script' => array( 'type' => 'text/javascript' ), 00332 'style' => array( 00333 'media' => 'all', 00334 'type' => 'text/css', 00335 ), 00336 'textarea' => array( 'wrap' => 'soft' ), 00337 ); 00338 00339 $element = strtolower( $element ); 00340 00341 foreach ( $attribs as $attrib => $value ) { 00342 $lcattrib = strtolower( $attrib ); 00343 if( is_array( $value ) ) { 00344 $value = implode( ' ', $value ); 00345 } else { 00346 $value = strval( $value ); 00347 } 00348 00349 // Simple checks using $attribDefaults 00350 if ( isset( $attribDefaults[$element][$lcattrib] ) && 00351 $attribDefaults[$element][$lcattrib] == $value ) { 00352 unset( $attribs[$attrib] ); 00353 } 00354 00355 if ( $lcattrib == 'class' && $value == '' ) { 00356 unset( $attribs[$attrib] ); 00357 } 00358 } 00359 00360 // More subtle checks 00361 if ( $element === 'link' && isset( $attribs['type'] ) 00362 && strval( $attribs['type'] ) == 'text/css' ) { 00363 unset( $attribs['type'] ); 00364 } 00365 if ( $element === 'input' ) { 00366 $type = isset( $attribs['type'] ) ? $attribs['type'] : null; 00367 $value = isset( $attribs['value'] ) ? $attribs['value'] : null; 00368 if ( $type === 'checkbox' || $type === 'radio' ) { 00369 // The default value for checkboxes and radio buttons is 'on' 00370 // not ''. By stripping value="" we break radio boxes that 00371 // actually wants empty values. 00372 if ( $value === 'on' ) { 00373 unset( $attribs['value'] ); 00374 } 00375 } elseif ( $type === 'submit' ) { 00376 // The default value for submit appears to be "Submit" but 00377 // let's not bother stripping out localized text that matches 00378 // that. 00379 } else { 00380 // The default value for nearly every other field type is '' 00381 // The 'range' and 'color' types use different defaults but 00382 // stripping a value="" does not hurt them. 00383 if ( $value === '' ) { 00384 unset( $attribs['value'] ); 00385 } 00386 } 00387 } 00388 if ( $element === 'select' && isset( $attribs['size'] ) ) { 00389 if ( in_array( 'multiple', $attribs ) 00390 || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false ) 00391 ) { 00392 // A multi-select 00393 if ( strval( $attribs['size'] ) == '4' ) { 00394 unset( $attribs['size'] ); 00395 } 00396 } else { 00397 // Single select 00398 if ( strval( $attribs['size'] ) == '1' ) { 00399 unset( $attribs['size'] ); 00400 } 00401 } 00402 } 00403 00404 return $attribs; 00405 } 00406 00446 public static function expandAttributes( $attribs ) { 00447 global $wgHtml5, $wgWellFormedXml; 00448 00449 $ret = ''; 00450 $attribs = (array)$attribs; 00451 foreach ( $attribs as $key => $value ) { 00452 if ( $value === false || is_null( $value ) ) { 00453 continue; 00454 } 00455 00456 // For boolean attributes, support array( 'foo' ) instead of 00457 // requiring array( 'foo' => 'meaningless' ). 00458 if ( is_int( $key ) 00459 && in_array( strtolower( $value ), self::$boolAttribs ) ) { 00460 $key = $value; 00461 } 00462 00463 // Not technically required in HTML5, but required in XHTML 1.0, 00464 // and we'd like consistency and better compression anyway. 00465 $key = strtolower( $key ); 00466 00467 // Here we're blacklisting some HTML5-only attributes... 00468 if ( !$wgHtml5 && in_array( $key, self::$HTMLFiveOnlyAttribs ) 00469 ) { 00470 continue; 00471 } 00472 00473 // Bug 23769: Blacklist all form validation attributes for now. Current 00474 // (June 2010) WebKit has no UI, so the form just refuses to submit 00475 // without telling the user why, which is much worse than failing 00476 // server-side validation. Opera is the only other implementation at 00477 // this time, and has ugly UI, so just kill the feature entirely until 00478 // we have at least one good implementation. 00479 00480 // As the default value of "1" for "step" rejects decimal 00481 // numbers to be entered in 'type="number"' fields, allow 00482 // the special case 'step="any"'. 00483 00484 if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) ) || 00485 $key === 'step' && $value !== 'any' ) { 00486 continue; 00487 } 00488 00489 // http://www.w3.org/TR/html401/index/attributes.html ("space-separated") 00490 // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") 00491 $spaceSeparatedListAttributes = array( 00492 'class', // html4, html5 00493 'accesskey', // as of html5, multiple space-separated values allowed 00494 // html4-spec doesn't document rel= as space-separated 00495 // but has been used like that and is now documented as such 00496 // in the html5-spec. 00497 'rel', 00498 ); 00499 00500 // Specific features for attributes that allow a list of space-separated values 00501 if ( in_array( $key, $spaceSeparatedListAttributes ) ) { 00502 // Apply some normalization and remove duplicates 00503 00504 // Convert into correct array. Array can contain space-seperated 00505 // values. Implode/explode to get those into the main array as well. 00506 if ( is_array( $value ) ) { 00507 // If input wasn't an array, we can skip this step 00508 $newValue = array(); 00509 foreach ( $value as $k => $v ) { 00510 if ( is_string( $v ) ) { 00511 // String values should be normal `array( 'foo' )` 00512 // Just append them 00513 if ( !isset( $value[$v] ) ) { 00514 // As a special case don't set 'foo' if a 00515 // separate 'foo' => true/false exists in the array 00516 // keys should be authoritive 00517 $newValue[] = $v; 00518 } 00519 } elseif ( $v ) { 00520 // If the value is truthy but not a string this is likely 00521 // an array( 'foo' => true ), falsy values don't add strings 00522 $newValue[] = $k; 00523 } 00524 } 00525 $value = implode( ' ', $newValue ); 00526 } 00527 $value = explode( ' ', $value ); 00528 00529 // Normalize spacing by fixing up cases where people used 00530 // more than 1 space and/or a trailing/leading space 00531 $value = array_diff( $value, array( '', ' ' ) ); 00532 00533 // Remove duplicates and create the string 00534 $value = implode( ' ', array_unique( $value ) ); 00535 } 00536 00537 // See the "Attributes" section in the HTML syntax part of HTML5, 00538 // 9.1.2.3 as of 2009-08-10. Most attributes can have quotation 00539 // marks omitted, but not all. (Although a literal " is not 00540 // permitted, we don't check for that, since it will be escaped 00541 // anyway.) 00542 # 00543 // See also research done on further characters that need to be 00544 // escaped: http://code.google.com/p/html5lib/issues/detail?id=93 00545 $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}" 00546 . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}" 00547 . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}"; 00548 if ( $wgWellFormedXml || $value === '' 00549 || preg_match( "![$badChars]!u", $value ) ) { 00550 $quote = '"'; 00551 } else { 00552 $quote = ''; 00553 } 00554 00555 if ( in_array( $key, self::$boolAttribs ) ) { 00556 // In XHTML 1.0 Transitional, the value needs to be equal to the 00557 // key. In HTML5, we can leave the value empty instead. If we 00558 // don't need well-formed XML, we can omit the = entirely. 00559 if ( !$wgWellFormedXml ) { 00560 $ret .= " $key"; 00561 } elseif ( $wgHtml5 ) { 00562 $ret .= " $key=\"\""; 00563 } else { 00564 $ret .= " $key=\"$key\""; 00565 } 00566 } else { 00567 // Apparently we need to entity-encode \n, \r, \t, although the 00568 // spec doesn't mention that. Since we're doing strtr() anyway, 00569 // and we don't need <> escaped here, we may as well not call 00570 // htmlspecialchars(). 00571 // @todo FIXME: Verify that we actually need to 00572 // escape \n\r\t here, and explain why, exactly. 00573 # 00574 // We could call Sanitizer::encodeAttribute() for this, but we 00575 // don't because we're stubborn and like our marginal savings on 00576 // byte size from not having to encode unnecessary quotes. 00577 $map = array( 00578 '&' => '&', 00579 '"' => '"', 00580 "\n" => ' ', 00581 "\r" => ' ', 00582 "\t" => '	' 00583 ); 00584 if ( $wgWellFormedXml ) { 00585 // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue> 00586 // But reportedly it breaks some XML tools? 00587 // @todo FIXME: Is this really true? 00588 $map['<'] = '<'; 00589 } 00590 $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; 00591 } 00592 } 00593 return $ret; 00594 } 00595 00605 public static function inlineScript( $contents ) { 00606 global $wgHtml5, $wgJsMimeType, $wgWellFormedXml; 00607 00608 $attrs = array(); 00609 00610 if ( !$wgHtml5 ) { 00611 $attrs['type'] = $wgJsMimeType; 00612 } 00613 00614 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00615 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00616 } 00617 00618 return self::rawElement( 'script', $attrs, $contents ); 00619 } 00620 00628 public static function linkedScript( $url ) { 00629 global $wgHtml5, $wgJsMimeType; 00630 00631 $attrs = array( 'src' => $url ); 00632 00633 if ( !$wgHtml5 ) { 00634 $attrs['type'] = $wgJsMimeType; 00635 } 00636 00637 return self::element( 'script', $attrs ); 00638 } 00639 00649 public static function inlineStyle( $contents, $media = 'all' ) { 00650 global $wgWellFormedXml; 00651 00652 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00653 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00654 } 00655 00656 return self::rawElement( 'style', array( 00657 'type' => 'text/css', 00658 'media' => $media, 00659 ), $contents ); 00660 } 00661 00670 public static function linkedStyle( $url, $media = 'all' ) { 00671 return self::element( 'link', array( 00672 'rel' => 'stylesheet', 00673 'href' => $url, 00674 'type' => 'text/css', 00675 'media' => $media, 00676 ) ); 00677 } 00678 00691 public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { 00692 $attribs['type'] = $type; 00693 $attribs['value'] = $value; 00694 $attribs['name'] = $name; 00695 00696 return self::element( 'input', $attribs ); 00697 } 00698 00708 public static function hidden( $name, $value, $attribs = array() ) { 00709 return self::input( $name, $value, 'hidden', $attribs ); 00710 } 00711 00726 public static function textarea( $name, $value = '', $attribs = array() ) { 00727 global $wgHtml5; 00728 00729 $attribs['name'] = $name; 00730 00731 if ( !$wgHtml5 ) { 00732 if ( !isset( $attribs['cols'] ) ) { 00733 $attribs['cols'] = ""; 00734 } 00735 00736 if ( !isset( $attribs['rows'] ) ) { 00737 $attribs['rows'] = ""; 00738 } 00739 } 00740 00741 if (substr($value, 0, 1) == "\n") { 00742 // Workaround for bug 12130: browsers eat the initial newline 00743 // assuming that it's just for show, but they do keep the later 00744 // newlines, which we may want to preserve during editing. 00745 // Prepending a single newline 00746 $spacedValue = "\n" . $value; 00747 } else { 00748 $spacedValue = $value; 00749 } 00750 return self::element( 'textarea', $attribs, $spacedValue ); 00751 } 00766 public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { 00767 global $wgContLang; 00768 00769 ksort( $selectAttribs ); 00770 00771 // Is a namespace selected? 00772 if ( isset( $params['selected'] ) ) { 00773 // If string only contains digits, convert to clean int. Selected could also 00774 // be "all" or "" etc. which needs to be left untouched. 00775 // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues 00776 // and returns false for already clean ints. Use regex instead.. 00777 if ( preg_match( '/^\d+$/', $params['selected'] ) ) { 00778 $params['selected'] = intval( $params['selected'] ); 00779 } 00780 // else: leaves it untouched for later processing 00781 } else { 00782 $params['selected'] = ''; 00783 } 00784 00785 if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { 00786 $params['exclude'] = array(); 00787 } 00788 if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { 00789 $params['disable'] = array(); 00790 } 00791 00792 // Associative array between option-values and option-labels 00793 $options = array(); 00794 00795 if ( isset( $params['all'] ) ) { 00796 // add an option that would let the user select all namespaces. 00797 // Value is provided by user, the name shown is localized for the user. 00798 $options[$params['all']] = wfMessage( 'namespacesall' )->text(); 00799 } 00800 // Add all namespaces as options (in the content langauge) 00801 $options += $wgContLang->getFormattedNamespaces(); 00802 00803 // Convert $options to HTML and filter out namespaces below 0 00804 $optionsHtml = array(); 00805 foreach ( $options as $nsId => $nsName ) { 00806 if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { 00807 continue; 00808 } 00809 if ( $nsId === NS_MAIN ) { 00810 // For other namespaces use use the namespace prefix as label, but for 00811 // main we don't use "" but the user message descripting it (e.g. "(Main)" or "(Article)") 00812 $nsName = wfMessage( 'blanknamespace' )->text(); 00813 } elseif ( is_int( $nsId ) ) { 00814 $nsName = $wgContLang->convertNamespace( $nsId ); 00815 } 00816 $optionsHtml[] = Html::element( 00817 'option', array( 00818 'disabled' => in_array( $nsId, $params['disable'] ), 00819 'value' => $nsId, 00820 'selected' => $nsId === $params['selected'], 00821 ), $nsName 00822 ); 00823 } 00824 00825 if ( !array_key_exists( 'id', $selectAttribs ) ) { 00826 $selectAttribs['id'] = 'namespace'; 00827 } 00828 00829 if ( !array_key_exists( 'name', $selectAttribs ) ) { 00830 $selectAttribs['name'] = 'namespace'; 00831 } 00832 00833 $ret = ''; 00834 if ( isset( $params['label'] ) ) { 00835 $ret .= Html::element( 00836 'label', array( 00837 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null, 00838 ), $params['label'] 00839 ) . ' '; 00840 } 00841 00842 // Wrap options in a <select> 00843 $ret .= Html::openElement( 'select', $selectAttribs ) 00844 . "\n" 00845 . implode( "\n", $optionsHtml ) 00846 . "\n" 00847 . Html::closeElement( 'select' ); 00848 00849 return $ret; 00850 } 00851 00860 public static function htmlHeader( $attribs = array() ) { 00861 $ret = ''; 00862 00863 global $wgMimeType; 00864 00865 if ( self::isXmlMimeType( $wgMimeType ) ) { 00866 $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n"; 00867 } 00868 00869 global $wgHtml5, $wgHtml5Version, $wgDocType, $wgDTD; 00870 global $wgXhtmlNamespaces, $wgXhtmlDefaultNamespace; 00871 00872 if ( $wgHtml5 ) { 00873 $ret .= "<!DOCTYPE html>\n"; 00874 00875 if ( $wgHtml5Version ) { 00876 $attribs['version'] = $wgHtml5Version; 00877 } 00878 } else { 00879 $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n"; 00880 $attribs['xmlns'] = $wgXhtmlDefaultNamespace; 00881 00882 foreach ( $wgXhtmlNamespaces as $tag => $ns ) { 00883 $attribs["xmlns:$tag"] = $ns; 00884 } 00885 } 00886 00887 $html = Html::openElement( 'html', $attribs ); 00888 00889 if ( $html ) { 00890 $html .= "\n"; 00891 } 00892 00893 $ret .= $html; 00894 00895 return $ret; 00896 } 00897 00904 public static function isXmlMimeType( $mimetype ) { 00905 switch ( $mimetype ) { 00906 case 'text/xml': 00907 case 'application/xhtml+xml': 00908 case 'application/xml': 00909 return true; 00910 default: 00911 return false; 00912 } 00913 } 00914 00926 static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = true ) { 00927 global $wgStylePath; 00928 00929 if ( $useStylePath ) { 00930 $icon = $wgStylePath.'/common/images/'.$icon; 00931 } 00932 00933 $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class") ); 00934 00935 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ). 00936 Html::element( 'img', 00937 array( 00938 'src' => $icon, 00939 'alt' => $alt, 00940 ) 00941 ). 00942 Html::closeElement( 'div' ); 00943 00944 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ). 00945 $text. 00946 Html::closeElement( 'div' ); 00947 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00948 00949 $s .= Html::closeElement( 'div' ); 00950 00951 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00952 00953 return $s; 00954 } 00955 00964 static function srcSet( $urls ) { 00965 $candidates = array(); 00966 foreach( $urls as $density => $url ) { 00967 // Image candidate syntax per current whatwg live spec, 2012-09-23: 00968 // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-1.html#attr-img-srcset 00969 $candidates[] = "{$url} {$density}x"; 00970 } 00971 return implode( ", ", $candidates ); 00972 } 00973 }