51 private static $voidElements = [
71 private static $boolAttribs = [
110 public static function buttonAttributes(
array $attrs,
array $modifiers = [] ) {
113 if ( isset( $attrs[
'class'] ) ) {
114 if ( is_array( $attrs[
'class'] ) ) {
115 $attrs[
'class'][] =
'mw-ui-button';
116 $attrs[
'class'] = array_merge( $attrs[
'class'], $modifiers );
118 $attrs[
'class'] = implode(
' ', $attrs[
'class'] );
120 $attrs[
'class'] .=
' mw-ui-button ' . implode(
' ', $modifiers );
124 $attrs[
'class'] =
'mw-ui-button ' . implode(
' ', $modifiers );
137 public static function getTextInputAttributes(
array $attrs ) {
140 if ( isset( $attrs[
'class'] ) ) {
141 if ( is_array( $attrs[
'class'] ) ) {
142 $attrs[
'class'][] =
'mw-ui-input';
144 $attrs[
'class'] .=
' mw-ui-input';
147 $attrs[
'class'] =
'mw-ui-input';
166 public static function linkButton( $contents,
array $attrs,
array $modifiers = [] ) {
167 return self::element(
'a',
168 self::buttonAttributes( $attrs, $modifiers ),
186 public static function submitButton( $contents,
array $attrs,
array $modifiers = [] ) {
187 $attrs[
'type'] =
'submit';
188 $attrs[
'value'] = $contents;
189 return self::element(
'input', self::buttonAttributes( $attrs, $modifiers ) );
210 public static function rawElement( $element,
$attribs = [], $contents =
'' ) {
211 $start = self::openElement( $element,
$attribs );
212 if ( in_array( $element, self::$voidElements ) ) {
214 return substr( $start, 0, -1 ) .
'/>';
216 return $start . $contents . self::closeElement( $element );
232 public static function element( $element,
$attribs = [], $contents =
'' ) {
233 return self::rawElement( $element,
$attribs, strtr( $contents, [
252 public static function openElement( $element,
$attribs = [] ) {
256 $element = strtolower( $element );
260 if ( strpos( $element,
' ' ) !==
false ) {
261 wfWarn( __METHOD__ .
" given element name with space '$element'" );
265 if ( $element ==
'input' ) {
293 if ( isset(
$attribs[
'type'] ) && !in_array(
$attribs[
'type'], $validTypes ) ) {
301 if ( $element ==
'button' && !isset(
$attribs[
'type'] ) ) {
305 return "<$element" . self::expandAttributes(
306 self::dropDefaults( $element,
$attribs ) ) .
'>';
316 public static function closeElement( $element ) {
317 $element = strtolower( $element );
319 return "</$element>";
339 private static function dropDefaults( $element,
array $attribs ) {
342 static $attribDefaults = [
343 'area' => [
'shape' =>
'rect' ],
345 'formaction' =>
'GET',
346 'formenctype' =>
'application/x-www-form-urlencoded',
354 'autocomplete' =>
'on',
355 'enctype' =>
'application/x-www-form-urlencoded',
358 'formaction' =>
'GET',
361 'keygen' => [
'keytype' =>
'rsa' ],
362 'link' => [
'media' =>
'all' ],
363 'menu' => [
'type' =>
'list' ],
364 'script' => [
'type' =>
'text/javascript' ],
367 'type' =>
'text/css',
369 'textarea' => [
'wrap' =>
'soft' ],
372 $element = strtolower( $element );
375 $lcattrib = strtolower( $attrib );
376 if ( is_array(
$value ) ) {
383 if ( isset( $attribDefaults[$element][$lcattrib] )
384 && $attribDefaults[$element][$lcattrib] ==
$value
389 if ( $lcattrib ==
'class' &&
$value ==
'' ) {
395 if ( $element ===
'link'
400 if ( $element ===
'input' ) {
403 if (
$type ===
'checkbox' ||
$type ===
'radio' ) {
410 } elseif (
$type ===
'submit' ) {
423 if ( $element ===
'select' && isset(
$attribs[
'size'] ) ) {
424 if ( in_array(
'multiple',
$attribs )
428 if ( strval(
$attribs[
'size'] ) ==
'4' ) {
433 if ( strval(
$attribs[
'size'] ) ==
'1' ) {
491 if ( is_int( $key ) && in_array( strtolower(
$value ), self::$boolAttribs ) ) {
497 $key = strtolower( $key );
501 $spaceSeparatedListAttributes = [
511 if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
516 if ( is_array(
$value ) ) {
520 if ( is_string( $v ) ) {
523 if ( !isset(
$value[$v] ) ) {
535 $value = implode(
' ', $newValue );
545 } elseif ( is_array(
$value ) ) {
546 throw new MWException(
"HTML attribute $key can not contain a list of values" );
551 if ( in_array( $key, self::$boolAttribs ) ) {
552 $ret .=
" $key=\"\"";
554 $ret .=
" $key=$quote" . Sanitizer::encodeAttribute(
$value ) . $quote;
573 public static function inlineScript( $contents, $nonce =
null ) {
575 if ( $nonce !==
null ) {
576 $attrs[
'nonce'] = $nonce;
578 wfWarn(
"no nonce set on script. CSP will break it" );
581 if ( preg_match(
'/<\/?script/i', $contents ) ) {
582 wfLogWarning( __METHOD__ .
': Illegal character sequence found in inline script.' );
583 $contents =
'/* ERROR: Invalid script */';
586 return self::rawElement(
'script', $attrs, $contents );
597 public static function linkedScript( $url, $nonce =
null ) {
598 $attrs = [
'src' => $url ];
599 if ( $nonce !==
null ) {
600 $attrs[
'nonce'] = $nonce;
602 wfWarn(
"no nonce set on script. CSP will break it" );
605 return self::element(
'script', $attrs );
620 public static function inlineStyle( $contents, $media =
'all',
$attribs = [] ) {
625 $contents = strtr( $contents, [
629 ']]>' =>
'\5D\5D\3E '
632 if ( preg_match(
'/[<&]/', $contents ) ) {
633 $contents =
"/*<![CDATA[*/$contents/*]]>*/";
636 return self::rawElement(
'style', [
649 public static function linkedStyle( $url, $media =
'all' ) {
650 return self::element(
'link', [
651 'rel' =>
'stylesheet',
672 if ( in_array(
$type, [
'text',
'search',
'email',
'password',
'number' ] ) ) {
675 if ( in_array(
$type, [
'button',
'reset',
'submit' ] ) ) {
678 return self::element(
'input',
$attribs );
712 private static function messageBox(
$html, $className, $heading =
'' ) {
713 if ( $heading !==
'' ) {
714 $html = self::element(
'h2', [], $heading ) .
$html;
716 return self::rawElement(
'div', [
'class' => $className ],
$html );
725 public static function warningBox(
$html ) {
726 return self::messageBox(
$html,
'warningbox' );
736 public static function errorBox(
$html, $heading =
'' ) {
737 return self::messageBox(
$html,
'errorbox', $heading );
746 public static function successBox(
$html ) {
747 return self::messageBox(
$html,
'successbox' );
781 public static function label( $label, $id,
array $attribs = [] ) {
785 return self::element(
'label',
$attribs, $label );
816 if ( substr(
$value, 0, 1 ) ==
"\n" ) {
821 $spacedValue =
"\n" .
$value;
825 return self::element(
'textarea', self::getTextInputAttributes(
$attribs ), $spacedValue );
833 public static function namespaceSelectorOptions(
array $params = [] ) {
836 if ( !isset(
$params[
'exclude'] ) || !is_array(
$params[
'exclude'] ) ) {
840 if ( isset(
$params[
'all'] ) ) {
845 if (
$params[
'in-user-lang'] ??
false ) {
849 $lang = MediaWikiServices::getInstance()->getContentLanguage();
863 $nsName =
wfMessage(
'blanknamespace' )->text();
864 } elseif ( is_int( $nsId ) ) {
865 $nsName =
$lang->convertNamespace( $nsId );
867 $optionsOut[$nsId] = $nsName;
889 public static function namespaceSelector(
array $params = [],
890 array $selectAttribs = []
892 ksort( $selectAttribs );
895 if ( isset(
$params[
'selected'] ) ) {
900 if ( preg_match(
'/^\d+$/',
$params[
'selected'] ) ) {
908 if ( !isset(
$params[
'disable'] ) || !is_array(
$params[
'disable'] ) ) {
918 $optionsHtml[] = self::element(
920 'disabled' => in_array( $nsId,
$params[
'disable'] ),
922 'selected' => $nsId ===
$params[
'selected'],
927 if ( !array_key_exists(
'id', $selectAttribs ) ) {
928 $selectAttribs[
'id'] =
'namespace';
931 if ( !array_key_exists(
'name', $selectAttribs ) ) {
932 $selectAttribs[
'name'] =
'namespace';
936 if ( isset(
$params[
'label'] ) ) {
937 $ret .= self::element(
939 'for' => $selectAttribs[
'id'] ??
null,
945 $ret .= self::openElement(
'select', $selectAttribs )
947 . implode(
"\n", $optionsHtml )
949 . self::closeElement(
'select' );
972 $ret .=
"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
975 $attribs[
'xmlns'] =
'http://www.w3.org/1999/xhtml';
982 $ret .=
"<!DOCTYPE html>\n";
1000 public static function isXmlMimeType( $mimetype ) {
1001 # https://html.spec.whatwg.org/multipage/infrastructure.html#xml-mime-type
1004 # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
1005 return (
bool)preg_match(
'!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
1018 static function infoBox( $text, $icon, $alt, $class =
'' ) {
1019 $s = self::openElement(
'div', [
'class' =>
"mw-infobox $class" ] );
1021 $s .= self::openElement(
'div', [
'class' =>
'mw-infobox-left' ] ) .
1022 self::element(
'img',
1028 self::closeElement(
'div' );
1030 $s .= self::openElement(
'div', [
'class' =>
'mw-infobox-right' ] ) .
1032 self::closeElement(
'div' );
1033 $s .= self::element(
'div', [
'style' =>
'clear: left;' ],
' ' );
1035 $s .= self::closeElement(
'div' );
1037 $s .= self::element(
'div', [
'style' =>
'clear: left;' ],
' ' );
1065 static function srcSet(
array $urls ) {
1067 foreach ( $urls
as $density => $url ) {
1070 $density = (
string)(
float)$density;
1071 $candidates[$density] = $url;
1075 ksort( $candidates, SORT_NUMERIC );
1076 $candidates = array_unique( $candidates );
1079 foreach ( $candidates
as $density => $url ) {
1080 $candidates[$density] = $url .
' ' . $density .
'x';
1083 return implode(
", ", $candidates );