jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/327671 )
Change subject: Improve tabular data styling and bring consistency with M82
......................................................................
Improve tabular data styling and bring consistency with M82
Bug: T153290
Bug: T152998
Change-Id: I44a4681f26b569dd5ab1017b7f036219b3880217
---
A bin/build.sh
M extension.json
M includes/JCTabularContentView.php
M includes/JCUtils.php
A lib/wmui-base.less
A modules/tabular-general.less
A modules/tabular-table.less
M package.json
8 files changed, 416 insertions(+), 75 deletions(-)
Approvals:
Yurik: Looks good to me, but someone else must approve
MaxSem: Looks good to me, approved
jenkins-bot: Verified
diff --git a/bin/build.sh b/bin/build.sh
new file mode 100644
index 0000000..78789fe
--- /dev/null
+++ b/bin/build.sh
@@ -0,0 +1 @@
+cp -v node_modules/wmui-base/wmui-base.less lib/wmui-base.less
diff --git a/extension.json b/extension.json
index 82ede04..43e4578 100644
--- a/extension.json
+++ b/extension.json
@@ -70,7 +70,11 @@
},
"ResourceModules": {
"ext.jsonConfig": {
- "styles": "JsonConfig.css",
+ "styles": [
+ "JsonConfig.css",
+ "tabular-general.less",
+ "tabular-table.less"
+ ],
"position": "top"
}
},
diff --git a/includes/JCTabularContentView.php
b/includes/JCTabularContentView.php
index 490a38f..8e4789b 100644
--- a/includes/JCTabularContentView.php
+++ b/includes/JCTabularContentView.php
@@ -18,91 +18,116 @@
* Called from an override of AbstractContent::fillParserOutput()
*
* @param JCContent|JCTabularContent $content
- * @param Title $title Context title for parsing
+ * @param Title $pageTitle Context title for parsing
* @param int|null $revId Revision ID (for {{REVISIONID}})
* @param ParserOptions $options Parser options
* @param bool $generateHtml Whether or not to generate HTML
* @param ParserOutput &$output The output object to fill (reference).
* @return string
*/
- public function valueToHtml( JCContent $content, Title $title, $revId,
ParserOptions $options,
- $generateHtml,
ParserOutput &$output ) {
+ public function valueToHtml( JCContent $content, Title $pageTitle,
$revId,
+ ParserOptions
$options, $generateHtml, ParserOutput &$output ) {
// Use user's language, and split parser cache. This should
not have a big
// impact because data namespace is rarely viewed, but viewing
it localized
// will be valuable
$lang = $options->getUserLangObj();
- $okClass = [ ];
- $infoClass = [ 'class' => 'mw-jsonconfig-value-info' ];
- $errorClass = [ 'class' => 'mw-jsonconfig-error' ];
- $result = [ ];
+ $infoClass = [ 'class' => 'mw-tabular-value-info' ];
+ $titleHeaders = [];
+ $nameHeaders = [];
+ $typeHeaders = [];
+ $rows = [];
+ $headerAttributes = [];
- $dataAttrs = [ 'class' => 'mw-jsonconfig sortable' ];
+ // Helper to add a class value to an array of attributes
+ $addErr = function ( array $attrs, $isValid ) {
+ if ( !$isValid ) {
+ $attrs['class'] = 'mw-tabular-error';
+ }
+ return $attrs;
+ };
+
+ // Helper to create a <tr> element out of an array of raw HTML
values
+ $makeRow = function ( array $values, array $attrs = [] ) {
+ return Html::rawElement( 'tr', $attrs, implode( '',
$values ) );
+ };
+
+ $dataAttrs = [ 'class' => 'mw-tabular sortable' ];
if ( !$content->getValidationData() ||
$content->getValidationData()->error() ) {
- $dataAttrs['class'] .= ' mw-jsonconfig-error';
+ $dataAttrs['class'] .= ' mw-tabular-error';
}
+
$flds = $content->getField( [ 'schema', 'fields' ] );
if ( $flds && !$flds->error() ) {
- $vals = [ ];
foreach ( $flds->getValue() as $fld ) {
$name = $content->getField( 'name', $fld );
+ $nameIsValid = $name && !$name->error();
+ $name = $nameIsValid ? $name->getValue() : '';
+
+ $title = $content->getField( 'title', $fld );
+ $titleIsValid = $title && !$title->error();
+ $title =
+ $titleIsValid ?
JCUtils::pickLocalizedString( $title->getValue(), $lang, $name )
+ : '';
+
$type = $content->getField( 'type', $fld );
- $label = $content->getField( 'title', $fld );
+ $typeIsValid = $type && !$type->error();
+ $type = $typeIsValid ? $type->getValue() : '';
- if ( $name && !$name->error() && $type &&
!$type->error() &&
- ( !$label || !$label->error() )
- ) {
- $labelAttrs = $okClass;
+ $thAttr = [];
+ if ( $nameIsValid ) {
+ $thAttr['data-name'] = $name;
+ }
+ if ( $typeIsValid ) {
+ $thAttr['data-type'] = $type;
+ $headerAttributes[] = [ 'data-type' =>
$type ];
} else {
- $labelAttrs = $errorClass;
+ $headerAttributes[] = [ ];
}
- if ( $label && !$label->error() ) {
- $label = JCUtils::pickLocalizedString(
$label->getValue(), $lang );
- } elseif ( $name && !$name->error() ) {
- $label = $name->getValue();
- } else {
- $label = '';
- }
+ $nameHeaders[] = Html::element( 'th', $addErr(
$thAttr, $nameIsValid ), $name );
- $type = !$type || $type->error() ? 'invalid' :
$type->getValue();
- $typeAttrs = $infoClass;
- $typeAttrs['title'] = wfMessage(
'jsonconfig-type-name-' . $type )->plain();
- $typeAbbr = wfMessage( 'jsonconfig-type-abbr-'
. $type )->plain();
+ $typeHeaders[] =
+ Html::element( 'th', $addErr( $thAttr,
$typeIsValid ),
+ $typeIsValid ? wfMessage(
'jsonconfig-type-name-' . $type )->plain() : '' );
- $label = htmlspecialchars( $label ) .
- Html::element( 'span',
$typeAttrs, $typeAbbr );
- $vals[] = Html::rawElement( 'th', $labelAttrs,
$label );
+ $titleHeaders[] = Html::element( 'th', $addErr(
$thAttr, $titleIsValid ), $title );
}
- $result[] = Html::rawElement( 'tr', $okClass, implode(
'', $vals ) );
}
$data = $content->getField( 'data' );
if ( $data && !$data->error() ) {
foreach ( $data->getValue() as $row ) {
- list( $row, $rowAttrs ) = self::split( $row );
- $vals = [ ];
+ $rowIsValid = $row && $row instanceof JCValue
&& !$row->error();
+ $row = ( $row && $row instanceof JCValue ) ?
$row->getValue() : $row;
+ if ( !is_array( $row ) ) {
+ continue;
+ }
+ $vals = [];
foreach ( $row as $column ) {
- list( $column, $columnAttrs ) =
self::split( $column, 'mw-jsonconfig-value' );
+ $colIsValid = $column && $column
instanceof JCValue && !$column->error();
+ $column =
+ ( $column && $column instanceof
JCValue ) ? $column->getValue() : $column;
+ $header = $headerAttributes[ count(
$vals ) ];
+ if ( !$colIsValid ) {
+ $header['class'] =
'mw-tabular-error';
+ }
+
if ( is_object( $column ) ) {
$valueSize = count(
(array)$column );
- $column = htmlspecialchars(
JCUtils::pickLocalizedString( $column, $lang ) ) .
-
Html::element( 'span', $infoClass, "($valueSize)" );
- $vals[] = Html::rawElement(
'td', $columnAttrs, $column );
- } else {
- if ( is_bool( $column ) ) {
- $column = $column ? '☑'
: '☐';
- } elseif ( $column === null ) {
- // TODO: Should we
append the CSS class instead?
- $columnAttrs['class'] =
'mw-jsonconfig-value-null';
- $column = '';
- }
- // TODO: We should probably
introduce one CSS class per type
- $vals[] = Html::element( 'td',
$columnAttrs, $column );
+ $column =
+ htmlspecialchars(
JCUtils::pickLocalizedString( $column, $lang ) ) .
+ Html::element( 'span',
$infoClass, "($valueSize)" );
+ } elseif ( is_bool( $column ) ) {
+ $column = $column ? '☑' : '☐';
+ } elseif ( $column === null ) {
+ $header['class'] =
'mw-tabular-value-null';
+ $column = '';
}
+ $vals[] = Html::rawElement( 'td',
$header, $column );
}
- $result[] = Html::rawElement( 'tr', $rowAttrs,
implode( '', $vals ) );
+ $rows[] = $makeRow( $vals, $rowIsValid ? [] : [
'class' => 'mw-tabular-error' ] );
}
}
@@ -110,31 +135,15 @@
$html =
$content->renderDescription( $lang ) .
- Html::rawElement( 'table', $dataAttrs,
- Html::rawElement( 'tbody', null, implode( "\n",
$result ) ) ) .
- $content->renderSources( $wgParser->getFreshParser(),
$title, $revId, $options ) .
+ Html::rawElement( 'table', $dataAttrs,
Html::rawElement( 'thead', null, implode( "\n", [
+ $makeRow( $nameHeaders, [ 'class' =>
'mw-tabular-row-key' ] ),
+ $makeRow( $typeHeaders, [ 'class' =>
'mw-tabular-row-type' ] ),
+ $makeRow( $titleHeaders, [ 'class' =>
'mw-tabular-row-name' ] ),
+ ] ) ) . Html::rawElement( 'tbody', null,
implode( "\n", $rows ) ) ) .
+ $content->renderSources( $wgParser->getFreshParser(),
$pageTitle, $revId, $options ) .
$content->renderLicense();
return $html;
- }
-
- /**
- * Converts JCValue into a raw data + class string. In case of an
error, adds error class
- * @param JCValue|mixed $data
- * @param string $class
- * @return array
- */
- private static function split( $data, $class = '' ) {
- if ( $data instanceof JCValue ) {
- if ( $data->error() ) {
- if ( $class ) {
- $class .= ' ';
- }
- $class .= 'mw-jsonconfig-error';
- }
- $data = $data->getValue();
- }
- return [ $data, $class ? [ 'class' => $class ] : null ];
}
/**
diff --git a/includes/JCUtils.php b/includes/JCUtils.php
index 2ad3f86..83328a1 100644
--- a/includes/JCUtils.php
+++ b/includes/JCUtils.php
@@ -270,9 +270,10 @@
* or use language fallbacks if message is not defined.
* @param stdClass $map Dictionary of languageCode => string
* @param Language|StubUserLang $lang language object
+ * @param bool|string $defaultValue if non-false, use this value in
case no fallback and no 'en'
* @return string message from the dictionary or "" if nothing found
*/
- public static function pickLocalizedString( stdClass $map, $lang ) {
+ public static function pickLocalizedString( stdClass $map, $lang,
$defaultValue = false ) {
$langCode = $lang->getCode();
if ( property_exists( $map, $langCode ) ) {
return $map->$langCode;
@@ -286,6 +287,12 @@
if ( property_exists( $map, 'en' ) ) {
return $map->en;
}
+
+ // We have a custom default, return that
+ if ( $defaultValue !== false ) {
+ return $defaultValue;
+ }
+
// Return first available value, or an empty string
// There might be a better way to get the first value from an
object
$map = (array)$map;
diff --git a/lib/wmui-base.less b/lib/wmui-base.less
new file mode 100644
index 0000000..4a139a2
--- /dev/null
+++ b/lib/wmui-base.less
@@ -0,0 +1,142 @@
+/**
+ * WikimediaUI base v0.7.1
+ * Wikimedia Foundation user interface base variables
+ */
+
+/* Colors */
+// WikimediaUI (WMUI) color palette
+@wmui-color-base0: #000; // = HSB 0°, 0%, 0%
+@wmui-color-base10: #222; // = HSB 0°, 0%, 13%
+@wmui-color-base20: #54595d; // = HSB 207°, 10%, 36%; WCAG 2.0 level AAA
7.09:1 contrast ratio on `#fff`
+@wmui-color-base30: #72777d; // = HSB 210°, 9%, 49%; WCAG 2.0 level AA at
4.52:1 contrast ratio on `#fff`
+@wmui-color-base50: #a2a9b1; // = HSB 212°, 8%, 69%
+@wmui-color-base70: #c8ccd1; // = HSB 213°, 4%, 82%
+@wmui-color-base80: #eaecf0; // = HSB 220°, 3%, 94%
+@wmui-color-base90: #f8f9fa; // = HSB 210°, 1%, 98%
+@wmui-color-base100: #fff; // = HSB 0°, 0%, 100%
+
+@wmui-color-accent30: #2a4b8d; // = HSB 220°, 70%, 55%
+@wmui-color-accent50: #36c; // = HSB 220°, 75%, 80%
+@wmui-color-accent90: #eaf3ff; // = HSB 214°, 8%, 100%
+
+@wmui-color-red30: #b32424; // = HSB 360°, 80%, 70%
+@wmui-color-red50: #d33; // = HSB 360°, 77%, 87%
+@wmui-color-red90: #fee7e6; // = HSB 3°, 9%, 100%
+
+@wmui-color-yellow30: #ac6600; // = HSB 36°, 100%, 67%
+@wmui-color-yellow50: #fc3; // = HSB 45°, 80%, 100%
+@wmui-color-yellow90: #fef6e7; // = HSB 39°, 9%, 100%
+
+@wmui-color-green30: #14866d; // = HSB 167°, 85%, 53%
+@wmui-color-green50: #00af89; // = HSB 167°, 100%, 69%
+@wmui-color-green90: #d5fdf4; // = HSB 166°, 16%, 99%
+
+// Background Colors
+@background-color-base: @wmui-color-base100;
+@background-color-code: @wmui-color-base90;
+// 'Framed' UI elements (Framed Buttons, Dropdowns, ToggleSwitches...)
+@background-color-framed: @wmui-color-base90;
+@background-color-framed--hover: @wmui-color-base100;
+@background-color-framed--active: @wmui-color-base70;
+// RGBA Colors include hex fallback on `#fff` for IE 6/7/8
+@background-color-highlight: rgba( 255, 182, 13, 0.4 );
+@background-color-highlight--fallback: #ffe29e;
+
+// Foreground Colors
+@color-base: @wmui-color-base10;
+@color-base--hover: #444;
+@color-base--active: @wmui-color-base0;
+@color-base--inverted: @wmui-color-base100;
+@color-base--emphasized: @wmui-color-base0;
+@color-base--disabled: @wmui-color-base30;
+@color-filled--disabled: @color-base--inverted;
+@color-placeholder: @wmui-color-base30;
+// Primary 'Progressive' Color, Background Color and states
+@background-color-primary: @wmui-color-base90;
+@background-color-primary--hover: rgba( 41, 98, 204, 0.1 );
+@color-primary: @wmui-color-accent50;
+@color-primary--hover: #447ff5; // = `lighten( @color-primary, 3 )`
+@color-primary--active: @wmui-color-accent30;
+@color-primary--focus: @wmui-color-accent50;
+// 'Destructive' Color, Background Color and states
+@background-color-destructive: @wmui-color-red90;
+@color-destructive: @wmui-color-red50;
+@color-destructive--hover: #e53939;
+@color-destructive--active: @wmui-color-red30;
+@color-destructive--focus: @wmui-color-red50;
+// Secondary Color and states (links only)
+@color-secondary: @wmui-color-green50;
+@color-secondary--hover: #1c6665;
+@color-secondary--active: @wmui-color-green30;
+@color-secondary--focus: @wmui-color-green50;
+
+
+// Opacity
+@opacity-base: 1;
+@opacity-base--disabled: 0.51; // = `#7d7d7d` on `background-color:
#fff`; HSB 0°, 0%, 49%
+
+
+/* Positioning */
+// Z-Index
+@z-index-base: 0;
+
+
+/* Box Model properties */
+/* Max Widths */
+@max-width-button: 28.75em; // = `460px` at `16px` base, see T95367
+@max-width-input-inline: 100%;
+
+// Border
+@border-base: @border-width-base solid @border-color-base;
+@border-dialog: @border-base;
+@border-menu: @border-base;
+// Border Colors
+@border-color-base: @wmui-color-base50;
+@border-color-base--hover: @wmui-color-base50;
+@border-color-base--active: @wmui-color-base30;
+@border-color-base--disabled: @color-base--disabled;
+@border-color-filled--disabled: @color-filled--disabled;
+@border-color-primary--active: #859dcc;
+@border-color-destructive--active: #b77c79;
+@border-color-inset--focus: @color-base--inverted;
+@border-color-heading: @wmui-color-base70;
+// Border Widths
+@border-width-base: 1px;
+// Border Radius
+@border-radius-base: 2px;
+
+// Box Shadows
+@box-shadow-base--focus: inset 0 0 0 1px @wmui-color-accent50;
+@box-shadow-inset--inverted: inset 0 0 0 1px @color-base--inverted;
+@box-shadow-filled--disabled: inset 0 0 0 1px @color-filled--disabled;
+@box-shadow-dialog: 0 0.15em 0 0 rgba( 0, 0, 0, 0.15 );
+@box-shadow-menu: @box-shadow-dialog;
+
+
+/* Typography incl. print properties */
+// Font Families
+@font-family-base: @font-family-sans;
+@font-family-heading-main: @font-family-serif;
+@font-family-sans: 'Helvetica Neue', 'Helvetica', 'Nimbus Sans L',
'Arial', 'Liberation Sans', sans-serif;
+@font-family-serif: 'Linux Libertine', 'Georgia', 'Times', serif;
+// Line Heights
+@line-height-base: 1.6;
+@line-height-heading: 1.25;
+@text-decoration-link--hover: none;
+// Text Shadows
+@text-shadow-base: 0 1px 1px @color-base--inverted; // 'coined'
effect
+@text-shadow-base--disabled: @text-shadow-base;
+
+
+/* Other Properties */
+// Cursors
+@cursor-base--disabled: default;
+
+
+/* Animation & Transition */
+// Transitions
+@transition-base: @transition-duration-base;
+@transition-medium: @transition-duration-medium;
+// Transitions > Durations
+@transition-duration-base: 100ms;
+@transition-duration-medium: 250ms;
diff --git a/modules/tabular-general.less b/modules/tabular-general.less
new file mode 100644
index 0000000..bffd86d
--- /dev/null
+++ b/modules/tabular-general.less
@@ -0,0 +1,53 @@
+/**
+ * CSS for styling HTML-formatted JSON Schema objects
+ *
+ * @file
+ * @author Yuri Astrakhan (yurik/at/wikimedia.org)
+ * @author Julien Girault (jgirault/at/wikimedia.org)
+ * @author Munaf Assaf <[email protected]> (adapted from)
+ * @ingroup Extensions
+ */
+@import '../lib/wmui-base.less';
+
+/* Tabular */
+.mw-tabular-default {
+ opacity: 0.3;
+}
+
+.mw-tabular-error {
+ color: @wmui-color-red50;
+ font-weight: bold;
+}
+
+.mw-tabular caption {
+ /* For stylistic reasons, suppress the caption of the outermost table */
+ display: none;
+}
+
+.mw-tabular table caption {
+ color: @wmui-color-base50;
+
+ display: inline-block;
+ margin-bottom: 0.5em;
+
+ font-size: 10px;
+ font-style: italic;
+ text-align: left;
+}
+
+.mw-tabular-editnotice {
+ background-color: @background-color-framed;
+ color: @wmui-color-base0;
+
+ margin: 0.5em 0;
+ padding: 0.5em;
+ border: @border-base;
+
+ font-size: 95%;
+ vertical-align: middle;
+}
+
+.mw-tabular-editnotice-icon {
+ width: 30px;
+ padding: 1em;
+}
diff --git a/modules/tabular-table.less b/modules/tabular-table.less
new file mode 100644
index 0000000..d62db2b
--- /dev/null
+++ b/modules/tabular-table.less
@@ -0,0 +1,123 @@
+@import '../lib/wmui-base.less';
+
+@monospaceFontFamily: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco,
Courier, monospace;
+
+.mw-tabular {
+ border-collapse: collapse;
+ border-spacing: 0;
+ font-style: normal;
+}
+
+.mw-tabular thead {
+ border: 1px solid @wmui-color-base70;
+}
+
+.mw-tabular th,
+.mw-tabular td {
+ padding: 0.5em;
+}
+
+.mw-tabular th {
+ background-color: @wmui-color-base90;
+
+ border: 1px inset @wmui-color-base80;
+ border-color: @wmui-color-base80;
+ border-left-color: @wmui-color-base70;
+ border-right-color: @wmui-color-base70;
+
+ color: @wmui-color-base100;
+ font-weight: normal;
+ font-size: 0.8em;
+}
+
+.mw-tabular .mw-tabular-row-name th {
+ background-color: @background-color-framed;
+
+ padding: 1em 1.5em;
+ border-color: @wmui-color-base70;
+ border-top: 1px solid @wmui-color-base80;
+ border-bottom: 1px solid @wmui-color-base70;
+
+ color: @color-primary;
+ font-size: 1em;
+ font-weight: bold;
+
+ &:hover {
+ background-color: @background-color-framed--hover;
+ border: 1px solid @color-primary;
+ color: @color-primary;
+ }
+ &:active {
+ background-color: @background-color-framed--active;
+ border: 1px solid @color-primary--active;
+ color: @color-primary--active;
+ }
+ &:focus {
+ background-color: @color-primary--focus;
+ border: 1px solid @color-primary--focus;
+ outline: 0;
+ box-shadow: inset 0 0 0 1px @color-primary--focus, inset 0 0 0
2px @color-base--inverted;
+ color: @color-base--inverted;
+ }
+ &:focus:active {
+ color: @color-base--inverted;
+ }
+
+ &.headerSortUp, &.headerSortDown {
+ color: @color-primary--active;
+ }
+}
+
+.mw-tabular .mw-tabular-row-type th {
+ background-color: @wmui-color-base90;
+ color: @wmui-color-base20;
+
+ font-family: @monospaceFontFamily;
+ font-size: 0.8em;
+ font-weight: normal;
+}
+
+.mw-tabular .mw-tabular-row-key th {
+ background-color: @wmui-color-base90;
+ border-top-color: @wmui-color-base70;
+ color: @wmui-color-base10;
+
+ font-family: @monospaceFontFamily;
+ font-size: 0.9em;
+}
+
+.mw-tabular td {
+ border: @border-base;
+ border-color: @wmui-color-base70;
+}
+
+.mw-tabular th[data-type="string"],
+.mw-tabular th[data-type="localized"] {
+ min-width: 6em;
+}
+
+.mw-tabular th[data-type="number"],
+.mw-tabular th[data-type="boolean"], {
+ min-width: 4em;
+ text-align: center;
+}
+
+.mw-tabular td[data-type="number"],
+.mw-tabular td[data-type="boolean"], {
+ text-align: center;
+}
+
+.mw-tabular td[data-type="localized"] {
+ font-style: italic;
+}
+
+.mw-tabular .mw-tabular-value-null {
+ background-color: @wmui-color-base80;
+}
+
+.mw-tabular-value-info {
+ float: right;
+ font-weight: bold;
+ font-size: 80%;
+ color: @wmui-color-green50;
+}
diff --git a/package.json b/package.json
index 4b23f73..dc97112 100644
--- a/package.json
+++ b/package.json
@@ -4,12 +4,14 @@
"private": true,
"description": "Build tools for the JsonConfig extension.",
"scripts": {
- "test": "grunt test"
+ "test": "grunt test",
+ "build-lib": ". ./bin/build.sh"
},
"devDependencies": {
"grunt": "0.4.5",
- "grunt-cli": "0.1.13",
"grunt-banana-checker": "0.4.0",
- "grunt-jsonlint": "1.0.7"
+ "grunt-cli": "0.1.13",
+ "grunt-jsonlint": "1.0.7",
+ "wmui-base":
"git+https://phabricator.wikimedia.org/diffusion/WMUI/wikimedia-ui-base.git#v0.7.1"
}
}
--
To view, visit https://gerrit.wikimedia.org/r/327671
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I44a4681f26b569dd5ab1017b7f036219b3880217
Gerrit-PatchSet: 3
Gerrit-Project: mediawiki/extensions/JsonConfig
Gerrit-Branch: master
Gerrit-Owner: JGirault <[email protected]>
Gerrit-Reviewer: MaxSem <[email protected]>
Gerrit-Reviewer: Yurik <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits