Bgerstle has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/215114

Change subject: hygiene: hash & equality utils plus title & site cleanup
......................................................................

hygiene: hash & equality utils plus title & site cleanup

This is a boyscout patch in preparation for T100687.

- Update bitwise rotation implementation to something that performs
  better
- Add macro to remove potential for error when duplicating properties
  (WMF_EQUAL_PROPERTIES)
- Integrate bitwise rotation & comparison updates to MWKSite & MWKTitle
- Deprecate & clean up MWKSite & MWKTitle cruft

Change-Id: If5906e1ad8f6f39899e3efbb703ce660bceeb6d9
---
M MediaWikiKit/MediaWikiKit/MWKImageInfo.m
M MediaWikiKit/MediaWikiKit/MWKSite.h
M MediaWikiKit/MediaWikiKit/MWKSite.m
M MediaWikiKit/MediaWikiKit/MWKTitle.h
M MediaWikiKit/MediaWikiKit/MWKTitle.m
M MediaWikiKit/MediaWikiKitTests/MWKTitleTests.m
M Wikipedia.xcodeproj/project.pbxproj
M Wikipedia/Categories/MWKArticle+WMFSharing.m
M Wikipedia/Categories/NSArray+WMFExtensions.h
M Wikipedia/Categories/NSArray+WMFExtensions.m
M Wikipedia/Categories/NSString+Extras.h
M Wikipedia/Categories/NSString+Extras.m
M Wikipedia/Networking/Fetchers/ArticleFetcher.m
M Wikipedia/View Controllers/History/HistoryViewController.m
M Wikipedia/View Controllers/Navigation/Center/CenterNavController.h
M Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
M Wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
M Wikipedia/View Controllers/WebView/WebViewController.m
A Wikipedia/mw-utils/NSObjectUtilities.h
A Wikipedia/mw-utils/WMFComparison.h
A Wikipedia/mw-utils/WMFHashing.h
A Wikipedia/mw-utils/WMFPageUtilities.h
A Wikipedia/mw-utils/WMFPageUtilities.m
M Wikipedia/mw-utils/WikipediaAppUtils.h
M Wikipedia/mw-utils/WikipediaAppUtils.m
M WikipediaUnitTests/CircularBitwiseRotationTests.m
M WikipediaUnitTests/MWKArticle+WMFSharingTests.m
27 files changed, 390 insertions(+), 253 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/ios/wikipedia 
refs/changes/14/215114/1

diff --git a/MediaWikiKit/MediaWikiKit/MWKImageInfo.m 
b/MediaWikiKit/MediaWikiKit/MWKImageInfo.m
index 023ebb3..5d64ec1 100644
--- a/MediaWikiKit/MediaWikiKit/MWKImageInfo.m
+++ b/MediaWikiKit/MediaWikiKit/MWKImageInfo.m
@@ -133,7 +133,7 @@
 }
 
 - (NSUInteger)hash {
-    return [self.canonicalPageTitle hash] ^ 
CircularBitwiseRotation([self.imageURL hash], 1);
+    return self.canonicalPageTitle.hash ^ 
flipBitsWithAdditionalRotation(self.imageURL.hash, 1);
 }
 
 - (NSString*)description {
diff --git a/MediaWikiKit/MediaWikiKit/MWKSite.h 
b/MediaWikiKit/MediaWikiKit/MWKSite.h
index f57e3b3..4b8ea70 100644
--- a/MediaWikiKit/MediaWikiKit/MWKSite.h
+++ b/MediaWikiKit/MediaWikiKit/MWKSite.h
@@ -1,26 +1,49 @@
 //  Created by Brion on 11/6/13.
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
-#pragma once
-
 #import <Foundation/Foundation.h>
 
-// forward decl
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSString* const WMFDefaultSiteDomain;
+
 @class MWKTitle;
 @class MWKUser;
 
 @interface MWKSite : NSObject
 
-@property (readonly, copy, nonatomic) NSString* domain;
-@property (readonly, copy, nonatomic) NSString* language;
+@property (nonatomic, copy, readonly) NSString* domain;
+@property (nonatomic, copy, readonly) NSString* language;
 
-- (instancetype)initWithDomain:(NSString*)domain language:(NSString*)language;
+- (instancetype)initWithDomain:(NSString*)domain language:(NSString*)language 
NS_DESIGNATED_INITIALIZER;
 
+/// Convenience factory method wrapping the designated initializer.
++ (instancetype)siteWithDomain:(NSString*)domain language:(NSString*)language;
+
+/// @return A site with the default domain and the language code returned by 
@c locale.
++ (instancetype)siteWithLocale:(NSLocale*)locale;
+
+/// @return A site with the default domain and the current locale's language 
code.
++ (instancetype)siteWithCurrentLocale;
+
+- (BOOL)isEqualToSite:(MWKSite* __nullable)other;
+
+///
+/// @name Title Factory Convenience Methods
+///
+
+/**
+ * @return A title initialized with the receiver as its @c site.
+ * @see -[MWKTitle initWithString:site:]
+ */
 - (MWKTitle*)titleWithString:(NSString*)string;
+
+/**
+ * @return A title initialized with the receiver as its @c site.
+ * @see -[MWKTitle initWithString:site:]
+ */
 - (MWKTitle*)titleWithInternalLink:(NSString*)path;
 
-+ (MWKSite*)siteWithDomain:(NSString*)domain language:(NSString*)language;
-
-- (BOOL)isEqualToSite:(MWKSite*)other;
-
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/MediaWikiKit/MediaWikiKit/MWKSite.m 
b/MediaWikiKit/MediaWikiKit/MWKSite.m
index aa33a74..ecfd382 100644
--- a/MediaWikiKit/MediaWikiKit/MWKSite.m
+++ b/MediaWikiKit/MediaWikiKit/MWKSite.m
@@ -2,7 +2,9 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import "MediaWikiKit.h"
-#import "WikipediaAppUtils.h"
+#import "NSObjectUtilities.h"
+
+NSString* const WMFDefaultSiteDomain = @"wikipedia.org";
 
 @interface MWKSite ()
 
@@ -13,23 +15,9 @@
 
 @implementation MWKSite
 
-#pragma mark - Setup
-
-+ (MWKSite*)siteWithDomain:(NSString*)domain language:(NSString*)language {
-    static NSMutableDictionary* cachedSites = nil;
-    if (cachedSites == nil) {
-        cachedSites = [[NSMutableDictionary alloc] init];
-    }
-    NSString* key = [NSString stringWithFormat:@"%@:%@", domain, language];
-    MWKSite* site = cachedSites[key];
-    if (site == nil) {
-        site             = [[MWKSite alloc] initWithDomain:domain 
language:language];
-        cachedSites[key] = site;
-    }
-    return site;
-}
-
 - (instancetype)initWithDomain:(NSString*)domain language:(NSString*)language {
+    NSParameterAssert(domain.length);
+    NSParameterAssert(language.length);
     self = [super init];
     if (self) {
         self.domain   = domain;
@@ -38,22 +26,26 @@
     return self;
 }
 
++ (MWKSite*)siteWithDomain:(NSString*)domain language:(NSString*)language {
+    return [[MWKSite alloc] initWithDomain:domain language:language];
+}
+
++ (instancetype)siteWithCurrentLocale {
+    return [self siteWithLocale:[NSLocale currentLocale]];
+}
+
++ (instancetype)siteWithLocale:(NSLocale*)locale {
+    return [self siteWithDomain:WMFDefaultSiteDomain language:[locale 
objectForKey:NSLocaleLanguageCode]];
+}
+
 #pragma mark - Title Helpers
 
 - (MWKTitle*)titleWithString:(NSString*)string {
     return [MWKTitle titleWithString:string site:self];
 }
 
-static NSString* localLinkPrefix = @"/wiki/";
-
 - (MWKTitle*)titleWithInternalLink:(NSString*)path {
-    if ([path hasPrefix:localLinkPrefix]) {
-        NSString* remainder = [[path substringFromIndex:localLinkPrefix.length]
-                               
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
-        return [self titleWithString:remainder];
-    } else {
-        @throw [NSException exceptionWithName:@"SiteBadLinkFormatException" 
reason:@"unexpected local link format" userInfo:nil];
-    }
+    return [[MWKTitle alloc] initWithInternalLink:path site:self];
 }
 
 #pragma mark - NSObject
@@ -69,12 +61,12 @@
 }
 
 - (BOOL)isEqualToSite:(MWKSite*)other {
-    return [self.domain isEqualToString:other.domain]
-           && [self.language isEqualToString:other.language];
+    return WMF_EQUAL_PROPERTIES(self, language, isEqualToString:, other)
+           && WMF_EQUAL_PROPERTIES(self, domain, isEqualToString:, other);
 }
 
 - (NSUInteger)hash {
-    return [self.domain hash] ^ CircularBitwiseRotation([self.language hash], 
1);
+    return self.domain.hash ^ 
flipBitsWithAdditionalRotation(self.language.hash, 1);
 }
 
 @end
diff --git a/MediaWikiKit/MediaWikiKit/MWKTitle.h 
b/MediaWikiKit/MediaWikiKit/MWKTitle.h
index 05f6a3a..1c9faab 100644
--- a/MediaWikiKit/MediaWikiKit/MWKTitle.h
+++ b/MediaWikiKit/MediaWikiKit/MWKTitle.h
@@ -1,78 +1,75 @@
 //  Created by Brion on 11/1/13.
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
-#pragma once
-
 #import <Foundation/Foundation.h>
-
 #import "MWKSite.h"
+
+NS_ASSUME_NONNULL_BEGIN
 
 @interface MWKTitle : NSObject <NSCopying>
 
-/**
- * Initialize a new MWKTitle object from string input
- */
-- (instancetype)initWithString:(NSString*)str site:(MWKSite*)site;
-
-/**
- * Create a new MWKTitle object from string input
- */
-+ (MWKTitle*)titleWithString:(NSString*)str site:(MWKSite*)site;
-
-/**
- * Normalize a title string portion to text form
- */
-+ (NSString*)normalize:(NSString*)str;
-
-/**
- * The site this title belongs to
- */
+/// The site this title belongs to
 @property (readonly, strong, nonatomic) MWKSite* site;
 
-/**
- * Normalized title component only (decoded, no underscores)
- */
+/// Normalized title component only (decoded, no underscores)
 @property (readonly, copy, nonatomic) NSString* text;
 
-/**
- * Fragment (component after the '#')
- * Warning: fragment may be nil!
- */
-@property (readonly, copy, nonatomic) NSString* fragment;
+/// The fragment component of the string used to initialize the receiver, if 
present.
+@property (readonly, copy, nonatomic, nullable) NSString* fragment;
 
+/// Percent-escaped fragment, prefixed with @c #, or an empty string if absent.
+@property (readonly, copy, nonatomic) NSString* encodedFragment;
 
-/**
- * Full text-normalized namespace+title
- * Decoded, with spaces
- */
-@property (readonly, copy, nonatomic) NSString* prefixedText;
-
-/**
- * Full DB-normalized namespace+title
- * Decoded, with underscores
- */
-@property (readonly, copy, nonatomic) NSString* prefixedDBKey;
-
-/**
- * Full URL-normalized namespace+title
- * Encoded, with underscores
- */
-@property (readonly, copy, nonatomic) NSString* prefixedURL;
-
-/**
- * URL-normalized fragment, including the # if applicable
- * Always returns a string, may be empty string.
- */
-@property (readonly, copy, nonatomic) NSString* fragmentForURL;
-
-/**
- * Absolute URL to mobile view of this article
- */
+/// Absolute URL to mobile view of this article
 @property (readonly, copy, nonatomic) NSURL* mobileURL;
 
-/**
- * Absolute URL to desktop view of this article
- */
+/// Absolute URL to desktop view of this article
 @property (readonly, copy, nonatomic) NSURL* desktopURL;
 
+///
+/// @name Deprecated Properties
+///
+
+/// Full text-normalized namespace+title decoded, with spaces
+/// @warning This method was added prematurely and never supported, so it's 
effectively an alias for `text`.
+@property (readonly, copy, nonatomic) NSString* prefixedText __deprecated;
+
+/// Full DB-normalized namespace+title
+/// @see prefixedText
+@property (readonly, copy, nonatomic) NSString* prefixedDBKey __deprecated;
+
+/// Full URL-normalized namespace+title
+/// @see prefixedText
+@property (readonly, copy, nonatomic) NSString* prefixedURL __deprecated;
+
+/**
+ * Initializes a new title belonging to @c site with an optional fragment.
+ *
+ * The preferred initializer is @c initWithString:site:, which parses 
components in the string.
+ *
+ * @param site      The site to which this title belongs.
+ * @param text      The text which makes up the title.
+ * @param fragment  An optional fragment, e.g. @"#section".
+ *
+ * @return A new title.
+ */
+- (instancetype)initWithSite:(MWKSite*)site
+             normalizedTitle:(NSString*)text
+                    fragment:(NSString* __nullable)fragment 
NS_DESIGNATED_INITIALIZER;
+
+// TODO: implement "fuzzy string" initializer which can take a title or an 
internal link?
+
+/// Initialize a new title from `str`, from which the title & an optional 
fragment are parsed.
+- (instancetype)initWithString:(NSString*)str site:(MWKSite*)site;
+
+/// Initialize a new title with `relativeInternalLink`, which is parsed after 
removing the `/wiki/` prefix.
+- (instancetype)initWithInternalLink:(NSString*)relativeInternalLink 
site:(MWKSite*)site;
+
+/// Convenience factory method wrapping `initWithString:site:`.
++ (MWKTitle*)titleWithString:(NSString*)str site:(MWKSite*)site;
+
+- (BOOL)isEqualToTitle:(MWKTitle*)title;
+
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/MediaWikiKit/MediaWikiKit/MWKTitle.m 
b/MediaWikiKit/MediaWikiKit/MWKTitle.m
index 6e05c71..114b141 100644
--- a/MediaWikiKit/MediaWikiKit/MWKTitle.m
+++ b/MediaWikiKit/MediaWikiKit/MWKTitle.m
@@ -2,6 +2,11 @@
 //  Copyright (c) 2013 Wikimedia Foundation. Provided under MIT-style license; 
please copy and modify!
 
 #import "MediaWikiKit.h"
+#import "WMFPageUtilities.h"
+#import "NSArray+WMFExtensions.h"
+#import "NSObjectUtilities.h"
+
+NS_ASSUME_NONNULL_BEGIN
 
 @interface MWKTitle ()
 
@@ -11,57 +16,47 @@
 @property (readwrite, copy, nonatomic) NSString* prefixedText;
 @property (readwrite, copy, nonatomic) NSString* prefixedDBKey;
 @property (readwrite, copy, nonatomic) NSString* prefixedURL;
-@property (readwrite, copy, nonatomic) NSString* fragmentForURL;
+@property (readwrite, copy, nonatomic) NSString* encodedFragment;
 @property (readwrite, copy, nonatomic) NSURL* mobileURL;
 @property (readwrite, copy, nonatomic) NSURL* desktopURL;
-
 
 @end
 
 @implementation MWKTitle
 
-#pragma mark - Class methods
+- (instancetype)initWithSite:(MWKSite*)site normalizedTitle:(NSString*)text 
fragment:(NSString* __nullable)fragment {
+    NSParameterAssert(site);
+    NSParameterAssert(text.length);
+    self = [super init];
+    if (self) {
+        self.site     = site;
+        self.text     = text;
+        self.fragment = fragment;
+    }
+    return self;
+}
+
+- (instancetype)initWithInternalLink:(NSString*)relativeInternalLink 
site:(MWKSite*)site {
+    return [self initWithString:WMFInternalLinkPath(relativeInternalLink) 
site:site];
+}
+
+- (instancetype)initWithString:(NSString*)str site:(MWKSite*)site {
+    NSAssert(!WMFIsInternalLink(str),
+             @"Didn't expect %@ to be an internal link. Use 
initWithInternalLink:site: instead.",
+             str);
+    NSArray* bits = [str componentsSeparatedByString:@"#"];
+    NSParameterAssert(bits.count >= 1);
+    return [self initWithSite:site
+              normalizedTitle:WMFNormalizedPageTitle(bits[0])
+                     fragment:[bits wmf_safeObjectAtIndex:1]];
+}
 
 + (MWKTitle*)titleWithString:(NSString*)str site:(MWKSite*)site {
     return [[MWKTitle alloc] initWithString:str site:site];
 }
 
-+ (NSString*)normalize:(NSString*)str {
-    // @todo implement fuller normalization?
-    return [str stringByReplacingOccurrencesOfString:@"_" withString:@" "];
-}
-
-#pragma mark - Initializers
-
-- (instancetype)initWithString:(NSString*)str site:(MWKSite*)site {
-    self = [self init];
-    if (self) {
-        self.site = site;
-        NSArray* bits = [str componentsSeparatedByString:@"#"];
-        self.text = [MWKTitle normalize:bits[0]];
-        if (bits.count > 1) {
-            self.fragment = bits[1];
-        }
-    }
-    return self;
-}
-
-#pragma mark - Property getters
-
-- (NSString*)namespace {
-    // @todo implement namespace detection and normalization
-    // rename the property from a reserved language name
-    // doing this right requires some site info
-    return nil;
-}
-
-- (NSString*)_prefix {
-    // @todo implement namespace prefixing once namespaces are handled
-    return @"";
-}
-
 - (NSString*)prefixedText {
-    return [[self _prefix] stringByAppendingString:self.text];
+    return self.text;
 }
 
 - (NSString*)prefixedDBKey {
@@ -72,7 +67,7 @@
     return [self.prefixedDBKey 
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
 }
 
-- (NSString*)fragmentForURL {
+- (NSString*)encodedFragment {
     if (self.fragment) {
         // @fixme we use some weird escaping system...?
         return [@"#" stringByAppendingString:[self.fragment 
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
@@ -81,34 +76,36 @@
     }
 }
 
-- (NSURL*)mobileURL;
-{
-    return [NSURL URLWithString:[NSString 
stringWithFormat:@"https://%@.m.%@/wiki/%@";,
+- (NSURL*)mobileURL {
+    return [NSURL URLWithString:[NSString 
stringWithFormat:@"https://%@.m.%@%@%@";,
                                  self.site.language,
                                  self.site.domain,
+                                 WMFInternalLinkPathPrefix,
                                  self.prefixedURL]];
 }
 
-- (NSURL*)desktopURL;
-{
-    return [NSURL URLWithString:[NSString 
stringWithFormat:@"https://%@.%@/wiki/%@";,
+- (NSURL*)desktopURL {
+    return [NSURL URLWithString:[NSString 
stringWithFormat:@"https://%@.%@%@%@";,
                                  self.site.language,
                                  self.site.domain,
+                                 WMFInternalLinkPathPrefix,
                                  self.prefixedURL]];
 }
-
 
 - (BOOL)isEqual:(id)object {
-    if (object == nil) {
-        return NO;
-    } else if (![object isKindOfClass:[MWKTitle class]]) {
-        return NO;
+    if (self == object) {
+        return YES;
+    } else if ([object isKindOfClass:[MWKTitle class]]) {
+        return [self isEqualToTitle:object];
     } else {
-        MWKTitle* other = object;
-        return [self.site isEqual:other.site] &&
-               [self.prefixedText isEqualToString:other.prefixedText] &&
-               ((self.fragment == nil && other.fragment == nil) || 
[self.fragment isEqualToString:other.fragment]);
+        return NO;
     }
+}
+
+- (BOOL)isEqualToTitle:(MWKTitle*)otherTitle {
+    return WMF_IS_EQUAL_PROPERTIES(self, site, otherTitle)
+           && WMF_EQUAL_PROPERTIES(self, prefixedText, isEqualToString:, 
otherTitle)
+           && WMF_EQUAL_PROPERTIES(self, fragment, isEqualToString:, 
otherTitle);
 }
 
 - (NSString*)description {
@@ -119,15 +116,17 @@
     }
 }
 
-#pragma mark - NSCopying protocol methods
+- (NSUInteger)hash {
+    return self.site.hash
+           ^ flipBitsWithAdditionalRotation(self.prefixedText.hash, 1)
+           ^ flipBitsWithAdditionalRotation(self.fragment.hash, 2);
+}
 
-- (id)copyWithZone:(NSZone*)zone {
-    // Titles are immutable
+- (instancetype)copyWithZone:(NSZone*)zone {
+    // immutable
     return self;
 }
 
-- (NSUInteger)hash {
-    return [self.site hash] ^ [self.prefixedText hash] ^ [self.fragment hash];
-}
-
 @end
+
+NS_ASSUME_NONNULL_END
\ No newline at end of file
diff --git a/MediaWikiKit/MediaWikiKitTests/MWKTitleTests.m 
b/MediaWikiKit/MediaWikiKitTests/MWKTitleTests.m
index ccb522b..6addca6 100644
--- a/MediaWikiKit/MediaWikiKitTests/MWKTitleTests.m
+++ b/MediaWikiKit/MediaWikiKitTests/MWKTitleTests.m
@@ -36,7 +36,7 @@
     XCTAssertEqualObjects(title.prefixedText, @"Simple", @"Text form is full");
     XCTAssertEqualObjects(title.prefixedURL, @"Simple", @"URL form is full");
     XCTAssertNil(title.fragment, @"Fragment is nil");
-    XCTAssertEqualObjects(title.fragmentForURL, @"", @"Fragment for URL is 
empty string");
+    XCTAssertEqualObjects(title.encodedFragment, @"", @"Fragment for URL is 
empty string");
 }
 
 - (void)testFancy {
@@ -48,7 +48,7 @@
         XCTAssertEqualObjects(title.prefixedText, @"Fancy title with spaces", 
@"Text form has spaces");
         XCTAssertEqualObjects(title.prefixedURL, @"Fancy_title_with_spaces", 
@"URL form has underscores");
         XCTAssertNil(title.fragment, @"Fragment is nil");
-        XCTAssertEqualObjects(title.fragmentForURL, @"", @"Fragment for URL is 
empty string");
+        XCTAssertEqualObjects(title.encodedFragment, @"", @"Fragment for URL 
is empty string");
     }
 }
 
@@ -58,7 +58,7 @@
     XCTAssertEqualObjects(title.prefixedText, @"Éclair", @"Text form has 
unicode");
     XCTAssertEqualObjects(title.prefixedURL, @"%C3%89clair", @"URL form has 
percent encoding");
     XCTAssertNil(title.fragment, @"Fragment is nil");
-    XCTAssertEqualObjects(title.fragmentForURL, @"", @"Fragment for URL is 
empty string");
+    XCTAssertEqualObjects(title.encodedFragment, @"", @"Fragment for URL is 
empty string");
 }
 
 - (void)testEquals {
diff --git a/Wikipedia.xcodeproj/project.pbxproj 
b/Wikipedia.xcodeproj/project.pbxproj
index 0f7ef2b..4b463f0 100644
--- a/Wikipedia.xcodeproj/project.pbxproj
+++ b/Wikipedia.xcodeproj/project.pbxproj
@@ -192,6 +192,7 @@
                0EE768811AFD25CC00A5D046 /* WMFSearchFunnel.m in Sources */ = 
{isa = PBXBuildFile; fileRef = 0EE768801AFD25CC00A5D046 /* WMFSearchFunnel.m 
*/; };
                701FF5EE601DEA3FCAB7EFD3 /* libPods.a in Frameworks */ = {isa = 
PBXBuildFile; fileRef = D82982ED992F47428037BDF2 /* libPods.a */; };
                954BA118838BF8BA6B01C34A /* libPods-WikipediaUnitTests.a in 
Frameworks */ = {isa = PBXBuildFile; fileRef = 8CE61C6963F825760822A28A /* 
libPods-WikipediaUnitTests.a */; };
+               BC092B961B18E89200093C59 /* WMFPageUtilities.m in Sources */ = 
{isa = PBXBuildFile; fileRef = BC092B951B18E89200093C59 /* WMFPageUtilities.m 
*/; };
                BC0FED621AAA0263002488D7 /* WMFCodingStyle.m in Sources */ = 
{isa = PBXBuildFile; fileRef = BC6FEAE01A9B7EFD00A1D890 /* WMFCodingStyle.m */; 
};
                BC0FED631AAA0263002488D7 /* MWKTestCase.m in Sources */ = {isa 
= PBXBuildFile; fileRef = BCB669BB1A83F6D300C7B1FE /* MWKTestCase.m */; };
                BC0FED641AAA0263002488D7 /* MWKArticleStoreTestCase.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BCB669BD1A83F6D300C7B1FE /* 
MWKArticleStoreTestCase.m */; };
@@ -281,7 +282,6 @@
                BCC185E01A9EC836005378F8 /* UIButton+FrameUtils.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCC185DF1A9EC836005378F8 /* 
UIButton+FrameUtils.m */; };
                BCC185E81A9FA498005378F8 /* 
UICollectionViewFlowLayout+AttributeUtils.m in Sources */ = {isa = 
PBXBuildFile; fileRef = BCC185E71A9FA498005378F8 /* 
UICollectionViewFlowLayout+AttributeUtils.m */; };
                BCCED2D01AE03BE20094EB7E /* MWKSectionListTests.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BCCED2CF1AE03BE20094EB7E /* 
MWKSectionListTests.m */; };
-               BCD41DDC1B11CBD400231BB1 /* (null) in Sources */ = {isa = 
PBXBuildFile; };
                BCD41DEA1B11CC5800231BB1 /* golden-gate.jpg in Resources */ = 
{isa = PBXBuildFile; fileRef = BCD41DDE1B11CC5800231BB1 /* golden-gate.jpg */; 
};
                BCD41DEB1B11CC5800231BB1 /* MainPageMobileView.json in 
Resources */ = {isa = PBXBuildFile; fileRef = BCD41DDF1B11CC5800231BB1 /* 
MainPageMobileView.json */; };
                BCD41DEC1B11CC5800231BB1 /* Obama.json in Resources */ = {isa = 
PBXBuildFile; fileRef = BCD41DE01B11CC5800231BB1 /* Obama.json */; };
@@ -734,6 +734,8 @@
                357504E50DA104E39C6ACFEB /* Pods.release.xcconfig */ = {isa = 
PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = 
Pods.release.xcconfig; path = "Pods/Target Support 
Files/Pods/Pods.release.xcconfig"; sourceTree = "<group>"; };
                429C152FC8B093B59D18CAD3 /* 
Pods-WikipediaUnitTests.beta.xcconfig */ = {isa = PBXFileReference; 
includeInIndex = 1; lastKnownFileType = text.xcconfig; name = 
"Pods-WikipediaUnitTests.beta.xcconfig"; path = "Pods/Target Support 
Files/Pods-WikipediaUnitTests/Pods-WikipediaUnitTests.beta.xcconfig"; 
sourceTree = "<group>"; };
                8CE61C6963F825760822A28A /* libPods-WikipediaUnitTests.a */ = 
{isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; 
path = "libPods-WikipediaUnitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+               BC092B951B18E89200093C59 /* WMFPageUtilities.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFPageUtilities.m; sourceTree = "<group>"; };
+               BC092B971B18E8AF00093C59 /* WMFPageUtilities.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = 
WMFPageUtilities.h; sourceTree = "<group>"; };
                BC2375981AB78D8A00B0BAA8 /* 
NSParagraphStyle+WMFNaturalAlignmentStyle.h */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"NSParagraphStyle+WMFNaturalAlignmentStyle.h"; sourceTree = "<group>"; };
                BC2375991AB78D8A00B0BAA8 /* 
NSParagraphStyle+WMFNaturalAlignmentStyle.m */ = {isa = PBXFileReference; 
fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = 
"NSParagraphStyle+WMFNaturalAlignmentStyle.m"; sourceTree = "<group>"; };
                BC23759D1AB8928600B0BAA8 /* WMFDateFormatterTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFDateFormatterTests.m; sourceTree = "<group>"; };
@@ -752,6 +754,7 @@
                BC69C3121AB0C1FF0090B039 /* WMFImageInfoController.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name 
= WMFImageInfoController.h; path = "Image Gallery/WMFImageInfoController.h"; 
sourceTree = "<group>"; };
                BC69C3131AB0C1FF0090B039 /* WMFImageInfoController.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
name = WMFImageInfoController.m; path = "Image 
Gallery/WMFImageInfoController.m"; sourceTree = "<group>"; };
                BC6FEAE01A9B7EFD00A1D890 /* WMFCodingStyle.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFCodingStyle.m; sourceTree = "<group>"; };
+               BC7A4A231B17B3510003E73E /* NSObjectUtilities.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = 
NSObjectUtilities.h; sourceTree = "<group>"; };
                BC7ACB621AB34C9C00791497 /* WMFAsyncTestCase.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = 
WMFAsyncTestCase.h; path = ../WMFAsyncTestCase.h; sourceTree = "<group>"; };
                BC7ACB631AB34C9C00791497 /* WMFAsyncTestCase.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name 
= WMFAsyncTestCase.m; path = ../WMFAsyncTestCase.m; sourceTree = "<group>"; };
                BC7DFCCB1AA4BA8A000035C3 /* WMFCodingStyle.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WMFCodingStyle.h; 
sourceTree = "<group>"; };
@@ -906,6 +909,8 @@
                BCD41DE91B11CC5800231BB1 /* user-loggedin.json */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = 
"user-loggedin.json"; sourceTree = "<group>"; };
                BCD41DFD1B11D17100231BB1 /* XCTestCase+MWKFixtures.h */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path 
= "XCTestCase+MWKFixtures.h"; sourceTree = "<group>"; };
                BCD41DFE1B11D17100231BB1 /* XCTestCase+MWKFixtures.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
path = "XCTestCase+MWKFixtures.m"; sourceTree = "<group>"; };
+               BCDA2F411B17A02A002FEB6A /* WMFComparison.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WMFComparison.h; 
sourceTree = "<group>"; };
+               BCDA2F421B17A056002FEB6A /* WMFHashing.h */ = {isa = 
PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WMFHashing.h; 
sourceTree = "<group>"; };
                BCDB75BC1AB0D3DE0005593F /* WMFImageInfoController_Private.h */ 
= {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = 
sourcecode.c.h; name = WMFImageInfoController_Private.h; path = "Image 
Gallery/WMFImageInfoController_Private.h"; sourceTree = "<group>"; };
                BCDB75BD1AB0DFC40005593F /* WMFRangeUtils.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
WMFRangeUtils.h; sourceTree = "<group>"; };
                BCDB75C31AB0E8300005593F /* WMFSubstringUtilsTests.m */ = {isa 
= PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; 
path = WMFSubstringUtilsTests.m; sourceTree = "<group>"; };
@@ -2347,6 +2352,11 @@
                                BCB848761AAAABF80077EC24 /* 
WMFRoundingUtilities.h */,
                                BCB848771AAAABF80077EC24 /* 
WMFRoundingUtilities.c */,
                                BCDB75BD1AB0DFC40005593F /* WMFRangeUtils.h */,
+                               BCDA2F411B17A02A002FEB6A /* WMFComparison.h */,
+                               BCDA2F421B17A056002FEB6A /* WMFHashing.h */,
+                               BC7A4A231B17B3510003E73E /* NSObjectUtilities.h 
*/,
+                               BC092B971B18E8AF00093C59 /* WMFPageUtilities.h 
*/,
+                               BC092B951B18E89200093C59 /* WMFPageUtilities.m 
*/,
                        );
                        path = "mw-utils";
                        sourceTree = "<group>";
@@ -2928,7 +2938,6 @@
                                0484B9071ABB50FA00874073 /* WMFArticleParsing.m 
in Sources */,
                                BC0FED751AAA026C002488D7 /* 
NSArray+BKIndexTests.m in Sources */,
                                BC31B2521AB1D9DC008138CA /* 
WMFImageInfoControllerTests.m in Sources */,
-                               BCD41DDC1B11CBD400231BB1 /* (null) in Sources 
*/,
                                BC0FED641AAA0263002488D7 /* 
MWKArticleStoreTestCase.m in Sources */,
                                BCA676561AC05FE200A16160 /* 
NSBundle+TestAssets.m in Sources */,
                                BCB8487B1AAAADF90077EC24 /* 
WMFRoundingUtilitiesTests.m in Sources */,
@@ -3074,6 +3083,7 @@
                                047801BE18AE987900DBB747 /* 
UIButton+ColorMask.m in Sources */,
                                BC86B93D1A929CC500B4C039 /* 
UICollectionViewFlowLayout+NSCopying.m in Sources */,
                                04D686C91AB28FE40009B44A /* 
UIImage+WMFFocalImageDrawing.m in Sources */,
+                               BC092B961B18E89200093C59 /* WMFPageUtilities.m 
in Sources */,
                                04490FD51AF16A83009FAB52 /* 
WMFBundledImageProtocol.m in Sources */,
                                0429300A18604898002A13FC /* 
SavedPagesResultCell.m in Sources */,
                                0487048419F8262600B7D307 /* CaptchaResetter.m 
in Sources */,
diff --git a/Wikipedia/Categories/MWKArticle+WMFSharing.m 
b/Wikipedia/Categories/MWKArticle+WMFSharing.m
index b6a8bc0..d949873 100644
--- a/Wikipedia/Categories/MWKArticle+WMFSharing.m
+++ b/Wikipedia/Categories/MWKArticle+WMFSharing.m
@@ -7,9 +7,9 @@
 //
 
 #import "MWKArticle+WMFSharing.h"
+#import "MWKArticle+isMain.h"
 #import "NSString+WMFHTMLParsing.h"
 #import "MWKSection+WMFSharing.h"
-#import "MWKArticle+isMain.h"
 #import <BlocksKit/BlocksKit.h>
 
 @implementation MWKArticle (WMFSharing)
diff --git a/Wikipedia/Categories/NSArray+WMFExtensions.h 
b/Wikipedia/Categories/NSArray+WMFExtensions.h
index 88d5afb..32409ff 100644
--- a/Wikipedia/Categories/NSArray+WMFExtensions.h
+++ b/Wikipedia/Categories/NSArray+WMFExtensions.h
@@ -3,6 +3,9 @@
 
 @interface NSArray (WMFExtensions)
 
+/// @return The object at `index` if it's within range of the receiver, 
otherwise `nil`.
+- (id)wmf_safeObjectAtIndex:(NSUInteger)index;
+
 /**
  *  Safely trim an array to a specified length.
  *  Will not throw an exception if
diff --git a/Wikipedia/Categories/NSArray+WMFExtensions.m 
b/Wikipedia/Categories/NSArray+WMFExtensions.m
index 6ebec9c..4b2a40f 100644
--- a/Wikipedia/Categories/NSArray+WMFExtensions.m
+++ b/Wikipedia/Categories/NSArray+WMFExtensions.m
@@ -10,6 +10,10 @@
 
 @implementation NSArray (WMFExtensions)
 
+- (id)wmf_safeObjectAtIndex:(NSUInteger)index {
+    return index < self.count ? self[index] : nil;
+}
+
 - (instancetype)wmf_arrayByTrimmingToLength:(NSUInteger)length {
     if ([self count] == 0) {
         return self;
diff --git a/Wikipedia/Categories/NSString+Extras.h 
b/Wikipedia/Categories/NSString+Extras.h
index d7bef31..2632500 100644
--- a/Wikipedia/Categories/NSString+Extras.h
+++ b/Wikipedia/Categories/NSString+Extras.h
@@ -6,6 +6,9 @@
 /// @return A substring of the receiver going up to @c index, or @c length, 
whichever is shorter.
 - (NSString*)wmf_safeSubstringToIndex:(NSUInteger)index;
 
+/// @return A substring of the receiver starting at @c index or an empty 
string if the recevier is too short.
+- (NSString*)wmf_safeSubstringFromIndex:(NSUInteger)index;
+
 - (NSString*)wmf_UTF8StringWithPercentEscapes;
 
 - (NSString*)wmf_schemelessURL;
diff --git a/Wikipedia/Categories/NSString+Extras.m 
b/Wikipedia/Categories/NSString+Extras.m
index 42f90c9..0c524f6 100644
--- a/Wikipedia/Categories/NSString+Extras.m
+++ b/Wikipedia/Categories/NSString+Extras.m
@@ -14,6 +14,10 @@
     return [self substringToIndex:MIN(self.length, index)];
 }
 
+- (NSString*)wmf_safeSubstringFromIndex:(NSUInteger)index {
+    return [self substringFromIndex:MIN(index, self.length - 1)];
+}
+
 - (NSString*)wmf_UTF8StringWithPercentEscapes {
     return (__bridge_transfer id)CFURLCreateStringByAddingPercentEscapes(0,
                                                                          
(__bridge CFStringRef)self,
diff --git a/Wikipedia/Networking/Fetchers/ArticleFetcher.m 
b/Wikipedia/Networking/Fetchers/ArticleFetcher.m
index 320b68a..2ea00a2 100644
--- a/Wikipedia/Networking/Fetchers/ArticleFetcher.m
+++ b/Wikipedia/Networking/Fetchers/ArticleFetcher.m
@@ -143,6 +143,7 @@
 - (NSDictionary*)getParamsForTitle:(NSString*)title {
     NSMutableDictionary* params = @{
         @"format": @"json",
+        @"formatversion": @2,
         @"action": @"mobileview",
         @"sectionprop": WMFJoinedPropertyParameters(@[@"toclevel", @"line", 
@"anchor", @"level", @"number",
                                                       @"fromtitle", @"index"]),
diff --git a/Wikipedia/View Controllers/History/HistoryViewController.m 
b/Wikipedia/View Controllers/History/HistoryViewController.m
index f20e5a5..5f29daa 100644
--- a/Wikipedia/View Controllers/History/HistoryViewController.m
+++ b/Wikipedia/View Controllers/History/HistoryViewController.m
@@ -251,7 +251,7 @@
     DataHousekeeping* dataHouseKeeping = [[DataHousekeeping alloc] init];
     [dataHouseKeeping performHouseKeeping];
 
-    [NAV loadTodaysArticleIfNoCoreDataForCurrentArticle];
+    [NAV loadTodaysArticle];
 }
 
 #pragma mark - History section titles
@@ -459,7 +459,7 @@
     DataHousekeeping* dataHouseKeeping = [[DataHousekeeping alloc] init];
     [dataHouseKeeping performHouseKeeping];
 
-    [NAV loadTodaysArticleIfNoCoreDataForCurrentArticle];
+    [NAV loadTodaysArticle];
 }
 
 #pragma mark - Discovery method icons
@@ -504,7 +504,7 @@
 
     [self setEmptyOverlayAndTrashIconVisibility];
 
-    [NAV loadTodaysArticleIfNoCoreDataForCurrentArticle];
+    [NAV loadTodaysArticle];
 }
 
 - (void)setEmptyOverlayAndTrashIconVisibility {
diff --git a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.h 
b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.h
index 845a22b..e896395 100644
--- a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.h
+++ b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.h
@@ -17,7 +17,6 @@
                   popToWebVC:(BOOL)popToWebVC;
 
 - (void)loadTodaysArticle;
-- (void)loadTodaysArticleIfNoCoreDataForCurrentArticle;
 - (void)loadRandomArticle;
 
 - (void)promptFirstTimeZeroOnWithTitleIfAppropriate:(NSString*)title;
diff --git a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m 
b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
index 402180d..44aab88 100644
--- a/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
+++ b/Wikipedia/View Controllers/Navigation/Center/CenterNavController.m
@@ -144,10 +144,6 @@
                     popToWebVC:NO];
 }
 
-- (void)loadTodaysArticleIfNoCoreDataForCurrentArticle {
-    [self loadTodaysArticle];
-}
-
 - (void)loadRandomArticle {
     [[QueuesSingleton sharedInstance].articleFetchManager.operationQueue 
cancelAllOperations];
 
diff --git a/Wikipedia/View Controllers/SavedPages/SavedPagesViewController.m 
b/Wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
index 2db94ae..1edbc6f 100644
--- a/Wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
+++ b/Wikipedia/View Controllers/SavedPages/SavedPagesViewController.m
@@ -388,7 +388,7 @@
     DataHousekeeping* dataHouseKeeping = [[DataHousekeeping alloc] init];
     [dataHouseKeeping performHouseKeeping];
 
-    [NAV loadTodaysArticleIfNoCoreDataForCurrentArticle];
+    [NAV loadTodaysArticle];
 }
 
 - (void)deleteAllSavedPages {
@@ -403,7 +403,7 @@
 
     [self setEmptyOverlayAndTrashIconVisibility];
 
-    [NAV loadTodaysArticleIfNoCoreDataForCurrentArticle];
+    [NAV loadTodaysArticle];
 }
 
 - (void)alertView:(UIAlertView*)alertView 
clickedButtonAtIndex:(NSInteger)buttonIndex {
diff --git a/Wikipedia/View Controllers/WebView/WebViewController.m 
b/Wikipedia/View Controllers/WebView/WebViewController.m
index f31777c..5e0e1d9 100644
--- a/Wikipedia/View Controllers/WebView/WebViewController.m
+++ b/Wikipedia/View Controllers/WebView/WebViewController.m
@@ -825,8 +825,7 @@
                 [strSelf referencesHide];
             }
 
-            // @todo merge this link title extraction into MWSite
-            if ([href hasPrefix:@"/wiki/"]) {
+            if ([href hasPrefix:WMFInternalLinkPathPrefix]) {
                 // Ensure the menu is visible when navigating to new page.
                 [strSelf animateTopAndBottomMenuReveal];
 
diff --git a/Wikipedia/mw-utils/NSObjectUtilities.h 
b/Wikipedia/mw-utils/NSObjectUtilities.h
new file mode 100644
index 0000000..c06bd49
--- /dev/null
+++ b/Wikipedia/mw-utils/NSObjectUtilities.h
@@ -0,0 +1,12 @@
+//
+//  NSObjectUtilities.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/28/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+// Collection of headers for implementing NSObject methods.
+
+#import "WMFComparison.h"
+#import "WMFHashing.h"
diff --git a/Wikipedia/mw-utils/WMFComparison.h 
b/Wikipedia/mw-utils/WMFComparison.h
new file mode 100644
index 0000000..2f43df9
--- /dev/null
+++ b/Wikipedia/mw-utils/WMFComparison.h
@@ -0,0 +1,54 @@
+//
+//  WMFComparison.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/28/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#ifndef Wikipedia_WMFComparison_h
+#define Wikipedia_WMFComparison_h
+
+/**
+ * Provides compile time checking for keypaths on a given object.
+ * @discussion Example usage:
+ *
+ *      WMF_SAFE_KEYPATH([NSString new], lowercaseString); //< 
@"lowercaseString"
+ *      WMF_SAFE_KEYPATH([NSString new], fooBar); //< compiler error!
+ *
+ * @note Inspired by 
[EXTKeypathCoding.h](https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/EXTKeyPathCoding.h#L14)
+ */
+#define WMF_SAFE_KEYPATH(obj, keyp) ((NO, (void)obj.keyp), @#keyp)
+
+/**
+ * Compare two *objects* using `==` and <code>[a sel b]</code>, where `sel` is 
an equality selector
+ * (e.g. `isEqualToString:`).
+ * @param a   First object, can be `nil`.
+ * @param sel The selector used to compare @c a to @c b, if <code>a == 
b</code> is @c false.
+ * @param b   Second object, can be `nil`.
+ * @return `YES` if the objects are the same pointer or invoking @c sel 
returns @c YES, otherwise @c NO.
+ */
+#define WMF_EQUAL(a, sel, b) (((a) == (b)) || ([(a) sel (b)]))
+
+/**
+ * Check if two objects have the same value for given property.
+ * @param a     First object, can be @c nil.
+ * @param prop  The property whose value is accessed from `a` and `b`, e.g. 
`count`.
+ * @param sel   The selector used to compare `a.prop` to `b.prop`.
+ * @param b     Second object, can be @c nil.
+ * @return `YES` if the values are equal or both `nil`, otherwise `NO`.
+ * @see WMF_EQUAL
+ */
+#define WMF_EQUAL_PROPERTIES(a, prop, sel, b) WMF_EQUAL([(a) prop], sel, [(b) 
prop])
+
+/// Convenience for `WMF_EQUAL_PROPERTIES` which passes `isEqual:` for the 
equality selector.
+#define WMF_IS_EQUAL_PROPERTIES(a, prop, b) WMF_EQUAL_PROPERTIES(a, prop, 
isEqual:, b)
+
+
+/**
+ * Compare two objects using `==` and `isEqual:`.
+ * @see WMF_EQUAL
+ */
+#define WMF_IS_EQUAL(a, b) (WMF_EQUAL(a, isEqual:, b))
+
+#endif
diff --git a/Wikipedia/mw-utils/WMFHashing.h b/Wikipedia/mw-utils/WMFHashing.h
new file mode 100644
index 0000000..6785532
--- /dev/null
+++ b/Wikipedia/mw-utils/WMFHashing.h
@@ -0,0 +1,23 @@
+//
+//  WMFHashing.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/28/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#ifndef Wikipedia_WMFHashing_h
+#define Wikipedia_WMFHashing_h
+
+static NSUInteger const NSUINT_BIT   = sizeof(NSUInteger) * CHAR_BIT;
+static NSUInteger const NSUINT_BIT_2 = NSUINT_BIT / 2;
+
+// taken from MA's blog:
+// 
https://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html
+static NSUInteger flipBitsWithAdditionalRotation(NSUInteger x, NSUInteger 
rotation) {
+    // take the amount and adjust it by half the size of x, so an s of 0 
results in a "flip" of the bits
+    rotation += NSUINT_BIT_2;
+    return (x << rotation) | (x >> (NSUINT_BIT - rotation));
+}
+
+#endif
diff --git a/Wikipedia/mw-utils/WMFPageUtilities.h 
b/Wikipedia/mw-utils/WMFPageUtilities.h
new file mode 100644
index 0000000..b6e1b8b
--- /dev/null
+++ b/Wikipedia/mw-utils/WMFPageUtilities.h
@@ -0,0 +1,26 @@
+//
+//  WMFPageUtilities.h
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+/// Expected prefix for links to pages from the wiki that the link's page 
belongs to.
+extern NSString* const WMFInternalLinkPathPrefix;
+
+/**
+ * @return Whether a URL is an internal link.
+ * @see WMFInternalLinkPrefix
+ */
+extern BOOL WMFIsInternalLink(NSString* urlString);
+
+/**
+ * Strips the internal link prefix from @c urlString, if present.
+ */
+extern NSString* WMFInternalLinkPath(NSString* urlString);
+
+/// Normalizes page titles extracted from URLs, replacing percent escapes and 
underscores.
+extern NSString* WMFNormalizedPageTitle(NSString* rawPageTitle);
diff --git a/Wikipedia/mw-utils/WMFPageUtilities.m 
b/Wikipedia/mw-utils/WMFPageUtilities.m
new file mode 100644
index 0000000..b8dcd24
--- /dev/null
+++ b/Wikipedia/mw-utils/WMFPageUtilities.m
@@ -0,0 +1,29 @@
+//
+//  WMFPageUtilities.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 5/29/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import "WMFPageUtilities.h"
+#import "NSString+Extras.h"
+#import "WMFRangeUtils.h"
+
+NSString* const WMFInternalLinkPathPrefix = @"/wiki/";
+
+BOOL WMFIsInternalLink(NSString* urlString) {
+    return [urlString rangeOfString:WMFInternalLinkPathPrefix].location != 
NSNotFound;
+}
+
+NSString* WMFInternalLinkPath(NSString* urlString) {
+    NSRange internalLinkRange = [urlString 
rangeOfString:WMFInternalLinkPathPrefix];
+    return internalLinkRange.location == NSNotFound ?
+           urlString
+           : [urlString 
wmf_safeSubstringFromIndex:WMFRangeGetMaxIndex(internalLinkRange)];
+}
+
+NSString* WMFNormalizedPageTitle(NSString* rawPageTitle) {
+    return [[rawPageTitle 
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
+            stringByReplacingOccurrencesOfString:@"_" withString:@" "];
+}
\ No newline at end of file
diff --git a/Wikipedia/mw-utils/WikipediaAppUtils.h 
b/Wikipedia/mw-utils/WikipediaAppUtils.h
index 5d82f3b..5e5d145 100644
--- a/Wikipedia/mw-utils/WikipediaAppUtils.h
+++ b/Wikipedia/mw-utils/WikipediaAppUtils.h
@@ -3,59 +3,19 @@
 
 #import <Foundation/Foundation.h>
 #import <UIKit/UIKit.h>
-
-// TODO: use developer constants?
-//extern NSString * const WMFHockeyAppDeveloperXcodeCFBundleIdentifier;
-//extern NSString * const WMFHockeyAppDeveloperXcodeAppId;
-extern NSString* const WMFHockeyAppAlphaHockeyCFBundleIdentifier;
-extern NSString* const WMFHockeyAppAlphaHockeyAppId;
-extern NSString* const WMFHockeyAppBetaTestFlightCFBundleIdentifier;
-extern NSString* const WMFHockeyAppBetaTestFlightAppId;
-//extern NSString * const WMFHockeyAppStableCFBundleIdentifier;
-//extern NSString * const WMFHockeyAppStableAppId;
-// TODO: use stable channel constants
+#import "NSObjectUtilities.h"
+#import "WMFPageUtilities.h"
 
 #define MWLocalizedString(key, throwaway) [WikipediaAppUtils 
localizedStringForKey : key]
 #define MWCurrentArticleLanguageLocalizedString(key, throwaway) 
[WikipediaAppUtils currentArticleLanguageLocalizedString : key]
 
-/**
- * Provides compile time checking for keypaths on a given object.
- * @discussion Example usage:
- *
- *      WMF_SAFE_KEYPATH([NSString new], lowercaseString); //< 
@"lowercaseString"
- *      WMF_SAFE_KEYPATH([NSString new], fooBar); //< compiler error!
- *
- * @note Inspired by 
[EXTKeypathCoding.h](https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/EXTKeyPathCoding.h#L14)
- */
-#define WMF_SAFE_KEYPATH(obj, keyp) ((NO, (void)obj.keyp), @#keyp)
+/// @return Number of bytes equivalent to `m` megabytes.
+static NSUInteger MegabytesToBytes(NSUInteger m) {
+    static NSUInteger const MEGABYTE = 1 << 20;
+    return m * MEGABYTE;
+}
 
-/**
- * Compare two *objects* using @c == and <code>[a sel b]</code>, where @c sel 
is a equality selector
- * (e.g. @c isEqualToString:).
- * @param a   First object, can be @c nil.
- * @param sel The selector used to compare @c a to @c b, if <code>a == 
b</code> is @c false.
- * @param b   Second object, can be @c nil.
- * @return @c YES if the objects are the same pointer or invoking @c sel 
returns @c YES, otherwise @c NO.
- */
-#define WMF_EQUAL(a, sel, b) (((a) == (b)) || ([(a) sel (b)]))
-
-/**
- * Compare two objects using `==` and `isEqual:`.
- * @see WMF_EQUAL
- */
-#define WMF_IS_EQUAL(a, b) (WMF_EQUAL(a, isEqual :, b))
-
-/// Circularly rotate an unsigned int (useful when implementing 
<code>-[NSObject hash]</code>).
-FOUNDATION_EXPORT NSUInteger CircularBitwiseRotation(NSUInteger x, NSUInteger 
s)
-__attribute__((pure, always_inline, const));
-
-/// Conert @c m megabytes to bytes.
-FOUNDATION_EXPORT NSUInteger MegabytesToBytes(NSUInteger m)
-__attribute__((pure, always_inline, const));
-
-/// Normalizes page titles extracted from URLs, replacing percent escapes and 
underscores.
-FOUNDATION_EXPORT NSString* WMFNormalizedPageTitle(NSString* rawPageTitle);
-
+/// Expected prefix for links pointing to pages within the current page's wiki.
 @interface WikipediaAppUtils : NSObject
 
 + (NSString*)appVersion;
diff --git a/Wikipedia/mw-utils/WikipediaAppUtils.m 
b/Wikipedia/mw-utils/WikipediaAppUtils.m
index bdfec2c..74011c5 100644
--- a/Wikipedia/mw-utils/WikipediaAppUtils.m
+++ b/Wikipedia/mw-utils/WikipediaAppUtils.m
@@ -7,20 +7,6 @@
 #import "NSBundle+WMFInfoUtils.h"
 #import <BlocksKit/BlocksKit.h>
 
-NSUInteger MegabytesToBytes(NSUInteger m){
-    static NSUInteger const MEGABYTE = 1 << 20;
-    return m * MEGABYTE;
-}
-
-NSUInteger CircularBitwiseRotation(NSUInteger x, NSUInteger s) {
-    return (x << s) | (x >> (sizeof(x) * CHAR_BIT - s));
-}
-
-NSString* WMFNormalizedPageTitle(NSString* rawPageTitle) {
-    return [[rawPageTitle 
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
-            stringByReplacingOccurrencesOfString:@"_" withString:@" "];
-}
-
 @implementation WikipediaAppUtils
 
 + (void)load {
diff --git a/WikipediaUnitTests/CircularBitwiseRotationTests.m 
b/WikipediaUnitTests/CircularBitwiseRotationTests.m
index 0f9fcdd..3124f4a 100644
--- a/WikipediaUnitTests/CircularBitwiseRotationTests.m
+++ b/WikipediaUnitTests/CircularBitwiseRotationTests.m
@@ -8,8 +8,7 @@
 
 #import <UIKit/UIKit.h>
 #import <XCTest/XCTest.h>
-
-#import "WikipediaAppUtils.h"
+#import "WMFHashing.h"
 
 @interface CircularBitwiseRotationTests : XCTestCase
 
@@ -17,14 +16,31 @@
 
 @implementation CircularBitwiseRotationTests
 
-- (void)testExamples {
-    NSUInteger testValue = 0b00000001;
-    NSUInteger len       = sizeof(testValue) * CHAR_BIT;
-    XCTAssertEqual(CircularBitwiseRotation(testValue, 0), 0b001);
-    XCTAssertEqual(CircularBitwiseRotation(testValue, 1), 0b00000010);
-    XCTAssertEqual(CircularBitwiseRotation(testValue, 2), 0b00000100);
-    XCTAssertEqual(CircularBitwiseRotation(testValue, len), 0b00000001);
-    XCTAssertEqual(CircularBitwiseRotation(testValue, len + 1), 0b00000010);
+- (void)testMatchesCorrespondingPowerOfTwo {
+    for (NSUInteger rotation; rotation < NSUINT_BIT; rotation++) {
+        NSUInteger actualResult = flipBitsWithAdditionalRotation(1, rotation);
+        // add by NSUINT_BIT_2 to model the "flipping," then modulo for 
rotation
+        NSUInteger exponent       = (rotation + NSUINT_BIT_2) % NSUINT_BIT;
+        NSUInteger expectedResult = powl(2, exponent);
+        XCTAssertEqual(actualResult, expectedResult,
+                       @"Rotating 1 by %lu should be equal to 2^%lu (%lu). Got 
%lu instead",
+                       rotation, exponent, expectedResult, actualResult);
+    }
+}
+
+- (void)testSymmetrical {
+    for (NSUInteger i; i < 50; i++) {
+        NSUInteger testValue = arc4random();
+        for (NSUInteger rotation; rotation < NSUINT_BIT; rotation++) {
+            NSUInteger symmetricalRotation = rotation + NSUINT_BIT;
+            NSUInteger original            = 
flipBitsWithAdditionalRotation(testValue, rotation);
+            NSUInteger symmetrical         = 
flipBitsWithAdditionalRotation(testValue, symmetricalRotation);
+            XCTAssertEqual(original, symmetrical,
+                           @"Rotating %lu by %lu should be the same as 
rotating by %lu + NSUINT_BIT (%lu)."
+                           "Got %lu expected %lu",
+                           testValue, rotation, rotation, symmetricalRotation, 
symmetrical, original);
+        }
+    }
 }
 
 @end
diff --git a/WikipediaUnitTests/MWKArticle+WMFSharingTests.m 
b/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
index 745bb02..52274c4 100644
--- a/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
+++ b/WikipediaUnitTests/MWKArticle+WMFSharingTests.m
@@ -10,11 +10,11 @@
 #import <XCTest/XCTest.h>
 
 #import "SessionSingleton.h"
-#import "MWKArticle+isMain.h"
 #import "WMFTestFixtureUtilities.h"
 #import "MWKTitle.h"
 #import "MWKSite.h"
 #import "MWKArticle+WMFSharing.h"
+#import "MWKArticle+isMain.h"
 #import "MWKDataStore+TemporaryDataStore.h"
 
 #define HC_SHORTHAND 1
@@ -28,13 +28,14 @@
 
 - (void)testMainPage {
     self.article =
-        [[MWKArticle alloc] initWithTitle:[[SessionSingleton sharedInstance] 
mainArticleTitle] dataStore:nil];
+        [[MWKArticle alloc] initWithTitle:[MWKTitle titleWithString:@"Main 
Page" site:[MWKSite siteWithCurrentLocale]]
+                                dataStore:nil];
 
     NSDictionary* mainPageMobileView = [[[self wmf_bundle]
                                          
wmf_jsonFromContentsOfFile:@"MainPageMobileView"]
                                         objectForKey:@"mobileview"];
 
-    NSAssert(self.article.isMain, @"supposed to be testing main pages!");
+    NSAssert([self.article isMain], @"supposed to be testing main pages!");
 
     [self.article importMobileViewJSON:mainPageMobileView];
     assertThat(self.article.shareSnippet, is(@"Gary Cooper was an American 
film actor known for his natural, authentic, and understated acting style. He 
was a movie star from the end of the silent film era through the end of the 
golden age of Classical Hollywood. Cooper began his career as a film extra and 
stunt rider and soon established himself as a Western hero in films such as The 
Virginian. He played the lead in adventure films and dramas such as A Farewell 
to Arms and The Lives of a Bengal Lancer, and extended his range of 
performances to include roles in most major film genres. He portrayed champions 
of the common man in films such as Mr. Deeds Goes to Town, Meet John Doe, 
Sergeant York, The Pride of the Yankees, and For Whom the Bell Tolls. In his 
later years, he delivered award-winning performances in High Noon and Friendly 
Persuasion. Cooper received three Academy Awards and appeared on the Motion 
Picture Herald exhibitors poll of top ten film personalities every year from 
1936 to 1958. His screen persona embodied the American folk hero. Ongoing: 
Nepal earthquake – Yemeni Civil WarRecent deaths: Ruth Rendell – Maya 
Plisetskaya"));

-- 
To view, visit https://gerrit.wikimedia.org/r/215114
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: If5906e1ad8f6f39899e3efbb703ce660bceeb6d9
Gerrit-PatchSet: 1
Gerrit-Project: apps/ios/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Bgerstle <bgers...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to