jenkins-bot has submitted this change and it was merged. Change subject: Hybridise MWTemplateNode ......................................................................
Hybridise MWTemplateNode * Create MWTemplateBlockNode & MWTemplateInlineNode (in ce and dm) * Move Alien's 'isInline' code to ve.dm.Node.static.isHybridInline * Move definition of ce.AlienBlock/InlineNode inside ce.Aline.js file to match dm.AlienBlock/InlineNode and MWTemplate * Duplicate AlienBlock/Inline styles for templates * Create test case for inline templates * Count test cases in ve.dm.Converter.test.js automatically Change-Id: Id9bc7f049ea974dd5e7f8b7a66080939e0948bbd --- M VisualEditor.php M demos/ve/index.php D modules/ve/ce/nodes/ve.ce.AlienBlockNode.js D modules/ve/ce/nodes/ve.ce.AlienInlineNode.js M modules/ve/ce/nodes/ve.ce.AlienNode.js M modules/ve/ce/nodes/ve.ce.MWTemplateNode.js M modules/ve/ce/styles/ve.ce.Node.css M modules/ve/dm/nodes/ve.dm.AlienNode.js M modules/ve/dm/nodes/ve.dm.MWTemplateNode.js M modules/ve/dm/ve.dm.Converter.js M modules/ve/dm/ve.dm.Node.js M modules/ve/test/dm/ve.dm.Converter.test.js M modules/ve/test/dm/ve.dm.example.js M modules/ve/test/index.php 14 files changed, 247 insertions(+), 113 deletions(-) Approvals: Catrope: Looks good to me, approved jenkins-bot: Verified diff --git a/VisualEditor.php b/VisualEditor.php index 47305a9..2453d51 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -303,8 +303,6 @@ 've/ce/nodes/ve.ce.GeneratedContentNode.js', 've/ce/nodes/ve.ce.AlienNode.js', - 've/ce/nodes/ve.ce.AlienInlineNode.js', - 've/ce/nodes/ve.ce.AlienBlockNode.js', 've/ce/nodes/ve.ce.BreakNode.js', 've/ce/nodes/ve.ce.CenterNode.js', 've/ce/nodes/ve.ce.DefinitionListItemNode.js', diff --git a/demos/ve/index.php b/demos/ve/index.php index d2d2906..57cd489 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -185,8 +185,6 @@ <script src="../../modules/ve/ce/ve.ce.SurfaceObserver.js"></script> <script src="../../modules/ve/ce/nodes/ve.ce.GeneratedContentNode.js"></script> <script src="../../modules/ve/ce/nodes/ve.ce.AlienNode.js"></script> - <script src="../../modules/ve/ce/nodes/ve.ce.AlienInlineNode.js"></script> - <script src="../../modules/ve/ce/nodes/ve.ce.AlienBlockNode.js"></script> <script src="../../modules/ve/ce/nodes/ve.ce.BreakNode.js"></script> <script src="../../modules/ve/ce/nodes/ve.ce.CenterNode.js"></script> <script src="../../modules/ve/ce/nodes/ve.ce.DefinitionListItemNode.js"></script> diff --git a/modules/ve/ce/nodes/ve.ce.AlienBlockNode.js b/modules/ve/ce/nodes/ve.ce.AlienBlockNode.js deleted file mode 100644 index e4cc7a0..0000000 --- a/modules/ve/ce/nodes/ve.ce.AlienBlockNode.js +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * VisualEditor ContentEditable AlienBlockNode class. - * - * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt - * @license The MIT License (MIT); see LICENSE.txt - */ - -/** - * ContentEditable alien block node. - * - * @class - * @extends ve.ce.AlienNode - * @constructor - * @param {ve.dm.AlienBlockNode} model Model to observe - */ -ve.ce.AlienBlockNode = function VeCeAlienBlockNode( model ) { - // Parent constructor - ve.ce.AlienNode.call( this, model ); - - // DOM Changes - this.$.addClass( 've-ce-alienBlockNode' ); -}; - -/* Inheritance */ - -ve.inheritClass( ve.ce.AlienBlockNode, ve.ce.AlienNode ); - -/* Static Properties */ - -ve.ce.AlienBlockNode.static.name = 'alienBlock'; - -/* Registration */ - -ve.ce.nodeFactory.register( ve.ce.AlienBlockNode ); diff --git a/modules/ve/ce/nodes/ve.ce.AlienInlineNode.js b/modules/ve/ce/nodes/ve.ce.AlienInlineNode.js deleted file mode 100644 index 588ec2c..0000000 --- a/modules/ve/ce/nodes/ve.ce.AlienInlineNode.js +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * VisualEditor ContentEditable AlienInlineNode class. - * - * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt - * @license The MIT License (MIT); see LICENSE.txt - */ - -/** - * ContentEditable alien inline node. - * - * @class - * @extends ve.ce.AlienNode - * @constructor - * @param {ve.dm.AlienInlineNode} model Model to observe - */ -ve.ce.AlienInlineNode = function VeCeAlienInlineNode( model ) { - // Parent constructor - ve.ce.AlienNode.call( this, model ); - - // DOM Changes - this.$.addClass( 've-ce-alienInlineNode' ); -}; - -/* Inheritance */ - -ve.inheritClass( ve.ce.AlienInlineNode, ve.ce.AlienNode ); - -/* Static Properties */ - -ve.ce.AlienInlineNode.static.name = 'alienInline'; - -/* Registration */ - -ve.ce.nodeFactory.register( ve.ce.AlienInlineNode ); diff --git a/modules/ve/ce/nodes/ve.ce.AlienNode.js b/modules/ve/ce/nodes/ve.ce.AlienNode.js index fa2b0da..b0705ea 100644 --- a/modules/ve/ce/nodes/ve.ce.AlienNode.js +++ b/modules/ve/ce/nodes/ve.ce.AlienNode.js @@ -9,6 +9,7 @@ * ContentEditable alien node. * * @class + * @abstract * @extends ve.ce.GeneratedContentNode * @constructor * @param {ve.dm.AlienNode} model Model to observe @@ -134,6 +135,58 @@ surface.$.unbind( '.phantoms' ); }; +/* Concrete subclasses */ + +/** + * ContentEditable alien block node. + * + * @class + * @extends ve.ce.AlienNode + * @constructor + * @param {ve.dm.AlienBlockNode} model Model to observe + */ +ve.ce.AlienBlockNode = function VeCeAlienBlockNode( model ) { + // Parent constructor + ve.ce.AlienNode.call( this, model ); + + // DOM Changes + this.$.addClass( 've-ce-alienBlockNode' ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.AlienBlockNode, ve.ce.AlienNode ); + +/* Static Properties */ + +ve.ce.AlienBlockNode.static.name = 'alienBlock'; + +/** + * ContentEditable alien inline node. + * + * @class + * @extends ve.ce.AlienNode + * @constructor + * @param {ve.dm.AlienInlineNode} model Model to observe + */ +ve.ce.AlienInlineNode = function VeCeAlienInlineNode( model ) { + // Parent constructor + ve.ce.AlienNode.call( this, model ); + + // DOM Changes + this.$.addClass( 've-ce-alienInlineNode' ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.AlienInlineNode, ve.ce.AlienNode ); + +/* Static Properties */ + +ve.ce.AlienInlineNode.static.name = 'alienInline'; + /* Registration */ ve.ce.nodeFactory.register( ve.ce.AlienNode ); +ve.ce.nodeFactory.register( ve.ce.AlienBlockNode ); +ve.ce.nodeFactory.register( ve.ce.AlienInlineNode ); diff --git a/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js b/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js index fab1119..59c560c 100644 --- a/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js @@ -19,7 +19,7 @@ ve.ce.GeneratedContentNode.call( this, model ); // DOM Changes - this.$.addClass( 've-ce-MWTemplateNode' ); + this.$.addClass( 've-ce-MWtemplateNode' ); }; /* Inheritance */ @@ -37,6 +37,58 @@ return new $.Deferred(); }; +/* Concrete subclasses */ + +/** + * ContentEditable MediaWiki template block node. + * + * @class + * @extends ve.ce.MWTemplateNode + * @constructor + * @param {ve.dm.MWTemplateBlockNode} model Model to observe + */ +ve.ce.MWTemplateBlockNode = function VeCeMWTemplateBlockNode( model ) { + // Parent constructor + ve.ce.MWTemplateNode.call( this, model ); + + // DOM Changes + this.$.addClass( 've-ce-MWtemplateBlockNode' ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.MWTemplateBlockNode, ve.ce.MWTemplateNode ); + +/* Static Properties */ + +ve.ce.MWTemplateBlockNode.static.name = 'MWtemplateBlock'; + +/** + * ContentEditable MediaWiki template inline node. + * + * @class + * @extends ve.ce.MWTemplateNode + * @constructor + * @param {ve.dm.MWTemplateInlineNode} model Model to observe + */ +ve.ce.MWTemplateInlineNode = function VeCeMWTemplateInlineNode( model ) { + // Parent constructor + ve.ce.MWTemplateNode.call( this, model ); + + // DOM Changes + this.$.addClass( 've-ce-MWtemplateInlineNode' ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.MWTemplateInlineNode, ve.ce.MWTemplateNode ); + +/* Static Properties */ + +ve.ce.MWTemplateInlineNode.static.name = 'MWtemplateInline'; + /* Registration */ ve.ce.nodeFactory.register( ve.ce.MWTemplateNode ); +ve.ce.nodeFactory.register( ve.ce.MWTemplateBlockNode ); +ve.ce.nodeFactory.register( ve.ce.MWTemplateInlineNode ); diff --git a/modules/ve/ce/styles/ve.ce.Node.css b/modules/ve/ce/styles/ve.ce.Node.css index 9591cde..47d5686 100644 --- a/modules/ve/ce/styles/ve.ce.Node.css +++ b/modules/ve/ce/styles/ve.ce.Node.css @@ -53,11 +53,13 @@ content: ''; } -.ve-ce-alienBlockNode { +.ve-ce-alienBlockNode, +.ve-ce-MWtemplateBlockNode, { display: block; } -.ve-ce-alienInlineNode { +.ve-ce-alienInlineNode, +.ve-ce-MWtemplateInlineNode { display: inline; } diff --git a/modules/ve/dm/nodes/ve.dm.AlienNode.js b/modules/ve/dm/nodes/ve.dm.AlienNode.js index a414b37..03b7261 100644 --- a/modules/ve/dm/nodes/ve.dm.AlienNode.js +++ b/modules/ve/dm/nodes/ve.dm.AlienNode.js @@ -31,28 +31,10 @@ ve.dm.AlienNode.static.storeHtmlAttributes = false; ve.dm.AlienNode.static.toDataElement = function ( domElements, converter ) { - var i, isInline, allTagsInline, type, html; - // Check whether all elements are inline elements - allTagsInline = true; - for ( i = 0; i < domElements.length; i++ ) { - if ( ve.isBlockElement( domElements[i] ) ) { - allTagsInline = false; - break; - } - } + var isInline = this.isHybridInline( domElements, converter ), + type = isInline ? 'alienInline' : 'alienBlock', + html = $( '<div>', domElements[0].ownerDocument ).append( $( domElements ).clone() ).html(); - // We generate alienBlock elements for block tags and alienInline elements for - // inline tags; unless we're in a content location, in which case we have no choice - // but to generate an alienInline element. - isInline = - // Force inline in content locations (but not wrappers) - ( converter.isExpectingContent() && !converter.isInWrapper() ) || - // Also force inline in wrappers that we can't close - ( converter.isInWrapper() && !converter.canCloseWrapper() ) || - // Look at the tag names otherwise - allTagsInline; - type = isInline ? 'alienInline' : 'alienBlock'; - html = $( '<div>', domElements[0].ownerDocument ).append( $( domElements ).clone() ).html(); return { 'type': type, 'attributes': { diff --git a/modules/ve/dm/nodes/ve.dm.MWTemplateNode.js b/modules/ve/dm/nodes/ve.dm.MWTemplateNode.js index 22d1ee7..0da0b13 100644 --- a/modules/ve/dm/nodes/ve.dm.MWTemplateNode.js +++ b/modules/ve/dm/nodes/ve.dm.MWTemplateNode.js @@ -42,9 +42,12 @@ ve.dm.MWTemplateNode.static.toDataElement = function ( domElements, converter ) { var dataElement, about = domElements[0].getAttribute( 'about' ), - mw = JSON.parse( domElements[0].getAttribute( 'data-mw' ) ); + mw = JSON.parse( domElements[0].getAttribute( 'data-mw' ) ), + isInline = this.isHybridInline( domElements, converter ), + type = isInline ? 'MWtemplateInline' : 'MWtemplateBlock'; + dataElement = { - 'type': this.name, + 'type': type, 'mw': mw, 'about': about }; @@ -62,6 +65,52 @@ return [ span ]; }; +/* Concrete subclasses */ + +/** + * DataModel MediaWiki template block node. + * + * @class + * @extends ve.dm.MWTemplateNode + * @constructor + * @param {number} [length] Length of content data in document; ignored and overridden to 0 + * @param {Object} [element] Reference to element in linear model + */ +ve.dm.MWTemplateBlockNode = function VeDmMWTemplateBlockNode( length, element ) { + // Parent constructor + ve.dm.MWTemplateNode.call( this, length, element ); +}; + +ve.inheritClass( ve.dm.MWTemplateBlockNode, ve.dm.MWTemplateNode ); + +ve.dm.MWTemplateBlockNode.static.matchTagNames = []; + +ve.dm.MWTemplateBlockNode.static.name = 'MWtemplateBlock'; + +/** + * DataModel MediaWiki template inline node. + * + * @class + * @extends ve.dm.MWTemplateNode + * @constructor + * @param {number} [length] Length of content data in document; ignored and overridden to 0 + * @param {Object} [element] Reference to element in linear model + */ +ve.dm.MWTemplateInlineNode = function VeDmMWTemplateInlineNode( length, element ) { + // Parent constructor + ve.dm.MWTemplateNode.call( this, length, element ); +}; + +ve.inheritClass( ve.dm.MWTemplateInlineNode, ve.dm.MWTemplateNode ); + +ve.dm.MWTemplateInlineNode.static.matchTagNames = []; + +ve.dm.MWTemplateInlineNode.static.name = 'MWtemplateInline'; + +ve.dm.MWTemplateInlineNode.static.isContent = true; + /* Registration */ ve.dm.modelRegistry.register( ve.dm.MWTemplateNode ); +ve.dm.modelRegistry.register( ve.dm.MWTemplateBlockNode ); +ve.dm.modelRegistry.register( ve.dm.MWTemplateInlineNode ); diff --git a/modules/ve/dm/ve.dm.Converter.js b/modules/ve/dm/ve.dm.Converter.js index 6c019ed..eae715b 100644 --- a/modules/ve/dm/ve.dm.Converter.js +++ b/modules/ve/dm/ve.dm.Converter.js @@ -104,7 +104,7 @@ * Check whether this converter instance is currently inside a getDataFromDom() conversion. * * @method - * @returns {Boolean} Whether we're converting + * @returns {boolean} Whether we're converting */ ve.dm.Converter.prototype.isConverting = function () { return this.contextStack !== null; diff --git a/modules/ve/dm/ve.dm.Node.js b/modules/ve/dm/ve.dm.Node.js index b3682ec..dd7a766 100644 --- a/modules/ve/dm/ve.dm.Node.js +++ b/modules/ve/dm/ve.dm.Node.js @@ -60,7 +60,7 @@ * If .static.childNodeTypes is set to [], this property is ignored and will be assumed to be true. * * @static - * @type {Boolean} static.handlesOwnChildren + * @type {boolean} static.handlesOwnChildren * @inheritable */ ve.dm.Node.static.handlesOwnChildren = false; @@ -174,6 +174,35 @@ }; }; +/** + * Determine if a hybrid element is inline and allowed to be inline in this context + * + * We generate block elements for block tags and inline elements for inline + * tags; unless we're in a content location, in which case we have no choice + * but to generate an inline element. + * + * @param {HTMLElement[]} domElements DOM elements being converted + * @param {ve.dm.Converter} converter Converter object + * @returns {boolean} The element is inline + */ +ve.dm.Node.static.isHybridInline = function ( domElements, converter ) { + var i, length, allTagsInline = true; + + for ( i = 0, length = domElements.length; i < length; i++ ) { + if ( ve.isBlockElement( domElements[i] ) ) { + allTagsInline = false; + break; + } + } + + // Force inline in content locations (but not wrappers) + return ( converter.isExpectingContent() && !converter.isInWrapper() ) || + // ..also force inline in wrappers that we can't close + ( converter.isInWrapper() && !converter.canCloseWrapper() ) || + // ..otherwise just look at the tag names + allTagsInline; +}; + /* Methods */ /** diff --git a/modules/ve/test/dm/ve.dm.Converter.test.js b/modules/ve/test/dm/ve.dm.Converter.test.js index 6c99437..9f63029 100644 --- a/modules/ve/test/dm/ve.dm.Converter.test.js +++ b/modules/ve/test/dm/ve.dm.Converter.test.js @@ -39,8 +39,8 @@ } } ); -QUnit.test( 'getDataFromDom', 52, function ( assert ) { - var msg, +QUnit.test( 'getDataFromDom', function ( assert ) { + var msg, n = 0, store = new ve.dm.IndexValueStore(), cases = ve.copyObject( ve.dm.example.domToDataCases ); @@ -48,6 +48,13 @@ // nodes the most recently registered, instead of the MW versions ve.dm.modelRegistry.register( ve.dm.HeadingNode ); ve.dm.modelRegistry.register( ve.dm.PreformattedNode ); + + for ( msg in cases ) { + if ( cases[msg].html !== null ) { + n++; + } + } + QUnit.expect( n ); for ( msg in cases ) { if ( cases[msg].html !== null ) { @@ -61,12 +68,17 @@ } } ); -QUnit.test( 'getDomFromData', 56, function ( assert ) { - var msg, +QUnit.test( 'getDomFromData', function ( assert ) { + var msg, n = 0, store = new ve.dm.IndexValueStore(), cases = ve.copyObject( ve.dm.example.domToDataCases ); for ( msg in cases ) { + n++; + } + QUnit.expect( n ); + + for ( msg in cases ) { ve.dm.example.preprocessAnnotations( cases[msg].data, store ); assert.equalDomElement( ve.dm.converter.getDomFromData( store, cases[msg].data ), diff --git a/modules/ve/test/dm/ve.dm.example.js b/modules/ve/test/dm/ve.dm.example.js index e2ad6a0..2a04071 100644 --- a/modules/ve/test/dm/ve.dm.example.js +++ b/modules/ve/test/dm/ve.dm.example.js @@ -715,8 +715,11 @@ }; ve.dm.example.MWImageHtml = '<a rel="mw:Image" href="./File:Wiki.png" data-parsoid="{"tsr":[158,216],"src":"[[Image:Wiki.png|500px|thumb|center|Example wiki file]]","optNames":{"width":"$1px"},"dsr":[158,216,null,null]}"><img height="" width="500" src="/index.php?title=Special:FilePath/Wiki.png&width=500" alt="Wiki.png"></a>'; -ve.dm.example.MWTemplateSpan = '<span about="#mwt1" typeof="mw:Object/Template" data-mw="{"id":"mwt1","target":{"wt":"Test"},"params":{"1":{"wt":"Hello, world!"}}}" data-parsoid="{"tsr":[18,40],"src":"{{Test|Hello, world!}}","dsr":[18,40,null,null]}"></span>'; -ve.dm.example.MWTemplateContent = '<p about="#mwt1" data-parsoid="{}">Hello, world!</p>'; +ve.dm.example.MWBlockTemplateSpan = '<span about="#mwt1" typeof="mw:Object/Template" data-mw="{"id":"mwt1","target":{"wt":"Test"},"params":{"1":{"wt":"Hello, world!"}}}" data-parsoid="{"tsr":[18,40],"src":"{{Test|Hello, world!}}","dsr":[18,40,null,null]}"></span>'; +ve.dm.example.MWBlockTemplateContent = '<p about="#mwt1" data-parsoid="{}">Hello, world!</p>'; +ve.dm.example.MWInlineTemplateOpen = '<span about="#mwt1" typeof="mw:Object/Template" data-mw="{"id":"mwt1","target":{"wt":"Inline"},"params":{"1":{"wt":"1,234"}}}" data-parsoid="{"tsr":[18,34],"src":"{{Inline|1,234}}","dsr":[18,34,null,null]}">'; +ve.dm.example.MWInlineTemplateContent = '$1,234.00'; +ve.dm.example.MWInlineTemplateClose = '</span>'; ve.dm.example.domToDataCases = { 'paragraph with plain text': { @@ -777,11 +780,11 @@ { 'type': '/paragraph' } ] }, - 'mw:Template': { - 'html': '<body>' + ve.dm.example.MWTemplateSpan + ve.dm.example.MWTemplateContent + '</body>', + 'mw:Template (block level)': { + 'html': '<body>' + ve.dm.example.MWBlockTemplateSpan + ve.dm.example.MWBlockTemplateContent + '</body>', 'data': [ { - 'type': 'MWtemplate', + 'type': 'MWtemplateBlock', 'mw': { 'id': 'mwt1', 'target': { 'wt' : 'Test' }, @@ -799,9 +802,35 @@ 'html/1/data-parsoid': '{}' }, }, - { 'type': '/MWtemplate' }, + { 'type': '/MWtemplateBlock' }, ], - 'normalizedHtml': ve.dm.example.MWTemplateSpan + 'normalizedHtml': ve.dm.example.MWBlockTemplateSpan + }, + 'mw:Template (inline)': { + 'html': '<body>' + ve.dm.example.MWInlineTemplateOpen + ve.dm.example.MWInlineTemplateContent + ve.dm.example.MWInlineTemplateClose + '</body>', + 'data': [ + { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } }, + { + 'type': 'MWtemplateInline', + 'mw': { + 'id': 'mwt1', + 'target': { 'wt' : 'Inline' }, + 'params': { + '1': { 'wt': '1,234' } + } + }, + 'about': '#mwt1', + 'attributes': { + 'html/0/about': '#mwt1', + 'html/0/data-mw': '{\"id\":\"mwt1\",\"target\":{\"wt\":\"Inline\"},\"params\":{\"1\":{\"wt\":\"1,234\"}}}', + 'html/0/data-parsoid': '{\"tsr\":[18,34],\"src\":\"{{Inline|1,234}}\",\"dsr\":[18,34,null,null]}', + 'html/0/typeof': 'mw:Object/Template' + }, + }, + { 'type': '/MWtemplateInline' }, + { 'type': '/paragraph' } + ], + 'normalizedHtml': ve.dm.example.MWInlineTemplateOpen + ve.dm.example.MWInlineTemplateClose }, 'paragraph with alienInline inside': { 'html': '<body><p>a<tt class="foo">b</tt>c</p></body>', diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 13910bb..fe9ccda 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -128,8 +128,6 @@ <script src="../../ve/ce/ve.ce.SurfaceObserver.js"></script> <script src="../../ve/ce/nodes/ve.ce.GeneratedContentNode.js"></script> <script src="../../ve/ce/nodes/ve.ce.AlienNode.js"></script> - <script src="../../ve/ce/nodes/ve.ce.AlienInlineNode.js"></script> - <script src="../../ve/ce/nodes/ve.ce.AlienBlockNode.js"></script> <script src="../../ve/ce/nodes/ve.ce.BreakNode.js"></script> <script src="../../ve/ce/nodes/ve.ce.CenterNode.js"></script> <script src="../../ve/ce/nodes/ve.ce.DefinitionListItemNode.js"></script> -- To view, visit https://gerrit.wikimedia.org/r/59062 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Id9bc7f049ea974dd5e7f8b7a66080939e0948bbd Gerrit-PatchSet: 2 Gerrit-Project: mediawiki/extensions/VisualEditor Gerrit-Branch: master Gerrit-Owner: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: Catrope <roan.katt...@gmail.com> Gerrit-Reviewer: Esanders <esand...@wikimedia.org> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits