Cscott has uploaded a new change for review. https://gerrit.wikimedia.org/r/306975
Change subject: Extend 'format' spec to include custom format strings. ...................................................................... Extend 'format' spec to include custom format strings. Thanks to Thiemo Mättig for the suggestion and specification at Wikimania 2016 in Esino Lario. This is an extended version of Thiemo's original specification. This version also allows specification of "own line" properties for templates; that is, whether the template should be preceded/followed by a newline, as requested by James Forrester. Bug: T138492 Bug: T135667 Change-Id: Idc6b2680330e6bf5caec2bf6fc86a705d25bc649 --- M Specification.md M TemplateDataBlob.php M extension.json M i18n/en.json M i18n/qqq.json M modules/ext.templateDataGenerator.ui.tdDialog.js M resources/ext.templateData.css M tests/ext.templateData.tests.js M tests/phpunit/TemplateDataBlobTest.php 9 files changed, 318 insertions(+), 36 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/TemplateData refs/changes/75/306975/1 diff --git a/Specification.md b/Specification.md index 3929441..d0133b9 100644 --- a/Specification.md +++ b/Specification.md @@ -98,25 +98,14 @@ Authors MUST ensure that the `maps` object contains only `Map` objects. Authors MAY include a parameter in multiple `Map` objects. Authors are NOT REQUIRED to reference each parameter in at least one `Map` object. #### 3.1.6 `format` -* Value: `null` or `string` of either `'inline'` or `'block'` +* Value: `null` or `FormatString` or `string` of either `'inline'` or `'block'` * Default: `null` How the template's wikitext representation SHOULD be laid out. Authors MAY choose to use this parameter to express that a template will be better understood by other human readers of the wikitext representation if a template is in one form or the other. -If the parameter is set to `'block'`, Consumers SHOULD create a wikitext representation with a single newline after the template invocation and each parameter value, a single space between each pipe and its subsequent parameter key, and a space either side of the assignment separator between the parameter key and value, like so: +If the parameter is set to `'block'`, it MUST be interpreted as the format string `'{{_\n| _ = _\n}}'`. If the parameter is set to `'inline'` it MUST be interpreted as the format string `'{{_|_=_}}'`. -``` -{{Foo -| bar = baz -| qux = quux -}} -``` - -If the parameter is set to `'inline'`, Consumers SHOULD create a wikitext representation with no whitespace, like so: - -``` -{{Foo|bar=baz|qux=quux}} -``` +If the parameter is not null, Consumers SHOULD create a wikitext representation corresponding to the given format string, as described in section 3.7. If the parameter is set to `null`, Consumers SHOULD create the same representation as for `'inline'` format for new template transclusions, and SHOULD attempt to use the same formatting for new parameters as for existing ones for existing transclusions that are edited. @@ -289,6 +278,87 @@ The key corresponds to the name of a Consumer variable that relates to the specified parameter(s). +### 3.7 FormatString +* Value: `string` + +A format string describes how whitespace should be added to a template instantiation in wikitext. A format string looks like `{{_|_=_}}`, with optional whitespace and extended underscores. Formally, its structure is given by the following grammar: +``` +FormatString = StartFormat ParameterFormat EndFormat +StartFormat = nl? "{{" ws* Hole ws* +ParameterFormat = nl? "|" nl? ws* Hole ws* "=" ws* Hole +EndFormat = nl? ws* "}}" nl? +Hole = "_"+ +ws = " " +nl = "\n" +``` + +To format a template invocation according to the format string, first split it in three parts, corresponding to `StartFormat`, `ParameterFormat`, and `EndFormat` in the grammar above. In the following, to "replace the `Hole`" with some value means to first pad a non-empty value with spaces on the right until it is at least as many characters long as the replaced underscore sequence. Zero-length values are not padded. + +Begin with `StartFormat`, and replace the `Hole` with the name of the template to create the "output string". If `StartFormat` begins with a newline and template is already at the start of a line (the character preceding this template invocation is a newline or the template is at the start of the output), delete the initial newline from the output string. + +For each parameter, append the `ParameterFormat` to the output string after replacing the first `Hole` with the name of the parameter and the second `Hole` with the value of the parameter. + +Finally, append the `EndFormat` to the output string. + +Some example format strings: + +Inline formatting: `{{_|_=_}}` +``` +{{Foo|bar=baz|qux=quux}}{{Bar}} +``` + +Block formatting: `{{_\n| _ = _\n}}` +``` +{{Foo +| bar = baz +| qux = quux +}}{{Bar +}} +``` + +No space before the parameter name, each template on its own line: `\n{{_\n|_ = _\n}}\n` +``` +{{Foo +|bar = baz +|qux = quux +}} +{{Bar +}} +``` + +Indent each parameter: `{{_\n |_ = _\n}}` +``` +{{Foo + |bar = baz + |qux = quux +}}{{Bar +}} +``` + +Align all parameter names to a given length: `{{_\n|_______________ = _\n}}\n` +``` +{{Foo +|bar = baz +|qux = quux +|veryverylongparameter = bat +}} +{{Bar +}} +``` + +Pipe characters at the end of the previous line: `{{_|\n _______________ = _}}` +``` +{{Foo| + bar = baz| + qux = quux}}{{Bar}} +``` + +Inline style with more spaces, must be at start of line: `\n{{_ | _ = _}}` +``` +{{Foo | bar = baz | qux = quux}} +{{Bar }} +``` + ## 4 Examples ### 4.1 The "Unsigned" template diff --git a/TemplateDataBlob.php b/TemplateDataBlob.php index b8c8078..42e8b82 100644 --- a/TemplateDataBlob.php +++ b/TemplateDataBlob.php @@ -132,9 +132,8 @@ ]; static $formats = [ - null, - 'block', - 'inline' + 'block' => "{{_\n| _ = _\n}}", + 'inline' => '{{_|_=_}}', ]; static $typeCompatMap = [ @@ -169,8 +168,13 @@ } // Root.format - if ( isset( $data->format ) ) { - if ( !in_array( $data->format, $formats ) ) { + if ( isset( $data->format ) && $data->format !== null ) { + $f = isset( $formats[$data->format] ) ? $formats[$data->format] : + $data->format; + if ( + !is_string( $f ) || + !preg_match( '/^\n?\{\{ *_+ *\n?\|\n? *_+ *= *_+\n? *\}\}\n?$/', $f ) + ) { return Status::newFatal( 'templatedata-invalid-format', 'format' @@ -701,6 +705,13 @@ public function getHtml( Language $lang ) { $data = $this->getDataInLanguage( $lang->getCode() ); + if ( $data->format === null ) { + $formatMsg = null; + } elseif ( isset( $formats[$data->format] ) ) { + $formatMsg = $data->format; + } else { + $formatMsg = 'custom'; + } $html = Html::openElement( 'div', [ 'class' => 'mw-templatedata-doc-wrap' ] ) . Html::element( @@ -724,16 +735,19 @@ [], wfMessage( 'templatedata-doc-params' )->inLanguage( $lang )->text() ) - . ( $data->format !== null ? + . ( $formatMsg !== null ? Html::rawElement( 'p', [], - new OOUI\IconWidget( [ 'icon' => 'template-format-' . $data->format ] ) + new OOUI\IconWidget( [ 'icon' => 'template-format-' . $formatMsg ] ) . Html::element( 'span', [ 'class' => 'mw-templatedata-format' ], - // Messages: templatedata-modal-format-inline, templatedata-modal-format-block - wfMessage( 'templatedata-doc-format-' . $data->format )->inLanguage( $lang )->text() + // Messages that can be used here: + // * templatedata-doc-format-block + // * templatedata-doc-format-custom + // * templatedata-doc-format-inline + wfMessage( 'templatedata-doc-format-' . $formatMsg )->inLanguage( $lang )->text() ) ) : '' ) diff --git a/extension.json b/extension.json index bc1d111..af80549 100644 --- a/extension.json +++ b/extension.json @@ -122,9 +122,11 @@ "templatedata-modal-current-language", "templatedata-modal-errormsg", "templatedata-modal-errormsg-import-noparams", - "templatedata-modal-format-inline", "templatedata-modal-format-block", + "templatedata-modal-format-custom", + "templatedata-modal-format-inline", "templatedata-modal-format-null", + "templatedata-modal-format-placeholder", "templatedata-modal-json-error-replace", "templatedata-modal-notice-import-numparams", "templatedata-modal-placeholder-paramkey", @@ -152,6 +154,7 @@ "templatedata-modal-title-paramorder", "templatedata-modal-title-templatedesc", "templatedata-modal-title-templateformat", + "templatedata-modal-title-templateformatstring", "templatedata-modal-title-templateparam-details", "templatedata-modal-title-templateparams", "templatedata-helplink", diff --git a/i18n/en.json b/i18n/en.json index 8f59418..aa41c90 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -12,8 +12,9 @@ "apihelp-templatedata-param-lang": "Return localized values in this language. By default all available translations are returned.", "templatedata-desc": "Implement data storage for template parameters (using JSON)", "templatedata-doc-desc-empty": "No description.", - "templatedata-doc-format-inline": "This template prefers inline formatting of parameters.", "templatedata-doc-format-block": "This template prefers block formatting of parameters.", + "templatedata-doc-format-custom": "This template has custom formatting.", + "templatedata-doc-format-inline": "This template prefers inline formatting of parameters.", "templatedata-doc-no-params-set": "No parameters specified", "templatedata-doc-param-autovalue": "Auto value", "templatedata-doc-param-autovalue-empty": "empty", @@ -52,7 +53,7 @@ "templatedata-helplink-target": "//www.mediawiki.org/wiki/Special:MyLanguage/Help:TemplateData", "templatedata-invalid-duplicate-value": "Property \"$1\" (\"$3\") is a duplicate of \"$2\".", "templatedata-invalid-empty-array": "Property \"$1\" must have at least one value in its array.", - "templatedata-invalid-format": "Property \"$1\" is expected to be \"inline\" or \"block\".", + "templatedata-invalid-format": "Property \"$1\" is expected to be \"inline\", \"block\", or a valid format string.", "templatedata-invalid-length": "Data too large to save ({{formatnum:$1}} {{PLURAL:$1|byte|bytes}}, {{PLURAL:$2|limit is}} {{formatnum:$2}})", "templatedata-invalid-missing": "Required property \"$1\" not found.", "templatedata-invalid-param": "Invalid parameter \"$1\" for property \"$2\".", @@ -75,9 +76,11 @@ "templatedata-modal-current-language": "Current language: $1", "templatedata-modal-errormsg": "Errors found. Please make sure there are no empty or duplicate parameter names, and that the parameter name does not include \"$1\", \"$2\" or \"$3\".", "templatedata-modal-errormsg-import-noparams": "No new parameters found during import.", - "templatedata-modal-format-inline": "Inline", "templatedata-modal-format-block": "Block", + "templatedata-modal-format-custom": "Custom", + "templatedata-modal-format-inline": "Inline", "templatedata-modal-format-null": "Unspecified", + "templatedata-modal-format-placeholder": "Template parameter format string", "templatedata-modal-json-error-replace": "Replace", "templatedata-modal-notice-import-numparams": "$1 new {{PLURAL:$1|parameter was|parameters were}} imported: $2", "templatedata-modal-placeholder-paramkey": "Parameter name", @@ -105,6 +108,7 @@ "templatedata-modal-title-paramorder": "Parameter order", "templatedata-modal-title-templatedesc": "Template description ($1)", "templatedata-modal-title-templateformat": "Template preferred format", + "templatedata-modal-title-templateformatstring": "Custom format string", "templatedata-modal-title-templateparam-details": "Parameter details: $1", "templatedata-modal-title-templateparams": "Template parameters" } diff --git a/i18n/qqq.json b/i18n/qqq.json index 23ddc0b..36965bd 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -22,8 +22,9 @@ "apihelp-templatedata-param-lang": "{{doc-apihelp-param|templatedata|lang}}", "templatedata-desc": "{{desc|name=Template Data|url=https://www.mediawiki.org/wiki/Extension:TemplateData}}", "templatedata-doc-desc-empty": "Displayed when a template has no description (should be a valid sentence).\n{{Identical|No description}}", - "templatedata-doc-format-inline": "Use inline formatting of the template parameters in wikitext.", "templatedata-doc-format-block": "Use block formatting of the template parameters in wikitext.", + "templatedata-doc-format-custom": "Use custom formatting of the template in wikitext.", + "templatedata-doc-format-inline": "Use inline formatting of the template parameters in wikitext.", "templatedata-doc-no-params-set": "A message shown when there are no parameters set in the TemplateData string.", "templatedata-doc-param-autovalue": "Used as column heading in the table for auto fill value.\n{{Related|Templatedata-doc-param}}", "templatedata-doc-param-autovalue-empty": "Displayed when a template parameter has no auto filled value (should not be a full sentence, used in a table).\n{{Identical|Empty}}", @@ -85,9 +86,11 @@ "templatedata-modal-current-language": "Label displaying the current language in the edit dialog. Parameters:\n* $1 - currently showing language\n{{Identical|Current language}}", "templatedata-modal-errormsg": "Error message that appears in the TemplateData generator GUI in case there are empty, duplicate or invalid parameter names.\n\nInvalid characters are supplied as parameters to avoid parsing errors in translation strings.\n\nParameters:\n* $1 - pipe (<code>|</code>)\n* $2 - equal sign (<code>=</code>)\n* $3 - double curly brackets (<code><nowiki>}}</nowiki></code>)", "templatedata-modal-errormsg-import-noparams": "Error message that appears in the TemplateData generator GUI in case no template parameters were found during the import attempt.", - "templatedata-modal-format-inline": "Label for inline format\n{{Identical|Inline}}", "templatedata-modal-format-block": "Label for block format\n{{Identical|Block}}", + "templatedata-modal-format-custom": "Label for custom format string\n{{Identical|Custom}}", + "templatedata-modal-format-inline": "Label for inline format\n{{Identical|Inline}}", "templatedata-modal-format-null": "Label for null format\n{{Identical|Unspecified}}", + "templatedata-modal-format-placeholder": "Placeholder text describing the format string entry widget", "templatedata-modal-json-error-replace": "Label for the button in the error message, agreeing to replace the existing faulty TemplateData string with a new one.\n{{Identical|Replace}}", "templatedata-modal-notice-import-numparams": "Message that appears in the TemplateData generator GUI showing how many new parameters were imported into the GUI from an existing template.\n\nParameters:\n* $1 - number of parameters\n* $2 - list of parameters that were imported", "templatedata-modal-placeholder-paramkey": "Placeholder for the input that contains new parameter name in the add parameter panel in the edit dialog.", @@ -115,6 +118,7 @@ "templatedata-modal-title-paramorder": "The title for the parameter order drag/drop widget in the edit dialog.", "templatedata-modal-title-templatedesc": "The title for the template description textbox. Parameters:\n* $1 - currently used language", "templatedata-modal-title-templateformat": "The title for the template preferred format", + "templatedata-modal-title-templateformatstring": "The title for the template custom format string", "templatedata-modal-title-templateparam-details": "The title for the parameter information section. Parameters:\n* $1 - The parameter name", "templatedata-modal-title-templateparams": "The title for the template parameters table\nof a single template." } diff --git a/modules/ext.templateDataGenerator.ui.tdDialog.js b/modules/ext.templateDataGenerator.ui.tdDialog.js index 4e23e6c..f58912e 100644 --- a/modules/ext.templateDataGenerator.ui.tdDialog.js +++ b/modules/ext.templateDataGenerator.ui.tdDialog.js @@ -153,6 +153,10 @@ this.templateFormatSelectWidget = new OO.ui.ButtonSelectWidget(); this.templateFormatSelectWidget.addItems( [ new OO.ui.ButtonOptionWidget( { + data: null, + label: mw.msg( 'templatedata-modal-format-null' ) + } ), + new OO.ui.ButtonOptionWidget( { data: 'inline', icon: 'template-format-inline', label: mw.msg( 'templatedata-modal-format-inline' ) @@ -163,15 +167,25 @@ label: mw.msg( 'templatedata-modal-format-block' ) } ), new OO.ui.ButtonOptionWidget( { - data: null, - label: mw.msg( 'templatedata-modal-format-null' ) + data: 'custom', + label: mw.msg( 'templatedata-modal-format-custom' ) } ) ] ); + this.templateFormatInputWidget = new OO.ui.TextInputWidget( { + placeholder: mw.msg( 'templatedata-modal-format-placeholder' ) + } ); templateFormatFieldSet = new OO.ui.FieldsetLayout( { label: mw.msg( 'templatedata-modal-title-templateformat' ) } ); - templateFormatFieldSet.$element.append( this.templateFormatSelectWidget.$element ); + templateFormatFieldSet.addItems( [ + new OO.ui.FieldLayout( this.templateFormatSelectWidget, { + } ), + new OO.ui.FieldLayout( this.templateFormatInputWidget, { + align: 'top', + label: mw.msg( 'templatedata-modal-title-templateformatstring' ) + } ) + ] ); // Param details panel this.$paramDetailsContainer = $( '<div>' ) @@ -236,6 +250,10 @@ } ); this.paramImport.connect( this, { click: 'importParametersFromTemplateCode' } ); this.templateFormatSelectWidget.connect( this, { choose: 'onTemplateFormatSelectWidgetChoose' } ); + this.templateFormatInputWidget.connect( this, { + change: 'onTemplateFormatInputWidgetChange', + enter: 'onTemplateFormatInputWidgetEnter' + } ); }; @@ -434,7 +452,67 @@ * @param {OO.ui.OptionWidget} item Format item */ mw.TemplateData.Dialog.prototype.onTemplateFormatSelectWidgetChoose = function ( item ) { - this.model.setTemplateFormat( item.getData() ); + var format = item.getData(), + shortcuts = { + inline: '{{_|_=_}}', + block: '{{_\n| _ = _\n}}' + }; + if ( format !== 'custom' ) { + this.model.setTemplateFormat( format ); + this.templateFormatInputWidget.setDisabled( true ); + if ( format !== null ) { + this.templateFormatInputWidget.setValue( + this.formatToDisplay( shortcuts[ format ] ) + ); + } + } else { + this.templateFormatInputWidget.setDisabled( false ); + this.onTemplateFormatInputWidgetChange( + this.templateFormatInputWidget.getValue() + ); + } +}; + +mw.TemplateData.Dialog.prototype.formatToDisplay = function ( s ) { + // Use '↵' (\u21b5) as a fancy newline (which doesn't start a new line). + return s.replace( /\n/g, '\u21b5' ); +}; +mw.TemplateData.Dialog.prototype.displayToFormat = function ( s ) { + // Allow user to type \n or \\n (literal backslash, n) for a new line. + return s.replace( /\n|\\n|\u21b5/g, '\n' ); +}; + +/** + * Respond to change event from the template format input widget + * + * @param {string} value Input widget value + */ +mw.TemplateData.Dialog.prototype.onTemplateFormatInputWidgetChange = function ( value ) { + var item = this.templateFormatSelectWidget.getSelectedItem(), + format, + newValue; + if ( item.getData() === 'custom' ) { + // Convert literal newlines or backslash-n to our fancy character + // replacement. + format = this.displayToFormat( value ); + newValue = this.formatToDisplay( format ); + if ( newValue !== value ) { + this.templateFormatInputWidget.setValue( newValue ); + // Will recurse to actually set value in model. + } else { + this.model.setTemplateFormat( format ); + } + } +}; + +/** + * Respond to enter event from the template format input widget + */ +mw.TemplateData.Dialog.prototype.onTemplateFormatInputWidgetEnter = function () { + /* Synthesize a '\n' when enter is pressed. */ + this.templateFormatInputWidget.insertContent( + this.formatToDisplay( '\n' ) + ); }; mw.TemplateData.Dialog.prototype.onParamPropertyInputChange = function ( property, value ) { @@ -874,11 +952,21 @@ * after initialization of the model. */ mw.TemplateData.Dialog.prototype.setupDetailsFromModel = function () { + var format; + // Set up description this.descriptionInput.setValue( this.model.getTemplateDescription( this.language ) ); // Set up format - this.templateFormatSelectWidget.selectItemByData( this.model.getTemplateFormat() ); + format = this.model.getTemplateFormat(); + if ( format === 'inline' || format === 'block' || format === null ) { + this.templateFormatSelectWidget.selectItemByData( format ); + this.templateFormatInputWidget.setDisabled( true ); + } else { + this.templateFormatSelectWidget.selectItemByData( 'custom' ); + this.templateFormatInputWidget.setValue( this.formatToDisplay( format ) ); + this.templateFormatInputWidget.setDisabled( false ); + } // Repopulate the parameter list this.repopulateParamSelectWidget(); diff --git a/resources/ext.templateData.css b/resources/ext.templateData.css index 2e06095..f1593f3 100644 --- a/resources/ext.templateData.css +++ b/resources/ext.templateData.css @@ -41,3 +41,8 @@ /* @embed */ background-image: url( ../modules/images/inline.svg ); } + +.oo-ui-icon-template-format-custom { + /* @embed */ + background-image: url( ../modules/images/block-ltr.svg ); +} diff --git a/tests/ext.templateData.tests.js b/tests/ext.templateData.tests.js index 7eea8c9..9770b74 100644 --- a/tests/ext.templateData.tests.js +++ b/tests/ext.templateData.tests.js @@ -144,7 +144,8 @@ 'day' ] } - ] + ], + format: 'inline' }; finalJson.description[ currLanguage ] = 'Label unsigned comments in a conversation.'; @@ -507,6 +508,8 @@ 2 * paramAddTest.length + // Change properties tests paramChangeTest.length + + // Format tests + 8 + // Json output 3 ); @@ -566,6 +569,29 @@ // Delete parameter model.deleteParam( 'day' ); + // Format checks + assert.deepEqual( + model.getTemplateFormat(), + null, + 'Initial template format (unspecified)' + ); + [ + '\n{{_ |\n__ = __}}', + '{{_|_=_\n}}\n', + '{{_|_=_}}', // No newlines + '\n{{ __ \n|\n __ = __\n }}\n', // Max newlines/ws + null, + 'block', + 'inline' + ].forEach( function ( f ) { + model.setTemplateFormat( f ); + assert.deepEqual( + model.getTemplateFormat(), + f, + 'Set template format to ' + JSON.stringify( f ) + ); + } ); + // Ouput a final templatedata assert.deepEqual( model.outputTemplateData(), diff --git a/tests/phpunit/TemplateDataBlobTest.php b/tests/phpunit/TemplateDataBlobTest.php index 0da7bdc..09a5c7d 100644 --- a/tests/phpunit/TemplateDataBlobTest.php +++ b/tests/phpunit/TemplateDataBlobTest.php @@ -499,7 +499,75 @@ }, "format": "meshuggah format" }', - 'status' => 'Property "format" is expected to be "inline" or "block".' + 'status' => 'Property "format" is expected to be "inline", "block", or a valid format string.' + ], + [ + 'input' => '{ + "params": {}, + "format": "inline" + }', + 'output' => '{ + "description": null, + "params": {}, + "paramOrder": [], + "sets": [], + "format": "inline", + "maps": {} + } + ', + 'msg' => '"inline" is a valid format string', + 'status' => true + ], + [ + 'input' => '{ + "params": {}, + "format": "block" + }', + 'output' => '{ + "description": null, + "params": {}, + "paramOrder": [], + "sets": [], + "format": "block", + "maps": {} + } + ', + 'msg' => '"block" is a valid format string', + 'status' => true + ], + [ + 'input' => '{ + "params": {}, + "format": "{{_ |\n ___ = _}}" + }', + 'output' => '{ + "description": null, + "params": {}, + "paramOrder": [], + "sets": [], + "format": "{{_ |\n ___ = _}}", + "maps": {} + } + ', + 'msg' => 'Custom parameter format string (1)', + 'status' => true + ], + [ + 'input' => '{ + "params": {}, + "format": "{{_|_=_\n}}\n" + }', + 'output' => '{ + "description": null, + "params": {}, + "paramOrder": [], + "sets": [], + "format": "{{_|_=_\n}}\n", + "maps": {} + } + ', + 'msg' => 'Custom parameter format string (2)', + 'status' => true ], [ // Should be long enough to trigger this condition after gzipping. -- To view, visit https://gerrit.wikimedia.org/r/306975 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Idc6b2680330e6bf5caec2bf6fc86a705d25bc649 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/TemplateData Gerrit-Branch: master Gerrit-Owner: Cscott <canan...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits