MediaWiki
REL1_21
|
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 continue; 00470 } 00471 00472 // Bug 23769: Blacklist all form validation attributes for now. Current 00473 // (June 2010) WebKit has no UI, so the form just refuses to submit 00474 // without telling the user why, which is much worse than failing 00475 // server-side validation. Opera is the only other implementation at 00476 // this time, and has ugly UI, so just kill the feature entirely until 00477 // we have at least one good implementation. 00478 00479 // As the default value of "1" for "step" rejects decimal 00480 // numbers to be entered in 'type="number"' fields, allow 00481 // the special case 'step="any"'. 00482 00483 if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) ) || 00484 $key === 'step' && $value !== 'any' ) { 00485 continue; 00486 } 00487 00488 // http://www.w3.org/TR/html401/index/attributes.html ("space-separated") 00489 // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") 00490 $spaceSeparatedListAttributes = array( 00491 'class', // html4, html5 00492 'accesskey', // as of html5, multiple space-separated values allowed 00493 // html4-spec doesn't document rel= as space-separated 00494 // but has been used like that and is now documented as such 00495 // in the html5-spec. 00496 'rel', 00497 ); 00498 00499 // Specific features for attributes that allow a list of space-separated values 00500 if ( in_array( $key, $spaceSeparatedListAttributes ) ) { 00501 // Apply some normalization and remove duplicates 00502 00503 // Convert into correct array. Array can contain space-separated 00504 // values. Implode/explode to get those into the main array as well. 00505 if ( is_array( $value ) ) { 00506 // If input wasn't an array, we can skip this step 00507 $newValue = array(); 00508 foreach ( $value as $k => $v ) { 00509 if ( is_string( $v ) ) { 00510 // String values should be normal `array( 'foo' )` 00511 // Just append them 00512 if ( !isset( $value[$v] ) ) { 00513 // As a special case don't set 'foo' if a 00514 // separate 'foo' => true/false exists in the array 00515 // keys should be authoritative 00516 $newValue[] = $v; 00517 } 00518 } elseif ( $v ) { 00519 // If the value is truthy but not a string this is likely 00520 // an array( 'foo' => true ), falsy values don't add strings 00521 $newValue[] = $k; 00522 } 00523 } 00524 $value = implode( ' ', $newValue ); 00525 } 00526 $value = explode( ' ', $value ); 00527 00528 // Normalize spacing by fixing up cases where people used 00529 // more than 1 space and/or a trailing/leading space 00530 $value = array_diff( $value, array( '', ' ' ) ); 00531 00532 // Remove duplicates and create the string 00533 $value = implode( ' ', array_unique( $value ) ); 00534 } 00535 00536 // See the "Attributes" section in the HTML syntax part of HTML5, 00537 // 9.1.2.3 as of 2009-08-10. Most attributes can have quotation 00538 // marks omitted, but not all. (Although a literal " is not 00539 // permitted, we don't check for that, since it will be escaped 00540 // anyway.) 00541 # 00542 // See also research done on further characters that need to be 00543 // escaped: http://code.google.com/p/html5lib/issues/detail?id=93 00544 $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}" 00545 . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}" 00546 . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}"; 00547 if ( $wgWellFormedXml || $value === '' 00548 || preg_match( "![$badChars]!u", $value ) ) { 00549 $quote = '"'; 00550 } else { 00551 $quote = ''; 00552 } 00553 00554 if ( in_array( $key, self::$boolAttribs ) ) { 00555 // In XHTML 1.0 Transitional, the value needs to be equal to the 00556 // key. In HTML5, we can leave the value empty instead. If we 00557 // don't need well-formed XML, we can omit the = entirely. 00558 if ( !$wgWellFormedXml ) { 00559 $ret .= " $key"; 00560 } elseif ( $wgHtml5 ) { 00561 $ret .= " $key=\"\""; 00562 } else { 00563 $ret .= " $key=\"$key\""; 00564 } 00565 } else { 00566 // Apparently we need to entity-encode \n, \r, \t, although the 00567 // spec doesn't mention that. Since we're doing strtr() anyway, 00568 // and we don't need <> escaped here, we may as well not call 00569 // htmlspecialchars(). 00570 // @todo FIXME: Verify that we actually need to 00571 // escape \n\r\t here, and explain why, exactly. 00572 # 00573 // We could call Sanitizer::encodeAttribute() for this, but we 00574 // don't because we're stubborn and like our marginal savings on 00575 // byte size from not having to encode unnecessary quotes. 00576 $map = array( 00577 '&' => '&', 00578 '"' => '"', 00579 "\n" => ' ', 00580 "\r" => ' ', 00581 "\t" => '	' 00582 ); 00583 if ( $wgWellFormedXml ) { 00584 // This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue> 00585 // But reportedly it breaks some XML tools? 00586 // @todo FIXME: Is this really true? 00587 $map['<'] = '<'; 00588 } 00589 $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; 00590 } 00591 } 00592 return $ret; 00593 } 00594 00604 public static function inlineScript( $contents ) { 00605 global $wgHtml5, $wgJsMimeType, $wgWellFormedXml; 00606 00607 $attrs = array(); 00608 00609 if ( !$wgHtml5 ) { 00610 $attrs['type'] = $wgJsMimeType; 00611 } 00612 00613 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00614 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00615 } 00616 00617 return self::rawElement( 'script', $attrs, $contents ); 00618 } 00619 00627 public static function linkedScript( $url ) { 00628 global $wgHtml5, $wgJsMimeType; 00629 00630 $attrs = array( 'src' => $url ); 00631 00632 if ( !$wgHtml5 ) { 00633 $attrs['type'] = $wgJsMimeType; 00634 } 00635 00636 return self::element( 'script', $attrs ); 00637 } 00638 00648 public static function inlineStyle( $contents, $media = 'all' ) { 00649 global $wgWellFormedXml; 00650 00651 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00652 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00653 } 00654 00655 return self::rawElement( 'style', array( 00656 'type' => 'text/css', 00657 'media' => $media, 00658 ), $contents ); 00659 } 00660 00669 public static function linkedStyle( $url, $media = 'all' ) { 00670 return self::element( 'link', array( 00671 'rel' => 'stylesheet', 00672 'href' => $url, 00673 'type' => 'text/css', 00674 'media' => $media, 00675 ) ); 00676 } 00677 00690 public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { 00691 $attribs['type'] = $type; 00692 $attribs['value'] = $value; 00693 $attribs['name'] = $name; 00694 00695 return self::element( 'input', $attribs ); 00696 } 00697 00707 public static function hidden( $name, $value, $attribs = array() ) { 00708 return self::input( $name, $value, 'hidden', $attribs ); 00709 } 00710 00725 public static function textarea( $name, $value = '', $attribs = array() ) { 00726 global $wgHtml5; 00727 00728 $attribs['name'] = $name; 00729 00730 if ( !$wgHtml5 ) { 00731 if ( !isset( $attribs['cols'] ) ) { 00732 $attribs['cols'] = ""; 00733 } 00734 00735 if ( !isset( $attribs['rows'] ) ) { 00736 $attribs['rows'] = ""; 00737 } 00738 } 00739 00740 if ( substr( $value, 0, 1 ) == "\n" ) { 00741 // Workaround for bug 12130: browsers eat the initial newline 00742 // assuming that it's just for show, but they do keep the later 00743 // newlines, which we may want to preserve during editing. 00744 // Prepending a single newline 00745 $spacedValue = "\n" . $value; 00746 } else { 00747 $spacedValue = $value; 00748 } 00749 return self::element( 'textarea', $attribs, $spacedValue ); 00750 } 00765 public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { 00766 global $wgContLang; 00767 00768 ksort( $selectAttribs ); 00769 00770 // Is a namespace selected? 00771 if ( isset( $params['selected'] ) ) { 00772 // If string only contains digits, convert to clean int. Selected could also 00773 // be "all" or "" etc. which needs to be left untouched. 00774 // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues 00775 // and returns false for already clean ints. Use regex instead.. 00776 if ( preg_match( '/^\d+$/', $params['selected'] ) ) { 00777 $params['selected'] = intval( $params['selected'] ); 00778 } 00779 // else: leaves it untouched for later processing 00780 } else { 00781 $params['selected'] = ''; 00782 } 00783 00784 if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { 00785 $params['exclude'] = array(); 00786 } 00787 if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { 00788 $params['disable'] = array(); 00789 } 00790 00791 // Associative array between option-values and option-labels 00792 $options = array(); 00793 00794 if ( isset( $params['all'] ) ) { 00795 // add an option that would let the user select all namespaces. 00796 // Value is provided by user, the name shown is localized for the user. 00797 $options[$params['all']] = wfMessage( 'namespacesall' )->text(); 00798 } 00799 // Add all namespaces as options (in the content language) 00800 $options += $wgContLang->getFormattedNamespaces(); 00801 00802 // Convert $options to HTML and filter out namespaces below 0 00803 $optionsHtml = array(); 00804 foreach ( $options as $nsId => $nsName ) { 00805 if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { 00806 continue; 00807 } 00808 if ( $nsId === NS_MAIN ) { 00809 // For other namespaces use use the namespace prefix as label, but for 00810 // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") 00811 $nsName = wfMessage( 'blanknamespace' )->text(); 00812 } elseif ( is_int( $nsId ) ) { 00813 $nsName = $wgContLang->convertNamespace( $nsId ); 00814 } 00815 $optionsHtml[] = Html::element( 00816 'option', array( 00817 'disabled' => in_array( $nsId, $params['disable'] ), 00818 'value' => $nsId, 00819 'selected' => $nsId === $params['selected'], 00820 ), $nsName 00821 ); 00822 } 00823 00824 if ( !array_key_exists( 'id', $selectAttribs ) ) { 00825 $selectAttribs['id'] = 'namespace'; 00826 } 00827 00828 if ( !array_key_exists( 'name', $selectAttribs ) ) { 00829 $selectAttribs['name'] = 'namespace'; 00830 } 00831 00832 $ret = ''; 00833 if ( isset( $params['label'] ) ) { 00834 $ret .= Html::element( 00835 'label', array( 00836 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null, 00837 ), $params['label'] 00838 ) . ' '; 00839 } 00840 00841 // Wrap options in a <select> 00842 $ret .= Html::openElement( 'select', $selectAttribs ) 00843 . "\n" 00844 . implode( "\n", $optionsHtml ) 00845 . "\n" 00846 . Html::closeElement( 'select' ); 00847 00848 return $ret; 00849 } 00850 00859 public static function htmlHeader( $attribs = array() ) { 00860 $ret = ''; 00861 00862 global $wgMimeType; 00863 00864 if ( self::isXmlMimeType( $wgMimeType ) ) { 00865 $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n"; 00866 } 00867 00868 global $wgHtml5, $wgHtml5Version, $wgDocType, $wgDTD; 00869 global $wgXhtmlNamespaces, $wgXhtmlDefaultNamespace; 00870 00871 if ( $wgHtml5 ) { 00872 $ret .= "<!DOCTYPE html>\n"; 00873 00874 if ( $wgHtml5Version ) { 00875 $attribs['version'] = $wgHtml5Version; 00876 } 00877 } else { 00878 $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n"; 00879 $attribs['xmlns'] = $wgXhtmlDefaultNamespace; 00880 00881 foreach ( $wgXhtmlNamespaces as $tag => $ns ) { 00882 $attribs["xmlns:$tag"] = $ns; 00883 } 00884 } 00885 00886 $html = Html::openElement( 'html', $attribs ); 00887 00888 if ( $html ) { 00889 $html .= "\n"; 00890 } 00891 00892 $ret .= $html; 00893 00894 return $ret; 00895 } 00896 00903 public static function isXmlMimeType( $mimetype ) { 00904 switch ( $mimetype ) { 00905 case 'text/xml': 00906 case 'application/xhtml+xml': 00907 case 'application/xml': 00908 return true; 00909 default: 00910 return false; 00911 } 00912 } 00913 00925 static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = true ) { 00926 global $wgStylePath; 00927 00928 if ( $useStylePath ) { 00929 $icon = $wgStylePath.'/common/images/'.$icon; 00930 } 00931 00932 $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class" ) ); 00933 00934 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ). 00935 Html::element( 'img', 00936 array( 00937 'src' => $icon, 00938 'alt' => $alt, 00939 ) 00940 ). 00941 Html::closeElement( 'div' ); 00942 00943 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ). 00944 $text. 00945 Html::closeElement( 'div' ); 00946 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00947 00948 $s .= Html::closeElement( 'div' ); 00949 00950 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00951 00952 return $s; 00953 } 00954 00963 static function srcSet( $urls ) { 00964 $candidates = array(); 00965 foreach( $urls as $density => $url ) { 00966 // Image candidate syntax per current whatwg live spec, 2012-09-23: 00967 // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-1.html#attr-img-srcset 00968 $candidates[] = "{$url} {$density}x"; 00969 } 00970 return implode( ", ", $candidates ); 00971 } 00972 }