Bgerstle has uploaded a new change for review. https://gerrit.wikimedia.org/r/209298
Change subject: fix section share snippet XPath to only grab top-level <p>'s ...................................................................... fix section share snippet XPath to only grab top-level <p>'s Refine regex as a stopgap until we can use a proper API for extracting article snippets. Also did some refactoring: - Moved snippet extraction into categories on MWKArticle & MWKSection. Main reason for this was to keep the shareSnippet logic context-specifc (i.e. a global NSString category for parsing HTML is harder to guarantee than one specific to article sections). - Created WMFSharing protocol for simplicity and potential expansion in the future (just one method atm). - Minor style/comment tweaks Bug: T95476 Change-Id: I9977feb9a0029d6515efa43dd199f370d16e82b2 --- M Wikipedia.xcodeproj/project.pbxproj M Wikipedia/Categories/MWKArticle+ShareSnippet.h M Wikipedia/Categories/MWKArticle+ShareSnippet.m A Wikipedia/Categories/MWKSection+WMFSharing.h A Wikipedia/Categories/MWKSection+WMFSharing.m M Wikipedia/Categories/NSString+WMFHTMLParsing.h M Wikipedia/Categories/NSString+WMFHTMLParsing.m M Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.h M Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.m A Wikipedia/View Controllers/ShareCard/WMFSharing.h A WikipediaUnitTests/MWKSection+WMFSharingTests.m M WikipediaUnitTests/NSString+WMFHTMLParsingTests.m 12 files changed, 167 insertions(+), 66 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/apps/ios/wikipedia refs/changes/98/209298/1 diff --git a/Wikipedia.xcodeproj/project.pbxproj b/Wikipedia.xcodeproj/project.pbxproj index a97cfe6..dddda46 100644 --- a/Wikipedia.xcodeproj/project.pbxproj +++ b/Wikipedia.xcodeproj/project.pbxproj @@ -227,6 +227,8 @@ BC86B93D1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m in Sources */ = {isa = PBXBuildFile; fileRef = BC86B93C1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m */; }; BC86B9401A929D7900B4C039 /* UICollectionViewFlowLayout+WMFItemSizeThatFits.m in Sources */ = {isa = PBXBuildFile; fileRef = BC86B93F1A929D7900B4C039 /* UICollectionViewFlowLayout+WMFItemSizeThatFits.m */; }; BC90C4BE1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = BC90C4BD1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.m */; }; + BC92A7711AFA841C003C4212 /* MWKSection+WMFSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = BC92A7701AFA841C003C4212 /* MWKSection+WMFSharing.m */; }; + BC92A7731AFA88D3003C4212 /* MWKSection+WMFSharingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BC92A7721AFA88D3003C4212 /* MWKSection+WMFSharingTests.m */; }; BC955BC71A82BEFD000EF9E4 /* MWKImageInfoFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BC955BC61A82BEFD000EF9E4 /* MWKImageInfoFetcher.m */; }; BC955BCF1A82C2FA000EF9E4 /* AFHTTPRequestOperationManager+WMFConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = BC955BCE1A82C2FA000EF9E4 /* AFHTTPRequestOperationManager+WMFConfig.m */; }; BCA676491AC05EDF00A16160 /* WMFRandomFileUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = BCA676481AC05EDF00A16160 /* WMFRandomFileUtilities.m */; }; @@ -759,6 +761,10 @@ BC86B93F1A929D7900B4C039 /* UICollectionViewFlowLayout+WMFItemSizeThatFits.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionViewFlowLayout+WMFItemSizeThatFits.m"; sourceTree = "<group>"; }; BC90C4BC1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWindow+WMFMainScreenWindow.h"; sourceTree = "<group>"; }; BC90C4BD1AC219FE009F36D2 /* UIWindow+WMFMainScreenWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWindow+WMFMainScreenWindow.m"; sourceTree = "<group>"; }; + BC92A76E1AFA83D2003C4212 /* WMFSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = WMFSharing.h; path = ShareCard/WMFSharing.h; sourceTree = "<group>"; }; + BC92A76F1AFA841C003C4212 /* MWKSection+WMFSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MWKSection+WMFSharing.h"; sourceTree = "<group>"; }; + BC92A7701AFA841C003C4212 /* MWKSection+WMFSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MWKSection+WMFSharing.m"; sourceTree = "<group>"; }; + BC92A7721AFA88D3003C4212 /* MWKSection+WMFSharingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MWKSection+WMFSharingTests.m"; sourceTree = "<group>"; }; BC955BC51A82BEFD000EF9E4 /* MWKImageInfoFetcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWKImageInfoFetcher.h; sourceTree = "<group>"; }; BC955BC61A82BEFD000EF9E4 /* MWKImageInfoFetcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MWKImageInfoFetcher.m; sourceTree = "<group>"; }; BC955BCD1A82C2FA000EF9E4 /* AFHTTPRequestOperationManager+WMFConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "AFHTTPRequestOperationManager+WMFConfig.h"; path = "Queues/AFHTTPRequestOperationManager+WMFConfig.h"; sourceTree = "<group>"; }; @@ -1884,6 +1890,8 @@ BCAC50C01AF3F7460015936C /* NSBundle+WMFInfoUtils.m */, BCC9B2691AFA61DB00FF9593 /* MWKArticle+ShareSnippet.h */, BCC9B26A1AFA61DB00FF9593 /* MWKArticle+ShareSnippet.m */, + BC92A76F1AFA841C003C4212 /* MWKSection+WMFSharing.h */, + BC92A7701AFA841C003C4212 /* MWKSection+WMFSharing.m */, ); path = Categories; sourceTree = "<group>"; @@ -2057,6 +2065,7 @@ BCCED2CF1AE03BE20094EB7E /* MWKSectionListTests.m */, BC49B3631AEECFD8009F55BE /* ArticleLoadingTests.m */, BCC9B2671AFA615000FF9593 /* WMFShareSnippetTests.m */, + BC92A7721AFA88D3003C4212 /* MWKSection+WMFSharingTests.m */, ); path = WikipediaUnitTests; sourceTree = "<group>"; @@ -2309,6 +2318,7 @@ C98990311A699DA100AF44FC /* ShareCard */ = { isa = PBXGroup; children = ( + BC92A76E1AFA83D2003C4212 /* WMFSharing.h */, C98990321A699DE000AF44FC /* WMFShareCardViewController.h */, C98990331A699DE000AF44FC /* WMFShareCardViewController.m */, C91A86F21A8BCB680088A801 /* WMFShareCardImageContainer.h */, @@ -2909,6 +2919,7 @@ BCA676571AC05FE200A16160 /* XCTestCase+WMFBundleConvenience.m in Sources */, BC0FED671AAA0268002488D7 /* MWKTitleTests.m in Sources */, BC0FED631AAA0263002488D7 /* MWKTestCase.m in Sources */, + BC92A7731AFA88D3003C4212 /* MWKSection+WMFSharingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3017,6 +3028,7 @@ 04D686FC1AB2949C0009B44A /* WikiGlyphButton.m in Sources */, C98990341A699DE000AF44FC /* WMFShareCardViewController.m in Sources */, C42D94861A937DE000A4871A /* WMFBorderButton.m in Sources */, + BC92A7711AFA841C003C4212 /* MWKSection+WMFSharing.m in Sources */, 047801BE18AE987900DBB747 /* UIButton+ColorMask.m in Sources */, BC86B93D1A929CC500B4C039 /* UICollectionViewFlowLayout+NSCopying.m in Sources */, 04D686C91AB28FE40009B44A /* UIImage+WMFFocalImageDrawing.m in Sources */, diff --git a/Wikipedia/Categories/MWKArticle+ShareSnippet.h b/Wikipedia/Categories/MWKArticle+ShareSnippet.h index b61ec24..564d744 100644 --- a/Wikipedia/Categories/MWKArticle+ShareSnippet.h +++ b/Wikipedia/Categories/MWKArticle+ShareSnippet.h @@ -7,10 +7,9 @@ // #import "MWKArticle.h" +#import "WMFSharing.h" @interface MWKArticle (ShareSnippet) - -/// @return A plain text string which is a snippet of the article's text, or an empty string on failure. -- (NSString*)shareSnippet; +<WMFSharing> @end diff --git a/Wikipedia/Categories/MWKArticle+ShareSnippet.m b/Wikipedia/Categories/MWKArticle+ShareSnippet.m index 37ccf0a..d03be50 100644 --- a/Wikipedia/Categories/MWKArticle+ShareSnippet.m +++ b/Wikipedia/Categories/MWKArticle+ShareSnippet.m @@ -8,22 +8,17 @@ #import "MWKArticle+ShareSnippet.h" #import "NSString+WMFHTMLParsing.h" +#import "MWKSection+WMFSharing.h" #import <BlocksKit/BlocksKit.h> @implementation MWKArticle (ShareSnippet) +/// @return The first non-empty `shareSnippet` from the receiver's `sections`. - (NSString*)shareSnippet { - NSString* heuristicText; for (MWKSection* section in self.sections) { - heuristicText = [section.text wmf_shareSnippetFromHTML]; - if (heuristicText) { - return heuristicText; - } - } - for (MWKSection* section in self.sections) { - heuristicText = [section.text wmf_shareSnippetFromText]; - if (heuristicText) { - return heuristicText; + NSString* snippet = section.shareSnippet; + if (snippet.length) { + return snippet; } } return @""; diff --git a/Wikipedia/Categories/MWKSection+WMFSharing.h b/Wikipedia/Categories/MWKSection+WMFSharing.h new file mode 100644 index 0000000..04d4705 --- /dev/null +++ b/Wikipedia/Categories/MWKSection+WMFSharing.h @@ -0,0 +1,15 @@ +// +// MWKSection+WMFSharing.h +// Wikipedia +// +// Created by Brian Gerstle on 5/6/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +#import "MWKSection.h" +#import "WMFSharing.h" + +@interface MWKSection (WMFSharing) +<WMFSharing> + +@end diff --git a/Wikipedia/Categories/MWKSection+WMFSharing.m b/Wikipedia/Categories/MWKSection+WMFSharing.m new file mode 100644 index 0000000..7c0a92b --- /dev/null +++ b/Wikipedia/Categories/MWKSection+WMFSharing.m @@ -0,0 +1,39 @@ +// +// MWKSection+WMFSharing.m +// Wikipedia +// +// Created by Brian Gerstle on 5/6/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +#import "MWKSection+WMFSharing.h" +#import "NSString+WMFHTMLParsing.h" +#import <hpple/TFHpple.h> +#import "WikipediaAppUtils.h" + +@implementation MWKSection (WMFSharing) + +/// @return The text from the first `<p>` tag in the receiver's `text`. +- (NSString*)shareSnippet { + /* + HAX: TFHpple implicitly wraps its data in html/body tags, which we need to reference explicitly since we want the + top-level <p> tag. + */ + NSArray* xpathResults = [[TFHpple + hppleWithHTMLData:[self.text dataUsingEncoding:NSUTF8StringEncoding]] + searchWithXPathQuery:@"/html/body/p[1]//text()"]; + if (xpathResults) { + NSString* shareSnippet = + [[[xpathResults valueForKey:WMF_SAFE_KEYPATH([TFHppleElement new], raw)] + componentsJoinedByString:@""] + wmf_shareSnippetFromText]; + if (shareSnippet.length) { + return shareSnippet; + } + } + // fall back to text processing if HTML parsing fails + NSString* shareSnippet = [self.text wmf_shareSnippetFromText]; + return shareSnippet.length ? shareSnippet : @""; +} + +@end diff --git a/Wikipedia/Categories/NSString+WMFHTMLParsing.h b/Wikipedia/Categories/NSString+WMFHTMLParsing.h index 82062a8..722cea7 100644 --- a/Wikipedia/Categories/NSString+WMFHTMLParsing.h +++ b/Wikipedia/Categories/NSString+WMFHTMLParsing.h @@ -23,11 +23,9 @@ - (NSString*)wmf_joinedHtmlTextNodesWithDelimiter:(NSString*)delimiter; /** - * Parse the receiver as HTML and return a heuristically defined snippet. + * String sanitation method to remove wiki markup & other text artifacts prior to sharing. + * @return A new string with sanitation processing applied. */ -- (NSString*)wmf_shareSnippetFromHTML; - -/// @return A new string which has been sanitized to remove unnecesary characters and wiki markup. - (NSString*)wmf_shareSnippetFromText; /** diff --git a/Wikipedia/Categories/NSString+WMFHTMLParsing.m b/Wikipedia/Categories/NSString+WMFHTMLParsing.m index 63edc71..d5e1dba 100644 --- a/Wikipedia/Categories/NSString+WMFHTMLParsing.m +++ b/Wikipedia/Categories/NSString+WMFHTMLParsing.m @@ -3,9 +3,6 @@ #import <hpple/TFHpple.h> #import "NSString+Extras.h" -static int const kMinimumLengthForPreTransformedHTMLForSnippet = 40; -static int const kHighestIndexForSubstringAfterHTMLRemoved = 350; - @implementation NSString (WMFHTMLParsing) - (NSArray*)wmf_htmlTextNodes { @@ -31,23 +28,8 @@ return [[self wmf_htmlTextNodes] componentsJoinedByString:delimiter]; } -- (NSString*)wmf_shareSnippetFromHTML { - if (self.length < kMinimumLengthForPreTransformedHTMLForSnippet) { - return nil; - } - NSString* result = - [[[[[[TFHpple hppleWithHTMLData:[self dataUsingEncoding:NSUTF8StringEncoding]] - searchWithXPathQuery:@"//p[1]//text()"] - valueForKey:WMF_SAFE_KEYPATH([TFHppleElement new], raw)] - componentsJoinedByString:@""] - wmf_safeSubstringToIndex:kHighestIndexForSubstringAfterHTMLRemoved] - wmf_shareSnippetFromText]; - return result.length >= kMinimumLengthForPreTransformedHTMLForSnippet ? result : nil; -} - #pragma mark - String simplification and cleanup -/// @return A new string after performing multiple transformations to remove wiki markup & other text artifacts. - (NSString*)wmf_shareSnippetFromText { return [[[[[[[[[[self stringByReplacingOccurrencesOfString:@"&" withString:@"&"] stringByReplacingOccurrencesOfString:@">" withString:@">"] diff --git a/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.h b/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.h index 7a889fd..ee9e691 100644 --- a/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.h +++ b/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.h @@ -19,15 +19,25 @@ @interface WMFShareOptionsViewController : UIViewController -@property (readonly) MWKArticle* article; -@property (readonly) NSString* snippet; -@property (readonly) NSString* snippetForTextOnlySharing; -@property (readonly) UIView* backgroundView; -@property (readonly) id<WMFShareOptionsViewControllerDelegate> delegate; +@property (nonatomic, readonly) MWKArticle* article; +@property (nonatomic, copy, readonly) NSString* snippet; +@property (nonatomic, copy, readonly) NSString* snippetForTextOnlySharing; +@property (nonatomic, readonly) UIView* backgroundView; +@property (nonatomic, weak, readonly) id<WMFShareOptionsViewControllerDelegate> delegate; +/** + * Initialize a new instance with an article and an optional snippet. + * + * @param article The article the snippet is derived from. + * @param snippet Optional. The snippet to share, with any necessary processing already applied. + * @param backgroundView The background of the share card. + * @param delegate The `WMFShareOptionsViewControllerDelegate`. + * + * @note Truncating `snippet` is not necessary, as it's done internally by the share view's `UILabel`. + */ - (instancetype)initWithMWKArticle:(MWKArticle*)article snippet:(NSString*)snippet backgroundView:(UIView*)backgroundView - delegate:(id)delegate; + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; @end diff --git a/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.m b/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.m index 0f27228..a9d8e33 100644 --- a/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.m +++ b/Wikipedia/View Controllers/ShareCard/WMFShareOptionsViewController.m @@ -65,17 +65,17 @@ _delegate = delegate; _article = article; - _shareTitle = article.title.prefixedText; + _shareTitle = [article.title.prefixedText copy]; WMFShareCardViewController* cardViewController = [[WMFShareCardViewController alloc] initWithNibName:@"ShareCard" bundle:nil]; - _snippet = snippet; - if (snippet.length == 0) { - _snippet = [article shareSnippet]; - _snippetForTextOnlySharing = @""; + if (snippet.length) { + _snippet = [snippet copy]; + _snippetForTextOnlySharing = [snippet copy]; } else { - _snippetForTextOnlySharing = snippet; + _snippet = [[article shareSnippet] copy]; + _snippetForTextOnlySharing = @""; } #warning FIXME: render card image lazily diff --git a/Wikipedia/View Controllers/ShareCard/WMFSharing.h b/Wikipedia/View Controllers/ShareCard/WMFSharing.h new file mode 100644 index 0000000..9942e65 --- /dev/null +++ b/Wikipedia/View Controllers/ShareCard/WMFSharing.h @@ -0,0 +1,19 @@ +// +// WMFSharing.h +// Wikipedia +// +// Created by Brian Gerstle on 5/6/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +#import <Foundation/Foundation.h> + +/** + * Protocol for shareable Wikipedia entities. + */ +@protocol WMFSharing <NSObject> + +/// @return A plain text string which is a snippet of the article's text, or an empty string on failure. +- (NSString*)shareSnippet; + +@end diff --git a/WikipediaUnitTests/MWKSection+WMFSharingTests.m b/WikipediaUnitTests/MWKSection+WMFSharingTests.m new file mode 100644 index 0000000..54eb21d --- /dev/null +++ b/WikipediaUnitTests/MWKSection+WMFSharingTests.m @@ -0,0 +1,44 @@ +// +// MWKSection+WMFSharingTests.m +// Wikipedia +// +// Created by Brian Gerstle on 5/6/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +#import <UIKit/UIKit.h> +#import <XCTest/XCTest.h> +#import "MWKSection+WMFSharing.h" + +#define HC_SHORTHAND 1 +#import <OCHamcrest/OCHamcrest.h> + +@interface MWKSection_WMFSharingTests : XCTestCase +@property (nonatomic) MWKSection* section; +@end + +@implementation MWKSection_WMFSharingTests + +- (void)setUp { + [super setUp]; +} + +- (void)testSimpleSnippet { + self.section = [[MWKSection alloc] initWithArticle:nil + dict:@{ + @"id": @0, + @"text": @"<p>Dog (woof (w00t)) [horse] adequately long string historically 40 characters.</p>" + }]; + assertThat(self.section.shareSnippet, is(@"Dog adequately long string historically 40 characters.")); +} + +- (void)testSimpleSnippetIncludingTable { + self.section = [[MWKSection alloc] initWithArticle:nil + dict:@{ + @"id": @0, + @"text": @"<table><p>Foo</p></table><p>Dog (woof (w00t)) [horse] adequately long string historically 40 characters.</p>" + }]; + assertThat(self.section.shareSnippet, is(@"Dog adequately long string historically 40 characters.")); +} + +@end diff --git a/WikipediaUnitTests/NSString+WMFHTMLParsingTests.m b/WikipediaUnitTests/NSString+WMFHTMLParsingTests.m index f49506b..2f54237 100644 --- a/WikipediaUnitTests/NSString+WMFHTMLParsingTests.m +++ b/WikipediaUnitTests/NSString+WMFHTMLParsingTests.m @@ -10,6 +10,7 @@ #import "NSString+WMFHTMLParsing.h" #import "WikipediaAppUtils.h" #import <hpple/TFHpple.h> +#import "WMFTestFixtureUtilities.h" #define HC_SHORTHAND 1 #import <OCHamcrest/OCHamcrest.h> @@ -20,29 +21,16 @@ @implementation NSString_WMFHTMLParsingTests -- (void)setUp { - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; -} +//- (void)testHillaryRodhamClintonHTML { +// NSString* hillaryHTML = +// [[NSString alloc] initWithData:[[self wmf_bundle] wmf_dataFromContentsOfFile:@"HillaryRodhamClinton" ofType:@"html"] +// encoding:NSUTF8StringEncoding]; +// assertThat(hillaryHTML.wmf_shareSnippetFromHTML, is(@"Hillary Diane Rodham Clinton is a former United States Secretary of State in the administration of President Barack Obama from 2009 to 2013; a former United States Senator representing New York from 2001 to 2009; and, as the wife of President Bill Clinton, was First Lady of the United State")); +//} - (void)testSnippetFromTextWithCitaiton { assertThat([@"March 2011.[9][10] It was the first spacecraft to orbit Mercury.[7]" wmf_shareSnippetFromText], is(@"March 2011. It was the first spacecraft to orbit Mercury.")); -} - -- (void)testTooShortSnippet { - NSString *string = @"<p>Cat (meow) [cow] too short</p>"; - XCTAssertNil([string wmf_shareSnippetFromHTML], @"Too short snippet non-nil after parsing"); -} - -- (void)testAdequateSnippet { - NSString *string = @"<p>Dog (woof (w00t)) [horse] adequately long string historically 40 characters.</p>"; - assertThat([string wmf_shareSnippetFromHTML], is(@"Dog adequately long string historically 40 characters.")); } - (void)testConsecutiveNewlinesCollapsing { -- To view, visit https://gerrit.wikimedia.org/r/209298 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I9977feb9a0029d6515efa43dd199f370d16e82b2 Gerrit-PatchSet: 1 Gerrit-Project: apps/ios/wikipedia Gerrit-Branch: master Gerrit-Owner: Bgerstle <bgers...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits