jenkins-bot has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/339211 )
Change subject: DiffElement: Avoid modifying class attributes on DM HTML ...................................................................... DiffElement: Avoid modifying class attributes on DM HTML Instead use unique data-diff attributes which can also be addressed with CSS selectors. This avoid complications with having to append to classLists, and issues with model types that handle their own rendering of the class attribute (e.g. ClassAttributeNodes). Change-Id: I2a3443b362a3e85befcc70eb7f23abe5be547312 --- M src/ui/elements/ve.ui.DiffElement.js M src/ui/styles/elements/ve.ui.DiffElement.css M tests/ui/ve.ui.DiffElement.test.js 3 files changed, 100 insertions(+), 126 deletions(-) Approvals: Tchanders: Looks good to me, approved jenkins-bot: Verified Jforrester: Looks good to me, but someone else must approve diff --git a/src/ui/elements/ve.ui.DiffElement.js b/src/ui/elements/ve.ui.DiffElement.js index 3724dc8..e82e94e 100644 --- a/src/ui/elements/ve.ui.DiffElement.js +++ b/src/ui/elements/ve.ui.DiffElement.js @@ -168,7 +168,7 @@ nodeData = documentSlice.data.data; // Add the classes to the outer element (in case there was a move) - nodeData[ 0 ] = this.addClassesToNode( nodeData[ 0 ], nodeDoc, action, move ); + nodeData[ 0 ] = this.addAttributesToNode( nodeData[ 0 ], nodeDoc, { 'data-diff-action': action, 'data-diff-move': move } ); // Get the html for the linear model with classes // Doc is always the new doc when inserting into the store @@ -233,7 +233,7 @@ // Get outer data for this node from the old doc and add remove class subTreeRootNode = oldNodes[ nodeIndex ]; subTreeRootNodeData = this.oldDoc.getData( subTreeRootNode.node.getOuterRange() ); - subTreeRootNodeData[ 0 ] = this.addClassesToNode( subTreeRootNodeData[ 0 ], this.oldDoc, 'remove' ); + subTreeRootNodeData[ 0 ] = this.addAttributesToNode( subTreeRootNodeData[ 0 ], this.oldDoc, { 'data-diff-action': 'remove' } ); // If this node is a child of the document node, then it won't have a "previous // node" (see below), in which case, insert it just before its corresponding @@ -295,8 +295,8 @@ subTreeRootNodeRangeStart = subTreeRootNode.node.getOuterRange().from - nodeRange.from; // Add insert class - nodeData[ subTreeRootNodeRangeStart ] = this.addClassesToNode( - nodeData[ subTreeRootNodeRangeStart ], this.newDoc, 'insert' + nodeData[ subTreeRootNodeRangeStart ] = this.addAttributesToNode( + nodeData[ subTreeRootNodeRangeStart ], this.newDoc, { 'data-diff-action': 'insert' } ); // Mark all children as already processed @@ -330,8 +330,8 @@ ve.batchSplice( nodeData, subTreeRootNodeRangeStart + 1, subTreeRootNode.node.length, annotatedData ); } else { // If there is no content change, just add change class - nodeData[ subTreeRootNodeRangeStart ] = this.addClassesToNode( - nodeData[ subTreeRootNodeRangeStart ], this.newDoc, 'change' + nodeData[ subTreeRootNodeRangeStart ] = this.addAttributesToNode( + nodeData[ subTreeRootNodeRangeStart ], this.newDoc, { 'data-diff-action': 'change' } ); } @@ -428,62 +428,37 @@ }; /** - * Add classes to highlight diff actions + * Add attributes to a node. * * @param {Object} nodeData Linear data to be highlighted * @param {ve.dm.Document} nodeDoc The document from which the data is taken - * @param {string} action 'remove', 'insert' or 'change' - * @param {string} [move] 'up' or 'down' if the node has moved + * @param {Object} attributes Attributes to set * @return {Object} Highlighted linear data */ -ve.ui.DiffElement.prototype.addClassesToNode = function ( nodeData, nodeDoc, action, move ) { - var originalDomElementsIndex, - domElement, domElements, - node = ve.copy( nodeData ), - classes = []; - - // The following classes are used here: - // * ve-ui-diffElement-up - // * ve-ui-diffElement-down - // * ve-ui-diffElement-remove - // * ve-ui-diffElement-insert - // * ve-ui-diffElement-change - if ( action ) { - classes.push( this.classPrefix + action ); - } - if ( move ) { - classes.push( this.classPrefix + move ); - } +ve.ui.DiffElement.prototype.addAttributesToNode = function ( nodeData, nodeDoc, attributes ) { + var key, originalDomElementsIndex, domElements, + node = ve.copy( nodeData ); // Don't let any nodes get unwrapped if ( ve.getProp( node, 'internal', 'generated' ) ) { delete node.internal.generated; } - if ( ve.dm.modelRegistry.lookup( node.type ).static.classAttributes ) { - // ClassAttributeNodes don't copy over original classes, so - // add to the unrecognizedClasses list instead - // TODO: Other node types may take control of their class lists - node.attributes = node.attributes || {}; - node.attributes.unrecognizedClasses = node.attributes.unrecognizedClasses || []; - node.attributes.unrecognizedClasses.push( classes ); + if ( node.originalDomElementsIndex ) { + domElements = ve.copy( nodeDoc.getStore().value( node.originalDomElementsIndex ) ); + domElements[ 0 ] = domElements[ 0 ].cloneNode( true ); } else { - if ( node.originalDomElementsIndex ) { - domElements = ve.copy( nodeDoc.getStore().value( node.originalDomElementsIndex ) ); - domElements[ 0 ] = domElements[ 0 ].cloneNode( true ); - domElements[ 0 ].classList.add.apply( domElements[ 0 ].classList, classes ); - } else { - domElement = document.createElement( 'span' ); - domElement.setAttribute( 'class', classes.join( ' ' ) ); - domElements = [ domElement ]; - } - - originalDomElementsIndex = this.newDoc.getStore().index( - domElements, domElements.map( ve.getNodeHtml ).join( '' ) - ); - - node.originalDomElementsIndex = originalDomElementsIndex; + domElements = [ document.createElement( 'span' ) ]; } + for ( key in attributes ) { + if ( attributes[ key ] !== undefined ) { + domElements[ 0 ].setAttribute( key, attributes[ key ] ); + } + } + originalDomElementsIndex = this.newDoc.getStore().index( + domElements, domElements.map( ve.getNodeHtml ).join( '' ) + ); + node.originalDomElementsIndex = originalDomElementsIndex; return node; }; @@ -500,7 +475,7 @@ start = 0, // The starting index for a range for building an annotation end, transaction, annotatedLinearDiff, domElement, domElements, originalDomElementsIndex, - diffDoc, diffDocData, diffClass; + diffDoc, diffDocData; // Make a new document from the diff diffDocData = linearDiff[ 0 ][ 1 ]; @@ -509,7 +484,7 @@ } diffDoc = this.newDoc.cloneWithData( diffDocData ); - // Add spans with the appropriate class for removes and inserts + // Add spans with the appropriate attributes for removes and inserts // TODO: do insert and remove outside of loop for ( i = 0; i < ilen; i++ ) { end = start + linearDiff[ i ][ 1 ].length; @@ -539,9 +514,8 @@ annType = 'textStyle/span'; break; } - diffClass = this.classPrefix + typeAsString; domElement = document.createElement( domElementType ); - domElement.setAttribute( 'class', diffClass ); + domElement.setAttribute( 'data-diff-action', typeAsString ); domElements = [ domElement ]; originalDomElementsIndex = diffDoc.getStore().index( domElements, diff --git a/src/ui/styles/elements/ve.ui.DiffElement.css b/src/ui/styles/elements/ve.ui.DiffElement.css index 829fe8a..dab9a2f 100644 --- a/src/ui/styles/elements/ve.ui.DiffElement.css +++ b/src/ui/styles/elements/ve.ui.DiffElement.css @@ -9,78 +9,78 @@ margin-left: 20px; } -.ve-ui-diffElement-insert, -.ve-ui-diffElement-remove, -.ve-ui-diffElement-change-insert, -.ve-ui-diffElement-change-remove { +[data-diff-action='insert'], +[data-diff-action='remove'], +[data-diff-action='change-insert'], +[data-diff-action='change-remove'] { text-decoration: inherit; border-radius: 0.15em; } -.ve-ui-diffElement-insert, +[data-diff-action='insert'], /* elements using display:table-caption need separate backgrounds */ -table.ve-ui-diffElement-insert > caption, -figure.ve-ui-diffElement-insert > figcaption { +table[data-diff-action='insert'] > caption, +figure[data-diff-action='insert'] > figcaption { background-color: #7fd7c4 !important; /* stylelint-disable-line declaration-no-important */ box-shadow: 0 0 0 0.1em #7fd7c4; } -.ve-ui-diffElement-remove, +[data-diff-action='remove'], /* elements using display:table-caption need separate backgrounds */ -table.ve-ui-diffElement-remove > caption, -figure.ve-ui-diffElement-remove > figcaption { +table[data-diff-action='remove'] > caption, +figure[data-diff-action='remove'] > figcaption { background-color: #e88e89 !important; /* stylelint-disable-line declaration-no-important */ box-shadow: 0 0 0 0.1em #e88e89; } -.ve-ui-diffElement-change-insert { +[data-diff-action='change-insert'] { background-color: #8ab4e8 !important; /* stylelint-disable-line declaration-no-important */ box-shadow: 0 0 0 0.1em #8ab4e8; } -.ve-ui-diffElement-change-remove { +[data-diff-action='change-remove'] { display: none; } -del.ve-ui-diffElement-remove { +del[data-diff-action='remove'] { text-decoration: line-through; } -.ve-ui-diffElement-insert [rel='ve:Comment'], -.ve-ui-diffElement-remove [rel='ve:Comment'], -.ve-ui-diffElement-change-insert [rel='ve:Comment'], -.ve-ui-diffElement-change-remove [rel='ve:Comment'] { +[data-diff-action='insert'] [rel='ve:Comment'], +[data-diff-action='remove'] [rel='ve:Comment'], +[data-diff-action='change-insert'] [rel='ve:Comment'], +[data-diff-action='change-remove'] [rel='ve:Comment'] { display: inline-block; width: 1em; } -.ve-ui-diffElement-remove + .ve-ui-diffElement-insert, -.ve-ui-diffElement-insert + .ve-ui-diffElement-remove, -.ve-ui-diffElement-remove + .ve-ui-diffElement-change-insert, -.ve-ui-diffElement-insert + .ve-ui-diffElement-change-remove { +[data-diff-action='remove'] + [data-diff-action='insert'], +[data-diff-action='insert'] + [data-diff-action='remove'], +[data-diff-action='remove'] + [data-diff-action='change-insert'], +[data-diff-action='insert'] + [data-diff-action='change-remove'] { margin-left: 0.2em; } -.ve-ui-diffElement-none { +[data-diff-action='none'] { opacity: 0.4; } -.ve-ui-diffElement-up, -.ve-ui-diffElement-down, +[data-diff-move='up'], +[data-diff-move='down'], .ve-ui-diffElement-doc-child-change { border-left: 6px solid #36c; padding-left: 14px; margin-left: -20px; } -.ve-ui-diffElement-up, -.ve-ui-diffElement-down { +[data-diff-move='up'], +[data-diff-move='down'] { position: relative; opacity: 1; } -.ve-ui-diffElement-up:before, -.ve-ui-diffElement-down:before { +[data-diff-move='up']:before, +[data-diff-move='down']:before { position: absolute; left: -6px; content: ' '; @@ -92,24 +92,24 @@ outline: 3px solid #fff; } -.ve-ui-diffElement-down:before { +[data-diff-move='down']:before { bottom: 0; border-top: 6px solid #36c; } -.ve-ui-diffElement-up:before { +[data-diff-move='up']:before { top: 0; border-bottom: 6px solid #36c; } -.ve-ui-diffElement-insert:empty:before, -.ve-ui-diffElement-remove:empty:before, -.ve-ui-diffElement-up:empty:before, -.ve-ui-diffElement-down:empty:before, -.ve-ui-diffElement-insert *:empty:before, -.ve-ui-diffElement-remove *:empty:before, -.ve-ui-diffElement-up *:empty:before, -.ve-ui-diffElement-down *:empty:before { +[data-diff-action='insert']:empty:before, +[data-diff-action='remove']:empty:before, +[data-diff-move='up']:empty:before, +[data-diff-move='down']:empty:before, +[data-diff-action='insert'] *:empty:before, +[data-diff-action='remove'] *:empty:before, +[data-diff-move='up'] *:empty:before, +[data-diff-move='down'] *:empty:before { content: url( data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 ); } diff --git a/tests/ui/ve.ui.DiffElement.test.js b/tests/ui/ve.ui.DiffElement.test.js index ec2f3a3..d3ddc55 100644 --- a/tests/ui/ve.ui.DiffElement.test.js +++ b/tests/ui/ve.ui.DiffElement.test.js @@ -21,7 +21,7 @@ '<div class="ve-ui-diffElement-doc-child-change">' + '<p>' + 'foo ' + - '<del class="ve-ui-diffElement-remove">bar</del><ins class="ve-ui-diffElement-insert">car</ins>' + + '<del data-diff-action="remove">bar</del><ins data-diff-action="insert">car</ins>' + ' baz' + '</p>' + '</div>' @@ -40,7 +40,7 @@ '<div class="ve-ui-diffElement-doc-child-change">' + '<p>' + 'foo"' + - '<del class="ve-ui-diffElement-remove">bar</del><ins class="ve-ui-diffElement-insert">bXr</ins>' + + '<del data-diff-action="remove">bar</del><ins data-diff-action="insert">bXr</ins>' + '"baz' + '</p>' + '</div>' @@ -53,7 +53,7 @@ '<div class="ve-ui-diffElement-doc-child-change">' + '<p>' + '粵文' + - '<ins class="ve-ui-diffElement-insert">唔</ins>' + + '<ins data-diff-action="insert">唔</ins>' + '係粵語嘅書面語' + '</p>' + '</div>' @@ -64,12 +64,12 @@ newDoc: '<p>boo</p><p>bar</p><p>baz</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p class="ve-ui-diffElement-remove">foo</p>' + + '<p data-diff-action="remove">foo</p>' + '</div>' + '<div class="ve-ui-diffElement-doc-child-change">' + - '<p class="ve-ui-diffElement-insert">boo</p>' + + '<p data-diff-action="insert">boo</p>' + '</div>' + - '<p class="ve-ui-diffElement-none">bar</p>' + + '<p data-diff-action="none">bar</p>' + spacer }, { @@ -78,28 +78,28 @@ newDoc: 'boo', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p class="ve-ui-diffElement-remove">foo</p>' + + '<p data-diff-action="remove">foo</p>' + '</div>' + '<div class="ve-ui-diffElement-doc-child-change">' + - '<p class="ve-ui-diffElement-insert">boo</p>' + + '<p data-diff-action="insert">boo</p>' + '</div>' }, { - msg: 'Classes added to ClassAttributeNodes', + msg: 'Attributes added to ClassAttributeNodes', oldDoc: '<figure><img src="foo.jpg"><figcaption>bar</figcaption></figure>', newDoc: '<figure><img src="boo.jpg"><figcaption>bar</figcaption></figure>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<figure class="ve-ui-diffElement-change"><img src="boo.jpg" width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' + + '<figure data-diff-action="change"><img src="boo.jpg" width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' + '</div>' }, { - msg: 'Classes added to ClassAttributeNodes with classes', + msg: 'Attributes added to ClassAttributeNodes with classes', oldDoc: '<figure class="ve-align-right"><img src="foo.jpg"><figcaption>bar</figcaption></figure>', newDoc: '<figure class="ve-align-right"><img src="boo.jpg"><figcaption>bar</figcaption></figure>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<figure class="ve-align-right ve-ui-diffElement-change"><img src="boo.jpg" width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' + + '<figure class="ve-align-right" data-diff-action="change"><img src="boo.jpg" width="0" height="0" alt="null"><figcaption>bar</figcaption></figure>' + '</div>' }, { @@ -107,9 +107,9 @@ oldDoc: '<p>foo</p>', newDoc: '<p>foo</p><div rel="ve:Alien">Alien</div>', expected: - '<p class="ve-ui-diffElement-none">foo</p>' + + '<p data-diff-action="none">foo</p>' + '<div class="ve-ui-diffElement-doc-child-change">' + - '<div rel="ve:Alien" class="ve-ui-diffElement-insert">Alien</div>' + + '<div rel="ve:Alien" data-diff-action="insert">Alien</div>' + '</div>' }, { @@ -117,9 +117,9 @@ oldDoc: '<p>foo</p><div rel="ve:Alien">Alien</div>', newDoc: '<p>foo</p>', expected: - '<p class="ve-ui-diffElement-none">foo</p>' + + '<p data-diff-action="none">foo</p>' + '<div class="ve-ui-diffElement-doc-child-change">' + - '<div rel="ve:Alien" class="ve-ui-diffElement-remove">Alien</div>' + + '<div rel="ve:Alien" data-diff-action="remove">Alien</div>' + '</div>' }, { @@ -128,10 +128,10 @@ newDoc: '<p>Foo</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<div rel="ve:Alien" class="ve-ui-diffElement-remove">Alien</div>' + + '<div rel="ve:Alien" data-diff-action="remove">Alien</div>' + '</div>' + '<div class="ve-ui-diffElement-doc-child-change">' + - '<p class="ve-ui-diffElement-insert">Foo</p>' + + '<p data-diff-action="insert">Foo</p>' + '</div>' }, { @@ -139,8 +139,8 @@ oldDoc: '<p>foo</p><p>bar</p>', newDoc: '<p>bar</p><p>foo</p>', expected: - '<p class="ve-ui-diffElement-none ve-ui-diffElement-up">bar</p>' + - '<p class="ve-ui-diffElement-none ve-ui-diffElement-down">foo</p>' + '<p data-diff-action="none" data-diff-move="up">bar</p>' + + '<p data-diff-action="none" data-diff-move="down">foo</p>' }, { msg: 'Paragraphs moved and modified', @@ -148,10 +148,10 @@ newDoc: '<p>quux whee!</p><p>foo bar baz!</p>', expected: '<div class="ve-ui-diffElement-doc-child-change ve-ui-diffElement-up">' + - '<p>quux whee<ins class="ve-ui-diffElement-insert">!</ins></p>' + + '<p>quux whee<ins data-diff-action="insert">!</ins></p>' + '</div>' + '<div class="ve-ui-diffElement-doc-child-change ve-ui-diffElement-down">' + - '<p>foo bar baz<ins class="ve-ui-diffElement-insert">!</ins></p>' + + '<p>foo bar baz<ins data-diff-action="insert">!</ins></p>' + '</div>' }, { @@ -161,8 +161,8 @@ expected: '<div class="ve-ui-diffElement-doc-child-change">' + '<table><tbody>' + - '<tr><td>A</td><td class="ve-ui-diffElement-insert">B</td></tr>' + - '<tr><td>C</td><td class="ve-ui-diffElement-insert">D</td></tr>' + + '<tr><td>A</td><td data-diff-action="insert">B</td></tr>' + + '<tr><td>C</td><td data-diff-action="insert">D</td></tr>' + '</tbody></table>' + '</div>' }, @@ -173,8 +173,8 @@ expected: '<div class="ve-ui-diffElement-doc-child-change">' + '<table><tbody>' + - '<tr><td>A</td><td class="ve-ui-diffElement-remove">B</td></tr>' + - '<tr><td>C</td><td class="ve-ui-diffElement-remove">D</td></tr>' + + '<tr><td>A</td><td data-diff-action="remove">B</td></tr>' + + '<tr><td>C</td><td data-diff-action="remove">D</td></tr>' + '</tbody></table>' + '</div>' }, @@ -192,9 +192,9 @@ expected: '<div class="ve-ui-diffElement-doc-child-change">' + '<table><tbody>' + - '<tr><td>A</td><td>B</td><td><del class="ve-ui-diffElement-remove">C</del><ins class="ve-ui-diffElement-insert">X</ins></td></tr>' + - '<tr class="ve-ui-diffElement-remove"><td>D</td><td>E</td><td>F</td></tr>' + - '<tr><td>G</td><td>H</td><td><del class="ve-ui-diffElement-remove">I</del><ins class="ve-ui-diffElement-insert">Y</ins></td></tr>' + + '<tr><td>A</td><td>B</td><td><del data-diff-action="remove">C</del><ins data-diff-action="insert">X</ins></td></tr>' + + '<tr data-diff-action="remove"><td>D</td><td>E</td><td>F</td></tr>' + + '<tr><td>G</td><td>H</td><td><del data-diff-action="remove">I</del><ins data-diff-action="insert">Y</ins></td></tr>' + '</tbody></table>' + '</div>' }, @@ -204,7 +204,7 @@ newDoc: '<p>foo <b>bar</b> baz</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p>foo <span class="ve-ui-diffElement-change-remove">bar</span><b><span class="ve-ui-diffElement-change-insert">bar</span></b> baz</p>' + + '<p>foo <span data-diff-action="change-remove">bar</span><b><span data-diff-action="change-insert">bar</span></b> baz</p>' + '</div>' }, { @@ -213,7 +213,7 @@ newDoc: '<p>foo bar baz</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p>foo <b><span class="ve-ui-diffElement-change-remove">bar</span></b><span class="ve-ui-diffElement-change-insert">bar</span> baz</p>' + + '<p>foo <b><span data-diff-action="change-remove">bar</span></b><span data-diff-action="change-insert">bar</span> baz</p>' + '</div>' }, { @@ -222,7 +222,7 @@ newDoc: '<p>foo <b>bar</b> baz</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p>foo <del class="ve-ui-diffElement-remove">car</del><b><ins class="ve-ui-diffElement-insert">bar</ins></b> baz</p>' + + '<p>foo <del data-diff-action="remove">car</del><b><ins data-diff-action="insert">bar</ins></b> baz</p>' + '</div>' }, { @@ -231,7 +231,7 @@ newDoc: '<p>foo car baz</p>', expected: '<div class="ve-ui-diffElement-doc-child-change">' + - '<p>foo <b><del class="ve-ui-diffElement-remove">bar</del></b><ins class="ve-ui-diffElement-insert">car</ins> baz</p>' + + '<p>foo <b><del data-diff-action="remove">bar</del></b><ins data-diff-action="insert">car</ins> baz</p>' + '</div>' }, { @@ -242,7 +242,7 @@ '<div class="ve-ui-diffElement-doc-child-change">' + '<p>' + 'foo bar' + - '<ins class="ve-ui-diffElement-insert"><span rel="ve:Comment" data-ve-comment="comment"> </span></ins>' + + '<ins data-diff-action="insert"><span rel="ve:Comment" data-ve-comment="comment"> </span></ins>' + ' baz' + '</p>' + '</div>' @@ -255,7 +255,7 @@ '<div class="ve-ui-diffElement-doc-child-change">' + '<p>' + 'foo bar' + - '<del class="ve-ui-diffElement-remove"><span rel="ve:Comment" data-ve-comment="comment"> </span></del>' + + '<del data-diff-action="remove"><span rel="ve:Comment" data-ve-comment="comment"> </span></del>' + ' baz' + '</p>' + '</div>' -- To view, visit https://gerrit.wikimedia.org/r/339211 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I2a3443b362a3e85befcc70eb7f23abe5be547312 Gerrit-PatchSet: 7 Gerrit-Project: VisualEditor/VisualEditor Gerrit-Branch: master Gerrit-Owner: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org> Gerrit-Reviewer: Tchanders <thalia.e.c...@googlemail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits